diff --git a/include/libcamera/color_filter_array.h b/include/libcamera/color_filter_array.h
new file mode 100644
index 000000000000..167a4a3ff323
--- /dev/null
+++ b/include/libcamera/color_filter_array.h
@@ -0,0 +1,58 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, Ideas on Board Oy
+ *
+ * Bayer Color Filter Array patterns
+ */
+
+#pragma once
+
+#include <optional>
+#include <ostream>
+#include <stdint.h>
+#include <string>
+
+#include "libcamera/internal/v4l2_pixelformat.h"
+
+namespace libcamera {
+
+enum class Transform;
+
+class ColorFilterArray
+{
+public:
+	enum Order : uint8_t {
+		BGGR = 0,
+		GBRG = 1,
+		GRBG = 2,
+		RGGB = 3,
+		MONO = 4
+	};
+
+	constexpr ColorFilterArray() : order(Order::BGGR)
+	{
+	}
+
+	constexpr ColorFilterArray(Order o)
+		: order(o)
+	{
+	}
+
+	static std::optional<ColorFilterArray> fromMbusCode(unsigned int mbusCode);
+	static std::optional<ColorFilterArray> fromV4L2PixelFormat(V4L2PixelFormat v4l2Format);
+	unsigned int toMbusCode(uint8_t bitDepth) const;
+
+	std::string toString() const;
+
+	bool operator==(const ColorFilterArray &other) const {
+		return this->order == other.order;
+	}
+
+	ColorFilterArray transform(Transform t) const;
+
+	Order order;
+};
+
+std::ostream &operator<<(std::ostream &out, const ColorFilterArray &f);
+
+} /* namespace libcamera */
diff --git a/include/libcamera/internal/bayer_format.h b/include/libcamera/internal/bayer_format.h
index 5c14bb5fb2d5..701e063edcc5 100644
--- a/include/libcamera/internal/bayer_format.h
+++ b/include/libcamera/internal/bayer_format.h
@@ -11,6 +11,7 @@
 #include <stdint.h>
 #include <string>
 
+#include <libcamera/color_filter_array.h>
 #include <libcamera/pixel_format.h>
 
 #include "libcamera/internal/v4l2_pixelformat.h"
