[libcamera-devel,v10,1/8] libcamera: Add ColorSpace class
diff mbox series

Message ID 20211209101245.6187-2-david.plowman@raspberrypi.com
State Superseded
Headers show
Series
  • Colour spaces
Related show

Commit Message

David Plowman Dec. 9, 2021, 10:12 a.m. UTC
This class represents a color space by defining its color primaries,
the transfer (gamma) function it uses, the YCbCr encoding and whether
the output is full or limited range.

Signed-off-by: David Plowman <david.plowman@raspberrypi.com>
Reviewed-by: Naushir Patuck <naush@raspberrypi.com>
Reviewed-by: Umang Jain <umang.jain@ideasonboard.com>
Reviewed-by: Jacopo Mondi <jacopo@jmondi.org>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
---
 include/libcamera/color_space.h |  70 +++++++
 include/libcamera/meson.build   |   1 +
 src/libcamera/color_space.cpp   | 318 ++++++++++++++++++++++++++++++++
 src/libcamera/meson.build       |   1 +
 4 files changed, 390 insertions(+)
 create mode 100644 include/libcamera/color_space.h
 create mode 100644 src/libcamera/color_space.cpp

Comments

Laurent Pinchart Dec. 9, 2021, 8:50 p.m. UTC | #1
Hi David,

Thank you for the patch.

