Skip to content

Commit 3d377bd

Browse files
committed
feat: Auto convert between oiio:ColorSpace and CICP attributes in I/O
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. Also add new ColorConfig get_cicp and get_color_interop_id API functions to share logic between file formats. Signed-off-by: Brecht Van Lommel <[email protected]>
1 parent 4a13f4b commit 3d377bd

File tree

19 files changed

+271
-20
lines changed

19 files changed

+271
-20
lines changed

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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,18 @@ 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 corresponding to the CICP code.
412+
/// Returns empty string if not found.
413+
///
414+
/// @version 3.1
415+
string_view get_color_interop_id(const int cicp[4]) const;
416+
405417
/// Return a filename or other identifier for the config we're using.
406418
std::string configname() const;
407419

src/libOpenImageIO/color_ocio.cpp

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1997,6 +1997,127 @@ 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, CICPPrimaries primaries,
2037+
CICPTransfer transfer, CICPMatrix matrix)
2038+
: interop_id(interop_id)
2039+
, cicp({ int(primaries), int(transfer), int(matrix),
2040+
int(CICPRange::Full) })
2041+
{
2042+
}
2043+
2044+
const char* interop_id;
2045+
std::array<int, 4> cicp;
2046+
};
2047+
2048+
// Mapping between color interop ID and CICP, based on Color Interop Forum
2049+
// recommendations.
2050+
constexpr ColorInteropID color_interop_ids[] = {
2051+
// Scene referred interop IDs first so they are the default in automatic
2052+
// conversion from CICP to interop ID.
2053+
{ "srgb_rec709_scene", CICPPrimaries::Rec709, CICPTransfer::sRGB,
2054+
CICPMatrix::BT709 },
2055+
{ "srgb_p3d65_scene", CICPPrimaries::P3D65, CICPTransfer::sRGB,
2056+
CICPMatrix::BT709 },
2057+
{ "g22_rec709_scene", CICPPrimaries::Rec709, CICPTransfer::Gamma22,
2058+
CICPMatrix::BT709 },
2059+
// These are not display color spaces at all, but can be represented by CICP.
2060+
{ "lin_rec709_scene", CICPPrimaries::Rec709, CICPTransfer::Linear,
2061+
CICPMatrix::BT709 },
2062+
{ "lin_p3d65_scene", CICPPrimaries::P3D65, CICPTransfer::Linear,
2063+
CICPMatrix::BT709 },
2064+
{ "lin_rec2020_scene", CICPPrimaries::Rec2020, CICPTransfer::Linear,
2065+
CICPMatrix::Rec2020_CL },
2066+
{ "lin_ciexyzd65_scene", CICPPrimaries::XYZD65, CICPTransfer::Linear,
2067+
CICPMatrix::Unspecified },
2068+
2069+
// Display referred interop IDs.
2070+
{ "srgb_rec709_display", CICPPrimaries::Rec709, CICPTransfer::sRGB,
2071+
CICPMatrix::BT709 },
2072+
{ "g24_rec709_display", CICPPrimaries::Rec709, CICPTransfer::BT709,
2073+
CICPMatrix::BT709 },
2074+
{ "srgb_p3d65_display", CICPPrimaries::P3D65, CICPTransfer::sRGB,
2075+
CICPMatrix::BT709 },
2076+
{ "srgbe_p3d65_display", CICPPrimaries::P3D65, CICPTransfer::sRGB,
2077+
CICPMatrix::BT709 },
2078+
{ "pq_p3d65_display", CICPPrimaries::P3D65, CICPTransfer::PQ,
2079+
CICPMatrix::Rec2020_NCL },
2080+
{ "pq_rec2020_display", CICPPrimaries::Rec2020, CICPTransfer::PQ,
2081+
CICPMatrix::Rec2020_NCL },
2082+
{ "hlg_rec2020_display", CICPPrimaries::Rec2020, CICPTransfer::HLG,
2083+
CICPMatrix::Rec2020_NCL },
2084+
{ "g22_rec709_display", CICPPrimaries::Rec709, CICPTransfer::Gamma22,
2085+
CICPMatrix::BT709 },
2086+
// No CICP code for Adobe RGB primaries.
2087+
// { "g22_adobergb_display" }
2088+
{ "g26_p3d65_display", CICPPrimaries::P3D65, CICPTransfer::Gamma26,
2089+
CICPMatrix::BT709 },
2090+
{ "g26_xyzd65_display", CICPPrimaries::XYZD65, CICPTransfer::Gamma26,
2091+
CICPMatrix::Unspecified },
2092+
{ "pq_xyzd65_display", CICPPrimaries::XYZD65, CICPTransfer::PQ,
2093+
CICPMatrix::Unspecified },
2094+
};
2095+
} // namespace
2096+
2097+
string_view
2098+
ColorConfig::get_color_interop_id(const int cicp[4]) const
2099+
{
2100+
for (const ColorInteropID& interop : color_interop_ids) {
2101+
if (interop.cicp[0] == cicp[0] && interop.cicp[1] == cicp[1]) {
2102+
return interop.interop_id;
2103+
}
2104+
}
2105+
return "";
2106+
}
2107+
2108+
cspan<int>
2109+
ColorConfig::get_cicp(string_view colorspace) const
2110+
{
2111+
if (!colorspace.empty()) {
2112+
for (const ColorInteropID& interop : color_interop_ids) {
2113+
if (equivalent(colorspace, interop.interop_id)) {
2114+
return interop.cicp;
2115+
}
2116+
}
2117+
}
2118+
return cspan<int>();
2119+
}
2120+
20002121