@@ -22,14 +23,6 @@ enum class Transform;
 class BayerFormat
 {
 public:
-	enum Order : uint8_t {
-		BGGR = 0,
-		GBRG = 1,
-		GRBG = 2,
-		RGGB = 3,
-		MONO = 4
-	};
-
 	enum class Packing : uint16_t {
 		None = 0,
 		CSI2 = 1,
@@ -39,12 +32,12 @@ public:
 	};
 
 	constexpr BayerFormat()
-		: order(Order::BGGR), bitDepth(0), packing(Packing::None)
+		: cfa(ColorFilterArray()), bitDepth(0), packing(Packing::None)
 	{
 	}
 
-	constexpr BayerFormat(Order o, uint8_t b, Packing p)
-		: order(o), bitDepth(b), packing(p)
+	constexpr BayerFormat(ColorFilterArray c, uint8_t b, Packing p)
+		: cfa(c), bitDepth(b), packing(p)
 	{
 	}
 
@@ -57,9 +50,8 @@ public:
 	static BayerFormat fromV4L2PixelFormat(V4L2PixelFormat v4l2Format);
 	PixelFormat toPixelFormat() const;
 	static BayerFormat fromPixelFormat(PixelFormat format);
-	BayerFormat transform(Transform t) const;
 
-	Order order;
+	ColorFilterArray cfa;
 	uint8_t bitDepth;
 
 	Packing packing;
diff --git a/include/libcamera/internal/camera_sensor.h b/include/libcamera/internal/camera_sensor.h
index 2a2f1f2c936d..a0f27ac11735 100644
--- a/include/libcamera/internal/camera_sensor.h
+++ b/include/libcamera/internal/camera_sensor.h
@@ -16,13 +16,13 @@
 
 #include <libcamera/base/class.h>
 
+#include <libcamera/color_filter_array.h>
 #include <libcamera/control_ids.h>
 #include <libcamera/controls.h>
 #include <libcamera/geometry.h>
 #include <libcamera/orientation.h>
 #include <libcamera/transform.h>
 
-#include "libcamera/internal/bayer_format.h"
 #include "libcamera/internal/camera_sensor_properties.h"
 #include "libcamera/internal/v4l2_subdevice.h"
 
@@ -72,7 +72,7 @@ public:
 	virtual const ControlList &properties() const = 0;
 	virtual int sensorInfo(IPACameraSensorInfo *info) const = 0;
 	virtual Transform computeTransform(Orientation *orientation) const = 0;
-	virtual BayerFormat::Order bayerOrder(Transform t) const = 0;
+	virtual ColorFilterArray bayerOrder(Transform t) const = 0;
 	virtual Orientation mountingOrientation() const = 0;
 
 	virtual const ControlInfoMap &controls() const = 0;
diff --git a/include/libcamera/internal/software_isp/swstats_cpu.h b/include/libcamera/internal/software_isp/swstats_cpu.h
index 551870921862..1594e520c5ba 100644
--- a/include/libcamera/internal/software_isp/swstats_cpu.h
+++ b/include/libcamera/internal/software_isp/swstats_cpu.h
@@ -88,7 +88,7 @@ private:
 	using statsProcessFn = void (SwStatsCpu::*)(const uint8_t *src[], SwIspStats &stats);
 	using processFrameFn = void (SwStatsCpu::*)(MappedFrameBuffer &in);
 
-	int setupStandardBayerOrder(BayerFormat::Order order);
+	int setupStandardBayerOrder(ColorFilterArray::Order order);
 	/* Bayer 8 bpp unpacked */
 	void statsBGGR8Line0(const uint8_t *src[], SwIspStats &stats);
 	/* Bayer 10 bpp unpacked */
diff --git a/include/libcamera/meson.build b/include/libcamera/meson.build
index 30ea76f9470a..70bd0ccbed9e 100644
--- a/include/libcamera/meson.build
+++ b/include/libcamera/meson.build
@@ -5,6 +5,7 @@ libcamera_include_dir = 'libcamera' / 'libcamera'
 libcamera_public_headers = files([
     'camera.h',
     'camera_manager.h',
+    'color_filter_array.h',
     'color_space.h',
     'controls.h',
     'fence.h',
diff --git a/src/ipa/mali-c55/algorithms/agc.cpp b/src/ipa/mali-c55/algorithms/agc.cpp
index 2c747da89170..8600e7d61922 100644
--- a/src/ipa/mali-c55/algorithms/agc.cpp
+++ b/src/ipa/mali-c55/algorithms/agc.cpp
@@ -54,28 +54,28 @@ uint32_t AgcStatistics::decodeBinValue(uint16_t binVal)
  * colour channels would be populated, and they may not be in the same
  * planes as calculated here.
  */
-int AgcStatistics::setBayerOrderIndices(BayerFormat::Order bayerOrder)
+int AgcStatistics::setBayerOrderIndices(ColorFilterArray::Order cfaOrder)
 {
-	switch (bayerOrder) {
-	case BayerFormat::Order::RGGB:
+	switch (cfaOrder) {
+	case ColorFilterArray::Order::RGGB:
 		rIndex_ = 0;
 		grIndex_ = 1;
 		gbIndex_ = 2;
 		bIndex_ = 3;
 		break;
-	case BayerFormat::Order::GRBG:
+	case ColorFilterArray::Order::GRBG:
 		grIndex_ = 0;
 		rIndex_ = 1;
 		bIndex_ = 2;
 		gbIndex_ = 3;
 		break;
-	case BayerFormat::Order::GBRG:
+	case ColorFilterArray::Order::GBRG:
 		gbIndex_ = 0;
 		bIndex_ = 1;
 		rIndex_ = 2;
 		grIndex_ = 3;
 		break;
-	case BayerFormat::Order::BGGR:
+	case ColorFilterArray::Order::BGGR:
 		bIndex_ = 0;
 		gbIndex_ = 1;
 		grIndex_ = 2;
@@ -83,7 +83,7 @@ int AgcStatistics::setBayerOrderIndices(BayerFormat::Order bayerOrder)
 		break;
 	default:
 		LOG(MaliC55Agc, Error)
-			<< "Invalid bayer format " << bayerOrder;
+			<< "Invalid bayer format " << cfaOrder;
 		return -EINVAL;
 	}
 
diff --git a/src/ipa/mali-c55/algorithms/agc.h b/src/ipa/mali-c55/algorithms/agc.h
index 809a57c53acc..26c8dc78bc4b 100644
--- a/src/ipa/mali-c55/algorithms/agc.h
+++ b/src/ipa/mali-c55/algorithms/agc.h
@@ -8,8 +8,7 @@
 #pragma once
 
 #include <libcamera/base/utils.h>
-
-#include "libcamera/internal/bayer_format.h"
+#include <libcamera/color_filter_array.h>
 
 #include "libipa/agc.h"
 #include "libipa/histogram.h"
@@ -28,7 +27,7 @@ public:
 	{
 	}
 
-	int setBayerOrderIndices(BayerFormat::Order bayerOrder);
+	int setBayerOrderIndices(ColorFilterArray::Order cfaOrder);
 	uint32_t decodeBinValue(uint16_t binVal);
 	void parseStatistics(const mali_c55_stats_buffer *stats);
 
diff --git a/src/ipa/mali-c55/ipa_context.h b/src/ipa/mali-c55/ipa_context.h
index cd370e25fcca..8915a8b82d57 100644
--- a/src/ipa/mali-c55/ipa_context.h
+++ b/src/ipa/mali-c55/ipa_context.h
@@ -8,12 +8,11 @@
 #pragma once
 
 #include <libcamera/base/utils.h>
+#include <libcamera/color_filter_array.h>
 #include <libcamera/controls.h>
 
 #include <libcamera/ipa/core_ipa_interface.h>
 
-#include "libcamera/internal/bayer_format.h"
-
 #include <libipa/agc.h>
 #include <libipa/camera_sensor_helper.h>
 #include <libipa/fc_queue.h>
@@ -31,7 +30,7 @@ struct IPASessionConfiguration {
 	agc::Session agc;
 
 	struct {
-		BayerFormat::Order bayerOrder;
+		ColorFilterArray::Order bayerOrder;
 		uint32_t blackLevel;
 	} sensor;
 };
diff --git a/src/ipa/mali-c55/mali-c55.cpp b/src/ipa/mali-c55/mali-c55.cpp
index 9a2918cf2cf1..d07c6666f7c5 100644
--- a/src/ipa/mali-c55/mali-c55.cpp
+++ b/src/ipa/mali-c55/mali-c55.cpp
@@ -17,11 +17,11 @@
 #include <libcamera/base/log.h>
 
 #include <libcamera/control_ids.h>
+#include <libcamera/color_filter_array.h>
 #include <libcamera/ipa/ipa_interface.h>
 #include <libcamera/ipa/ipa_module_info.h>
 #include <libcamera/ipa/mali-c55_ipa_interface.h>
 
-#include "libcamera/internal/bayer_format.h"
 #include "libcamera/internal/mapped_framebuffer.h"
 #include "libcamera/internal/yaml_parser.h"
 
@@ -171,7 +171,7 @@ int IPAMaliC55::configure(const IPAConfigInfo &ipaConfig, uint8_t bayerOrder,
 
 	const IPACameraSensorInfo &info = ipaConfig.sensorInfo;
 
-	context_.configuration.sensor.bayerOrder = static_cast<BayerFormat::Order>(bayerOrder);
+	context_.configuration.sensor.bayerOrder = static_cast<ColorFilterArray::Order>(bayerOrder);
 
 	if (auto bl = context_.camHelper->blackLevel()) {
 		/*
diff --git a/src/libcamera/bayer_format.cpp b/src/libcamera/bayer_format.cpp
index 3dab91fc5f4b..c0d5af33dd17 100644
--- a/src/libcamera/bayer_format.cpp
+++ b/src/libcamera/bayer_format.cpp
@@ -14,6 +14,7 @@
 
 #include <linux/media-bus-format.h>
 
+#include <libcamera/color_filter_array.h>
 #include <libcamera/formats.h>
 #include <libcamera/transform.h>
 
@@ -29,26 +30,8 @@ namespace libcamera {
  * \brief Class to represent a raw image Bayer format
  *
  * This class encodes the different Bayer formats in such a way that they can
- * be easily manipulated. For example, the bit depth or Bayer order can be
- * easily altered - the Bayer order can even be "transformed" in the same
- * manner as happens in many sensors when their horizontal or vertical "flip"
- * controls are set.
- */
-
-/**
- * \enum BayerFormat::Order
- * \brief The order of the colour channels in the Bayer pattern
- *
- * \var BayerFormat::BGGR
- * \brief B then G on the first row, G then R on the second row.
- * \var BayerFormat::GBRG
- * \brief G then B on the first row, R then G on the second row.
- * \var BayerFormat::GRBG
- * \brief G then R on the first row, B then G on the second row.
- * \var BayerFormat::RGGB
- * \brief R then G on the first row, G then B on the second row.
- * \var BayerFormat::MONO
- * \brief Monochrome image data, there is no colour filter array.
+ * be easily manipulated. For example the bit depth, packing and color order of
+ * the format transmitted over the bus can be modified and transformed.
  */
 
 /**
@@ -78,9 +61,9 @@ struct BayerFormatComparator {
 		else if (lhs.bitDepth > rhs.bitDepth)
 			return false;
 
-		if (lhs.order < rhs.order)
+		if (lhs.cfa.order < rhs.cfa.order)
 			return true;
-		else if (lhs.order > rhs.order)
+		else if (lhs.cfa.order > rhs.cfa.order)
 			return false;
 
 		if (lhs.packing < rhs.packing)
@@ -96,143 +79,143 @@ struct Formats {
 };
 
 const std::map<BayerFormat, Formats, BayerFormatComparator> bayerToFormat{
-	{ { BayerFormat::BGGR, 8, BayerFormat::Packing::None },
+	{ { ColorFilterArray::BGGR, 8, BayerFormat::Packing::None },
 		{ formats::SBGGR8, V4L2PixelFormat(V4L2_PIX_FMT_SBGGR8) } },
-	{ { BayerFormat::GBRG, 8, BayerFormat::Packing::None },
+	{ { ColorFilterArray::GBRG, 8, BayerFormat::Packing::None },
 		{ formats::SGBRG8, V4L2PixelFormat(V4L2_PIX_FMT_SGBRG8) } },
-	{ { BayerFormat::GRBG, 8, BayerFormat::Packing::None },
+	{ { ColorFilterArray::GRBG, 8, BayerFormat::Packing::None },
 		{ formats::SGRBG8, V4L2PixelFormat(V4L2_PIX_FMT_SGRBG8) } },
-	{ { BayerFormat::RGGB, 8, BayerFormat::Packing::None },
+	{ { ColorFilterArray::RGGB, 8, BayerFormat::Packing::None },
 		{ formats::SRGGB8, V4L2PixelFormat(V4L2_PIX_FMT_SRGGB8) } },
-	{ { BayerFormat::BGGR, 10, BayerFormat::Packing::None },
+	{ { ColorFilterArray::BGGR, 10, BayerFormat::Packing::None },
 		{ formats::SBGGR10, V4L2PixelFormat(V4L2_PIX_FMT_SBGGR10) } },
-	{ { BayerFormat::GBRG, 10, BayerFormat::Packing::None },
+	{ { ColorFilterArray::GBRG, 10, BayerFormat::Packing::None },
 		{ formats::SGBRG10, V4L2PixelFormat(V4L2_PIX_FMT_SGBRG10) } },
-	{ { BayerFormat::GRBG, 10, BayerFormat::Packing::None },
+	{ { ColorFilterArray::GRBG, 10, BayerFormat::Packing::None },
 		{ formats::SGRBG10, V4L2PixelFormat(V4L2_PIX_FMT_SGRBG10) } },
-	{ { BayerFormat::RGGB, 10, BayerFormat::Packing::None },
+	{ { ColorFilterArray::RGGB, 10, BayerFormat::Packing::None },
 		{ formats::SRGGB10, V4L2PixelFormat(V4L2_PIX_FMT_SRGGB10) } },
-	{ { BayerFormat::BGGR, 10, BayerFormat::Packing::CSI2 },
+	{ { ColorFilterArray::BGGR, 10, BayerFormat::Packing::CSI2 },
 		{ formats::SBGGR10_CSI2P, V4L2PixelFormat(V4L2_PIX_FMT_SBGGR10P) } },
-	{ { BayerFormat::GBRG, 10, BayerFormat::Packing::CSI2 },
+	{ { ColorFilterArray::GBRG, 10, BayerFormat::Packing::CSI2 },
 		{ formats::SGBRG10_CSI2P, V4L2PixelFormat(V4L2_PIX_FMT_SGBRG10P) } },
-	{ { BayerFormat::GRBG, 10, BayerFormat::Packing::CSI2 },
+	{ { ColorFilterArray::GRBG, 10, BayerFormat::Packing::CSI2 },
 		{ formats::SGRBG10_CSI2P, V4L2PixelFormat(V4L2_PIX_FMT_SGRBG10P) } },
-	{ { BayerFormat::RGGB, 10, BayerFormat::Packing::CSI2 },
+	{ { ColorFilterArray::RGGB, 10, BayerFormat::Packing::CSI2 },
 		{ formats::SRGGB10_CSI2P, V4L2PixelFormat(V4L2_PIX_FMT_SRGGB10P) } },
-	{ { BayerFormat::BGGR, 10, BayerFormat::Packing::IPU3 },
+	{ { ColorFilterArray::BGGR, 10, BayerFormat::Packing::IPU3 },
 		{ formats::SBGGR10_IPU3, V4L2PixelFormat(V4L2_PIX_FMT_IPU3_SBGGR10) } },
-	{ { BayerFormat::GBRG, 10, BayerFormat::Packing::IPU3 },
+	{ { ColorFilterArray::GBRG, 10, BayerFormat::Packing::IPU3 },
 		{ formats::SGBRG10_IPU3, V4L2PixelFormat(V4L2_PIX_FMT_IPU3_SGBRG10) } },
-	{ { BayerFormat::GRBG, 10, BayerFormat::Packing::IPU3 },
+	{ { ColorFilterArray::GRBG, 10, BayerFormat::Packing::IPU3 },
 		{ formats::SGRBG10_IPU3, V4L2PixelFormat(V4L2_PIX_FMT_IPU3_SGRBG10) } },
-	{ { BayerFormat::RGGB, 10, BayerFormat::Packing::IPU3 },
+	{ { ColorFilterArray::RGGB, 10, BayerFormat::Packing::IPU3 },
 		{ formats::SRGGB10_IPU3, V4L2PixelFormat(V4L2_PIX_FMT_IPU3_SRGGB10) } },
-	{ { BayerFormat::BGGR, 12, BayerFormat::Packing::None },
+	{ { ColorFilterArray::BGGR, 12, BayerFormat::Packing::None },
 		{ formats::SBGGR12, V4L2PixelFormat(V4L2_PIX_FMT_SBGGR12) } },
-	{ { BayerFormat::GBRG, 12, BayerFormat::Packing::None },
+	{ { ColorFilterArray::GBRG, 12, BayerFormat::Packing::None },
 		{ formats::SGBRG12, V4L2PixelFormat(V4L2_PIX_FMT_SGBRG12) } },
-	{ { BayerFormat::GRBG, 12, BayerFormat::Packing::None },
+	{ { ColorFilterArray::GRBG, 12, BayerFormat::Packing::None },
 		{ formats::SGRBG12, V4L2PixelFormat(V4L2_PIX_FMT_SGRBG12) } },
-	{ { BayerFormat::RGGB, 12, BayerFormat::Packing::None },
+	{ { ColorFilterArray::RGGB, 12, BayerFormat::Packing::None },
 		{ formats::SRGGB12, V4L2PixelFormat(V4L2_PIX_FMT_SRGGB12) } },
-	{ { BayerFormat::BGGR, 12, BayerFormat::Packing::CSI2 },
+	{ { ColorFilterArray::BGGR, 12, BayerFormat::Packing::CSI2 },
 		{ formats::SBGGR12_CSI2P, V4L2PixelFormat(V4L2_PIX_FMT_SBGGR12P) } },
-	{ { BayerFormat::GBRG, 12, BayerFormat::Packing::CSI2 },
+	{ { ColorFilterArray::GBRG, 12, BayerFormat::Packing::CSI2 },
 		{ formats::SGBRG12_CSI2P, V4L2PixelFormat(V4L2_PIX_FMT_SGBRG12P) } },
-	{ { BayerFormat::GRBG, 12, BayerFormat::Packing::CSI2 },
+	{ { ColorFilterArray::GRBG, 12, BayerFormat::Packing::CSI2 },
 		{ formats::SGRBG12_CSI2P, V4L2PixelFormat(V4L2_PIX_FMT_SGRBG12P) } },
-	{ { BayerFormat::RGGB, 12, BayerFormat::Packing::CSI2 },
+	{ { ColorFilterArray::RGGB, 12, BayerFormat::Packing::CSI2 },
 		{ formats::SRGGB12_CSI2P, V4L2PixelFormat(V4L2_PIX_FMT_SRGGB12P) } },
-	{ { BayerFormat::BGGR, 14, BayerFormat::Packing::None },
+	{ { ColorFilterArray::BGGR, 14, BayerFormat::Packing::None },
 		{ formats::SBGGR14, V4L2PixelFormat(V4L2_PIX_FMT_SBGGR14) } },
-	{ { BayerFormat::GBRG, 14, BayerFormat::Packing::None },
+	{ { ColorFilterArray::GBRG, 14, BayerFormat::Packing::None },
 		{ formats::SGBRG14, V4L2PixelFormat(V4L2_PIX_FMT_SGBRG14) } },
-	{ { BayerFormat::GRBG, 14, BayerFormat::Packing::None },
+	{ { ColorFilterArray::GRBG, 14, BayerFormat::Packing::None },
 		{ formats::SGRBG14, V4L2PixelFormat(V4L2_PIX_FMT_SGRBG14) } },
-	{ { BayerFormat::RGGB, 14, BayerFormat::Packing::None },
+	{ { ColorFilterArray::RGGB, 14, BayerFormat::Packing::None },
 		{ formats::SRGGB14, V4L2PixelFormat(V4L2_PIX_FMT_SRGGB14) } },
-	{ { BayerFormat::BGGR, 14, BayerFormat::Packing::CSI2 },
+	{ { ColorFilterArray::BGGR, 14, BayerFormat::Packing::CSI2 },
 		{ formats::SBGGR14_CSI2P, V4L2PixelFormat(V4L2_PIX_FMT_SBGGR14P) } },
-	{ { BayerFormat::GBRG, 14, BayerFormat::Packing::CSI2 },
+	{ { ColorFilterArray::GBRG, 14, BayerFormat::Packing::CSI2 },
 		{ formats::SGBRG14_CSI2P, V4L2PixelFormat(V4L2_PIX_FMT_SGBRG14P) } },
-	{ { BayerFormat::GRBG, 14, BayerFormat::Packing::CSI2 },
+	{ { ColorFilterArray::GRBG, 14, BayerFormat::Packing::CSI2 },
 		{ formats::SGRBG14_CSI2P, V4L2PixelFormat(V4L2_PIX_FMT_SGRBG14P) } },
-	{ { BayerFormat::RGGB, 14, BayerFormat::Packing::CSI2 },
+	{ { ColorFilterArray::RGGB, 14, BayerFormat::Packing::CSI2 },
 		{ formats::SRGGB14_CSI2P, V4L2PixelFormat(V4L2_PIX_FMT_SRGGB14P) } },
-	{ { BayerFormat::BGGR, 16, BayerFormat::Packing::None },
+	{ { ColorFilterArray::BGGR, 16, BayerFormat::Packing::None },
 		{ formats::SBGGR16, V4L2PixelFormat(V4L2_PIX_FMT_SBGGR16) } },
-	{ { BayerFormat::GBRG, 16, BayerFormat::Packing::None },
+	{ { ColorFilterArray::GBRG, 16, BayerFormat::Packing::None },
 		{ formats::SGBRG16, V4L2PixelFormat(V4L2_PIX_FMT_SGBRG16) } },
-	{ { BayerFormat::GRBG, 16, BayerFormat::Packing::None },
+	{ { ColorFilterArray::GRBG, 16, BayerFormat::Packing::None },
 		{ formats::SGRBG16, V4L2PixelFormat(V4L2_PIX_FMT_SGRBG16) } },
-	{ { BayerFormat::RGGB, 16, BayerFormat::Packing::None },
+	{ { ColorFilterArray::RGGB, 16, BayerFormat::Packing::None },
 		{ formats::SRGGB16, V4L2PixelFormat(V4L2_PIX_FMT_SRGGB16) } },
-	{ { BayerFormat::BGGR, 16, BayerFormat::Packing::PISP1 },
+	{ { ColorFilterArray::BGGR, 16, BayerFormat::Packing::PISP1 },
 		{ formats::BGGR_PISP_COMP1, V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_BGGR) } },
-	{ { BayerFormat::GBRG, 16, BayerFormat::Packing::PISP1 },
+	{ { ColorFilterArray::GBRG, 16, BayerFormat::Packing::PISP1 },
 		{ formats::GBRG_PISP_COMP1, V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_GBRG) } },
-	{ { BayerFormat::GRBG, 16, BayerFormat::Packing::PISP1 },
+	{ { ColorFilterArray::GRBG, 16, BayerFormat::Packing::PISP1 },
 		{ formats::GRBG_PISP_COMP1, V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_GRBG) } },
-	{ { BayerFormat::RGGB, 16, BayerFormat::Packing::PISP1 },
+	{ { ColorFilterArray::RGGB, 16, BayerFormat::Packing::PISP1 },
 		{ formats::RGGB_PISP_COMP1, V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_RGGB) } },
-	{ { BayerFormat::MONO, 8, BayerFormat::Packing::None },
+	{ { ColorFilterArray::MONO, 8, BayerFormat::Packing::None },
 		{ formats::R8, V4L2PixelFormat(V4L2_PIX_FMT_GREY) } },
-	{ { BayerFormat::MONO, 10, BayerFormat::Packing::None },
+	{ { ColorFilterArray::MONO, 10, BayerFormat::Packing::None },
 		{ formats::R10, V4L2PixelFormat(V4L2_PIX_FMT_Y10) } },
-	{ { BayerFormat::MONO, 10, BayerFormat::Packing::CSI2 },
+	{ { ColorFilterArray::MONO, 10, BayerFormat::Packing::CSI2 },
 		{ formats::R10_CSI2P, V4L2PixelFormat(V4L2_PIX_FMT_Y10P) } },
-	{ { BayerFormat::MONO, 12, BayerFormat::Packing::None },
+	{ { ColorFilterArray::MONO, 12, BayerFormat::Packing::None },
 		{ formats::R12, V4L2PixelFormat(V4L2_PIX_FMT_Y12) } },
-	{ { BayerFormat::MONO, 12, BayerFormat::Packing::CSI2 },
+	{ { ColorFilterArray::MONO, 12, BayerFormat::Packing::CSI2 },
 		{ formats::R12_CSI2P, V4L2PixelFormat(V4L2_PIX_FMT_Y12P) } },
-	{ { BayerFormat::MONO, 16, BayerFormat::Packing::None },
+	{ { ColorFilterArray::MONO, 16, BayerFormat::Packing::None },
 		{ formats::R16, V4L2PixelFormat(V4L2_PIX_FMT_Y16) } },
-	{ { BayerFormat::MONO, 16, BayerFormat::Packing::PISP1 },
+	{ { ColorFilterArray::MONO, 16, BayerFormat::Packing::PISP1 },
 		{ formats::MONO_PISP_COMP1, V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_MONO) } },
 };
 
 const std::unordered_map<unsigned int, BayerFormat> mbusCodeToBayer{
-	{ MEDIA_BUS_FMT_SBGGR8_1X8, { BayerFormat::BGGR, 8, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SGBRG8_1X8, { BayerFormat::GBRG, 8, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SGRBG8_1X8, { BayerFormat::GRBG, 8, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SRGGB8_1X8, { BayerFormat::RGGB, 8, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SBGGR10_ALAW8_1X8, { BayerFormat::BGGR, 8, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SGBRG10_ALAW8_1X8, { BayerFormat::GBRG, 8, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SGRBG10_ALAW8_1X8, { BayerFormat::GRBG, 8, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SRGGB10_ALAW8_1X8, { BayerFormat::RGGB, 8, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SBGGR10_DPCM8_1X8, { BayerFormat::BGGR, 8, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SGBRG10_DPCM8_1X8, { BayerFormat::GBRG, 8, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SGRBG10_DPCM8_1X8, { BayerFormat::GRBG, 8, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SRGGB10_DPCM8_1X8, { BayerFormat::RGGB, 8, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SBGGR10_2X8_PADHI_BE, { BayerFormat::BGGR, 10, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SBGGR10_2X8_PADHI_LE, { BayerFormat::BGGR, 10, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SBGGR10_2X8_PADLO_BE, { BayerFormat::BGGR, 10, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SBGGR10_2X8_PADLO_LE, { BayerFormat::BGGR, 10, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SBGGR10_1X10, { BayerFormat::BGGR, 10, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SGBRG10_1X10, { BayerFormat::GBRG, 10, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SGRBG10_1X10, { BayerFormat::GRBG, 10, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SRGGB10_1X10, { BayerFormat::RGGB, 10, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SBGGR12_1X12, { BayerFormat::BGGR, 12, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SGBRG12_1X12, { BayerFormat::GBRG, 12, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SGRBG12_1X12, { BayerFormat::GRBG, 12, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SRGGB12_1X12, { BayerFormat::RGGB, 12, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SBGGR14_1X14, { BayerFormat::BGGR, 14, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SGBRG14_1X14, { BayerFormat::GBRG, 14, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SGRBG14_1X14, { BayerFormat::GRBG, 14, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SRGGB14_1X14, { BayerFormat::RGGB, 14, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SBGGR16_1X16, { BayerFormat::BGGR, 16, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SGBRG16_1X16, { BayerFormat::GBRG, 16, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SGRBG16_1X16, { BayerFormat::GRBG, 16, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SRGGB16_1X16, { BayerFormat::RGGB, 16, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SBGGR20_1X20, { BayerFormat::BGGR, 20, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SGBRG20_1X20, { BayerFormat::GBRG, 20, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SGRBG20_1X20, { BayerFormat::GRBG, 20, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_SRGGB20_1X20, { BayerFormat::RGGB, 20, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_Y8_1X8, { BayerFormat::MONO, 8, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_Y10_1X10, { BayerFormat::MONO, 10, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_Y12_1X12, { BayerFormat::MONO, 12, BayerFormat::Packing::None } },
-	{ MEDIA_BUS_FMT_Y16_1X16, { BayerFormat::MONO, 16, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SBGGR8_1X8, { ColorFilterArray::BGGR, 8, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SGBRG8_1X8, { ColorFilterArray::GBRG, 8, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SGRBG8_1X8, { ColorFilterArray::GRBG, 8, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SRGGB8_1X8, { ColorFilterArray::RGGB, 8, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SBGGR10_ALAW8_1X8, { ColorFilterArray::BGGR, 8, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SGBRG10_ALAW8_1X8, { ColorFilterArray::GBRG, 8, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SGRBG10_ALAW8_1X8, { ColorFilterArray::GRBG, 8, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SRGGB10_ALAW8_1X8, { ColorFilterArray::RGGB, 8, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SBGGR10_DPCM8_1X8, { ColorFilterArray::BGGR, 8, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SGBRG10_DPCM8_1X8, { ColorFilterArray::GBRG, 8, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SGRBG10_DPCM8_1X8, { ColorFilterArray::GRBG, 8, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SRGGB10_DPCM8_1X8, { ColorFilterArray::RGGB, 8, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SBGGR10_2X8_PADHI_BE, { ColorFilterArray::BGGR, 10, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SBGGR10_2X8_PADHI_LE, { ColorFilterArray::BGGR, 10, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SBGGR10_2X8_PADLO_BE, { ColorFilterArray::BGGR, 10, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SBGGR10_2X8_PADLO_LE, { ColorFilterArray::BGGR, 10, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SBGGR10_1X10, { ColorFilterArray::BGGR, 10, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SGBRG10_1X10, { ColorFilterArray::GBRG, 10, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SGRBG10_1X10, { ColorFilterArray::GRBG, 10, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SRGGB10_1X10, { ColorFilterArray::RGGB, 10, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SBGGR12_1X12, { ColorFilterArray::BGGR, 12, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SGBRG12_1X12, { ColorFilterArray::GBRG, 12, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SGRBG12_1X12, { ColorFilterArray::GRBG, 12, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SRGGB12_1X12, { ColorFilterArray::RGGB, 12, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SBGGR14_1X14, { ColorFilterArray::BGGR, 14, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SGBRG14_1X14, { ColorFilterArray::GBRG, 14, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SGRBG14_1X14, { ColorFilterArray::GRBG, 14, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SRGGB14_1X14, { ColorFilterArray::RGGB, 14, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SBGGR16_1X16, { ColorFilterArray::BGGR, 16, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SGBRG16_1X16, { ColorFilterArray::GBRG, 16, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SGRBG16_1X16, { ColorFilterArray::GRBG, 16, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SRGGB16_1X16, { ColorFilterArray::RGGB, 16, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SBGGR20_1X20, { ColorFilterArray::BGGR, 20, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SGBRG20_1X20, { ColorFilterArray::GBRG, 20, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SGRBG20_1X20, { ColorFilterArray::GRBG, 20, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_SRGGB20_1X20, { ColorFilterArray::RGGB, 20, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_Y8_1X8, { ColorFilterArray::MONO, 8, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_Y10_1X10, { ColorFilterArray::MONO, 10, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_Y12_1X12, { ColorFilterArray::MONO, 12, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_Y16_1X16, { ColorFilterArray::MONO, 16, BayerFormat::Packing::None } },
 };
 
 } /* namespace */
@@ -243,9 +226,9 @@ const std::unordered_map<unsigned int, BayerFormat> mbusCodeToBayer{
  */
 
 /**
- * \fn BayerFormat::BayerFormat(Order o, uint8_t b, Packing p)
+ * \fn BayerFormat::BayerFormat(ColorFilterArray c, uint8_t b, Packing p)
  * \brief Construct a BayerFormat from explicit values
- * \param[in] o The order of the Bayer pattern
+ * \param[in] c The color filter array pattern
  * \param[in] b The bit depth of the Bayer samples
  * \param[in] p The type of packing applied to the pixel values
  */
@@ -291,7 +274,7 @@ std::string BayerFormat::toString() const
  */
 bool operator==(const BayerFormat &lhs, const BayerFormat &rhs)
 {
-	return lhs.order == rhs.order && lhs.bitDepth == rhs.bitDepth &&
+	return lhs.cfa.order == rhs.cfa.order && lhs.bitDepth == rhs.bitDepth &&
 	       lhs.packing == rhs.packing;
 }
 
@@ -303,21 +286,13 @@ bool operator==(const BayerFormat &lhs, const BayerFormat &rhs)
  */
 std::ostream &operator<<(std::ostream &out, const BayerFormat &f)
 {
-	static const char *orderStrings[] = {
-		"BGGR-",
-		"GBRG-",
-		"GRBG-",
-		"RGGB-",
-		"MONO-"
-	};
-
-	if (!f.isValid() || f.order > BayerFormat::MONO) {
+	if (!f.isValid() || f.cfa.order > ColorFilterArray::MONO) {
 		out << "INVALID";
 		return out;
 	}
 
 	/* The cast is required to avoid bitDepth being interpreted as a char. */
-	out << orderStrings[f.order] << static_cast<unsigned int>(f.bitDepth);
+	out << f.cfa << "-" << static_cast<unsigned int>(f.bitDepth);
 
 	if (f.packing == BayerFormat::Packing::CSI2)
 		out << "-CSI2P";
@@ -398,53 +373,8 @@ BayerFormat BayerFormat::fromPixelFormat(PixelFormat format)
 }
 
 /**
- * \brief Apply a transform to this BayerFormat
- * \param[in] t The transform to apply
- *
- * Applying a transform to an image stored in a Bayer format affects the Bayer
- * order. For example, performing a horizontal flip on the Bayer pattern RGGB
- * causes the RG rows of pixels to become GR, and the GB rows to become BG. The
- * transformed image would have a GRBG order. Performing a vertical flip on the
- * Bayer pattern RGGB causes the GB rows to come before the RG ones and the
- * transformed image would have GBRG order. Applying both vertical and
- * horizontal flips on the Bayer patter RGGB results in transformed images with
- * BGGR order. The bit depth and modifiers are not affected.
- *
- * Horizontal and vertical flips are applied before transpose.
- *
- * \return The transformed Bayer format
- */
-BayerFormat BayerFormat::transform(Transform t) const
-{
-	BayerFormat result = *this;
-
-	if (order == MONO)
-		return result;
-
-	/*
-	 * Observe that flipping bit 0 of the Order enum performs a horizontal
-	 * mirror on the Bayer pattern (e.g. RG/GB goes to GR/BG). Similarly,
-	 * flipping bit 1 performs a vertical mirror operation on it (e.g RG/GB
-	 * goes to GB/RG). Applying both vertical and horizontal flips
-	 * combines vertical and horizontal mirroring on the Bayer pattern
-	 * (e.g. RG/GB goes to BG/GR). Hence:
-	 */
-	if (!!(t & Transform::HFlip))
-		result.order = static_cast<Order>(result.order ^ 1);
-	if (!!(t & Transform::VFlip))
-		result.order = static_cast<Order>(result.order ^ 2);
-
-	if (!!(t & Transform::Transpose) && result.order == 1)
-		result.order = static_cast<Order>(2);
-	else if (!!(t & Transform::Transpose) && result.order == 2)
-		result.order = static_cast<Order>(1);
-
-	return result;
-}
-
-/**
- * \var BayerFormat::order
- * \brief The order of the colour channels in the Bayer pattern
+ * \var BayerFormat::cfa
+ * \brief The color filter array pattern
  */
 
 /**
diff --git a/src/libcamera/color_filter_array.cpp b/src/libcamera/color_filter_array.cpp
new file mode 100644
index 000000000000..7cec14d2dce4
--- /dev/null
+++ b/src/libcamera/color_filter_array.cpp
@@ -0,0 +1,334 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, Ideas on Board Oy
+ *
+ * Class to represent Bayer Color Filter Array patterns
+ */
+
+#include <libcamera/color_filter_array.h>
+
+#include <optional>
+#include <map>
+#include <sstream>
+#include <unordered_map>
+
+#include <linux/media-bus-format.h>
+
+#include <libcamera/transform.h>
+#include "libcamera/internal/v4l2_pixelformat.h"
+
+/**
+ * \file color_filter_array.cpp
+ * \brief Class to represent and manipulate patterns of the color filter array
+ */
+
+namespace libcamera {
+
+/**
+ * \class ColorFilterArray
+ * \brief Class to represent a color filter array
+ *
+ * This class encodes the different patterns of the color filter array in such
+ * a way that they can be easily manipulated. For example the order can be
+ * "transformed" in the same manner as happens in many sensors when their
+ * horizontal or vertical "flip" controls are set.
+ */
+
+/**
+ * \enum ColorFilterArray::Order
+ * \brief Different ordering of color channels in a filter array
+ *
+ * \var ColorFilterArray::BGGR
+ * \brief B then G on the first row, G then R on the second row.
+ * \var ColorFilterArray::GBRG
+ * \brief G then B on the first row, R then G on the second row.
+ * \var ColorFilterArray::GRBG
+ * \brief G then R on the first row, B then G on the second row.
+ * \var ColorFilterArray::RGGB
+ * \brief R then G on the first row, G then B on the second row.
+ * \var ColorFilterArray::MONO
+ * \brief Monochrome image data, there is no color filter array.
+ */
+
+namespace {
+
+const std::unordered_map<unsigned int, ColorFilterArray> mbusCodeToCFA {
+	{ MEDIA_BUS_FMT_SBGGR8_1X8, ColorFilterArray::BGGR },
+	{ MEDIA_BUS_FMT_SGBRG8_1X8, ColorFilterArray::GBRG },
+	{ MEDIA_BUS_FMT_SGRBG8_1X8, ColorFilterArray::GRBG },
+	{ MEDIA_BUS_FMT_SRGGB8_1X8, ColorFilterArray::RGGB },
+	{ MEDIA_BUS_FMT_SBGGR10_ALAW8_1X8, ColorFilterArray::BGGR },
+	{ MEDIA_BUS_FMT_SGBRG10_ALAW8_1X8, ColorFilterArray::GBRG },
+	{ MEDIA_BUS_FMT_SGRBG10_ALAW8_1X8, ColorFilterArray::GRBG },
+	{ MEDIA_BUS_FMT_SRGGB10_ALAW8_1X8, ColorFilterArray::RGGB },
+	{ MEDIA_BUS_FMT_SBGGR10_DPCM8_1X8, ColorFilterArray::BGGR },
+	{ MEDIA_BUS_FMT_SGBRG10_DPCM8_1X8, ColorFilterArray::GBRG },
+	{ MEDIA_BUS_FMT_SGRBG10_DPCM8_1X8, ColorFilterArray::GRBG },
+	{ MEDIA_BUS_FMT_SRGGB10_DPCM8_1X8, ColorFilterArray::RGGB },
+	{ MEDIA_BUS_FMT_SBGGR10_2X8_PADHI_BE, ColorFilterArray::BGGR },
+	{ MEDIA_BUS_FMT_SBGGR10_2X8_PADHI_LE, ColorFilterArray::BGGR },
+	{ MEDIA_BUS_FMT_SBGGR10_2X8_PADLO_BE, ColorFilterArray::BGGR },
+	{ MEDIA_BUS_FMT_SBGGR10_2X8_PADLO_LE, ColorFilterArray::BGGR },
+	{ MEDIA_BUS_FMT_SBGGR10_1X10, ColorFilterArray::BGGR },
+	{ MEDIA_BUS_FMT_SGBRG10_1X10, ColorFilterArray::GBRG },
+	{ MEDIA_BUS_FMT_SGRBG10_1X10, ColorFilterArray::GRBG },
+	{ MEDIA_BUS_FMT_SRGGB10_1X10, ColorFilterArray::RGGB },
+	{ MEDIA_BUS_FMT_SBGGR12_1X12, ColorFilterArray::BGGR },
+	{ MEDIA_BUS_FMT_SGBRG12_1X12, ColorFilterArray::GBRG },
+	{ MEDIA_BUS_FMT_SGRBG12_1X12, ColorFilterArray::GRBG },
+	{ MEDIA_BUS_FMT_SRGGB12_1X12, ColorFilterArray::RGGB },
+	{ MEDIA_BUS_FMT_SBGGR14_1X14, ColorFilterArray::BGGR },
+	{ MEDIA_BUS_FMT_SGBRG14_1X14, ColorFilterArray::GBRG },
+	{ MEDIA_BUS_FMT_SGRBG14_1X14, ColorFilterArray::GRBG },
+	{ MEDIA_BUS_FMT_SRGGB14_1X14, ColorFilterArray::RGGB },
+	{ MEDIA_BUS_FMT_SBGGR16_1X16, ColorFilterArray::BGGR },
+	{ MEDIA_BUS_FMT_SGBRG16_1X16, ColorFilterArray::GBRG },
+	{ MEDIA_BUS_FMT_SGRBG16_1X16, ColorFilterArray::GRBG },
+	{ MEDIA_BUS_FMT_SRGGB16_1X16, ColorFilterArray::RGGB },
+	{ MEDIA_BUS_FMT_SBGGR20_1X20, ColorFilterArray::BGGR },
+	{ MEDIA_BUS_FMT_SGBRG20_1X20, ColorFilterArray::GBRG },
+	{ MEDIA_BUS_FMT_SGRBG20_1X20, ColorFilterArray::GRBG },
+	{ MEDIA_BUS_FMT_SRGGB20_1X20, ColorFilterArray::RGGB },
+	{ MEDIA_BUS_FMT_Y8_1X8, ColorFilterArray::MONO },
+	{ MEDIA_BUS_FMT_Y10_1X10, ColorFilterArray::MONO },
+	{ MEDIA_BUS_FMT_Y12_1X12, ColorFilterArray::MONO },
+	{ MEDIA_BUS_FMT_Y16_1X16, ColorFilterArray::MONO },
+};
+
+struct CfaMbusEntry {
+	ColorFilterArray cfa;
+	uint8_t bitDepth;
+
+	bool operator<(const CfaMbusEntry& o) const {
+		return std::tie(cfa.order, bitDepth) < std::tie(o.cfa.order, o.bitDepth);
+	}
+};
+
+const std::map<CfaMbusEntry, unsigned int> cfaBitDepthToMbusCode{
+	{ { ColorFilterArray::BGGR, 8 }, MEDIA_BUS_FMT_SBGGR8_1X8 },
+	{ { ColorFilterArray::BGGR, 10 }, MEDIA_BUS_FMT_SBGGR10_1X10 },
+	{ { ColorFilterArray::BGGR, 12 }, MEDIA_BUS_FMT_SBGGR12_1X12 },
+	{ { ColorFilterArray::BGGR, 14 }, MEDIA_BUS_FMT_SBGGR14_1X14 },
+	{ { ColorFilterArray::BGGR, 16 }, MEDIA_BUS_FMT_SBGGR16_1X16 },
+	{ { ColorFilterArray::BGGR, 20 }, MEDIA_BUS_FMT_SBGGR20_1X20 },
+	{ { ColorFilterArray::GBRG, 8 }, MEDIA_BUS_FMT_SGBRG8_1X8 },
+	{ { ColorFilterArray::GBRG, 10 }, MEDIA_BUS_FMT_SGBRG10_1X10 },
+	{ { ColorFilterArray::GBRG, 12 }, MEDIA_BUS_FMT_SGBRG12_1X12 },
+	{ { ColorFilterArray::GBRG, 14 }, MEDIA_BUS_FMT_SGBRG14_1X14 },
+	{ { ColorFilterArray::GBRG, 16 }, MEDIA_BUS_FMT_SGBRG16_1X16 },
+	{ { ColorFilterArray::GBRG, 20 }, MEDIA_BUS_FMT_SGBRG20_1X20 },
+	{ { ColorFilterArray::GRBG, 8 }, MEDIA_BUS_FMT_SGRBG8_1X8 },
+	{ { ColorFilterArray::GRBG, 10 }, MEDIA_BUS_FMT_SGRBG10_1X10 },
+	{ { ColorFilterArray::GRBG, 12 }, MEDIA_BUS_FMT_SGRBG12_1X12 },
+	{ { ColorFilterArray::GRBG, 14 }, MEDIA_BUS_FMT_SGRBG14_1X14 },
+	{ { ColorFilterArray::GRBG, 16 }, MEDIA_BUS_FMT_SGRBG16_1X16 },
+	{ { ColorFilterArray::GRBG, 20 }, MEDIA_BUS_FMT_SGRBG20_1X20 },
+	{ { ColorFilterArray::RGGB, 8 }, MEDIA_BUS_FMT_SRGGB8_1X8 },
+	{ { ColorFilterArray::RGGB, 10 }, MEDIA_BUS_FMT_SRGGB10_1X10 },
+	{ { ColorFilterArray::RGGB, 12 }, MEDIA_BUS_FMT_SRGGB12_1X12 },
+	{ { ColorFilterArray::RGGB, 14 }, MEDIA_BUS_FMT_SRGGB14_1X14 },
+	{ { ColorFilterArray::RGGB, 16 }, MEDIA_BUS_FMT_SRGGB16_1X16 },
+	{ { ColorFilterArray::RGGB, 20 }, MEDIA_BUS_FMT_SRGGB20_1X20 },
+	{ { ColorFilterArray::MONO, 8 }, MEDIA_BUS_FMT_Y8_1X8 },
+	{ { ColorFilterArray::MONO, 10 }, MEDIA_BUS_FMT_Y10_1X10 },
+	{ { ColorFilterArray::MONO, 12 }, MEDIA_BUS_FMT_Y12_1X12 },
+	{ { ColorFilterArray::MONO, 16 }, MEDIA_BUS_FMT_Y16_1X16 },
+};
+
+const std::unordered_map<V4L2PixelFormat, ColorFilterArray> v4l2FormatToCFA {
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SBGGR8), ColorFilterArray::BGGR },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SGBRG8), ColorFilterArray::GBRG },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SGRBG8), ColorFilterArray::GRBG },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SRGGB8), ColorFilterArray::RGGB },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SBGGR10), ColorFilterArray::BGGR },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SGBRG10), ColorFilterArray::GBRG },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SGRBG10), ColorFilterArray::GRBG },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SRGGB10), ColorFilterArray::RGGB },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SBGGR10P), ColorFilterArray::BGGR },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SGBRG10P), ColorFilterArray::GBRG },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SGRBG10P), ColorFilterArray::GRBG },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SRGGB10P), ColorFilterArray::RGGB },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SBGGR12), ColorFilterArray::BGGR },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SGBRG12), ColorFilterArray::GBRG },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SGRBG12), ColorFilterArray::GRBG },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SRGGB12), ColorFilterArray::RGGB },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SBGGR12P), ColorFilterArray::BGGR },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SGBRG12P), ColorFilterArray::GBRG },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SGRBG12P), ColorFilterArray::GRBG },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SRGGB12P), ColorFilterArray::RGGB },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SBGGR14), ColorFilterArray::BGGR },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SGBRG14), ColorFilterArray::GBRG },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SGRBG14), ColorFilterArray::GRBG },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SRGGB14), ColorFilterArray::RGGB },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SBGGR14P), ColorFilterArray::BGGR },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SGBRG14P), ColorFilterArray::GBRG },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SGRBG14P), ColorFilterArray::GRBG },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SRGGB14P), ColorFilterArray::RGGB },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SBGGR16), ColorFilterArray::BGGR },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SGBRG16), ColorFilterArray::GBRG },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SGRBG16), ColorFilterArray::GRBG },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_SRGGB16), ColorFilterArray::RGGB },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_BGGR), ColorFilterArray::BGGR },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_GBRG), ColorFilterArray::GBRG },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_GRBG), ColorFilterArray::GRBG },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_RGGB), ColorFilterArray::RGGB },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_MONO), ColorFilterArray::MONO },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_GREY), ColorFilterArray::MONO },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_Y10), ColorFilterArray::MONO },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_Y10P), ColorFilterArray::MONO },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_Y12), ColorFilterArray::MONO },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_Y12P), ColorFilterArray::MONO },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_Y16), ColorFilterArray::MONO },
+};
+
+} /* namespace */
+
+/**
+ * \fn ColorFilterArray::ColorFilterArray()
+ * \brief Construct an empty (and invalid) ColorFilterArray
+ */
+
+/**
+ * \fn ColorFilterArray::ColorFilterArray(Order o)
+ * \brief Construct a ColorFilterArray from an explicit order value
+ * \param[in] o The order of color channels in the filter array
+ */
+
+/**
+ * \brief Retrieve the ColorFilterArray associated with a media bus code
+ * \param[in] mbusCode The media bus code to convert into a ColorFilterArray
+ *
+ * The media bus code numeric identifiers are defined by the V4L2 specification.
+ */
+std::optional<ColorFilterArray> ColorFilterArray::fromMbusCode(unsigned int mbusCode)
+{
+	const auto it = mbusCodeToCFA.find(mbusCode);
+
+	if (it == mbusCodeToCFA.end())
+		return std::nullopt;
+	else
+		return it->second;
+}
+
+/**
+ * \brief Retrieve the ColorFilterArray associated with a V4L2PixelFormat
+ * \param[in] v4l2Format The raw format to convert into a ColorFilterArray
+ *
+ * The media bus code numeric identifiers are defined by the V4L2 specification.
+ */
+std::optional<ColorFilterArray> ColorFilterArray::fromV4L2PixelFormat(V4L2PixelFormat v4l2Format)
+{
+	const auto it = v4l2FormatToCFA.find(v4l2Format);
+
+	if (it == v4l2FormatToCFA.end())
+		return std::nullopt;
+	else
+		return it->second;
+}
+
+/**
+ * \brief Retrieve the media bus code for this ColorFilterArray and bit depth
+ * \param[in] bitDepth The bit depth of the samples transmitted over the bus
+ * \return The media bus code, or 0 if no code exists for the combination
+ *
+ * The returned code is the order-specific sensor bus code (e.g.
+ * MEDIA_BUS_FMT_SBGGR10_1X10).
+ */
+unsigned int ColorFilterArray::toMbusCode(uint8_t bitDepth) const
+{
+	const auto it = cfaBitDepthToMbusCode.find({*this, bitDepth});
+
+	if (it == cfaBitDepthToMbusCode.end())
+		return 0;
+	else
+		return it->second;
+}
+
+/**
+ * \brief Assemble and return a readable string representation of the
+ * ColorFilterArray
+ * \return A string describing the ColorFilterArray
+ */
+std::string ColorFilterArray::toString() const
+{
+	std::stringstream ss;
+	ss << *this;
+
+	return ss.str();
+}
+
+/**
+ * \brief Insert a text representation of a ColorFilterArrays into an output stream
+ * \param[in] out The output stream
+ * \param[in] f The ColorFilterArray
+ * \return The output stream \a out
+ */
+std::ostream &operator<<(std::ostream &out, const ColorFilterArray &f)
+{
+	switch (f.order) {
+	case ColorFilterArray::BGGR:
+		return out << "BGGR";
+	case ColorFilterArray::GBRG:
+		return out << "GBRG";
+	case ColorFilterArray::GRBG:
+		return out << "GRBG";
+	case ColorFilterArray::RGGB:
+		return out << "RGGB";
+	case ColorFilterArray::MONO:
+		return out << "MONO";
+	default:
+		return out << "INVALID";
+	}
+}
+
+/**
+ * \brief Apply a transform to this ColorFilterArray
+ * \param[in] t The transform to apply
+ *
+ * Applying a transform to an image captured via a ColorFilterArray affects the
+ * color order. For example, performing a horizontal flip on the pattern 2x2
+ * RGGB causes the RG rows of pixels to become GR, and the GB rows to become
+ * BG. The transformed image would have a GRBG order. Performing a vertical
+ * flip on the 2x2 pattern RGGB causes the GB rows to come before the RG ones
+ * and the transformed image would have GBRG order. Applying both vertical and
+ * horizontal flips on the 2x2 pattern RGGB results in transformed images with
+ * BGGR order.
+ *
+ * Horizontal and vertical flips are applied before transpose.
+ *
+ * \return The transformed pattern as a ColorFilterArray
+ */
+ColorFilterArray ColorFilterArray::transform(Transform t) const
+{
+	ColorFilterArray result = *this;
+
+	if (order == MONO)
+		return result;
+
+	/*
+	 * Observe that flipping bit 0 of the Order enum performs a horizontal
+	 * mirror on the Bayer pattern (e.g. RG/GB goes to GR/BG). Similarly,
+	 * flipping bit 1 performs a vertical mirror operation on it (e.g RG/GB
+	 * goes to GB/RG). Applying both vertical and horizontal flips
+	 * combines vertical and horizontal mirroring on the Bayer pattern
+	 * (e.g. RG/GB goes to BG/GR). Hence:
+	 */
+	if (!!(t & Transform::HFlip))
+		result.order = static_cast<Order>(result.order ^ 1);
+	if (!!(t & Transform::VFlip))
+		result.order = static_cast<Order>(result.order ^ 2);
+
+	if (!!(t & Transform::Transpose) && result.order == 1)
+		result.order = static_cast<Order>(2);
+	else if (!!(t & Transform::Transpose) && result.order == 2)
+		result.order = static_cast<Order>(1);
+
+	return result;
+}
+
+/**
+ * \var ColorFilterArray::order
+ * \brief The order of the color channels in the filter array
+ */
+
+} /* namespace libcamera */
diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
index 17c1b2cb3479..056f30252eaf 100644
--- a/src/libcamera/meson.build
+++ b/src/libcamera/meson.build
@@ -22,6 +22,7 @@ libcamera_internal_sources = files([
     'camera_controls.cpp',
     'camera_lens.cpp',
     'clock_recovery.cpp',
+    'color_filter_array.cpp',
     'control_serializer.cpp',
     'control_validator.cpp',
     'converter.cpp',
diff --git a/src/libcamera/pipeline/mali-c55/mali-c55.cpp b/src/libcamera/pipeline/mali-c55/mali-c55.cpp
index 73a03373c833..af518d0b2c28 100644
--- a/src/libcamera/pipeline/mali-c55/mali-c55.cpp
+++ b/src/libcamera/pipeline/mali-c55/mali-c55.cpp
@@ -22,6 +22,7 @@
 #include <libcamera/base/utils.h>
 
 #include <libcamera/camera.h>
+#include <libcamera/color_filter_array.h>
 #include <libcamera/controls.h>
 #include <libcamera/formats.h>
 #include <libcamera/geometry.h>
@@ -328,7 +329,7 @@ int MaliC55CameraData::pixfmtToMbusCode(const PixelFormat &pixFmt) const
 		if (!sdBayerFormat.isValid())
 			continue;
 
-		if (sdBayerFormat.order != bayerFormat.order)
+		if (sdBayerFormat.cfa.order != bayerFormat.cfa.order)
 			continue;
 
 		if (sdBayerFormat.bitDepth > bitDepth) {
@@ -368,7 +369,7 @@ const PixelFormat &MaliC55CameraData::bestRawFormat() const
 				continue;
 
 			BayerFormat bayer = BayerFormat::fromMbusCode(rawCode);
-			if (bayer.order == sensorBayer.order)
+			if (bayer.cfa.order == sensorBayer.cfa.order)
 				return pixFmt;
 		}
 	}
@@ -1175,14 +1176,14 @@ int PipelineHandlerMaliC55::configure(Camera *camera,
 	ipaConfig.sensorControls = data->sensor()->controls();
 
 	/*
-	 * And we also need to tell the IPA the bayerOrder of the data (as
+	 * And we also need to tell the IPA the cfaOrder of the data (as
 	 * affected by any flips that we've configured)
 	 */
 	const Transform &combinedTransform = maliConfig->combinedTransform();
-	BayerFormat::Order bayerOrder = data->sensor()->bayerOrder(combinedTransform);
+	ColorFilterArray::Order cfaOrder = data->sensor()->bayerOrder(combinedTransform);
 
 	ControlInfoMap ipaControls;
-	ret = data->ipa_->configure(ipaConfig, utils::to_underlying(bayerOrder),
+	ret = data->ipa_->configure(ipaConfig, utils::to_underlying(cfaOrder),
 				    &ipaControls);
 	if (ret) {
 		LOG(MaliC55, Error) << "Failed to configure IPA";
diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
index d6ce2dbf5cee..7ea48f62cc3d 100644
--- a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
+++ b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
@@ -16,6 +16,7 @@
 #include <libcamera/base/file.h>
 #include <libcamera/base/utils.h>
 
+#include <libcamera/color_filter_array.h>
 #include <libcamera/formats.h>
 #include <libcamera/logging.h>
 #include <libcamera/property_ids.h>
@@ -75,7 +76,7 @@ bool isMonoSensor(std::unique_ptr<CameraSensor> &sensor)
 	unsigned int mbusCode = sensor->mbusCodes()[0];
 	const BayerFormat &bayer = BayerFormat::fromMbusCode(mbusCode);
 
-	return bayer.order == BayerFormat::Order::MONO;
+	return bayer.cfa.order == ColorFilterArray::Order::MONO;
 }
 
 const std::vector<ColorSpace> validColorSpaces = {
@@ -256,7 +257,7 @@ CameraConfiguration::Status RPiCameraConfiguration::validate()
 		 */
 
 		BayerFormat cfgBayer = BayerFormat::fromPixelFormat(rawStream->pixelFormat);
-		cfgBayer.order = data_->sensor_->bayerOrder(combinedTransform_);
+		cfgBayer.cfa = data_->sensor_->bayerOrder(combinedTransform_);
 
 		if (rawStream->pixelFormat != cfgBayer.toPixelFormat()) {
 			rawStream->pixelFormat = cfgBayer.toPixelFormat();
diff --git a/src/libcamera/pipeline/rpi/pisp/pisp.cpp b/src/libcamera/pipeline/rpi/pisp/pisp.cpp
index d0103adbe17c..b4a5f87aa93d 100644
--- a/src/libcamera/pipeline/rpi/pisp/pisp.cpp
+++ b/src/libcamera/pipeline/rpi/pisp/pisp.cpp
@@ -56,32 +56,32 @@ constexpr unsigned int DefaultCompressionOffset = 2048;
 constexpr unsigned int DefaultCompressionMode = 1;
 
 const std::vector<std::pair<BayerFormat, unsigned int>> BayerToMbusCodeMap{
-	{ { BayerFormat::BGGR, 8, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SBGGR8_1X8, },
-	{ { BayerFormat::GBRG, 8, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SGBRG8_1X8, },
-	{ { BayerFormat::GRBG, 8, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SGRBG8_1X8, },
-	{ { BayerFormat::RGGB, 8, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SRGGB8_1X8, },
-	{ { BayerFormat::BGGR, 10, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SBGGR10_1X10, },
-	{ { BayerFormat::GBRG, 10, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SGBRG10_1X10, },
-	{ { BayerFormat::GRBG, 10, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SGRBG10_1X10, },
-	{ { BayerFormat::RGGB, 10, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SRGGB10_1X10, },
-	{ { BayerFormat::BGGR, 12, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SBGGR12_1X12, },
-	{ { BayerFormat::GBRG, 12, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SGBRG12_1X12, },
-	{ { BayerFormat::GRBG, 12, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SGRBG12_1X12, },
-	{ { BayerFormat::RGGB, 12, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SRGGB12_1X12, },
-	{ { BayerFormat::BGGR, 14, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SBGGR14_1X14, },
-	{ { BayerFormat::GBRG, 14, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SGBRG14_1X14, },
-	{ { BayerFormat::GRBG, 14, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SGRBG14_1X14, },
-	{ { BayerFormat::RGGB, 14, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SRGGB14_1X14, },
-	{ { BayerFormat::BGGR, 16, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SBGGR16_1X16, },
-	{ { BayerFormat::GBRG, 16, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SGBRG16_1X16, },
-	{ { BayerFormat::GRBG, 16, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SGRBG16_1X16, },
-	{ { BayerFormat::RGGB, 16, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SRGGB16_1X16, },
-	{ { BayerFormat::BGGR, 16, BayerFormat::Packing::PISP1 }, MEDIA_BUS_FMT_SBGGR16_1X16, },
-	{ { BayerFormat::GBRG, 16, BayerFormat::Packing::PISP1 }, MEDIA_BUS_FMT_SGBRG16_1X16, },
-	{ { BayerFormat::GRBG, 16, BayerFormat::Packing::PISP1 }, MEDIA_BUS_FMT_SGRBG16_1X16, },
-	{ { BayerFormat::RGGB, 16, BayerFormat::Packing::PISP1 }, MEDIA_BUS_FMT_SRGGB16_1X16, },
-	{ { BayerFormat::MONO, 16, BayerFormat::Packing::None }, MEDIA_BUS_FMT_Y16_1X16, },
-	{ { BayerFormat::MONO, 16, BayerFormat::Packing::PISP1 }, MEDIA_BUS_FMT_Y16_1X16, },
+	{ { ColorFilterArray::BGGR, 8, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SBGGR8_1X8, },
+	{ { ColorFilterArray::GBRG, 8, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SGBRG8_1X8, },
+	{ { ColorFilterArray::GRBG, 8, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SGRBG8_1X8, },
+	{ { ColorFilterArray::RGGB, 8, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SRGGB8_1X8, },
+	{ { ColorFilterArray::BGGR, 10, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SBGGR10_1X10, },
+	{ { ColorFilterArray::GBRG, 10, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SGBRG10_1X10, },
+	{ { ColorFilterArray::GRBG, 10, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SGRBG10_1X10, },
+	{ { ColorFilterArray::RGGB, 10, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SRGGB10_1X10, },
+	{ { ColorFilterArray::BGGR, 12, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SBGGR12_1X12, },
+	{ { ColorFilterArray::GBRG, 12, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SGBRG12_1X12, },
+	{ { ColorFilterArray::GRBG, 12, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SGRBG12_1X12, },
+	{ { ColorFilterArray::RGGB, 12, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SRGGB12_1X12, },
+	{ { ColorFilterArray::BGGR, 14, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SBGGR14_1X14, },
+	{ { ColorFilterArray::GBRG, 14, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SGBRG14_1X14, },
+	{ { ColorFilterArray::GRBG, 14, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SGRBG14_1X14, },
+	{ { ColorFilterArray::RGGB, 14, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SRGGB14_1X14, },
+	{ { ColorFilterArray::BGGR, 16, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SBGGR16_1X16, },
+	{ { ColorFilterArray::GBRG, 16, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SGBRG16_1X16, },
+	{ { ColorFilterArray::GRBG, 16, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SGRBG16_1X16, },
+	{ { ColorFilterArray::RGGB, 16, BayerFormat::Packing::None }, MEDIA_BUS_FMT_SRGGB16_1X16, },
+	{ { ColorFilterArray::BGGR, 16, BayerFormat::Packing::PISP1 }, MEDIA_BUS_FMT_SBGGR16_1X16, },
+	{ { ColorFilterArray::GBRG, 16, BayerFormat::Packing::PISP1 }, MEDIA_BUS_FMT_SGBRG16_1X16, },
+	{ { ColorFilterArray::GRBG, 16, BayerFormat::Packing::PISP1 }, MEDIA_BUS_FMT_SGRBG16_1X16, },
+	{ { ColorFilterArray::RGGB, 16, BayerFormat::Packing::PISP1 }, MEDIA_BUS_FMT_SRGGB16_1X16, },
+	{ { ColorFilterArray::MONO, 16, BayerFormat::Packing::None }, MEDIA_BUS_FMT_Y16_1X16, },
+	{ { ColorFilterArray::MONO, 16, BayerFormat::Packing::PISP1 }, MEDIA_BUS_FMT_Y16_1X16, },
 };
 
 unsigned int bayerToMbusCode(const BayerFormat &bayer)
@@ -100,7 +100,7 @@ unsigned int bayerToMbusCode(const BayerFormat &bayer)
 uint32_t mbusCodeUnpacked16(unsigned int code)
 {
 	BayerFormat bayer = BayerFormat::fromMbusCode(code);
-	BayerFormat bayer16(bayer.order, 16, BayerFormat::Packing::None);
+	BayerFormat bayer16(bayer.cfa.order, 16, BayerFormat::Packing::None);
 
 	return bayerToMbusCode(bayer16);
 }
@@ -109,16 +109,16 @@ uint8_t toPiSPBayerOrder(V4L2PixelFormat format)
 {
 	BayerFormat bayer = BayerFormat::fromV4L2PixelFormat(format);
 
-	switch (bayer.order) {
-	case BayerFormat::Order::BGGR:
+	switch (bayer.cfa.order) {
+	case ColorFilterArray::Order::BGGR:
 		return PISP_BAYER_ORDER_BGGR;
-	case BayerFormat::Order::GBRG:
+	case ColorFilterArray::Order::GBRG:
 		return PISP_BAYER_ORDER_GBRG;
-	case BayerFormat::Order::GRBG:
+	case ColorFilterArray::Order::GRBG:
 		return PISP_BAYER_ORDER_GRBG;
-	case BayerFormat::Order::RGGB:
+	case ColorFilterArray::Order::RGGB:
 		return PISP_BAYER_ORDER_RGGB;
-	case BayerFormat::Order::MONO:
+	case ColorFilterArray::Order::MONO:
 		return PISP_BAYER_ORDER_GREYSCALE;
 	default:
 		ASSERT(0);
diff --git a/src/libcamera/pipeline/simple/simple.cpp b/src/libcamera/pipeline/simple/simple.cpp
index 35c29ceca1bb..816759632976 100644
--- a/src/libcamera/pipeline/simple/simple.cpp
+++ b/src/libcamera/pipeline/simple/simple.cpp
@@ -32,6 +32,7 @@
 #include <libcamera/pixel_format.h>
 #include <libcamera/stream.h>
 
+#include "libcamera/internal/bayer_format.h"
 #include "libcamera/internal/camera.h"
 #include "libcamera/internal/camera_manager.h"
 #include "libcamera/internal/camera_sensor.h"
@@ -1534,7 +1535,7 @@ int SimplePipelineHandler::configure(Camera *camera, CameraConfiguration *c)
 		 * pattern.
 		 */
 		BayerFormat cfgBayer = BayerFormat::fromPixelFormat(pipeConfig->captureFormat);
-		cfgBayer.order = data->sensor_->bayerOrder(config->combinedTransform());
+		cfgBayer.cfa.order = data->sensor_->bayerOrder(config->combinedTransform());
 		videoFormat = cfgBayer.toV4L2PixelFormat();
 	}
 
diff --git a/src/libcamera/sensor/camera_sensor_legacy.cpp b/src/libcamera/sensor/camera_sensor_legacy.cpp
index cf7410bcee4c..8f47b77dc7c4 100644
--- a/src/libcamera/sensor/camera_sensor_legacy.cpp
+++ b/src/libcamera/sensor/camera_sensor_legacy.cpp
@@ -12,6 +12,7 @@
 #include <limits.h>
 #include <map>
 #include <memory>
+#include <optional>
 #include <string.h>
 #include <string>
 #include <vector>
@@ -21,6 +22,7 @@
 #include <libcamera/base/utils.h>
 
 #include <libcamera/camera.h>
+#include <libcamera/color_filter_array.h>
 #include <libcamera/control_ids.h>
 #include <libcamera/controls.h>
 #include <libcamera/geometry.h>
@@ -41,7 +43,6 @@
 
 namespace libcamera {
 
-class BayerFormat;
 class CameraLens;
 class MediaEntity;
 class SensorConfiguration;
@@ -88,7 +89,7 @@ public:
 	const ControlList &properties() const override { return properties_; }
 	int sensorInfo(IPACameraSensorInfo *info) const override;
 	Transform computeTransform(Orientation *orientation) const override;
-	BayerFormat::Order bayerOrder(Transform t) const override;
+	ColorFilterArray bayerOrder(Transform t) const override;
 	Orientation mountingOrientation() const override { return mountingOrientation_; }
 
 	const ControlInfoMap &controls() const override;
@@ -134,7 +135,7 @@ private:
 	Rectangle pixelArrayArea_;
 	Rectangle readableArea_;
 	Rectangle activeArea_;
-	const BayerFormat *bayerFormat_;
+	std::optional<ColorFilterArray> cfa_;
 	bool supportHFlips_;
 	bool supportVFlips_;
 	bool flipsAlterBayerOrder_;
@@ -156,7 +157,7 @@ private:
 
 CameraSensorLegacy::CameraSensorLegacy(const MediaEntity *entity)
 	: entity_(entity), pad_(UINT_MAX), staticProps_(nullptr),
-	  bayerFormat_(nullptr), supportHFlips_(false), supportVFlips_(false),
+	  cfa_(std::nullopt), supportHFlips_(false), supportVFlips_(false),
 	  flipsAlterBayerOrder_(false), properties_(properties::properties)
 {
 }
@@ -264,9 +265,9 @@ int CameraSensorLegacy::init()
 
 	/* Get the color filter array pattern (only for RAW sensors). */
 	for (unsigned int mbusCode : mbusCodes_) {
-		const BayerFormat &bayerFormat = BayerFormat::fromMbusCode(mbusCode);
-		if (bayerFormat.isValid()) {
-			bayerFormat_ = &bayerFormat;
+		auto cfa = ColorFilterArray::fromMbusCode(mbusCode);
+		if (cfa) {
+			cfa_ = *cfa;
 			break;
 		}
 	}
@@ -445,7 +446,7 @@ int CameraSensorLegacy::validateSensorDriver()
 			<< "See Documentation/sensor_driver_requirements.rst in the libcamera sources for more information";
 	}
 
-	if (!bayerFormat_)
+	if (!cfa_)
 		return 0;
 
 	/*
@@ -640,22 +641,22 @@ int CameraSensorLegacy::initProperties()
 	properties_.set(properties::PixelArrayActiveAreas, { activeArea_ });
 
 	/* Color filter array pattern, register only for RAW sensors. */
-	if (bayerFormat_) {
+	if (cfa_) {
 		int32_t cfa;
-		switch (bayerFormat_->order) {
-		case BayerFormat::BGGR:
+		switch (cfa_->order) {
+		case ColorFilterArray::BGGR:
 			cfa = properties::draft::BGGR;
 			break;
-		case BayerFormat::GBRG:
+		case ColorFilterArray::GBRG:
 			cfa = properties::draft::GBRG;
 			break;
-		case BayerFormat::GRBG:
+		case ColorFilterArray::GRBG:
 			cfa = properties::draft::GRBG;
 			break;
-		case BayerFormat::RGGB:
+		case ColorFilterArray::RGGB:
 			cfa = properties::draft::RGGB;
 			break;
-		case BayerFormat::MONO:
+		case ColorFilterArray::MONO:
 			cfa = properties::draft::MONO;
 			break;
 		}
@@ -881,7 +882,7 @@ V4L2Subdevice::Stream CameraSensorLegacy::imageStream() const
 
 int CameraSensorLegacy::sensorInfo(IPACameraSensorInfo *info) const
 {
-	if (!bayerFormat_)
+	if (!cfa_)
 		return -EINVAL;
 
 	info->model = model();
@@ -982,20 +983,20 @@ Transform CameraSensorLegacy::computeTransform(Orientation *orientation) const
 	return transform;
 }
 
-BayerFormat::Order CameraSensorLegacy::bayerOrder(Transform t) const
+ColorFilterArray CameraSensorLegacy::bayerOrder(Transform t) const
 {
 	/* Return a defined by meaningless value for non-Bayer sensors. */
-	if (!bayerFormat_)
-		return BayerFormat::Order::BGGR;
+	if (!cfa_)
+		return ColorFilterArray::BGGR;
 
 	if (!flipsAlterBayerOrder_)
-		return bayerFormat_->order;
+		return *cfa_;
 
 	/*
 	 * Apply the transform to the native (i.e. untransformed) Bayer order,
 	 * using the rest of the Bayer format supplied by the caller.
 	 */
-	return bayerFormat_->transform(t).order;
+	return cfa_->transform(t);
 }
 
 const ControlInfoMap &CameraSensorLegacy::controls() const
diff --git a/src/libcamera/sensor/camera_sensor_raw.cpp b/src/libcamera/sensor/camera_sensor_raw.cpp
index 5301bc6d9f8c..c703db31d658 100644
--- a/src/libcamera/sensor/camera_sensor_raw.cpp
+++ b/src/libcamera/sensor/camera_sensor_raw.cpp
@@ -22,6 +22,7 @@
 #include <libcamera/base/utils.h>
 
 #include <libcamera/camera.h>
+#include <libcamera/color_filter_array.h>
 #include <libcamera/control_ids.h>
 #include <libcamera/controls.h>
 #include <libcamera/geometry.h>
@@ -42,7 +43,6 @@
 
 namespace libcamera {
 
-class BayerFormat;
 class CameraLens;
 class MediaEntity;
 class SensorConfiguration;
@@ -93,7 +93,7 @@ public:
 	const ControlList &properties() const override { return properties_; }
 	int sensorInfo(IPACameraSensorInfo *info) const override;
 	Transform computeTransform(Orientation *orientation) const override;
-	BayerFormat::Order bayerOrder(Transform t) const override;
+	ColorFilterArray bayerOrder(Transform t) const override;
 	Orientation mountingOrientation() const override { return mountingOrientation_; }
 
 	const ControlInfoMap &controls() const override;
@@ -144,7 +144,7 @@ private:
 	Rectangle pixelArrayArea_;
 	Rectangle readableArea_;
 	Rectangle activeArea_;
-	BayerFormat::Order cfaPattern_;
+	ColorFilterArray cfa_;
 	bool supportHFlips_;
 	bool supportVFlips_;
 	bool flipsAlterBayerOrder_;
@@ -489,16 +489,19 @@ std::optional<int> CameraSensorRaw::init()
 
 	switch (cfaPattern) {
 	case V4L2_CFA_PATTERN_GRBG:
-		cfaPattern_ = BayerFormat::GRBG;
+		cfa_ = ColorFilterArray::GRBG;
 		break;
 	case V4L2_CFA_PATTERN_RGGB:
-		cfaPattern_ = BayerFormat::RGGB;
+		cfa_ = ColorFilterArray::RGGB;
 		break;
 	case V4L2_CFA_PATTERN_BGGR:
-		cfaPattern_ = BayerFormat::BGGR;
+		cfa_ = ColorFilterArray::BGGR;
 		break;
 	case V4L2_CFA_PATTERN_GBRG:
-		cfaPattern_ = BayerFormat::GBRG;
+		cfa_ = ColorFilterArray::GBRG;
+		break;
+	case V4L2_CFA_PATTERN_MONO:
+		cfa_ = ColorFilterArray::MONO;
 		break;
 	default:
 		LOG(CameraSensor, Error)
@@ -667,20 +670,20 @@ int CameraSensorRaw::initProperties()
 	/* Color filter array pattern. */
 	uint32_t cfa;
 
-	switch (cfaPattern_) {
-	case BayerFormat::BGGR:
+	switch (cfa_.order) {
+	case ColorFilterArray::BGGR:
 		cfa = properties::draft::BGGR;
 		break;
-	case BayerFormat::GBRG:
+	case ColorFilterArray::GBRG:
 		cfa = properties::draft::GBRG;
 		break;
-	case BayerFormat::GRBG:
+	case ColorFilterArray::GRBG:
 		cfa = properties::draft::GRBG;
 		break;
-	case BayerFormat::RGGB:
+	case ColorFilterArray::RGGB:
 		cfa = properties::draft::RGGB;
 		break;
-	case BayerFormat::MONO:
+	case ColorFilterArray::MONO:
 	default:
 		cfa = properties::draft::MONO;
 		break;
@@ -1112,17 +1115,16 @@ Transform CameraSensorRaw::computeTransform(Orientation *orientation) const
 	return transform;
 }
 
-BayerFormat::Order CameraSensorRaw::bayerOrder(Transform t) const
+ColorFilterArray CameraSensorRaw::bayerOrder(Transform t) const
 {
 	if (!flipsAlterBayerOrder_)
-		return cfaPattern_;
+		return cfa_;
 
 	/*
 	 * Apply the transform to the native (i.e. untransformed) Bayer order,
 	 * using the rest of the Bayer format supplied by the caller.
 	 */
-	BayerFormat format{ cfaPattern_, 8, BayerFormat::Packing::None };
-	return format.transform(t).order;
+	return cfa_.transform(t);
 }
 
 const ControlInfoMap &CameraSensorRaw::controls() const
diff --git a/src/libcamera/software_isp/debayer.cpp b/src/libcamera/software_isp/debayer.cpp
index 4f6659000edb..9862f4e29538 100644
--- a/src/libcamera/software_isp/debayer.cpp
+++ b/src/libcamera/software_isp/debayer.cpp
@@ -330,13 +330,13 @@ void Debayer::dmaSyncBegin(std::vector<DmaSyncer> &dmaSyncers, FrameBuffer *inpu
 }
 
 /**
- * \fn void Debayer::isStandardBayerOrder(BayerFormat::Order order)
+ * \fn void Debayer::isStandardBayerOrder(ColorFilterArray::Order order)
  * \brief Common method to validate standard 2x2 Bayer pattern of 2 Green, 1 Blue, 1 Red pixels
  */
-bool Debayer::isStandardBayerOrder(BayerFormat::Order order)
+bool Debayer::isStandardBayerOrder(ColorFilterArray::Order order)
 {
-	return order == BayerFormat::BGGR || order == BayerFormat::GBRG ||
-	       order == BayerFormat::GRBG || order == BayerFormat::RGGB;
+	return order == ColorFilterArray::BGGR || order == ColorFilterArray::GBRG ||
+	       order == ColorFilterArray::GRBG || order == ColorFilterArray::RGGB;
 }
 
 } /* namespace libcamera */
diff --git a/src/libcamera/software_isp/debayer.h b/src/libcamera/software_isp/debayer.h
index e30a05279c18..d5240a1e42a2 100644
--- a/src/libcamera/software_isp/debayer.h
+++ b/src/libcamera/software_isp/debayer.h
@@ -87,7 +87,7 @@ private:
 
 protected:
 	void dmaSyncBegin(std::vector<DmaSyncer> &dmaSyncers, FrameBuffer *input, FrameBuffer *output);
-	static bool isStandardBayerOrder(BayerFormat::Order order);
+	static bool isStandardBayerOrder(ColorFilterArray::Order order);
 };
 
 } /* namespace libcamera */
diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp
index 91867baa656a..56850a7b30c7 100644
--- a/src/libcamera/software_isp/debayer_cpu.cpp
+++ b/src/libcamera/software_isp/debayer_cpu.cpp
@@ -442,7 +442,7 @@ int DebayerCpu::getInputConfig(PixelFormat inputFormat, DebayerInputConfig &conf
 
 	if ((bayerFormat.bitDepth == 8 || bayerFormat.bitDepth == 10 || bayerFormat.bitDepth == 12) &&
 	    bayerFormat.packing == BayerFormat::Packing::None &&
-	    isStandardBayerOrder(bayerFormat.order)) {
+	    isStandardBayerOrder(bayerFormat.cfa.order)) {
 		config.bpp = (bayerFormat.bitDepth + 7) & ~7;
 		config.patternSize.width = 2;
 		config.patternSize.height = 2;
@@ -452,7 +452,7 @@ int DebayerCpu::getInputConfig(PixelFormat inputFormat, DebayerInputConfig &conf
 
 	if (bayerFormat.bitDepth == 10 &&
 	    bayerFormat.packing == BayerFormat::Packing::CSI2 &&
-	    isStandardBayerOrder(bayerFormat.order)) {
+	    isStandardBayerOrder(bayerFormat.cfa.order)) {
 		config.bpp = 10;
 		config.patternSize.width = 4; /* 5 bytes per *4* pixels */
 		config.patternSize.height = 2;
@@ -462,7 +462,7 @@ int DebayerCpu::getInputConfig(PixelFormat inputFormat, DebayerInputConfig &conf
 
 	if (bayerFormat.bitDepth == 12 &&
 	    bayerFormat.packing == BayerFormat::Packing::CSI2 &&
-	    isStandardBayerOrder(bayerFormat.order)) {
+	    isStandardBayerOrder(bayerFormat.cfa.order)) {
 		config.bpp = 12;
 		config.patternSize.width = 2; /* 3 bytes per *2* pixels */
 		config.patternSize.height = 2;
@@ -479,18 +479,18 @@ int DebayerCpu::getInputConfig(PixelFormat inputFormat, DebayerInputConfig &conf
  * Check for standard Bayer orders and set xShift_ and swap debayer0/1, so that
  * a single pair of BGGR debayer functions can be used for all 4 standard orders.
  */
-int DebayerCpu::setupStandardBayerOrder(BayerFormat::Order order)
+int DebayerCpu::setupStandardBayerOrder(ColorFilterArray::Order order)
 {
 	switch (order) {
-	case BayerFormat::BGGR:
+	case ColorFilterArray::BGGR:
 		break;
-	case BayerFormat::GBRG:
+	case ColorFilterArray::GBRG:
 		xShift_ = 1; /* BGGR -> GBRG */
 		break;
-	case BayerFormat::GRBG:
+	case ColorFilterArray::GRBG:
 		std::swap(debayer0_, debayer1_); /* BGGR -> GRBG */
 		break;
-	case BayerFormat::RGGB:
+	case ColorFilterArray::RGGB:
 		xShift_ = 1; /* BGGR -> GBRG */
 		std::swap(debayer0_, debayer1_); /* GBRG -> RGGB */
 		break;
@@ -540,18 +540,18 @@ int DebayerCpu::setDebayerFunctions(PixelFormat inputFormat,
 		/* Swap R and B in bayer order to generate BGR888 instead of RGB888 */
 		swapRedBlueGains_ = true;
 
-		switch (bayerFormat.order) {
-		case BayerFormat::BGGR:
-			bayerFormat.order = BayerFormat::RGGB;
+		switch (bayerFormat.cfa.order) {
+		case ColorFilterArray::BGGR:
+			bayerFormat.cfa.order = ColorFilterArray::RGGB;
 			break;
-		case BayerFormat::GBRG:
-			bayerFormat.order = BayerFormat::GRBG;
+		case ColorFilterArray::GBRG:
+			bayerFormat.cfa.order = ColorFilterArray::GRBG;
 			break;
-		case BayerFormat::GRBG:
-			bayerFormat.order = BayerFormat::GBRG;
+		case ColorFilterArray::GRBG:
+			bayerFormat.cfa.order = ColorFilterArray::GBRG;
 			break;
-		case BayerFormat::RGGB:
-			bayerFormat.order = BayerFormat::BGGR;
+		case ColorFilterArray::RGGB:
+			bayerFormat.cfa.order = ColorFilterArray::BGGR;
 			break;
 		default:
 			return invalidFmt();
@@ -563,7 +563,7 @@ int DebayerCpu::setDebayerFunctions(PixelFormat inputFormat,
 
 	if ((bayerFormat.bitDepth == 8 || bayerFormat.bitDepth == 10 || bayerFormat.bitDepth == 12) &&
 	    bayerFormat.packing == BayerFormat::Packing::None &&
-	    isStandardBayerOrder(bayerFormat.order)) {
+	    isStandardBayerOrder(bayerFormat.cfa.order)) {
 		switch (bayerFormat.bitDepth) {
 		case 8:
 			SET_DEBAYER_METHODS(debayer8_BGBG_BGR888, debayer8_GRGR_BGR888)
@@ -575,23 +575,23 @@ int DebayerCpu::setDebayerFunctions(PixelFormat inputFormat,
 			SET_DEBAYER_METHODS(debayer12_BGBG_BGR888, debayer12_GRGR_BGR888)
 			break;
 		}
-		setupStandardBayerOrder(bayerFormat.order);
+		setupStandardBayerOrder(bayerFormat.cfa.order);
 		return 0;
 	}
 
 	if (bayerFormat.bitDepth == 10 &&
 	    bayerFormat.packing == BayerFormat::Packing::CSI2) {
-		switch (bayerFormat.order) {
-		case BayerFormat::BGGR:
+		switch (bayerFormat.cfa.order) {
+		case ColorFilterArray::BGGR:
 			SET_DEBAYER_METHODS(debayer10P_BGBG_BGR888, debayer10P_GRGR_BGR888)
 			return 0;
-		case BayerFormat::GBRG:
+		case ColorFilterArray::GBRG:
 			SET_DEBAYER_METHODS(debayer10P_GBGB_BGR888, debayer10P_RGRG_BGR888)
 			return 0;
-		case BayerFormat::GRBG:
+		case ColorFilterArray::GRBG:
 			SET_DEBAYER_METHODS(debayer10P_GRGR_BGR888, debayer10P_BGBG_BGR888)
 			return 0;
-		case BayerFormat::RGGB:
+		case ColorFilterArray::RGGB:
 			SET_DEBAYER_METHODS(debayer10P_RGRG_BGR888, debayer10P_GBGB_BGR888)
 			return 0;
 		default:
@@ -601,17 +601,17 @@ int DebayerCpu::setDebayerFunctions(PixelFormat inputFormat,
 
 	if (bayerFormat.bitDepth == 12 &&
 	    bayerFormat.packing == BayerFormat::Packing::CSI2) {
-		switch (bayerFormat.order) {
-		case BayerFormat::BGGR:
+		switch (bayerFormat.cfa.order) {
+		case ColorFilterArray::BGGR:
 			SET_DEBAYER_METHODS(debayer12P_BGBG_BGR888, debayer12P_GRGR_BGR888)
 			return 0;
-		case BayerFormat::GBRG:
+		case ColorFilterArray::GBRG:
 			SET_DEBAYER_METHODS(debayer12P_GBGB_BGR888, debayer12P_RGRG_BGR888)
 			return 0;
-		case BayerFormat::GRBG:
+		case ColorFilterArray::GRBG:
 			SET_DEBAYER_METHODS(debayer12P_GRGR_BGR888, debayer12P_BGBG_BGR888)
 			return 0;
-		case BayerFormat::RGGB:
+		case ColorFilterArray::RGGB:
 			SET_DEBAYER_METHODS(debayer12P_RGRG_BGR888, debayer12P_GBGB_BGR888)
 			return 0;
 		default:
diff --git a/src/libcamera/software_isp/debayer_cpu.h b/src/libcamera/software_isp/debayer_cpu.h
index ae570893bf13..8e27cbae4763 100644
--- a/src/libcamera/software_isp/debayer_cpu.h
+++ b/src/libcamera/software_isp/debayer_cpu.h
@@ -122,7 +122,7 @@ private:
 	void debayer12P_RGRG_BGR888(uint8_t *dst, const uint8_t *src[]);
 
 	static int getInputConfig(PixelFormat inputFormat, DebayerInputConfig &config);
-	int setupStandardBayerOrder(BayerFormat::Order order);
+	int setupStandardBayerOrder(ColorFilterArray::Order order);
 	int setDebayerFunctions(PixelFormat inputFormat,
 				PixelFormat outputFormat,
 				bool ccmEnabled);
diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
index 33933b11db70..3f3d2d5f0763 100644
--- a/src/libcamera/software_isp/debayer_egl.cpp
+++ b/src/libcamera/software_isp/debayer_egl.cpp
@@ -63,7 +63,7 @@ int DebayerEGL::getInputConfig(PixelFormat inputFormat, DebayerInputConfig &conf
 
 	if ((bayerFormat.bitDepth == 8 || bayerFormat.bitDepth == 10) &&
 	    bayerFormat.packing == BayerFormat::Packing::None &&
-	    isStandardBayerOrder(bayerFormat.order)) {
+	    isStandardBayerOrder(bayerFormat.cfa.order)) {
 		config.bpp = (bayerFormat.bitDepth + 7) & ~7;
 		config.patternSize.width = 2;
 		config.patternSize.height = 2;
@@ -73,7 +73,7 @@ int DebayerEGL::getInputConfig(PixelFormat inputFormat, DebayerInputConfig &conf
 
 	if (bayerFormat.bitDepth == 10 &&
 	    bayerFormat.packing == BayerFormat::Packing::CSI2 &&
-	    isStandardBayerOrder(bayerFormat.order)) {
+	    isStandardBayerOrder(bayerFormat.cfa.order)) {
 		config.bpp = 10;
 		config.patternSize.width = 4; /* 5 bytes per *4* pixels */
 		config.patternSize.height = 2;
@@ -83,7 +83,7 @@ int DebayerEGL::getInputConfig(PixelFormat inputFormat, DebayerInputConfig &conf
 
 	if (bayerFormat.bitDepth == 12 &&
 	    bayerFormat.packing == BayerFormat::Packing::CSI2 &&
-	    isStandardBayerOrder(bayerFormat.order)) {
+	    isStandardBayerOrder(bayerFormat.cfa.order)) {
 		config.bpp = 12;
 		config.patternSize.width = 2; /* 3 bytes per *2* pixels */
 		config.patternSize.height = 2;
diff --git a/src/libcamera/software_isp/swstats_cpu.cpp b/src/libcamera/software_isp/swstats_cpu.cpp
index 7fb77ce7ddd6..58503f30a818 100644
--- a/src/libcamera/software_isp/swstats_cpu.cpp
+++ b/src/libcamera/software_isp/swstats_cpu.cpp
@@ -429,22 +429,22 @@ void SwStatsCpu::finishFrame(uint32_t frame, uint32_t bufferId)
  * Check if order is a standard Bayer order and setup xShift_ and swapLines_
  * so that a single BGGR stats function can be used for all 4 standard orders.
  */
-int SwStatsCpu::setupStandardBayerOrder(BayerFormat::Order order)
+int SwStatsCpu::setupStandardBayerOrder(ColorFilterArray::Order order)
 {
 	switch (order) {
-	case BayerFormat::BGGR:
+	case ColorFilterArray::BGGR:
 		xShift_ = 0;
 		swapLines_ = false;
 		break;
-	case BayerFormat::GBRG:
+	case ColorFilterArray::GBRG:
 		xShift_ = 1; /* BGGR -> GBRG */
 		swapLines_ = false;
 		break;
-	case BayerFormat::GRBG:
+	case ColorFilterArray::GRBG:
 		xShift_ = 0;
 		swapLines_ = true; /* BGGR -> GRBG */
 		break;
-	case BayerFormat::RGGB:
+	case ColorFilterArray::RGGB:
 		xShift_ = 1; /* BGGR -> GBRG */
 		swapLines_ = true; /* GBRG -> RGGB */
 		break;
@@ -474,7 +474,7 @@ int SwStatsCpu::configure(const StreamConfiguration &inputCfg, unsigned int stat
 		BayerFormat::fromPixelFormat(inputCfg.pixelFormat);
 
 	if (bayerFormat.packing == BayerFormat::Packing::None &&
-	    setupStandardBayerOrder(bayerFormat.order) == 0) {
+	    setupStandardBayerOrder(bayerFormat.cfa.order) == 0) {
 		processFrame_ = &SwStatsCpu::processBayerFrame2;
 		switch (bayerFormat.bitDepth) {
 		case 8:
@@ -509,16 +509,16 @@ int SwStatsCpu::configure(const StreamConfiguration &inputCfg, unsigned int stat
 		sumShift_ = 0;
 		processFrame_ = &SwStatsCpu::processBayerFrame2;
 
-		switch (bayerFormat.order) {
-		case BayerFormat::BGGR:
-		case BayerFormat::GRBG:
+		switch (bayerFormat.cfa.order) {
+		case ColorFilterArray::BGGR:
+		case ColorFilterArray::GRBG:
 			stats0_ = (bitDepth == 10) ? &SwStatsCpu::statsBGGR10PLine0 : &SwStatsCpu::statsBGGR12PLine0;
-			swapLines_ = bayerFormat.order == BayerFormat::GRBG;
+			swapLines_ = bayerFormat.cfa.order == ColorFilterArray::GRBG;
 			return 0;
-		case BayerFormat::GBRG:
-		case BayerFormat::RGGB:
+		case ColorFilterArray::GBRG:
+		case ColorFilterArray::RGGB:
 			stats0_ = (bitDepth == 10) ? &SwStatsCpu::statsGBRG10PLine0 : &SwStatsCpu::statsGBRG12PLine0;
-			swapLines_ = bayerFormat.order == BayerFormat::RGGB;
+			swapLines_ = bayerFormat.cfa.order == ColorFilterArray::RGGB;
 			return 0;
 		default:
 			break;
diff --git a/test/bayer-format.cpp b/test/bayer-format.cpp
index f8d1980498c1..3ff7d3bfab97 100644
--- a/test/bayer-format.cpp
+++ b/test/bayer-format.cpp
@@ -30,7 +30,7 @@ protected:
 		}
 
 		/* A correct Bayer format has to be valid. */
-		bayerFmt = BayerFormat(BayerFormat::BGGR, 8, BayerFormat::Packing::None);
+		bayerFmt = BayerFormat(ColorFilterArray::BGGR, 8, BayerFormat::Packing::None);
 		if (!bayerFmt.isValid()) {
 			cerr << "A correct BayerFormat has to be valid."
 			     << endl;
@@ -41,8 +41,8 @@ protected:
 		 * Two bayer formats created with the same order and bit depth
 		 * have to be equal.
 		 */
-		bayerFmt = BayerFormat(BayerFormat::BGGR, 8, BayerFormat::Packing::None);
-		BayerFormat bayerFmtExpect = BayerFormat(BayerFormat::BGGR, 8,
+		bayerFmt = BayerFormat(ColorFilterArray::BGGR, 8, BayerFormat::Packing::None);
+		BayerFormat bayerFmtExpect = BayerFormat(ColorFilterArray::BGGR, 8,
 							 BayerFormat::Packing::None);
 		if (bayerFmt != bayerFmtExpect) {
 			cerr << "Comparison of identical formats failed."
@@ -54,8 +54,8 @@ protected:
 		 * Two Bayer formats created with the same order but with a
 		 * different bitDepth are not equal.
 		 */
-		bayerFmt = BayerFormat(BayerFormat::BGGR, 8, BayerFormat::Packing::None);
-		bayerFmtExpect = BayerFormat(BayerFormat::BGGR, 12,
+		bayerFmt = BayerFormat(ColorFilterArray::BGGR, 8, BayerFormat::Packing::None);
+		bayerFmtExpect = BayerFormat(ColorFilterArray::BGGR, 12,
 					     BayerFormat::Packing::None);
 		if (bayerFmt == bayerFmtExpect) {
 			cerr << "Comparison of different formats failed."
@@ -95,7 +95,7 @@ protected:
 		 * when we convert the V4L2PixelFormat (V4L2_PIX_FMT_SBGGR8)
 		 * to a Bayer format.
 		 */
-		bayerFmtExpect = BayerFormat(BayerFormat::BGGR, 8,
+		bayerFmtExpect = BayerFormat(ColorFilterArray::BGGR, 8,
 					     BayerFormat::Packing::None);
 		v4l2Fmt = V4L2PixelFormat(V4L2_PIX_FMT_SBGGR8);
 		bayerFmt = BayerFormat::fromV4L2PixelFormat(v4l2Fmt);
@@ -123,7 +123,7 @@ protected:
 		 * Test if a valid Bayer format can be converted to a
 		 * string representation.
 		 */
-		bayerFmt = BayerFormat(BayerFormat::BGGR, 8, BayerFormat::Packing::None);
+		bayerFmt = BayerFormat(ColorFilterArray::BGGR, 8, BayerFormat::Packing::None);
 		if (bayerFmt.toString() != "BGGR-8") {
 			cerr << "String representation != 'BGGR-8' (got: '"
 			     << bayerFmt.toString() << "' ) " << endl;
@@ -145,10 +145,11 @@ protected:
 		 * Perform a horizontal Flip and make sure that the
 		 * order is adjusted accordingly.
 		 */
-		bayerFmt = BayerFormat(BayerFormat::BGGR, 8, BayerFormat::Packing::None);
-		bayerFmtExpect = BayerFormat(BayerFormat::GBRG, 8,
+		bayerFmt = BayerFormat(ColorFilterArray::BGGR, 8, BayerFormat::Packing::None);
+		bayerFmtExpect = BayerFormat(ColorFilterArray::GBRG, 8,
 					     BayerFormat::Packing::None);
-		BayerFormat hFlipFmt = bayerFmt.transform(Transform::HFlip);
+		BayerFormat hFlipFmt = bayerFmt;
+		hFlipFmt.cfa = bayerFmt.cfa.transform(Transform::HFlip);
 		if (hFlipFmt != bayerFmtExpect) {
 			cerr << "Horizontal flip of 'BGGR-8' should result in '"
 			     << bayerFmtExpect << "', got: '"
@@ -160,10 +161,11 @@ protected:
 		 * Perform a vertical Flip and make sure that
 		 * the order is adjusted accordingly.
 		 */
-		bayerFmt = BayerFormat(BayerFormat::BGGR, 8, BayerFormat::Packing::None);
-		bayerFmtExpect = BayerFormat(BayerFormat::GRBG, 8,
+		bayerFmt = BayerFormat(ColorFilterArray::BGGR, 8, BayerFormat::Packing::None);
+		bayerFmtExpect = BayerFormat(ColorFilterArray::GRBG, 8,
 					     BayerFormat::Packing::None);
-		BayerFormat vFlipFmt = bayerFmt.transform(Transform::VFlip);
+		BayerFormat vFlipFmt = bayerFmt;
+		vFlipFmt.cfa = bayerFmt.cfa.transform(Transform::VFlip);
 		if (vFlipFmt != bayerFmtExpect) {
 			cerr << "Vertical flip of 'BGGR-8' should result in '"
 			     << bayerFmtExpect << "', got: '"
@@ -176,9 +178,9 @@ protected:
 		 * pixels on the bottom left to top right diagonal and make
 		 * sure, that it doesn't change.
 		 */
-		bayerFmt = BayerFormat(BayerFormat::BGGR, 8, BayerFormat::Packing::None);
-		BayerFormat transposeFmt = bayerFmt.transform(
-			Transform::Transpose);
+		bayerFmt = BayerFormat(ColorFilterArray::BGGR, 8, BayerFormat::Packing::None);
+		BayerFormat transposeFmt = bayerFmt;
+		transposeFmt.cfa = bayerFmt.cfa.transform( Transform::Transpose);
 		if (transposeFmt != bayerFmt) {
 			cerr << "Transpose with both green pixels on the "
 			     << "antidiagonal should not change the order "
@@ -192,10 +194,10 @@ protected:
 		 * on the bottom left to top right diagonal and make sure
 		 * that their positions are switched.
 		 */
-		bayerFmt = BayerFormat(BayerFormat::GBRG, 8, BayerFormat::Packing::None);
-		bayerFmtExpect = BayerFormat(BayerFormat::GRBG, 8,
+		bayerFmt = BayerFormat(ColorFilterArray::GBRG, 8, BayerFormat::Packing::None);
+		bayerFmtExpect = BayerFormat(ColorFilterArray::GRBG, 8,
 					     BayerFormat::Packing::None);
-		transposeFmt = bayerFmt.transform(Transform::Transpose);
+		transposeFmt.cfa = bayerFmt.cfa.transform(Transform::Transpose);
 		if (transposeFmt != bayerFmtExpect) {
 			cerr << "Transpose with the red & blue pixels on the "
 			     << "antidiagonal should switch their position "
