Skip to content

Commit 9229cf3

Browse files
authored
feat: Auto convert between oiio:ColorSpace and CICP attributes in I/O (#4964)
When reading image files with CICP metadata, automatically set the corresponding "oiio:ColorSpace". When writing files that support CICP and no other colorspace metadata can represent "oiio:ColorSpace", automatically write CICP metadata. Setting "oiio:ColorSpace" on read prefers scene referred over display referred color spaces, changing existing behavior as little as possible. The alternative would have been to interpret the presence of CICP metadata as an indication that the image is likely display referred, which might be reasonable too. Either way it's a guess. There is no automatic mapping from `g22_rec709_display` to CICP currently to keep behavior unchanged, as this is often not what you want and further discussion is needed to decided on the right behavior. Also add new ColorConfig `get_cicp` and `get_color_interop_id` API functions to share logic between file formats. ## Tests Tests were updated, the auto detected color space name appears in the output. Commands like these should also do the right thing automatically. ``` oiiotool in.exr --ociodisplay "Rec.2100-PQ - Display" "ACES 2.0 - HDR 1000 nits (P3 D65)" -o out.avif oiiotool out.avif --autocc -o out.exr ``` --------- Signed-off-by: Brecht Van Lommel <[email protected]>
1 parent fd5ac81 commit 9229cf3

File tree

22 files changed

+412
-20
lines changed

22 files changed

+412
-20
lines changed

src/doc/pythonbindings.rst

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3888,6 +3888,73 @@ sections) work with deep inputs::
38883888
38893889
|
38903890
3891+
.. _sec-pythoncolorconfig:
3892+
3893+
3894+
ColorConfig
3895+
===========
3896+
3897+
The `ColorConfig` class that represents the set of color transformations that
3898+
are allowed.
3899+
3900+
If OpenColorIO is enabled at build time, this configuration is loaded at
3901+
runtime, allowing the user to have complete control of all color transformation
3902+
math. See the
3903+
`OpenColorIO documentation <https://opencolorio.readthedocs.io>`_ for details.
3904+
3905+
If OpenColorIO is not enabled at build time, a generic color configuration
3906+
is provided for minimal color support.
3907+
3908+
..
3909+
TODO: The documentation for this class is incomplete.
3910+
3911+
.. py:method:: get_cicp (colorspace)
3912+
3913+
Find CICP code corresponding to the colorspace.
3914+
Return a sequence of 4 ints, or None if not found.
3915+
3916+
Example:
3917+
3918+
.. code-block:: python
3919+
3920+
colorconfig = oiio.ColorConfig()
3921+
cicp = colorconfig.get_cicp("pq_rec2020_display")
3922+
if cicp:
3923+
primaries, transfer, matrix, color_range = cicp
3924+
3925+
This function was added in OpenImageIO 3.1.
3926+
3927+
3928+
.. py:method:: get_color_interop_id (colorspace)
3929+
3930+
Find color interop ID for the given colorspace.
3931+
Returns empty string if not found.
3932+
3933+
Example:
3934+
3935+
.. code-block:: python
3936+
3937+
colorconfig = oiio.ColorConfig()
3938+
interop_id = colorconfig.get_color_interop_id("Rec.2100-PQ - Display")
3939+
3940+
This function was added in OpenImageIO 3.1.
3941+
3942+
3943+
.. py:method:: get_color_interop_id (cicp)
3944+
3945+
Find color interop ID corresponding to the CICP code.
3946+
Returns empty string if not found.
3947+
3948+
Example:
3949+
3950+
.. code-block:: python
3951+
3952+
colorconfig = oiio.ColorConfig()
3953+
interop_id = colorconfig.get_color_interop_id([9, 16, 9, 1])
3954+
3955+
This function was added in OpenImageIO 3.1.
3956+
3957+
38913958
.. _sec-pythonmiscapi:
38923959
38933960
Miscellaneous Utilities

src/ffmpeg.imageio/ffmpeginput.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ receive_frame(AVCodecContext* avctx, AVFrame* picture, AVPacket* avpkt)
7171

7272

7373

