Skip to content

Commit

Permalink
feat(openexr): Add support for luminance-chroma OpenEXR images. (#4070)
Browse files Browse the repository at this point in the history
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 <43646584+jreichel-nvidia@users.noreply.github.com>
  • Loading branch information
jreichel-nvidia authored Jan 23, 2024
1 parent 1427fd3 commit f71dfcc
Show file tree
Hide file tree
Showing 7 changed files with 362 additions and 28 deletions.
2 changes: 1 addition & 1 deletion src/cmake/testing.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ macro (oiio_add_all_tests)
list (APPEND all_openexr_tests openexr-compression)
endif ()
# Run all OpenEXR tests without core library
oiio_add_tests (${all_openexr_tests}
oiio_add_tests (${all_openexr_tests} openexr-luminance-chroma
ENVIRONMENT OPENIMAGEIO_OPTIONS=openexr:core=0
IMAGEDIR openexr-images
URL http://github.com/AcademySoftwareFoundation/openexr-images)
Expand Down
10 changes: 10 additions & 0 deletions src/doc/builtinplugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1365,6 +1365,11 @@ The official OpenEXR site is http://www.openexr.com/.
* - ``openexr:dwaCompressionLevel``
- float
- compression level for dwaa or dwab compression (default: 45.0).
* - ``openexr::luminancechroma``
- int
- If nonzero, indicates whether the image is a luminance-chroma image.
Upon reading, the subsampled Y/BY/RY(/A) channels of luminance-chroma
images are automatically converted to RGB(A) channels.
* - *other*
-
- All other attributes will be added to the ImageSpec by their name and
Expand Down Expand Up @@ -1446,6 +1451,11 @@ by :file:`libIlmImf`.
data. OpenImageIO's OpenEXR writer will silently convert data in formats
(including the common UINT8 and UINT16 cases) to HALF data for output.

* 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.


|
Expand Down
173 changes: 153 additions & 20 deletions src/openexr.imageio/exrinput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@

#include <boost/version.hpp>

#include <OpenEXR/ImfArray.h>
#include <OpenEXR/ImfChannelList.h>
#include <OpenEXR/ImfEnvmap.h>
#include <OpenEXR/ImfInputFile.h>
#include <OpenEXR/ImfRgba.h>
#include <OpenEXR/ImfTestFile.h>
#include <OpenEXR/ImfTiledInputFile.h>

Expand Down Expand Up @@ -51,6 +53,7 @@ OIIO_GCC_PRAGMA(GCC diagnostic ignored "-Wunused-parameter")
#include <OpenEXR/ImfMultiPartInputFile.h>
#include <OpenEXR/ImfPartType.h>
#include <OpenEXR/ImfRationalAttribute.h>
#include <OpenEXR/ImfRgbaFile.h>
#include <OpenEXR/ImfStringAttribute.h>
#include <OpenEXR/ImfStringVectorAttribute.h>
#include <OpenEXR/ImfTiledInputPart.h>
Expand Down Expand Up @@ -179,12 +182,13 @@ class OpenEXRInput final : public ImageInput {
struct PartInfo {
std::atomic_bool initialized;
ImageSpec spec;
int topwidth; ///< Width of top mip level
int topheight; ///< Height of top mip level
int levelmode; ///< The level mode
int roundingmode; ///< Rounding mode
bool cubeface; ///< It's a cubeface environment map
int nmiplevels; ///< How many MIP levels are there?
int topwidth; ///< Width of top mip level
int topheight; ///< Height of top mip level
int levelmode; ///< The level mode
int roundingmode; ///< Rounding mode
bool cubeface; ///< It's a cubeface environment map
bool luminance_chroma; ///< It's a luminance chroma image
int nmiplevels; ///< How many MIP levels are there?
Imath::Box2i top_datawindow;
Imath::Box2i top_displaywindow;
std::vector<Imf::PixelType> pixeltype; ///< Imf pixel type for each chan
Expand All @@ -202,6 +206,7 @@ class OpenEXRInput final : public ImageInput {
, levelmode(p.levelmode)
, roundingmode(p.roundingmode)
, cubeface(p.cubeface)
, luminance_chroma(p.luminance_chroma)
, nmiplevels(p.nmiplevels)
, top_datawindow(p.top_datawindow)
, top_displaywindow(p.top_displaywindow)
Expand All @@ -223,6 +228,7 @@ class OpenEXRInput final : public ImageInput {
Imf::TiledInputPart* m_tiled_input_part;
Imf::DeepScanLineInputPart* m_deep_scanline_input_part;
Imf::DeepTiledInputPart* m_deep_tiled_input_part;
Imf::RgbaInputFile* m_input_rgba;
Filesystem::IOProxy* m_io = nullptr;
std::unique_ptr<Filesystem::IOProxy> m_local_io;
int m_subimage; ///< What subimage are we looking at?
Expand All @@ -238,6 +244,7 @@ class OpenEXRInput final : public ImageInput {
m_tiled_input_part = NULL;
m_deep_scanline_input_part = NULL;
m_deep_tiled_input_part = NULL;
m_input_rgba = NULL;
m_subimage = -1;
m_miplevel = -1;
m_io = nullptr;
Expand Down Expand Up @@ -910,6 +917,38 @@ suffixfound(string_view name, span<ChanNameHolder> chans)
}


// Returns the index of that channel name (suffix only) in the list, or -1 in case of failure.
static int
get_index_of_suffix(string_view name, span<ChanNameHolder> chans)
{
for (size_t i = 0, n = chans.size(); i < n; ++i)
if (Strutil::iequals(name, chans[i].suffix))
return static_cast<int>(i);
return -1;
}


// Is this a luminance-chroma image, i.e., Y/BY/RY or Y/BY/RY/A or Y/BY/RY/Alpha?
//
// Note that extra channels are not supported.
static bool
is_luminance_chroma(span<ChanNameHolder> chans)
{
if (chans.size() < 3 || chans.size() > 4)
return false;
if (!suffixfound("Y", chans))
return false;
if (!suffixfound("BY", chans))
return false;
if (!suffixfound("RY", chans))
return false;
if (chans.size() == 4 && !suffixfound("A", chans)
&& !suffixfound("Alpha", chans))
return false;
return true;
}


} // namespace


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

// Limitations for luminance-chroma images: no tiling, no deep samples, no
// miplevels/subimages, no extra channels.
luminance_chroma = is_luminance_chroma(cnh);
if (luminance_chroma) {
spec.attribute("openexr:luminancechroma", 1);
spec.format = TypeDesc::HALF;
spec.nchannels = cnh.size();
if (spec.nchannels == 3) {
spec.channelnames = { "R", "G", "B" };
spec.alpha_channel = -1;
spec.z_channel = -1;
} else {
OIIO_ASSERT(spec.nchannels == 4);
int index_a = get_index_of_suffix("A", cnh);
if (index_a != -1) {
spec.channelnames = { "R", "G", "B", "A" };
spec.alpha_channel = index_a;
} else {
spec.channelnames = { "R", "G", "B", "Alpha" };
spec.alpha_channel = get_index_of_suffix("Alpha", cnh);
OIIO_ASSERT(spec.alpha_channel != -1);
}
spec.z_channel = -1;
}
spec.channelformats.clear();
return true;
}

spec.format = TypeDesc::UNKNOWN;
bool all_one_format = true;
for (int c = 0; c < spec.nchannels; ++c) {
Expand All @@ -991,7 +1056,8 @@ OpenEXRInput::PartInfo::query_channels(OpenEXRInput* in,
in->errorfmt(
"Subsampled channels are not supported (channel \"{}\" has sampling {},{}).",
cnh[c].fullname, cnh[c].xSampling, cnh[c].ySampling);
// FIXME: Some day, we should handle channel subsampling.
// FIXME: Some day, we should handle channel subsampling (beyond the luminance chroma
// special case, possibly replacing it).
}
}
OIIO_DASSERT((int)spec.channelnames.size() == spec.nchannels);
Expand Down Expand Up @@ -1088,8 +1154,18 @@ OpenEXRInput::seek_subimage(int subimage, int miplevel)
m_deep_scanline_input_part = NULL;
delete m_deep_tiled_input_part;
m_deep_tiled_input_part = NULL;
delete m_input_rgba;
m_input_rgba = NULL;
try {
if (part.spec.deep) {
if (part.luminance_chroma) {
if (subimage != 0 || miplevel != 0) {
errorf(
"Non-zero subimage or miplevel are not supported for luminance-chroma images.");
return false;
}
m_input_stream->seekg(0);
m_input_rgba = new Imf::RgbaInputFile(*m_input_stream);
} else if (part.spec.deep) {
if (part.spec.tile_width)
m_deep_tiled_input_part
= new Imf::DeepTiledInputPart(*m_input_multipart,
Expand All @@ -1112,13 +1188,15 @@ OpenEXRInput::seek_subimage(int subimage, int miplevel)
m_tiled_input_part = NULL;
m_deep_scanline_input_part = NULL;
m_deep_tiled_input_part = NULL;
m_input_rgba = NULL;
return false;
} catch (...) { // catch-all for edge cases or compiler bugs
errorf("OpenEXR exception: unknown");
m_scanline_input_part = NULL;
m_tiled_input_part = NULL;
m_deep_scanline_input_part = NULL;
m_deep_tiled_input_part = NULL;
m_input_rgba = NULL;
return false;
}
}
Expand Down Expand Up @@ -1200,6 +1278,7 @@ OpenEXRInput::close()
delete m_tiled_input_part;
delete m_deep_scanline_input_part;
delete m_deep_tiled_input_part;
delete m_input_rgba;
delete m_input_stream;
init(); // Reset to initial state
return true;
Expand All @@ -1226,7 +1305,6 @@ OpenEXRInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
}



bool
OpenEXRInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
int yend, int /*z*/, int chbegin, int chend,
Expand All @@ -1238,11 +1316,6 @@ OpenEXRInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
chend = clamp(chend, chbegin + 1, m_spec.nchannels);
// std::cerr << "openexr rns " << ybegin << ' ' << yend << ", channels "
// << chbegin << "-" << (chend-1) << "\n";
if (!m_scanline_input_part) {
errorf(
"called OpenEXRInput::read_native_scanlines without an open file");
return false;
}

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

try {
if (part.luminance_chroma) {
Imath::Box2i dw = m_input_rgba->dataWindow();
if (dw.min.x != 0 || dw.min.y != 0
|| dw != m_input_rgba->displayWindow()) {
errorf(
"Non-trivial data and/or display windows are not supported for luminance-chroma images.");
return false;
}
int dw_width = dw.max.x - dw.min.x + 1;
int dw_height = dw.max.y - dw.min.y + 1;
int chunk_height = yend - ybegin;
// FIXME Are these assumptions correct?
OIIO_ASSERT(ybegin >= dw.min.y);
OIIO_ASSERT(yend <= dw.max.y + 1);
OIIO_ASSERT(chunk_height <= dw_height);

Imf::Array2D<Imf::Rgba> pixels(chunk_height, dw_width);
m_input_rgba->setFrameBuffer(&pixels[0][0] - dw.min.x
- ybegin * dw_width,
1, dw_width);
m_input_rgba->readPixels(ybegin, yend - 1);

// FIXME There is probably some optimized code for this somewhere.
for (int c = chbegin; c < chend; ++c) {
size_t chanbytes = m_spec.channelformat(c).size();
half* src = &pixels[0][0].r + c;
half* dst = (half*)((char*)data + c * chanbytes);
for (int y = ybegin; y < yend; ++y) {
for (int x = 0; x < m_spec.width; ++x) {
*dst = *src;
src += 4; // always advance 4 RGBA halfs
dst += m_spec.nchannels;
}
}
}

return true;
}

if (!m_scanline_input_part) {
errorf(
"called OpenEXRInput::read_native_scanlines without an open file");
return false;
}

Imf::FrameBuffer frameBuffer;
size_t chanoffset = 0;
for (int c = chbegin; c < chend; ++c) {
Expand Down Expand Up @@ -1320,6 +1438,12 @@ OpenEXRInput::read_native_tiles(int subimage, int miplevel, int xbegin,
if (!seek_subimage(subimage, miplevel))
return false;
chend = clamp(chend, chbegin + 1, m_spec.nchannels);
const PartInfo& part(m_parts[m_subimage]);
if (part.luminance_chroma) {
errorf(
"OpenEXRInput::read_native_tiles is not supported for luminance-chroma images");
return false;
}
#if 0
std::cerr << "openexr rnt " << xbegin << ' ' << xend << ' ' << ybegin
<< ' ' << yend << ", chans " << chbegin
Expand All @@ -1336,7 +1460,6 @@ OpenEXRInput::read_native_tiles(int subimage, int miplevel, int xbegin,
// to put the pixels being read, but OpenEXR's frameBuffer.insert()
// wants where the address of the "virtual framebuffer" for the
// whole image.
const PartInfo& part(m_parts[m_subimage]);
size_t pixelbytes = m_spec.pixel_bytes(chbegin, chend, true);
int firstxtile = (xbegin - m_spec.x) / m_spec.tile_width;
int firstytile = (ybegin - m_spec.y) / m_spec.tile_height;
Expand Down Expand Up @@ -1492,14 +1615,19 @@ OpenEXRInput::read_native_deep_scanlines(int subimage, int miplevel, int ybegin,
lock_guard lock(*this);
if (!seek_subimage(subimage, miplevel))
return false;
const PartInfo& part(m_parts[m_subimage]);
if (part.luminance_chroma) {
errorf(
"OpenEXRInput::read_native_deep_scanlines is not supported for luminance-chroma images");
return false;
}
if (m_deep_scanline_input_part == NULL) {
errorf(
"called OpenEXRInput::read_native_deep_scanlines without an open file");
return false;
}

try {
const PartInfo& part(m_parts[m_subimage]);
size_t npixels = (yend - ybegin) * m_spec.width;
chend = clamp(chend, chbegin + 1, m_spec.nchannels);
int nchans = chend - chbegin;
Expand Down Expand Up @@ -1564,14 +1692,19 @@ OpenEXRInput::read_native_deep_tiles(int subimage, int miplevel, int xbegin,
lock_guard lock(*this);
if (!seek_subimage(subimage, miplevel))
return false;
const PartInfo& part(m_parts[m_subimage]);
if (part.luminance_chroma) {
errorf(
"OpenEXRInput::read_native_deep_tiles is not supported for luminance-chroma images");
return false;
}
if (m_deep_tiled_input_part == NULL) {
errorf(
"called OpenEXRInput::read_native_deep_tiles without an open file");
return false;
}

try {
const PartInfo& part(m_parts[m_subimage]);
size_t width = xend - xbegin;
size_t height = yend - ybegin;
size_t npixels = width * height;
Expand Down
7 changes: 0 additions & 7 deletions testsuite/openexr-chroma/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,9 @@
]
for f in files:
command += rw_command (imagedir, f)
# FIXME - we don't currently subsampled images (Rec709_YC.exr and XYZ_YC.exr)

# ../openexr-images/LuminanceChroma:
# CrissyField.exr Garden.exr StarField.exr
# Flowers.exr MtTamNorth.exr
imagedir = OIIO_TESTSUITE_IMAGEDIR + "/LuminanceChroma"
#command += rw_command (imagedir, "CrissyField.exr", extraargs="--compression zip")
#command += rw_command (imagedir, "Flowers.exr", extraargs="--compression zip")
command += rw_command (imagedir, "Garden.exr")
#command += rw_command (imagedir, "MtTamNorth.exr")
#command += rw_command (imagedir, "StarField.exr")
# FIXME -- most of these are broken, we don't read LuminanceChroma images,
# nor do we currently support subsampled channels
Loading

0 comments on commit f71dfcc

Please sign in to comment.