On Thu, Dec 09, 2021 at 10:12:38AM +0000, David Plowman wrote:
> This class represents a color space by defining its color primaries,
> the transfer (gamma) function it uses, the YCbCr encoding and whether
> the output is full or limited range.
> 
> Signed-off-by: David Plowman <david.plowman@raspberrypi.com>
> Reviewed-by: Naushir Patuck <naush@raspberrypi.com>
> Reviewed-by: Umang Jain <umang.jain@ideasonboard.com>
> Reviewed-by: Jacopo Mondi <jacopo@jmondi.org>
> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
> ---
>  include/libcamera/color_space.h |  70 +++++++
>  include/libcamera/meson.build   |   1 +
>  src/libcamera/color_space.cpp   | 318 ++++++++++++++++++++++++++++++++
>  src/libcamera/meson.build       |   1 +
>  4 files changed, 390 insertions(+)
>  create mode 100644 include/libcamera/color_space.h
>  create mode 100644 src/libcamera/color_space.cpp
> 
> diff --git a/include/libcamera/color_space.h b/include/libcamera/color_space.h
> new file mode 100644
> index 00000000..086c56c1
> --- /dev/null
> +++ b/include/libcamera/color_space.h
> @@ -0,0 +1,70 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2021, Raspberry Pi (Trading) Limited
> + *
> + * color_space.h - color space definitions
> + */
> +
> +#pragma once
> +
> +#include <optional>
> +#include <string>
> +
> +namespace libcamera {
> +
> +class ColorSpace
> +{
> +public:
> +	enum class Primaries {
> +		Raw,
> +		Smpte170m,
> +		Rec709,
> +		Rec2020,
> +	};
> +
> +	enum class TransferFunction {
> +		Linear,
> +		Srgb,
> +		Rec709,
> +	};
> +
> +	enum class YcbcrEncoding {
> +		None,
> +		Rec601,
> +		Rec709,
> +		Rec2020,
> +	};
> +
> +	enum class Range {
> +		Full,
> +		Limited,
> +	};
> +
> +	constexpr ColorSpace(Primaries p, TransferFunction t, YcbcrEncoding e, Range r)
> +		: primaries(p), transferFunction(t), ycbcrEncoding(e), range(r)
> +	{
> +	}
> +
> +	static const ColorSpace Raw;
> +	static const ColorSpace Jpeg;
> +	static const ColorSpace Srgb;
> +	static const ColorSpace Smpte170m;
> +	static const ColorSpace Rec709;
> +	static const ColorSpace Rec2020;
> +
> +	Primaries primaries;
> +	TransferFunction transferFunction;
> +	YcbcrEncoding ycbcrEncoding;
> +	Range range;
> +
> +	std::string toString() const;
> +	static std::string toString(const std::optional<ColorSpace> &colorSpace);
> +};
> +
> +bool operator==(const ColorSpace &lhs, const ColorSpace &rhs);
> +static inline bool operator!=(const ColorSpace &lhs, const ColorSpace &rhs)
> +{
> +	return !(lhs == rhs);
> +}
> +
> +} /* namespace libcamera */
> diff --git a/include/libcamera/meson.build b/include/libcamera/meson.build
> index 5f42977c..fd767b11 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_space.h',
>      'controls.h',
>      'framebuffer.h',
>      'framebuffer_allocator.h',
> diff --git a/src/libcamera/color_space.cpp b/src/libcamera/color_space.cpp
> new file mode 100644
> index 00000000..6c587068
> --- /dev/null
> +++ b/src/libcamera/color_space.cpp
> @@ -0,0 +1,318 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2021, Raspberry Pi (Trading) Limited
> + *
> + * color_space.cpp - color spaces.
> + */
> +
> +#include <libcamera/color_space.h>
> +
> +#include <algorithm>
> +#include <array>
> +#include <map>
> +#include <sstream>
> +#include <utility>
> +
> +/**
> + * \file color_space.h
> + * \brief Class and enums to represent color spaces
> + */
> +
> +namespace libcamera {
> +
> +/**
> + * \class ColorSpace
> + * \brief Class to describe a color space
> + *
> + * The ColorSpace class defines the color primaries, the Y'CbCr encoding,
> + * the transfer function associated with the color space, and the range
> + * (sometimes also referred to as the quantisation) of the color space.
> + *
> + * Certain combinations of these fields form well-known standard color
> + * spaces such as "JPEG" or "REC709".
> + *
> + * In the strictest sense a "color space" formally only refers to the
> + * color primaries and white point. Here, however, the ColorSpace class
> + * adopts the common broader usage that includes the transfer function,
> + * Y'CbCr encoding method and quantisation.
> + *
> + * For more information on the specific color spaces described here, please
> + * see:
> + *
> + * - <a href="https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/colorspaces-details.html#col-srgb">sRGB</a>
> + * - <a href="https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/colorspaces-details.html#col-jpeg">JPEG</a>
> + * - <a href="https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/colorspaces-details.html#col-smpte-170m">SMPTE 170M</a>
> + * - <a href="https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/colorspaces-details.html#col-rec709">Rec.709</a>
> + * - <a href="https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/colorspaces-details.html#col-bt2020">Rec.2020</a>
> + */
> +
> +/**
> + * \enum ColorSpace::Primaries
> + * \brief The color primaries for this color space
> + *
> + * \var ColorSpace::Primaries::Raw
> + * \brief These are raw colors directly from a sensor, the primaries are
> + * unspecified
> + *
> + * \var ColorSpace::Primaries::Smpte170m
> + * \brief SMPTE 170M color primaries
> + *
> + * \var ColorSpace::Primaries::Rec709
> + * \brief Rec.709 color primaries
> + *
> + * \var ColorSpace::Primaries::Rec2020
> + * \brief Rec.2020 color primaries
> + */
> +
> +/**
> + * \enum ColorSpace::TransferFunction
> + * \brief The transfer function used for this color space
> + *
> + * \var ColorSpace::TransferFunction::Linear
> + * \brief This color space uses a linear (identity) transfer function
> + *
> + * \var ColorSpace::TransferFunction::Srgb
> + * \brief sRGB transfer function
> + *
> + * \var ColorSpace::TransferFunction::Rec709
> + * \brief Rec.709 transfer function
> + */
> +
> +/**
> + * \enum ColorSpace::YcbcrEncoding
> + * \brief The Y'CbCr encoding
> + *
> + * \var ColorSpace::YcbcrEncoding::None
> + * \brief There is no defined Y'CbCr encoding

I'd write

 * \brief There is no defined Y'CbCr encoding (used for non-YUV formats)

> + *
> + * \var ColorSpace::YcbcrEncoding::Rec601
> + * \brief Rec.601 Y'CbCr encoding
> + *
> + * \var ColorSpace::YcbcrEncoding::Rec709
> + * \brief Rec.709 Y'CbCr encoding
> + *
> + * \var ColorSpace::YcbcrEncoding::Rec2020
> + * \brief Rec.2020 Y'CbCr encoding
> + */
> +
> +/**
> + * \enum ColorSpace::Range
> + * \brief The range (sometimes "quantisation") for this color space
> + *
> + * \var ColorSpace::Range::Full
> + * \brief This color space uses full range pixel values
> + *
> + * \var ColorSpace::Range::Limited
> + * \brief This color space uses limited range pixel values, being
> + * 16 to 235 for Y' and 16 to 240 for Cb and Cr (8 bits per sample)
> + * or 64 to 940 for Y' and 16 to 960 for Cb and Cr (10 bits)
> + */
> +
> +/**
> + * \fn ColorSpace::ColorSpace(Primaries p, TransferFunction t, Encoding e, Range r)
> + * \brief Construct a ColorSpace from explicit values
> + * \param[in] p The color primaries
> + * \param[in] t The transfer function for the color space
> + * \param[in] e The Y'CbCr encoding
> + * \param[in] r The range of the pixel values in this color space
> + */
> +
> +/**
> + * \brief Assemble and return a readable string representation of the
> + * ColorSpace
> + *
> + * If the color space matches a standard ColorSpace (such as ColorSpace::Jpeg)
> + * then the short name of the color space ("JPEG") is returned. Otherwise
> + * the four constituent parts of the ColorSpace are assembled into a longer
> + * string.
> + *
> + * \return A string describing the ColorSpace
> + */
> +std::string ColorSpace::toString() const
> +{
> +	/* Print out a brief name only for standard color spaces. */
> +
> +	static const std::array<std::pair<ColorSpace, const char *>, 6> colorSpaceNames = {
> +		{
> +			{ ColorSpace::Raw, "RAW" },
> +			{ ColorSpace::Jpeg, "JPEG" },
> +			{ ColorSpace::Srgb, "sRGB" },
> +			{ ColorSpace::Smpte170m, "SMPTE170M" },
> +			{ ColorSpace::Rec709, "Rec709" },
> +			{ ColorSpace::Rec2020, "Rec2020" },
> +		}
> +	};

That's a bit of a peculiar construct, I wonder why the compiler accepts

	static const std::array<std::pair<ColorSpace, const char *>, 6> colorSpaceNames = {
		std::pair<ColorSpace, const char *>{ ColorSpace::Raw, "RAW" },
		std::pair<ColorSpace, const char *>{ ColorSpace::Jpeg, "JPEG" },
		std::pair<ColorSpace, const char *>{ ColorSpace::Srgb, "sRGB" },
		std::pair<ColorSpace, const char *>{ ColorSpace::Smpte170m, "SMPTE170M" },
		std::pair<ColorSpace, const char *>{ ColorSpace::Rec709, "Rec709" },
		std::pair<ColorSpace, const char *>{ ColorSpace::Rec2020, "Rec2020" },
	};

but requires double braces when not specifying the type of the elements
explicitly. I'm sure there's an explanation.

We could make it a bit nicer-looking with

	static const std::array<std::pair<ColorSpace, const char *>, 6> colorSpaceNames = { {
		{ ColorSpace::Raw, "RAW" },
		{ ColorSpace::Jpeg, "JPEG" },
		{ ColorSpace::Srgb, "sRGB" },
		{ ColorSpace::Smpte170m, "SMPTE170M" },
		{ ColorSpace::Rec709, "Rec709" },
		{ ColorSpace::Rec2020, "Rec2020" },
	} };

> +	auto it = std::find_if(colorSpaceNames.begin(), colorSpaceNames.end(),
> +			       [this](const auto &item) {
> +				       return *this == item.first;
> +			       });
> +	if (it != colorSpaceNames.end())
> +		return std::string(it->second);
> +
> +	/* Assemble a name made of the constituent fields. */
> +
> +	static const std::map<Primaries, std::string> primariesNames = {
> +		{ Primaries::Raw, "Raw" },
> +		{ Primaries::Smpte170m, "Smpte170m" },

SMPTE170M here too.

> +		{ Primaries::Rec709, "Rec709" },
> +		{ Primaries::Rec2020, "Rec2020" },
> +	};
> +	static const std::map<TransferFunction, std::string> transferNames = {
> +		{ TransferFunction::Linear, "Linear" },
> +		{ TransferFunction::Srgb, "Srgb" },

And sRGB.

> +		{ TransferFunction::Rec709, "Rec709" },
> +	};
> +	static const std::map<YcbcrEncoding, std::string> encodingNames = {
> +		{ YcbcrEncoding::None, "None" },
> +		{ YcbcrEncoding::Rec601, "Rec601" },
> +		{ YcbcrEncoding::Rec709, "Rec709" },
> +		{ YcbcrEncoding::Rec2020, "Rec2020" },
> +	};
> +	static const std::map<Range, std::string> rangeNames = {
> +		{ Range::Full, "Full" },
> +		{ Range::Limited, "Limited" },
> +	};
> +
> +	auto itPrimaries = primariesNames.find(primaries);
> +	std::string primariesName =
> +		itPrimaries == primariesNames.end() ? "Invalid" : itPrimaries->second;
> +
> +	auto itTransfer = transferNames.find(transferFunction);
> +	std::string transferName =
> +		itTransfer == transferNames.end() ? "Invalid" : itTransfer->second;
> +
> +	auto itEncoding = encodingNames.find(ycbcrEncoding);
> +	std::string encodingName =
> +		itEncoding == encodingNames.end() ? "Invalid" : itEncoding->second;
> +
> +	auto itRange = rangeNames.find(range);
> +	std::string rangeName =
> +		itRange == rangeNames.end() ? "Invalid" : itRange->second;
> +
> +	std::stringstream ss;
> +	ss << primariesName << "/" << transferName << "/" << encodingName << "/" << rangeName;
> +
> +	return ss.str();
> +}
> +
> +/**
> + * \brief Assemble and return a readable string representation of an
> + * optional ColorSpace
> + *
> + * This is a convenience helper to easily obtain a string representation
> + * for a ColorSpace in parts of the libcamera API where it is stored in a
> + * std::optional<>. If the ColorSpace is set, this function returns
> + * \a colorSpace.toString(), otherwise it returns "Unset".
> + *
> + * \return A string describing the optional ColorSpace
> + */
> +std::string ColorSpace::toString(const std::optional<ColorSpace> &colorSpace)
> +{
> +	if (!colorSpace)
> +		return "Unset";
> +
> +	return colorSpace->toString();
> +}
> +
> +/**
> + * \var ColorSpace::primaries
> + * \brief The color primaries of this color space
> + */
> +
> +/**
> + * \var ColorSpace::ycbcrEncoding
> + * \brief The Y'CbCr encoding used by this color space
> + */
> +
> +/**
> + * \var ColorSpace::transferFunction
> + * \brief The transfer function used by this color space
> + */
> +
> +/**
> + * \var ColorSpace::range
> + * \brief The pixel range used with by color space
> + */
> +
> +/**
> + * \brief A constant representing a raw color space (from a sensor)
> + */
> +const ColorSpace ColorSpace::Raw = {
> +	Primaries::Raw,
> +	TransferFunction::Linear,
> +	YcbcrEncoding::None,
> +	Range::Full
> +};
> +
> +/**
> + * \brief A constant representing the JPEG color space used for
> + * encoding JPEG images
> + */
> +const ColorSpace ColorSpace::Jpeg = {
> +	Primaries::Rec709,
> +	TransferFunction::Srgb,
> +	YcbcrEncoding::Rec601,
> +	Range::Full
> +};
> +
> +/**
> + * \brief A constant representing the sRGB color space.

Blank line after \brief, or it won't be split to a different paragraph.

If you're fine with these small changes, I'll apply them when merging
the series.

> + * This is identical to the JPEG color space except that the Y'CbCr
> + * range is limited rather than full.
> + */
> +const ColorSpace ColorSpace::Srgb = {
> +	Primaries::Rec709,
> +	TransferFunction::Srgb,
> +	YcbcrEncoding::Rec601,
> +	Range::Limited
> +};
> +
> +/**
> + * \brief A constant representing the SMPTE170M color space
> + */
> +const ColorSpace ColorSpace::Smpte170m = {
> +	Primaries::Smpte170m,
> +	TransferFunction::Rec709,
> +	YcbcrEncoding::Rec601,
> +	Range::Limited
> +};
> +
> +/**
> + * \brief A constant representing the Rec.709 color space
> + */
> +const ColorSpace ColorSpace::Rec709 = {
> +	Primaries::Rec709,
> +	TransferFunction::Rec709,
> +	YcbcrEncoding::Rec709,
> +	Range::Limited
> +};
> +
> +/**
> + * \brief A constant representing the Rec.2020 color space
> + */
> +const ColorSpace ColorSpace::Rec2020 = {
> +	Primaries::Rec2020,
> +	TransferFunction::Rec709,
> +	YcbcrEncoding::Rec2020,
> +	Range::Limited
> +};
> +
> +/**
> + * \brief Compare color spaces for equality
> + * \return True if the two color spaces are identical, false otherwise
> + */
> +bool operator==(const ColorSpace &lhs, const ColorSpace &rhs)
> +{
> +	return lhs.primaries == rhs.primaries &&
> +	       lhs.transferFunction == rhs.transferFunction &&
> +	       lhs.ycbcrEncoding == rhs.ycbcrEncoding &&
> +	       lhs.range == rhs.range;
> +}
> +
> +/**
> + * \fn bool operator!=(const ColorSpace &lhs, const ColorSpace &rhs)
> + * \brief Compare color spaces for inequality
> + * \return True if the two color spaces are not identical, false otherwise
> + */
> +
> +} /* namespace libcamera */
> diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
> index 2e54cc04..4045e24e 100644
> --- a/src/libcamera/meson.build
> +++ b/src/libcamera/meson.build
> @@ -9,6 +9,7 @@ libcamera_sources = files([
>      'camera_manager.cpp',
>      'camera_sensor.cpp',
>      'camera_sensor_properties.cpp',
> +    'color_space.cpp',
>      'controls.cpp',
>      'control_serializer.cpp',
>      'control_validator.cpp',

Patch
diff mbox series

diff --git a/include/libcamera/color_space.h b/include/libcamera/color_space.h
new file mode 100644
index 00000000..086c56c1
--- /dev/null
+++ b/include/libcamera/color_space.h
@@ -0,0 +1,70 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2021, Raspberry Pi (Trading) Limited
+ *
+ * color_space.h - color space definitions
+ */
+
+#pragma once
+
+#include <optional>
+#include <string>
+
+namespace libcamera {
+
+class ColorSpace
+{
+public:
+	enum class Primaries {
+		Raw,
+		Smpte170m,
+		Rec709,
+		Rec2020,
+	};
+
+	enum class TransferFunction {
+		Linear,
+		Srgb,
+		Rec709,
+	};
+
+	enum class YcbcrEncoding {
+		None,
+		Rec601,
+		Rec709,
+		Rec2020,
+	};
+
+	enum class Range {
+		Full,
+		Limited,
+	};
+
+	constexpr ColorSpace(Primaries p, TransferFunction t, YcbcrEncoding e, Range r)
+		: primaries(p), transferFunction(t), ycbcrEncoding(e), range(r)
+	{
+	}
+
+	static const ColorSpace Raw;
+	static const ColorSpace Jpeg;
+	static const ColorSpace Srgb;
+	static const ColorSpace Smpte170m;
+	static const ColorSpace Rec709;
+	static const ColorSpace Rec2020;
+
+	Primaries primaries;
+	TransferFunction transferFunction;
+	YcbcrEncoding ycbcrEncoding;
+	Range range;
+
+	std::string toString() const;
+	static std::string toString(const std::optional<ColorSpace> &colorSpace);
+};
+
+bool operator==(const ColorSpace &lhs, const ColorSpace &rhs);
+static inline bool operator!=(const ColorSpace &lhs, const ColorSpace &rhs)
+{
+	return !(lhs == rhs);
+}
+
+} /* namespace libcamera */
diff --git a/include/libcamera/meson.build b/include/libcamera/meson.build
index 5f42977c..fd767b11 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_space.h',
     'controls.h',
     'framebuffer.h',
     'framebuffer_allocator.h',
diff --git a/src/libcamera/color_space.cpp b/src/libcamera/color_space.cpp
new file mode 100644
index 00000000..6c587068
--- /dev/null
+++ b/src/libcamera/color_space.cpp
@@ -0,0 +1,318 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2021, Raspberry Pi (Trading) Limited
+ *
+ * color_space.cpp - color spaces.
+ */
+
+#include <libcamera/color_space.h>
+
+#include <algorithm>
+#include <array>
+#include <map>
+#include <sstream>
+#include <utility>
+
+/**
+ * \file color_space.h
+ * \brief Class and enums to represent color spaces
+ */
+
+namespace libcamera {
+
+/**
+ * \class ColorSpace
+ * \brief Class to describe a color space
+ *
+ * The ColorSpace class defines the color primaries, the Y'CbCr encoding,
+ * the transfer function associated with the color space, and the range
+ * (sometimes also referred to as the quantisation) of the color space.
+ *
+ * Certain combinations of these fields form well-known standard color
+ * spaces such as "JPEG" or "REC709".
+ *
+ * In the strictest sense a "color space" formally only refers to the
+ * color primaries and white point. Here, however, the ColorSpace class
+ * adopts the common broader usage that includes the transfer function,
+ * Y'CbCr encoding method and quantisation.
+ *
+ * For more information on the specific color spaces described here, please
+ * see:
+ *
+ * - <a href="https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/colorspaces-details.html#col-srgb">sRGB</a>
+ * - <a href="https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/colorspaces-details.html#col-jpeg">JPEG</a>
+ * - <a href="https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/colorspaces-details.html#col-smpte-170m">SMPTE 170M</a>
+ * - <a href="https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/colorspaces-details.html#col-rec709">Rec.709</a>
+ * - <a href="https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/colorspaces-details.html#col-bt2020">Rec.2020</a>
+ */
+
+/**
+ * \enum ColorSpace::Primaries
+ * \brief The color primaries for this color space
+ *
+ * \var ColorSpace::Primaries::Raw
+ * \brief These are raw colors directly from a sensor, the primaries are
+ * unspecified
+ *
+ * \var ColorSpace::Primaries::Smpte170m
+ * \brief SMPTE 170M color primaries
+ *
+ * \var ColorSpace::Primaries::Rec709
+ * \brief Rec.709 color primaries
+ *
+ * \var ColorSpace::Primaries::Rec2020
+ * \brief Rec.2020 color primaries
+ */
+
+/**
+ * \enum ColorSpace::TransferFunction
+ * \brief The transfer function used for this color space
+ *
+ * \var ColorSpace::TransferFunction::Linear
+ * \brief This color space uses a linear (identity) transfer function
+ *
+ * \var ColorSpace::TransferFunction::Srgb
+ * \brief sRGB transfer function
+ *
+ * \var ColorSpace::TransferFunction::Rec709
+ * \brief Rec.709 transfer function
+ */
+
+/**
+ * \enum ColorSpace::YcbcrEncoding
+ * \brief The Y'CbCr encoding
+ *
+ * \var ColorSpace::YcbcrEncoding::None
+ * \brief There is no defined Y'CbCr encoding
+ *
+ * \var ColorSpace::YcbcrEncoding::Rec601
+ * \brief Rec.601 Y'CbCr encoding
+ *
+ * \var ColorSpace::YcbcrEncoding::Rec709
+ * \brief Rec.709 Y'CbCr encoding
+ *
+ * \var ColorSpace::YcbcrEncoding::Rec2020
+ * \brief Rec.2020 Y'CbCr encoding
+ */
+
+/**
+ * \enum ColorSpace::Range
+ * \brief The range (sometimes "quantisation") for this color space
+ *
+ * \var ColorSpace::Range::Full
+ * \brief This color space uses full range pixel values
+ *
+ * \var ColorSpace::Range::Limited
+ * \brief This color space uses limited range pixel values, being
+ * 16 to 235 for Y' and 16 to 240 for Cb and Cr (8 bits per sample)
+ * or 64 to 940 for Y' and 16 to 960 for Cb and Cr (10 bits)
+ */
+
+/**
+ * \fn ColorSpace::ColorSpace(Primaries p, TransferFunction t, Encoding e, Range r)
+ * \brief Construct a ColorSpace from explicit values
+ * \param[in] p The color primaries
+ * \param[in] t The transfer function for the color space
+ * \param[in] e The Y'CbCr encoding
+ * \param[in] r The range of the pixel values in this color space
+ */
+
+/**
+ * \brief Assemble and return a readable string representation of the
+ * ColorSpace
+ *
+ * If the color space matches a standard ColorSpace (such as ColorSpace::Jpeg)
+ * then the short name of the color space ("JPEG") is returned. Otherwise
+ * the four constituent parts of the ColorSpace are assembled into a longer
+ * string.
+ *
+ * \return A string describing the ColorSpace
+ */
+std::string ColorSpace::toString() const
+{
+	/* Print out a brief name only for standard color spaces. */
+
+	static const std::array<std::pair<ColorSpace, const char *>, 6> colorSpaceNames = {
+		{
+			{ ColorSpace::Raw, "RAW" },
+			{ ColorSpace::Jpeg, "JPEG" },
+			{ ColorSpace::Srgb, "sRGB" },
+			{ ColorSpace::Smpte170m, "SMPTE170M" },
+			{ ColorSpace::Rec709, "Rec709" },
+			{ ColorSpace::Rec2020, "Rec2020" },
+		}
+	};
+	auto it = std::find_if(colorSpaceNames.begin(), colorSpaceNames.end(),
+			       [this](const auto &item) {
+				       return *this == item.first;
+			       });
+	if (it != colorSpaceNames.end())
+		return std::string(it->second);
+
+	/* Assemble a name made of the constituent fields. */
+
+	static const std::map<Primaries, std::string> primariesNames = {
+		{ Primaries::Raw, "Raw" },
+		{ Primaries::Smpte170m, "Smpte170m" },
+		{ Primaries::Rec709, "Rec709" },
+		{ Primaries::Rec2020, "Rec2020" },
+	};
+	static const std::map<TransferFunction, std::string> transferNames = {
+		{ TransferFunction::Linear, "Linear" },
+		{ TransferFunction::Srgb, "Srgb" },
+		{ TransferFunction::Rec709, "Rec709" },
+	};
+	static const std::map<YcbcrEncoding, std::string> encodingNames = {
+		{ YcbcrEncoding::None, "None" },
+		{ YcbcrEncoding::Rec601, "Rec601" },
+		{ YcbcrEncoding::Rec709, "Rec709" },
+		{ YcbcrEncoding::Rec2020, "Rec2020" },
+	};
+	static const std::map<Range, std::string> rangeNames = {
+		{ Range::Full, "Full" },
+		{ Range::Limited, "Limited" },
+	};
+
+	auto itPrimaries = primariesNames.find(primaries);
+	std::string primariesName =
+		itPrimaries == primariesNames.end() ? "Invalid" : itPrimaries->second;
+
+	auto itTransfer = transferNames.find(transferFunction);
+	std::string transferName =
+		itTransfer == transferNames.end() ? "Invalid" : itTransfer->second;
+
+	auto itEncoding = encodingNames.find(ycbcrEncoding);
+	std::string encodingName =
+		itEncoding == encodingNames.end() ? "Invalid" : itEncoding->second;
+
+	auto itRange = rangeNames.find(range);
+	std::string rangeName =
+		itRange == rangeNames.end() ? "Invalid" : itRange->second;
+
+	std::stringstream ss;
+	ss << primariesName << "/" << transferName << "/" << encodingName << "/" << rangeName;
+
+	return ss.str();
+}
+
+/**
+ * \brief Assemble and return a readable string representation of an
+ * optional ColorSpace
+ *
+ * This is a convenience helper to easily obtain a string representation
+ * for a ColorSpace in parts of the libcamera API where it is stored in a
+ * std::optional<>. If the ColorSpace is set, this function returns
+ * \a colorSpace.toString(), otherwise it returns "Unset".
+ *
+ * \return A string describing the optional ColorSpace
+ */
+std::string ColorSpace::toString(const std::optional<ColorSpace> &colorSpace)
+{
+	if (!colorSpace)
+		return "Unset";
+
+	return colorSpace->toString();
+}
+
+/**
+ * \var ColorSpace::primaries
+ * \brief The color primaries of this color space
+ */
+
+/**
+ * \var ColorSpace::ycbcrEncoding
+ * \brief The Y'CbCr encoding used by this color space
+ */
+
+/**
+ * \var ColorSpace::transferFunction
+ * \brief The transfer function used by this color space
+ */
+
+/**
+ * \var ColorSpace::range
+ * \brief The pixel range used with by color space
+ */
+
+/**
+ * \brief A constant representing a raw color space (from a sensor)
+ */
+const ColorSpace ColorSpace::Raw = {
+	Primaries::Raw,
+	TransferFunction::Linear,
+	YcbcrEncoding::None,
+	Range::Full
+};
+
+/**
+ * \brief A constant representing the JPEG color space used for
+ * encoding JPEG images
+ */
+const ColorSpace ColorSpace::Jpeg = {
+	Primaries::Rec709,
+	TransferFunction::Srgb,
+	YcbcrEncoding::Rec601,
+	Range::Full
+};
+
+/**
+ * \brief A constant representing the sRGB color space.
+ * This is identical to the JPEG color space except that the Y'CbCr
+ * range is limited rather than full.
+ */
+const ColorSpace ColorSpace::Srgb = {
+	Primaries::Rec709,
+	TransferFunction::Srgb,
+	YcbcrEncoding::Rec601,
+	Range::Limited
+};
+
+/**
+ * \brief A constant representing the SMPTE170M color space
+ */
+const ColorSpace ColorSpace::Smpte170m = {
+	Primaries::Smpte170m,
+	TransferFunction::Rec709,
+	YcbcrEncoding::Rec601,
+	Range::Limited
+};
+
+/**
+ * \brief A constant representing the Rec.709 color space
+ */
+const ColorSpace ColorSpace::Rec709 = {
+	Primaries::Rec709,
+	TransferFunction::Rec709,
+	YcbcrEncoding::Rec709,
+	Range::Limited
+};
+
+/**
+ * \brief A constant representing the Rec.2020 color space
+ */
+const ColorSpace ColorSpace::Rec2020 = {
+	Primaries::Rec2020,
+	TransferFunction::Rec709,
+	YcbcrEncoding::Rec2020,
+	Range::Limited
+};
+
+/**
+ * \brief Compare color spaces for equality
+ * \return True if the two color spaces are identical, false otherwise
+ */
+bool operator==(const ColorSpace &lhs, const ColorSpace &rhs)
+{
+	return lhs.primaries == rhs.primaries &&
+	       lhs.transferFunction == rhs.transferFunction &&
+	       lhs.ycbcrEncoding == rhs.ycbcrEncoding &&
+	       lhs.range == rhs.range;
+}
+
+/**
+ * \fn bool operator!=(const ColorSpace &lhs, const ColorSpace &rhs)
+ * \brief Compare color spaces for inequality
+ * \return True if the two color spaces are not identical, false otherwise
+ */
+
+} /* namespace libcamera */
diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
index 2e54cc04..4045e24e 100644
--- a/src/libcamera/meson.build
+++ b/src/libcamera/meson.build
@@ -9,6 +9,7 @@  libcamera_sources = files([
     'camera_manager.cpp',
     'camera_sensor.cpp',
     'camera_sensor_properties.cpp',
+    'color_space.cpp',
     'controls.cpp',
     'control_serializer.cpp',
     'control_validator.cpp',