Skip to content

Commit f71dfcc

Browse files
feat(openexr): Add support for luminance-chroma OpenEXR images. (#4070)
Upon reading, the subsampled Y/BY/RY(/A) channels of luminance-chroma images are automatically converted to RGB(A) channels. These images will set a metadata "openexr::luminancechroma" to 1 in the ImageSpec, to indicate that the original image was luminance/chroma. Subsampled channels are not supported with the exception of reading luminance-chroma images with vertical and horizontal sampling rates of 2. This limited support does not work when OpenEXR's C Core API in used, only when OpenEXR's C++ API is used. Furthermore, it does not work in combination with tiles, multiple subimages, mipmapping, or deep pixels. The test images from the OpenEXR testsuite have been added and work with the C++ variant of the test. Fixes #4051. --------- Signed-off-by: Joachim Reichel <[email protected]>
1 parent 1427fd3 commit f71dfcc

File tree

7 files changed

+362
-28
lines changed

7 files changed

+362
-28
lines changed

src/cmake/testing.cmake

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ macro (oiio_add_all_tests)
283283
list (APPEND all_openexr_tests openexr-compression)
284284
endif ()
285285
# Run all OpenEXR tests without core library
286-
oiio_add_tests (${all_openexr_tests}
286+
oiio_add_tests (${all_openexr_tests} openexr-luminance-chroma
287287
ENVIRONMENT OPENIMAGEIO_OPTIONS=openexr:core=0
288288
IMAGEDIR openexr-images
289289
URL http://github.com/AcademySoftwareFoundation/openexr-images)

src/doc/builtinplugins.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,6 +1365,11 @@ The official OpenEXR site is http://www.openexr.com/.
13651365
* - ``openexr:dwaCompressionLevel``
13661366
- float
13671367
- compression level for dwaa or dwab compression (default: 45.0).
1368+
* - ``openexr::luminancechroma``
1369+
- int
1370+
- If nonzero, indicates whether the image is a luminance-chroma image.
1371+
Upon reading, the subsampled Y/BY/RY(/A) channels of luminance-chroma
1372+
images are automatically converted to RGB(A) channels.
13681373
* - *other*
13691374
-
13701375
- All other attributes will be added to the ImageSpec by their name and
@@ -1446,6 +1451,11 @@ by :file:`libIlmImf`.
14461451
data. OpenImageIO's OpenEXR writer will silently convert data in formats
14471452
(including the common UINT8 and UINT16 cases) to HALF data for output.
14481453

1454+
* Subsampled channels are not supported with the exception of reading
1455+
luminance-chroma images with vertical and horizontal sampling rates of 2.
1456+
This limited support does not work when OpenEXR's C Core API in used, only
1457+
when OpenEXR's C++ API is used. Furthermore, it does not work in
1458+
combination with tiles, multiple subimages, mipmapping, or deep pixels.
14491459

14501460

14511461
|

src/openexr.imageio/exrinput.cpp

Lines changed: 153 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616

1717
#include <boost/version.hpp>
1818

19+
#include <OpenEXR/ImfArray.h>
1920
#include <OpenEXR/ImfChannelList.h>
2021
#include <OpenEXR/ImfEnvmap.h>
2122
#include <OpenEXR/ImfInputFile.h>
23+
#include <OpenEXR/ImfRgba.h>
2224
#include <OpenEXR/ImfTestFile.h>
2325
#include <OpenEXR/ImfTiledInputFile.h>
2426

@@ -51,6 +53,7 @@ OIIO_GCC_PRAGMA(GCC diagnostic ignored "-Wunused-parameter")
5153
#include <OpenEXR/ImfMultiPartInputFile.h>
5254
#include <OpenEXR/ImfPartType.h>
5355
#include <OpenEXR/ImfRationalAttribute.h>
56+
#include <OpenEXR/ImfRgbaFile.h>
5457
#include <OpenEXR/ImfStringAttribute.h>
5558
#include <OpenEXR/ImfStringVectorAttribute.h>
5659
#include <OpenEXR/ImfTiledInputPart.h>
@@ -179,12 +182,13 @@ class OpenEXRInput final : public ImageInput {
179182
struct PartInfo {
180183
std::atomic_bool initialized;
181184
ImageSpec spec;
182-
int topwidth; ///< Width of top mip level
183-
int topheight; ///< Height of top mip level
184-
int levelmode; ///< The level mode
185-
int roundingmode; ///< Rounding mode
186-
bool cubeface; ///< It's a cubeface environment map
187-
int nmiplevels; ///< How many MIP levels are there?
185+
int topwidth; ///< Width of top mip level
186+
int topheight; ///< Height of top mip level
187+
int levelmode; ///< The level mode
188+
int roundingmode; ///< Rounding mode
189+
bool cubeface; ///< It's a cubeface environment map
190+
bool luminance_chroma; ///< It's a luminance chroma image
191+
int nmiplevels; ///< How many MIP levels are there?
188192
Imath::Box2i top_datawindow;
189193
Imath::Box2i top_displaywindow;
190194
std::vector<Imf::PixelType> pixeltype; ///< Imf pixel type for each chan
@@ -202,6 +206,7 @@ class OpenEXRInput final : public ImageInput {
202206
, levelmode(p.levelmode)
203207
, roundingmode(p.roundingmode)
204208
, cubeface(p.cubeface)
209+
, luminance_chroma(p.luminance_chroma)
205210
, nmiplevels(p.nmiplevels)
206211
, top_datawindow(p.top_datawindow)
207212
, top_displaywindow(p.top_displaywindow)
@@ -223,6 +228,7 @@ class OpenEXRInput final : public ImageInput {
223228
Imf::TiledInputPart* m_tiled_input_part;
224229
Imf::DeepScanLineInputPart* m_deep_scanline_input_part;
225230
Imf::DeepTiledInputPart* m_deep_tiled_input_part;
231+
Imf::RgbaInputFile* m_input_rgba;
226232
Filesystem::IOProxy* m_io = nullptr;
227233
std::unique_ptr<Filesystem::IOProxy> m_local_io;
228234
int m_subimage; ///< What subimage are we looking at?
@@ -238,6 +244,7 @@ class OpenEXRInput final : public ImageInput {
238244
m_tiled_input_part = NULL;
239245
m_deep_scanline_input_part = NULL;
240246
m_deep_tiled_input_part = NULL;
247+
m_input_rgba = NULL;
241248
m_subimage = -1;
242249
m_miplevel = -1;
243250
m_io = nullptr;
@@ -910,6 +917,38 @@ suffixfound(string_view name, span<ChanNameHolder> chans)
910917
}
911918

912919

920+
// Returns the index of that channel name (suffix only) in the list, or -1 in case of failure.
921+
static int
922+
get_index_of_suffix(string_view name, span<ChanNameHolder> chans)
923+
{
924+
for (size_t i = 0, n = chans.size(); i < n; ++i)
925+
if (Strutil::iequals(name, chans[i].suffix))
926+
return static_cast<int>(i);
927+
return -1;
928+
}
929+
930+
931+
// Is this a luminance-chroma image, i.e., Y/BY/RY or Y/BY/RY/A or Y/BY/RY/Alpha?
932+
//
933+
// Note that extra channels are not supported.
934+
static bool
935+
is_luminance_chroma(span<ChanNameHolder> chans)
936+
{
937+
if (chans.size() < 3 || chans.size() > 4)
938+
return false;
939+
if (!suffixfound("Y", chans))
940+
return false;
941+
if (!suffixfound("BY", chans))
942+
return false;
943+
if (!suffixfound("RY", chans))
944+
return false;
945+
if (chans.size() == 4 && !suffixfound("A", chans)
946+
&& !suffixfound("Alpha", chans))
947+
return false;
948+
return true;
949+
}
950+
951+
913952
} // namespace
914953

915954

@@ -919,10 +958,8 @@ OpenEXRInput::PartInfo::query_channels(OpenEXRInput* in,
919958
const Imf::Header* header)
920959
{
921960
OIIO_DASSERT(!initialized);
922-
bool ok = true;
923-
spec.nchannels = 0;
961+
bool ok = true;
924962
const Imf::ChannelList& channels(header->channels());
925-
std::vector<std::string> channelnames; // Order of channels in file
926963
std::vector<ChanNameHolder> cnh;
927964
int c = 0;
928965
for (auto ci = channels.begin(); ci != channels.end(); ++c, ++ci)
@@ -969,6 +1006,34 @@ OpenEXRInput::PartInfo::query_channels(OpenEXRInput* in,
9691006
// Now we should have cnh sorted into the order that we want to present
9701007
// to the OIIO client.
9711008

1009+
// Limitations for luminance-chroma images: no tiling, no deep samples, no
1010+
// miplevels/subimages, no extra channels.
1011+
luminance_chroma = is_luminance_chroma(cnh);
1012+
if (luminance_chroma) {
1013+
spec.attribute("openexr:luminancechroma", 1);
1014+
spec.format = TypeDesc::HALF;
1015+
spec.nchannels = cnh.size();
1016+
if (spec.nchannels == 3) {
1017+
spec.channelnames = { "R", "G", "B" };
1018+
spec.alpha_channel = -1;
1019+
spec.z_channel = -1;
1020+
} else {
1021+
OIIO_ASSERT(spec.nchannels == 4);
1022+
int index_a = get_index_of_suffix("A", cnh);
1023+
if (index_a != -1) {
1024+
spec.channelnames = { "R", "G", "B", "A" };
1025+
spec.alpha_channel = index_a;
1026+
} else {
1027+
spec.channelnames = { "R", "G", "B", "Alpha" };
1028+
spec.alpha_channel = get_index_of_suffix("Alpha", cnh);
1029+
OIIO_ASSERT(spec.alpha_channel != -1);
1030+
}
1031+
spec.z_channel = -1;
1032+
}
1033+
spec.channelformats.clear();
1034+
return true;
1035+
}
1036+
9721037
spec.format = TypeDesc::UNKNOWN;
9731038
bool all_one_format = true;
9741039
for (int c = 0; c < spec.nchannels; ++c) {
@@ -991,7 +1056,8 @@ OpenEXRInput::PartInfo::query_channels(OpenEXRInput* in,
9911056
in->errorfmt(
9921057
"Subsampled channels are not supported (channel \"{}\" has sampling {},{}).",
9931058
cnh[c].fullname, cnh[c].xSampling, cnh[c].ySampling);
994-
// FIXME: Some day, we should handle channel subsampling.
1059+
// FIXME: Some day, we should handle channel subsampling (beyond the luminance chroma
1060+
// special case, possibly replacing it).
9951061
}
9961062
}
9971063
OIIO_DASSERT((int)spec.channelnames.size() == spec.nchannels);
@@ -1088,8 +1154,18 @@ OpenEXRInput::seek_subimage(int subimage, int miplevel)
10881154
m_deep_scanline_input_part = NULL;
10891155
delete m_deep_tiled_input_part;
10901156
m_deep_tiled_input_part = NULL;
1157+
delete m_input_rgba;
1158+
m_input_rgba = NULL;
10911159
try {
1092-
if (part.spec.deep) {
1160+
if (part.luminance_chroma) {
1161+
if (subimage != 0 || miplevel != 0) {
1162+
errorf(
1163+
"Non-zero subimage or miplevel are not supported for luminance-chroma images.");
1164+
return false;
1165+
}
1166+
m_input_stream->seekg(0);
1167+
m_input_rgba = new Imf::RgbaInputFile(*m_input_stream);
1168+
} else if (part.spec.deep) {
10931169
if (part.spec.tile_width)
10941170
m_deep_tiled_input_part
10951171
= new Imf::DeepTiledInputPart(*m_input_multipart,
@@ -1112,13 +1188,15 @@ OpenEXRInput::seek_subimage(int subimage, int miplevel)
11121188
m_tiled_input_part = NULL;
11131189
m_deep_scanline_input_part = NULL;
11141190
m_deep_tiled_input_part = NULL;
1191+
m_input_rgba = NULL;
11151192
return false;
11161193
} catch (...) { // catch-all for edge cases or compiler bugs
11171194
errorf("OpenEXR exception: unknown");
11181195
m_scanline_input_part = NULL;
11191196
m_tiled_input_part = NULL;
11201197
m_deep_scanline_input_part = NULL;
11211198
m_deep_tiled_input_part = NULL;
1199+
m_input_rgba = NULL;
11221200
return false;
11231201
}
11241202
}
@@ -1200,6 +1278,7 @@ OpenEXRInput::close()
12001278
delete m_tiled_input_part;
12011279
delete m_deep_scanline_input_part;
12021280
delete m_deep_tiled_input_part;
1281+
delete m_input_rgba;
12031282
delete m_input_stream;
12041283
init(); // Reset to initial state
12051284
return true;
@@ -1226,7 +1305,6 @@ OpenEXRInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
12261305
}
12271306

12281307

1229-
12301308
bool
12311309
OpenEXRInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
12321310
int yend, int /*z*/, int chbegin, int chend,
@@ -1238,11 +1316,6 @@ OpenEXRInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
12381316
chend = clamp(chend, chbegin + 1, m_spec.nchannels);
12391317
// std::cerr << "openexr rns " << ybegin << ' ' << yend << ", channels "
12401318
// << chbegin << "-" << (chend-1) << "\n";
1241-
if (!m_scanline_input_part) {
1242-
errorf(
1243-
"called OpenEXRInput::read_native_scanlines without an open file");
1244-
return false;
1245-
}
12461319

12471320
// Compute where OpenEXR needs to think the full buffers starts.
12481321
// OpenImageIO requires that 'data' points to where the client wants
@@ -1255,6 +1328,51 @@ OpenEXRInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
12551328
char* buf = (char*)data - m_spec.x * pixelbytes - ybegin * scanlinebytes;
12561329

12571330
try {
1331+
if (part.luminance_chroma) {
1332+
Imath::Box2i dw = m_input_rgba->dataWindow();
1333+
if (dw.min.x != 0 || dw.min.y != 0
1334+
|| dw != m_input_rgba->displayWindow()) {
1335+
errorf(
1336+
"Non-trivial data and/or display windows are not supported for luminance-chroma images.");
1337+
return false;
1338+
}
1339+
int dw_width = dw.max.x - dw.min.x + 1;
1340+
int dw_height = dw.max.y - dw.min.y + 1;
1341+
int chunk_height = yend - ybegin;
1342+
// FIXME Are these assumptions correct?
1343+
OIIO_ASSERT(ybegin >= dw.min.y);
1344+
OIIO_ASSERT(yend <= dw.max.y + 1);
1345+
OIIO_ASSERT(chunk_height <= dw_height);
1346+
1347+
Imf::Array2D<Imf::Rgba> pixels(chunk_height, dw_width);
1348+
m_input_rgba->setFrameBuffer(&pixels[0][0] - dw.min.x
1349+
- ybegin * dw_width,
1350+
1, dw_width);
1351+
m_input_rgba->readPixels(ybegin, yend - 1);
1352+
1353+
// FIXME There is probably some optimized code for this somewhere.
1354+
for (int c = chbegin; c < chend; ++c) {
1355+
size_t chanbytes = m_spec.channelformat(c).size();
1356+
half* src = &pixels[0][0].r + c;
1357+
half* dst = (half*)((char*)data + c * chanbytes);
1358+
for (int y = ybegin; y < yend; ++y) {
1359+
for (int x = 0; x < m_spec.width; ++x) {
1360+
*dst = *src;
1361+
src += 4; // always advance 4 RGBA halfs
1362+
dst += m_spec.nchannels;
1363+
}
1364+
}
1365+
}
1366+
1367+
return true;
1368+
}
1369+
1370+
if (!m_scanline_input_part) {
1371+
errorf(
1372+
"called OpenEXRInput::read_native_scanlines without an open file");
1373+
return false;
1374+
}
1375+
12581376
Imf::FrameBuffer frameBuffer;
12591377
size_t chanoffset = 0;
12601378
for (int c = chbegin; c < chend; ++c) {
@@ -1320,6 +1438,12 @@ OpenEXRInput::read_native_tiles(int subimage, int miplevel, int xbegin,
13201438
if (!seek_subimage(subimage, miplevel))
13211439
return false;
13221440
chend = clamp(chend, chbegin + 1, m_spec.nchannels);
1441+
const PartInfo& part(m_parts[m_subimage]);
1442+
if (part.luminance_chroma) {
1443+
errorf(
1444+
"OpenEXRInput::read_native_tiles is not supported for luminance-chroma images");
1445+
return false;
1446+
}
13231447
#if 0
13241448
std::cerr << "openexr rnt " << xbegin << ' ' << xend << ' ' << ybegin
13251449
<< ' ' << yend << ", chans " << chbegin
@@ -1336,7 +1460,6 @@ OpenEXRInput::read_native_tiles(int subimage, int miplevel, int xbegin,
13361460
// to put the pixels being read, but OpenEXR's frameBuffer.insert()
13371461
// wants where the address of the "virtual framebuffer" for the
13381462
// whole image.
1339-
const PartInfo& part(m_parts[m_subimage]);
13401463
size_t pixelbytes = m_spec.pixel_bytes(chbegin, chend, true);
13411464
int firstxtile = (xbegin - m_spec.x) / m_spec.tile_width;
13421465
int firstytile = (ybegin - m_spec.y) / m_spec.tile_height;
@@ -1492,14 +1615,19 @@ OpenEXRInput::read_native_deep_scanlines(int subimage, int miplevel, int ybegin,
14921615
lock_guard lock(*this);
14931616
if (!seek_subimage(subimage, miplevel))
14941617
return false;
1618+
const PartInfo& part(m_parts[m_subimage]);
1619+
if (part.luminance_chroma) {
1620+
errorf(
1621+
"OpenEXRInput::read_native_deep_scanlines is not supported for luminance-chroma images");
1622+
return false;
1623+
}
14951624
if (m_deep_scanline_input_part == NULL) {
14961625
errorf(
14971626
"called OpenEXRInput::read_native_deep_scanlines without an open file");
14981627
return false;
14991628
}
15001629

15011630
try {
1502-
const PartInfo& part(m_parts[m_subimage]);
15031631
size_t npixels = (yend - ybegin) * m_spec.width;
15041632
chend = clamp(chend, chbegin + 1, m_spec.nchannels);
15051633
int nchans = chend - chbegin;
@@ -1564,14 +1692,19 @@ OpenEXRInput::read_native_deep_tiles(int subimage, int miplevel, int xbegin,
15641692
lock_guard lock(*this);
15651693
if (!seek_subimage(subimage, miplevel))
15661694
return false;
1695+
const PartInfo& part(m_parts[m_subimage]);
1696+
if (part.luminance_chroma) {
1697+
errorf(
1698+
"OpenEXRInput::read_native_deep_tiles is not supported for luminance-chroma images");
1699+
return false;
1700+
}
15671701
if (m_deep_tiled_input_part == NULL) {
15681702
errorf(
15691703
"called OpenEXRInput::read_native_deep_tiles without an open file");
15701704
return false;
15711705
}
15721706

15731707
try {
1574-
const PartInfo& part(m_parts[m_subimage]);
15751708
size_t width = xend - xbegin;
15761709
size_t height = yend - ybegin;
15771710
size_t npixels = width * height;

testsuite/openexr-chroma/run.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,9 @@
1212
]
1313
for f in files:
1414
command += rw_command (imagedir, f)
15-
# FIXME - we don't currently subsampled images (Rec709_YC.exr and XYZ_YC.exr)
1615

1716
# ../openexr-images/LuminanceChroma:
1817
# CrissyField.exr Garden.exr StarField.exr
1918
# Flowers.exr MtTamNorth.exr
2019
imagedir = OIIO_TESTSUITE_IMAGEDIR + "/LuminanceChroma"
21-
#command += rw_command (imagedir, "CrissyField.exr", extraargs="--compression zip")
22-
#command += rw_command (imagedir, "Flowers.exr", extraargs="--compression zip")
2320
command += rw_command (imagedir, "Garden.exr")
24-
#command += rw_command (imagedir, "MtTamNorth.exr")
25-
#command += rw_command (imagedir, "StarField.exr")
26-
# FIXME -- most of these are broken, we don't read LuminanceChroma images,
27-
# nor do we currently support subsampled channels

0 commit comments

Comments
 (0)