20012122
//////////////////////////////////////////////////////////////////////////
20022123
//

src/png.imageio/png_pvt.h

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -330,8 +330,12 @@ read_info(png_structp& sp, png_infop& ip, int& bit_depth, int& color_type,
330330
{
331331
png_byte pri = 0, trc = 0, mtx = 0, vfr = 0;
332332
if (png_get_cICP(sp, ip, &pri, &trc, &mtx, &vfr)) {
333-
int cicp[4] = { pri, trc, mtx, vfr };
333+
const int cicp[4] = { pri, trc, mtx, vfr };
334334
spec.attribute(CICP_ATTR, TypeDesc(TypeDesc::INT, 4), cicp);
335+
const ColorConfig& colorconfig(ColorConfig::default_colorconfig());
336+
string_view interop_id = colorconfig.get_color_interop_id(cicp);
337+
if (!interop_id.empty())
338+
spec.attribute("oiio:ColorSpace", interop_id);
335339
}
336340
}
337341
#endif
@@ -608,7 +612,8 @@ write_info(png_structp& sp, png_infop& ip, int& color_type, ImageSpec& spec,
608612
string_view colorspace = spec.get_string_attribute("oiio:ColorSpace",
609613
"srgb_rec709_scene");
610614
const ColorConfig& colorconfig(ColorConfig::default_colorconfig());
611-
srgb = false;
615+
OIIO_MAYBE_UNUSED bool wrote_colorspace = false;
616+
srgb = false;
612617
if (colorconfig.equivalent(colorspace, "srgb_rec709_scene")) {
613618
srgb = true;
614619
gamma = 1.0f;
@@ -628,7 +633,8 @@ write_info(png_structp& sp, png_infop& ip, int& color_type, ImageSpec& spec,
628633
if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp)
629634
return "Could not set PNG gAMA chunk";
630635
png_set_gAMA(sp, ip, 1.0);
631-
srgb = false;
636+
srgb = false;
637+
wrote_colorspace = true;
632638
} else if (Strutil::istarts_with(colorspace, "Gamma")) {
633639
// Back compatible, this is DEPRECATED(3.1)
634640
Strutil::parse_word(colorspace);
@@ -638,24 +644,28 @@ write_info(png_structp& sp, png_infop& ip, int& color_type, ImageSpec& spec,
638644
if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp)
639645
return "Could not set PNG gAMA chunk";
640646
png_set_gAMA(sp, ip, 1.0f / gamma);
641-
srgb = false;
647+
srgb = false;
648+
wrote_colorspace = true;
642649
} else if (colorconfig.equivalent(colorspace, "g22_rec709_scene")) {
643650
gamma = 2.2f;
644651
if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp)
645652
return "Could not set PNG gAMA chunk";
646653
png_set_gAMA(sp, ip, 1.0f / gamma);
647-
srgb = false;
654+
srgb = false;
655+
wrote_colorspace = true;
648656
} else if (colorconfig.equivalent(colorspace, "g18_rec709_scene")) {
649657
gamma = 1.8f;
650658
if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp)
651659
return "Could not set PNG gAMA chunk";
652660
png_set_gAMA(sp, ip, 1.0f / gamma);
653-
srgb = false;
661+
srgb = false;
662+
wrote_colorspace = true;
654663
} else if (colorconfig.equivalent(colorspace, "srgb_rec709_scene")) {
655664
if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp)
656665
return "Could not set PNG gAMA and cHRM chunk";
657666
png_set_sRGB_gAMA_and_cHRM(sp, ip, PNG_sRGB_INTENT_ABSOLUTE);
658-
srgb = true;
667+
srgb = true;
668+
wrote_colorspace = true;
659669
}
660670