74+
#include <OpenImageIO/color.h>
7475
#include <OpenImageIO/imageio.h>
7576
#include <iostream>
7677
#include <mutex>
@@ -549,6 +550,11 @@ FFmpegInput::open(const std::string& name, ImageSpec& spec)
549550
m_codec_context->colorspace,
550551
m_codec_context->color_range == AVCOL_RANGE_MPEG ? 0 : 1 };
551552
m_spec.attribute("CICP", TypeDesc(TypeDesc::INT, 4), cicp);
553+
const ColorConfig& colorconfig(ColorConfig::default_colorconfig());
554+
string_view interop_id = colorconfig.get_color_interop_id(cicp);
555+
if (!interop_id.empty())
556+
m_spec.attribute("oiio:ColorSpace", interop_id);
557+
552558
m_nsubimages = m_frames;
553559
spec = m_spec;
554560
m_filename = name;

src/heif.imageio/heifinput.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33
// https://github.com/AcademySoftwareFoundation/OpenImageIO
44

5+
#include <OpenImageIO/color.h>
56
#include <OpenImageIO/filesystem.h>
67
#include <OpenImageIO/fmath.h>
78
#include <OpenImageIO/imageio.h>
@@ -292,6 +293,11 @@ HeifInput::seek_subimage(int subimage, int miplevel)
292293
int(nclx->matrix_coefficients),
293294
int(nclx->full_range_flag ? 1 : 0) };
294295
m_spec.attribute("CICP", TypeDesc(TypeDesc::INT, 4), cicp);
296+
const ColorConfig& colorconfig(
297+
ColorConfig::default_colorconfig());
298+
string_view interop_id = colorconfig.get_color_interop_id(cicp);
299+
if (!interop_id.empty())
300+
m_spec.attribute("oiio:ColorSpace", interop_id);
295301
}
296302
heif_nclx_color_profile_free(nclx);
297303
}

src/heif.imageio/heifoutput.cpp

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// https://github.com/AcademySoftwareFoundation/OpenImageIO
44

55

