Skip to content

Commit

Permalink
feat(jpeg): Support reading Ultra HDR images (#4484)
Browse files Browse the repository at this point in the history
Initial feature request: #4424

Add support in the `jpeg` plugin for reading Ultra HDR images using the reference codec `libultrahdr`: https://github.com/google/libultrahdr

In short, "ultra hdr" images are a clever extension of JPEG where the image file really is an old school JPEG file and will be interpreted correctly as such by old readers not aware of ultra hdr, but readers that are aware will see an extra piece of metadata that contains a gain map, that when applied to the base layer then yields an HDR image. Pretty clever approach!

Images used for testing during development:
https://github.com/MishaalRahmanGH/Ultra_HDR_Samples

---------

Signed-off-by: loicvital <[email protected]>
  • Loading branch information
mugulmd authored Nov 6, 2024
1 parent d8fe20e commit 30f6a0a
Show file tree
Hide file tree
Showing 11 changed files with 273 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ jobs:
setenvs: export USE_ICC=1 USE_OPENVDB=0
OIIO_EXTRA_CPP_ARGS="-fp-model=precise"
FREETYPE_VERSION=VER-2-13-0
DISABLE_libuhdr=1
# For icc, use fp-model precise to eliminate needless LSB errors
# that make test results differ from other platforms.
- desc: icx/C++17 py3.9 exr3.1 ocio2.2 qt5.15
Expand All @@ -120,8 +121,12 @@ jobs:
simd: "avx2,f16c"
setenvs: export USE_OPENVDB=0
OPENCOLORIO_CXX=g++
UHDR_CMAKE_C_COMPILER=gcc
UHDR_CMAKE_CXX_COMPILER=g++
# OCIO doesn't build with icx, so we have to force the ocio build
# to use g++.
# Building libuhdr with icx results in test failures
# so we force using gcc/g++.
- desc: sanitizers
nametag: sanitizer
runner: ubuntu-latest
Expand Down
60 changes: 60 additions & 0 deletions src/cmake/build_libuhdr.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Copyright Contributors to the OpenImageIO project.
# SPDX-License-Identifier: Apache-2.0
# https://github.com/AcademySoftwareFoundation/OpenImageIO

######################################################################
# libuhdr by hand!
######################################################################

set_cache (libuhdr_BUILD_VERSION 1.2.0 "libuhdr version for local builds")
set (libuhdr_GIT_REPOSITORY "https://github.com/google/libultrahdr")
set (libuhdr_GIT_TAG "v${libuhdr_BUILD_VERSION}")

set_cache (libuhdr_BUILD_SHARED_LIBS OFF
DOC "Should execute a local libuhdr build, if necessary, build shared libraries" ADVANCED)

if (TARGET libjpeg-turbo::jpeg)
# We've had some trouble with libuhdr finding the JPEG resources it needs to
# build if we're using libjpeg-turbo, libuhdr needs an extra nudge.
get_target_property(JPEG_INCLUDE_DIR JPEG::JPEG INTERFACE_INCLUDE_DIRECTORIES)
get_target_property(JPEG_LIBRARY JPEG::JPEG INTERFACE_LINK_LIBRARIES)
endif ()

set_cache (UHDR_CMAKE_C_COMPILER ${CMAKE_C_COMPILER} "libuhdr build C compiler override" ADVANCED)
set_cache (UHDR_CMAKE_CXX_COMPILER ${CMAKE_CXX_COMPILER} "libuhdr build C++ compiler override" ADVANCED)

build_dependency_with_cmake(libuhdr
VERSION ${libuhdr_BUILD_VERSION}
GIT_REPOSITORY ${libuhdr_GIT_REPOSITORY}
GIT_TAG ${libuhdr_GIT_TAG}
CMAKE_ARGS
-D BUILD_SHARED_LIBS=${libuhdr_BUILD_SHARED_LIBS}
-D CMAKE_INSTALL_LIBDIR=lib
-D CMAKE_POSITION_INDEPENDENT_CODE=ON
-D UHDR_BUILD_EXAMPLES=FALSE
-D UHDR_BUILD_DEPS=FALSE
-D UHDR_ENABLE_LOGS=TRUE
-D JPEG_INCLUDE_DIR=${JPEG_INCLUDE_DIR}
-D JPEG_LIBRARY=${JPEG_LIBRARY}
-D CMAKE_C_COMPILER=${UHDR_CMAKE_C_COMPILER}
-D CMAKE_CXX_COMPILER=${UHDR_CMAKE_CXX_COMPILER}
)

if (WIN32)
file (GLOB _lib_files "${libuhdr_LOCAL_BUILD_DIR}/Release/*.lib")
file (COPY ${_lib_files} DESTINATION ${libuhdr_LOCAL_INSTALL_DIR}/lib)
unset (_lib_files)
file (GLOB _header_files "${libuhdr_LOCAL_SOURCE_DIR}/ultrahdr_api.h")
file (COPY ${_header_files} DESTINATION ${libuhdr_LOCAL_INSTALL_DIR}/include)
unset (_header_files)
endif ()

set (libuhdr_ROOT ${libuhdr_LOCAL_INSTALL_DIR})

find_package(libuhdr REQUIRED)

set (libuhdr_VERSION ${libuhdr_BUILD_VERSION})

if (libuhdr_BUILD_SHARED_LIBS)
install_local_dependency_libs (uhdr uhdr)
endif ()
4 changes: 4 additions & 0 deletions src/cmake/externalpackages.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ else ()
endif ()


# Ultra HDR
checked_find_package (libuhdr)


checked_find_package (TIFF REQUIRED
VERSION_MIN 4.0)
alias_library_if_not_exists (TIFF::TIFF TIFF::tiff)
Expand Down
30 changes: 30 additions & 0 deletions src/cmake/modules/Findlibuhdr.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Module to find libuhdr
#
# Copyright Contributors to the OpenImageIO project.
# SPDX-License-Identifier: Apache-2.0
# https://github.com/AcademySoftwareFoundation/OpenImageIO
#
# This module defines the following variables:
#
# libuhdr_FOUND True if libuhdr was found.
# LIBUHDR_INCLUDE_DIR Where to find libuhdr headers
# LIBUHDR_LIBRARY Library for uhdr

include (FindPackageHandleStandardArgs)

find_path(LIBUHDR_INCLUDE_DIR
NAMES
ultrahdr_api.h
PATH_SUFFIXES
include
)

find_library(LIBUHDR_LIBRARY uhdr
PATH_SUFFIXES
lib
)

find_package_handle_standard_args (libuhdr
REQUIRED_VARS LIBUHDR_INCLUDE_DIR
LIBUHDR_LIBRARY
)
3 changes: 3 additions & 0 deletions src/cmake/testing.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,9 @@ macro (oiio_add_all_tests)
oiio_add_tests (iff
ENABLEVAR ENABLE_IFF
IMAGEDIR oiio-images URL "Recent checkout of OpenImageIO-images")
oiio_add_tests (jpeg-ultrahdr
FOUNDVAR libuhdr_FOUND
IMAGEDIR oiio-images URL "Recent checkout of OpenImageIO-images")
oiio_add_tests (jpeg2000
FOUNDVAR OPENJPEG_FOUND
IMAGEDIR oiio-images URL "Recent checkout of OpenImageIO-images")
Expand Down
12 changes: 12 additions & 0 deletions src/doc/builtinplugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1105,6 +1105,18 @@ via the `ImageInput::set_ioproxy()` method and the special
mode and do not support tiled image input or output.


**Ultra HDR**

JPEG input also suports Ultra HDR images.
Ultra HDR is an image format that encodes a high dynamic range image
in a JPEG image file by including a gain map in addition to the
primary image.
See https://developer.android.com/media/platform/hdr-image-format for
a complete reference on the Ultra HDR image format.
In the specific case of reading an Ultra HDR image, JPEG input will also
support alpha channels and high dynamic range imagery (`half` pixels).



|
Expand Down
11 changes: 11 additions & 0 deletions src/jpeg.imageio/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,19 @@
# SPDX-License-Identifier: Apache-2.0
# https://github.com/AcademySoftwareFoundation/OpenImageIO

if (libuhdr_FOUND)
set (UHDR_DEFS USE_UHDR)
else ()
set (LIBUHDR_INCLUDE_DIR "")
set (LIBUHDR_LIBRARY "")
set (UHDR_DEFS "")
endif ()

add_oiio_plugin (jpeginput.cpp jpegoutput.cpp
INCLUDE_DIRS ${LIBUHDR_INCLUDE_DIR}
LINK_LIBRARIES
$<TARGET_NAME_IF_EXISTS:libjpeg-turbo::jpeg>
$<TARGET_NAME_IF_EXISTS:JPEG::JPEG>
${LIBUHDR_LIBRARY}
DEFINITIONS "${UHDR_DEFS}"
)
14 changes: 14 additions & 0 deletions src/jpeg.imageio/jpeg_pvt.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ extern "C" {
# define OIIO_JPEG_LIB_VERSION JPEG_LIB_VERSION
#endif

#if defined(USE_UHDR)
# include <ultrahdr_api.h>
#endif


OIIO_PLUGIN_NAMESPACE_BEGIN

Expand Down Expand Up @@ -95,6 +99,10 @@ class JpgInput final : public ImageInput {
jvirt_barray_ptr* m_coeffs;
std::vector<unsigned char> m_cmyk_buf; // For CMYK translation
std::unique_ptr<ImageSpec> m_config; // Saved copy of configuration spec
bool m_is_uhdr; // Is interpreted as Ultra HDR image
#if defined(USE_UHDR)
uhdr_codec_private_t* m_uhdr_dec;
#endif

void init()
{
Expand All @@ -106,6 +114,10 @@ class JpgInput final : public ImageInput {
m_jerr.jpginput = this;
ioproxy_clear();
m_config.reset();
m_is_uhdr = false;
#if defined(USE_UHDR)
m_uhdr_dec = NULL;
#endif
}

// Rummage through the JPEG "APP1" marker pointed to by buf, decoding
Expand All @@ -117,6 +129,8 @@ class JpgInput final : public ImageInput {

bool read_icc_profile(j_decompress_ptr cinfo, ImageSpec& spec);

bool read_uhdr(Filesystem::IOProxy* ioproxy);

void close_file() { init(); }

friend class JpgOutput;
Expand Down
115 changes: 115 additions & 0 deletions src/jpeg.imageio/jpeginput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,14 @@ JpgInput::open(const std::string& name, ImageSpec& newspec)

read_icc_profile(&m_cinfo, m_spec); /// try to read icc profile

// Try to interpret as Ultra HDR image.
// The libultrahdr API requires to load the whole file content in memory
// therefore we first check for the presence of the "hdrgm:Version" metadata
// to avoid this costly process when not necessary.
// https://developer.android.com/media/platform/hdr-image-format#signal_of_the_format
if (m_spec.find_attribute("hdrgm:Version"))
m_is_uhdr = read_uhdr(m_io);

newspec = m_spec;
return true;
}
Expand Down Expand Up @@ -406,6 +414,86 @@ JpgInput::read_icc_profile(j_decompress_ptr cinfo, ImageSpec& spec)



bool
JpgInput::read_uhdr(Filesystem::IOProxy* ioproxy)
{
#if defined(USE_UHDR)
// Read entire file content into buffer.
const size_t buffer_size = ioproxy->size();
std::vector<unsigned char> buffer(buffer_size);
ioproxy->pread(buffer.data(), buffer_size, 0);

// Check if this is an actual Ultra HDR image.
const bool detect_uhdr = is_uhdr_image(buffer.data(), buffer.size());
if (!detect_uhdr)
return false;

// Create Ultra HDR decoder.
// Do not forget to release it once we don't need it,
// i.e if this function returns false
// or when we call close().
m_uhdr_dec = uhdr_create_decoder();

// Prepare decoder input.
// Note: we currently do not override any of the
// default settings.
uhdr_compressed_image_t uhdr_compressed;
uhdr_compressed.data = buffer.data();
uhdr_compressed.data_sz = buffer.size();
uhdr_compressed.capacity = buffer.size();
uhdr_dec_set_image(m_uhdr_dec, &uhdr_compressed);

// Decode Ultra HDR image
// and check for decoding errors.
uhdr_error_info_t err_info = uhdr_decode(m_uhdr_dec);

if (err_info.error_code != UHDR_CODEC_OK) {
errorfmt("Ultra HDR decoding failed with error code {}",
int(err_info.error_code));
if (err_info.has_detail != 0)
errorfmt("Additional error details: {}", err_info.detail);
uhdr_release_decoder(m_uhdr_dec);
return false;
}

// Update spec with decoded image properties.
// Note: we currently only support a subset of all possible
// Ultra HDR image formats.
uhdr_raw_image_t* uhdr_raw = uhdr_get_decoded_image(m_uhdr_dec);

int nchannels;
TypeDesc desc;
switch (uhdr_raw->fmt) {
case UHDR_IMG_FMT_32bppRGBA8888:
nchannels = 4;
desc = TypeDesc::UINT8;
break;
case UHDR_IMG_FMT_64bppRGBAHalfFloat:
nchannels = 4;
desc = TypeDesc::HALF;
break;
case UHDR_IMG_FMT_24bppRGB888:
nchannels = 3;
desc = TypeDesc::UINT8;
break;
default:
errorfmt("Unsupported Ultra HDR image format: {}", int(uhdr_raw->fmt));
uhdr_release_decoder(m_uhdr_dec);
return false;
}

ImageSpec newspec = ImageSpec(uhdr_raw->w, uhdr_raw->h, nchannels, desc);
newspec.extra_attribs = std::move(m_spec.extra_attribs);
m_spec = newspec;

return true;
#else
return false;
#endif
}



static void
cmyk_to_rgb(int n, const unsigned char* cmyk, size_t cmyk_stride,
unsigned char* rgb, size_t rgb_stride)
Expand Down Expand Up @@ -453,6 +541,28 @@ JpgInput::read_native_scanline(int subimage, int miplevel, int y, int /*z*/,
OIIO_DASSERT(m_next_scanline == 0 && current_subimage() == subimage);
}

#if defined(USE_UHDR)
if (m_is_uhdr) {
uhdr_raw_image_t* uhdr_raw = uhdr_get_decoded_image(m_uhdr_dec);

unsigned int nbytes;
switch (uhdr_raw->fmt) {
case UHDR_IMG_FMT_32bppRGBA8888: nbytes = 4; break;
case UHDR_IMG_FMT_64bppRGBAHalfFloat: nbytes = 8; break;
case UHDR_IMG_FMT_24bppRGB888: nbytes = 3; break;
default: return false;
}

const size_t row_size = uhdr_raw->stride[UHDR_PLANE_PACKED] * nbytes;
unsigned char* top_left = static_cast<unsigned char*>(
uhdr_raw->planes[UHDR_PLANE_PACKED]);
unsigned char* row_data_start = top_left + row_size * y;
memcpy(data, row_data_start, row_size);

return true;
}
#endif

// Set up our custom error handler
if (setjmp(m_jerr.setjmp_buffer)) {
// Jump to here if there's a libjpeg internal error
Expand Down Expand Up @@ -494,6 +604,11 @@ JpgInput::close()
if (m_decomp_create)
jpeg_destroy_decompress(&m_cinfo);
m_decomp_create = false;
#if defined(USE_UHDR)
if (m_is_uhdr)
uhdr_release_decoder(m_uhdr_dec);
m_is_uhdr = false;
#endif
close_file();
}
init(); // Reset to initial state
Expand Down
10 changes: 10 additions & 0 deletions testsuite/jpeg-ultrahdr/ref/out.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
4080 x 3072, 4 channel, float jpeg
Stats Min: 0.000000 0.000000 0.000000 1.000000 (float)
Stats Max: 1.000000 1.000000 1.000000 1.000000 (float)
Stats Avg: 0.068257 0.077759 0.100931 1.000000 (float)
Stats StdDev: 0.100425 0.102336 0.107618 0.000000 (float)
Stats NanCount: 0 0 0 0
Stats InfCount: 0 0 0 0
Stats FiniteCount: 12533760 12533760 12533760 12533760
Constant: No
Monochrome: No
9 changes: 9 additions & 0 deletions testsuite/jpeg-ultrahdr/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env python

# Copyright Contributors to the OpenImageIO project.
# SPDX-License-Identifier: Apache-2.0
# https://github.com/AcademySoftwareFoundation/OpenImageIO


# read and print stats, that tests the read
command += oiiotool (OIIO_TESTSUITE_IMAGEDIR+"/jpeg/ultrahdr/sky.jpg --printstats")

0 comments on commit 30f6a0a

Please sign in to comment.