661671
// Write ICC profile, if we have anything
@@ -667,8 +677,10 @@ write_info(png_structp& sp, png_infop& ip, int& color_type, ImageSpec& spec,
667677
return "Could not set PNG iCCP chunk";
668678
unsigned char* icc_profile
669679
= (unsigned char*)icc_profile_parameter->data();
670-
if (icc_profile && length)
680+
if (icc_profile && length) {
671681
png_set_iCCP(sp, ip, "Embedded Profile", 0, icc_profile, length);
682+
wrote_colorspace = true;
683+
}
672684
}
673685

674686
if (false && !spec.find_attribute("DateTime")) {
@@ -724,13 +736,17 @@ write_info(png_structp& sp, png_infop& ip, int& color_type, ImageSpec& spec,
724736
}
725737

726738
#ifdef PNG_cICP_SUPPORTED
739+
// Only automatically determine CICP from oiio::ColorSpace if we didn't
740+
// write colorspace metadata yet.
727741
const ParamValue* p = spec.find_attribute(CICP_ATTR,
728742
TypeDesc(TypeDesc::INT, 4));
729-
if (p) {
730-
const int* int_vals = static_cast<const int*>(p->data());
743+
cspan<int> cicp = (p) ? p->as_cspan<int>()
744+
: (!wrote_colorspace) ? colorconfig.get_cicp(colorspace)
745+
: cspan<int>();
746+
if (!cicp.empty()) {
731747
png_byte vals[4];
732748
for (int i = 0; i < 4; ++i)
733-
vals[i] = static_cast<png_byte>(int_vals[i]);
749+
vals[i] = static_cast<png_byte>(cicp[i]);
734750
if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp)
735751
return "Could not set PNG cICP chunk";
736752
// libpng will only write the chunk if the third byte is 0

src/python/py_colorconfig.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include "py_oiio.h"
66
#include <OpenImageIO/color.h>
7+
#include <optional>
78
#include <utility>
89

910
namespace PyOpenImageIO {
@@ -160,6 +161,20 @@ declare_colorconfig(py::module& m)
160161
return self.equivalent(color_space, other_color_space);
161162
},
162163
"color_space"_a, "other_color_space"_a)
164+
.def("get_color_interop_id",
165+
[](const ColorConfig& self, const std::array<int, 4> cicp) {
166+
return std::string(self.get_color_interop_id(cicp.data()));
167+
})
168+
.def("get_cicp",
169+
[](const ColorConfig& self, const std::string& colorspace)
170+
-> std::optional<std::array<int, 4>> {
171+
cspan<int> cicp = self.get_cicp(colorspace);
172+
if (!cicp.empty()) {
173+
return std::array<int, 4>(
174+
{ cicp[0], cicp[1], cicp[2], cicp[3] });
175+
}
176+
return std::nullopt;
177+
})
163178
.def("configname", &ColorConfig::configname)
164179
.def_static("default_colorconfig", []() -> const ColorConfig& {
165180
return ColorConfig::default_colorconfig();

testsuite/ffmpeg/ref/out-ffmpeg6.1.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ ref/vp9_rec2100_pq.mkv : 384 x 216, 3 channel, uint10 FFmpeg movie
9696
FramesPerSecond: 24/1 (24)
9797
ffmpeg:codec_name: "Google VP9"
9898
oiio:BitsPerSample: 10
99+
oiio:ColorSpace: "pq_rec2020_display"
99100
oiio:Movie: 1
100101
oiio:subimages: 2
101102
subimage 1: 384 x 216, 3 channel, uint10 FFmpeg movie
@@ -106,5 +107,6 @@ ref/vp9_rec2100_pq.mkv : 384 x 216, 3 channel, uint10 FFmpeg movie
106107
FramesPerSecond: 24/1 (24)
107108
ffmpeg:codec_name: "Google VP9"
108109
oiio:BitsPerSample: 10
110+
oiio:ColorSpace: "pq_rec2020_display"
109111
oiio:Movie: 1
110112
oiio:subimages: 2

testsuite/ffmpeg/ref/out-ffmpeg8.0.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ ref/vp9_rec2100_pq.mkv : 384 x 216, 3 channel, uint10 FFmpeg movie
9696
FramesPerSecond: 24/1 (24)
9797
ffmpeg:codec_name: "Google VP9"
9898
oiio:BitsPerSample: 10
99+
oiio:ColorSpace: "pq_rec2020_display"
99100
oiio:Movie: 1
100101
oiio:subimages: 2
101102
subimage 1: 384 x 216, 3 channel, uint10 FFmpeg movie
@@ -106,5 +107,6 @@ ref/vp9_rec2100_pq.mkv : 384 x 216, 3 channel, uint10 FFmpeg movie
106107
FramesPerSecond: 24/1 (24)
107108
ffmpeg:codec_name: "Google VP9"
108109
oiio:BitsPerSample: 10
110+
oiio:ColorSpace: "pq_rec2020_display"
109111
oiio:Movie: 1
110112
oiio:subimages: 2

testsuite/heif/ref/out-libheif1.12-orient.txt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,17 @@ cicp_pq.avif : 16 x 16, 4 channel, uint10 heif
6464
Exif:FlashPixVersion: "0100"
6565
heif:UnassociatedAlpha: 1
6666
oiio:BitsPerSample: 10
67-
oiio:ColorSpace: "srgb_rec709_scene"
67+
oiio:ColorSpace: "pq_rec2020_display"
68+
Reading colorspace_hlg.avif
69+
colorspace_hlg.avif : 16 x 16, 4 channel, uint10 heif
70+
SHA-1: 0F3CAB52D479BC23E9C981DBADDFEF1F792E5540
71+
channel list: R, G, B, A
72+
CICP: 9, 18, 9, 1
73+
Exif:ExifVersion: "0230"
74+
Exif:FlashPixVersion: "0100"
75+
heif:UnassociatedAlpha: 1
76+
oiio:BitsPerSample: 10
77+
oiio:ColorSpace: "hlg_rec2020_display"
6878
Reading ../oiio-images/heif/greyhounds-looking-for-a-table.heic
6979
../oiio-images/heif/greyhounds-looking-for-a-table.heic : 3024 x 4032, 3 channel, uint8 heif
7080
SHA-1: 8211F56BBABDC7615CCAF67CBF49741D1A292D2E

0 commit comments

Comments
 (0)