6+
#include <OpenImageIO/color.h>
67
#include <OpenImageIO/filesystem.h>
78
#include <OpenImageIO/fmath.h>
89
#include <OpenImageIO/imageio.h>
@@ -249,10 +250,13 @@ HeifOutput::close()
249250
std::unique_ptr<heif_color_profile_nclx,
250251
void (*)(heif_color_profile_nclx*)>
251252
nclx(heif_nclx_color_profile_alloc(), heif_nclx_color_profile_free);
252-
const ParamValue* p = m_spec.find_attribute("CICP",
253-
TypeDesc(TypeDesc::INT, 4));
254-
if (p) {
255-
const int* cicp = static_cast<const int*>(p->data());
253+
const ColorConfig& colorconfig(ColorConfig::default_colorconfig());
254+
const ParamValue* p = m_spec.find_attribute("CICP",
255+
TypeDesc(TypeDesc::INT, 4));
256+
string_view colorspace = m_spec.get_string_attribute("oiio:ColorSpace");
257+
cspan<int> cicp = (p) ? p->as_cspan<int>()
258+
: colorconfig.get_cicp(colorspace);
259+
if (!cicp.empty()) {
256260
nclx->color_primaries = heif_color_primaries(cicp[0]);
257261
nclx->transfer_characteristics = heif_transfer_characteristics(
258262
cicp[1]);

src/include/OpenImageIO/color.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,24 @@ class OIIO_API ColorConfig {
402402
bool equivalent(string_view color_space,
403403
string_view other_color_space) const;
404404

405+
/// Find CICP code corresponding to the colorspace.
406+
/// Return a cspan of 4 ints, or an empty span if not found.
407+
///
408+
/// @version 3.1
409+
cspan<int> get_cicp(string_view colorspace) const;
410+
411+
/// Find color interop ID for the given colorspace.
412+
/// Returns empty string if not found.
413+
///
414+
/// @version 3.1
415+
string_view get_color_interop_id(string_view colorspace) const;
416+
417+
/// Find color interop ID corresponding to the CICP code.
418+
/// Returns empty string if not found.
419+
///
420+
/// @version 3.1
421+
string_view get_color_interop_id(const int cicp[4]) const;
422+
405423
/// Return a filename or other identifier for the config we're using.
406424
std::string configname() const;
407425

src/libOpenImageIO/color_ocio.cpp

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1997,6 +1997,173 @@ ColorConfig::parseColorSpaceFromString(string_view str) const
19971997
}
19981998

19991999

2000+
//////////////////////////////////////////////////////////////////////////
2001+
//
2002+
// Color Interop ID
2003+
2004+
namespace {
2005+
enum class CICPPrimaries : int {
2006+
Rec709 = 1,
2007+
Rec2020 = 9,
2008+
XYZD65 = 10,
2009+
P3D65 = 12,
2010+
};
2011+
2012+
enum class CICPTransfer : int {
2013+
BT709 = 1,
2014+
Gamma22 = 4,
2015+
Linear = 8,
2016+
sRGB = 13,
2017+
PQ = 16,
2018+
Gamma26 = 17,
2019+
HLG = 18,
2020+
};
2021+
2022+
enum class CICPMatrix : int {
2023+
RGB = 0,
2024+
BT709 = 1,
2025+
Unspecified = 2,
2026+
Rec2020_NCL = 9,
2027+
Rec2020_CL = 10,
2028+
};
2029+
2030+
enum class CICPRange : int {
2031+
Narrow = 0,
2032+
Full = 1,
2033+
};
2034+
2035+
struct ColorInteropID {
2036+
constexpr ColorInteropID(const char* interop_id)
2037+
: interop_id(interop_id)
2038+
, cicp({ 0, 0, 0, 0 })
2039+
, has_cicp(false)
2040+
{
2041+
}
2042+
2043+
constexpr ColorInteropID(const char* interop_id, CICPPrimaries primaries,
2044+
CICPTransfer transfer, CICPMatrix matrix)
2045+
: interop_id(interop_id)
2046+
, cicp({ int(primaries), int(transfer), int(matrix),
2047+
int(CICPRange::Full) })
2048+
, has_cicp(true)
2049+
{
2050+
}
2051+
2052+
const char* interop_id;
2053+
std::array<int, 4> cicp;
2054+
bool has_cicp;
2055+
};
2056+
2057+
// Mapping between color interop ID and CICP, based on Color Interop Forum
2058+
// recommendations.
2059+
constexpr ColorInteropID color_interop_ids[] = {
2060+
// Scene referred interop IDs first so they are the default in automatic
2061+
// conversion from CICP to interop ID. Some are not display color spaces
2062+
// at all, but can be represented by CICP anyway.
2063+
{ "lin_ap1_scene" },
2064+
{ "lin_ap0_scene" },
2065+
{ "lin_rec709_scene", CICPPrimaries::Rec709, CICPTransfer::Linear,
2066+
CICPMatrix::BT709 },
2067+
{ "lin_p3d65_scene", CICPPrimaries::P3D65, CICPTransfer::Linear,
2068+
CICPMatrix::BT709 },
2069+
{ "lin_rec2020_scene", CICPPrimaries::Rec2020, CICPTransfer::Linear,
2070+
CICPMatrix::Rec2020_CL },
2071+
{ "lin_adobergb_scene" },
2072+
{ "lin_ciexyzd65_scene", CICPPrimaries::XYZD65, CICPTransfer::Linear,
2073+
CICPMatrix::Unspecified },
2074+
{ "srgb_rec709_scene", CICPPrimaries::Rec709, CICPTransfer::sRGB,
2075+
CICPMatrix::BT709 },
2076+
{ "g22_rec709_scene", CICPPrimaries::Rec709, CICPTransfer::Gamma22,
2077+
CICPMatrix::BT709 },
2078+
{ "g18_rec709_scene" },
2079+
{ "srgb_ap1_scene" },
2080+
{ "g22_ap1_scene" },
2081+
{ "srgb_p3d65_scene", CICPPrimaries::P3D65, CICPTransfer::sRGB,
2082+
CICPMatrix::BT709 },
2083+
{ "g22_adobergb_scene" },
2084+
{ "data" },
2085+
{ "unknown" },
2086+
2087+
// Display referred interop IDs.
2088+
{ "srgb_rec709_display", CICPPrimaries::Rec709, CICPTransfer::sRGB,
2089+
CICPMatrix::BT709 },
2090+
{ "g24_rec709_display", CICPPrimaries::Rec709, CICPTransfer::BT709,
2091+
CICPMatrix::BT709 },
2092+
{ "srgb_p3d65_display", CICPPrimaries::P3D65, CICPTransfer::sRGB,
2093+
CICPMatrix::BT709 },
2094+
{ "srgbe_p3d65_display", CICPPrimaries::P3D65, CICPTransfer::sRGB,
2095+
CICPMatrix::BT709 },
2096+
{ "pq_p3d65_display", CICPPrimaries::P3D65, CICPTransfer::PQ,
2097+
CICPMatrix::Rec2020_NCL },
2098+
{ "pq_rec2020_display", CICPPrimaries::Rec2020, CICPTransfer::PQ,
2099+
CICPMatrix::Rec2020_NCL },
2100+
{ "hlg_rec2020_display", CICPPrimaries::Rec2020, CICPTransfer::HLG,
2101+
CICPMatrix::Rec2020_NCL },
2102+
// No CICP mapping to keep previous behavior unchanged, as Gamma 2.2
2103+
// display is more likely meant to be written as sRGB. On read the
2104+
// scene referred interop ID will be used.
2105+
{ "g22_rec709_display",
2106+
/* CICPPrimaries::Rec709, CICPTransfer::Gamma22, CICPMatrix::BT709 */ },
2107+
// No CICP code for Adobe RGB primaries.
2108+
{ "g22_adobergb_display" },
2109+
{ "g26_p3d65_display", CICPPrimaries::P3D65, CICPTransfer::Gamma26,
2110+
CICPMatrix::BT709 },
2111+
{ "g26_xyzd65_display", CICPPrimaries::XYZD65, CICPTransfer::Gamma26,
2112+
CICPMatrix::Unspecified },
2113+
{ "pq_xyzd65_display", CICPPrimaries::XYZD65, CICPTransfer::PQ,
2114+
CICPMatrix::Unspecified },
2115+
};
2116+
} // namespace
2117+
2118+
string_view
2119+
ColorConfig::get_color_interop_id(string_view colorspace) const
2120+
{
2121+
if (colorspace.empty())
2122+
return "";
2123+
#if OCIO_VERSION_HEX >= MAKE_OCIO_VERSION_HEX(2, 5, 0)
2124+
if (getImpl()->config_ && !disable_ocio) {
2125+
OCIO::ConstColorSpaceRcPtr c = getImpl()->config_->getColorSpace(
2126+
std::string(resolve(colorspace)).c_str());
2127+
const char* interop_id = (c) ? c->getInteropID() : nullptr;
2128+
if (interop_id) {
2129+
return interop_id;
2130+
}
2131+
}
2132+
#endif
2133+
for (const ColorInteropID& interop : color_interop_ids) {
2134+
if (equivalent(colorspace, interop.interop_id)) {
2135+
return interop.interop_id;
2136+
}
2137+
}
2138+
return "";
2139+
}
2140+
2141+
string_view
2142+
ColorConfig::get_color_interop_id(const int cicp[4]) const
2143+
{
2144+
for (const ColorInteropID& interop : color_interop_ids) {
2145+
if (interop.has_cicp && interop.cicp[0] == cicp[0]
2146+
&& interop.cicp[1] == cicp[1]) {
2147+
return interop.interop_id;
2148+
}
2149+
}
2150+
return "";
2151+
}
2152+
2153+
cspan<int>
2154+
ColorConfig::get_cicp(string_view colorspace) const
2155+
{
2156+
string_view interop_id = get_color_interop_id(colorspace);
2157+
if (!interop_id.empty()) {
2158+
for (const ColorInteropID& interop : color_interop_ids) {
2159+
if (interop.has_cicp && interop_id == interop.interop_id) {
2160+
return interop.cicp;
2161+
}
2162+
}
2163+
}
2164+
return cspan<int>();
2165+
}
2166+
20002167

20012168
//////////////////////////////////////////////////////////////////////////
20022169
//

0 commit comments

Comments
 (0)