From 873890998bb151ba54a865cdbd61df22af29774c Mon Sep 17 00:00:00 2001
From: Sami Boukortt <sboukortt@google.com>
Date: Mon, 22 Aug 2022 16:08:20 +0200
Subject: [PATCH] Tool for converting EXR images to PQ PNGs
---
lib/extras/dec/exr.cc | 2 +-
tools/CMakeLists.txt | 2 +
tools/hdr/README.md | 16 +++++
tools/hdr/exr_to_pq.cc | 155 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 174 insertions(+), 1 deletion(-)
create mode 100644 tools/hdr/exr_to_pq.cc
diff --git a/lib/extras/dec/exr.cc b/lib/extras/dec/exr.cc
index ddb6d534e5..e63c005628 100644
--- a/lib/extras/dec/exr.cc
+++ b/lib/extras/dec/exr.cc
@@ -87,7 +87,7 @@ Status DecodeImageEXR(Span<const uint8_t> bytes, const ColorHints& color_hints,
const float intensity_target = OpenEXR::hasWhiteLuminance(input.header())
? OpenEXR::whiteLuminance(input.header())
- : kDefaultIntensityTarget;
+ : 0;
auto image_size = input.displayWindow().size();
// Size is computed as max - min, but both bounds are inclusive.
diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt
index ed773190ec..739d4bcede 100644
--- a/tools/CMakeLists.txt
+++ b/tools/CMakeLists.txt
@@ -164,6 +164,7 @@ if(JPEGXL_ENABLE_DEVTOOLS)
butteraugli_main
decode_and_encode
display_to_hlg
+ exr_to_pq
pq_to_hlg
render_hlg
tone_map
@@ -180,6 +181,7 @@ if(JPEGXL_ENABLE_DEVTOOLS)
add_executable(butteraugli_main butteraugli_main.cc)
add_executable(decode_and_encode decode_and_encode.cc)
add_executable(display_to_hlg hdr/display_to_hlg.cc)
+ add_executable(exr_to_pq hdr/exr_to_pq.cc)
add_executable(pq_to_hlg hdr/pq_to_hlg.cc)
add_executable(render_hlg hdr/render_hlg.cc)
add_executable(tone_map hdr/tone_map.cc)
diff --git a/tools/hdr/README.md b/tools/hdr/README.md
index 227b22b3e4..85eb1bd774 100644
--- a/tools/hdr/README.md
+++ b/tools/hdr/README.md
@@ -99,6 +99,22 @@ This is the mathematical inverse of `tools/render_hlg`. Furthermore,
`tools/pq_to_hlg` is equivalent to `tools/tone_map -t 1000` followed by
`tools/display_to_hlg -m 1000`.
+## OpenEXR to PQ
+
+`tools/exr_to_pq` converts an OpenEXR image into a Rec. 2020 + PQ image, which
+can be saved as a PNG or PPM file. Luminance information is taken from the
+`whiteLuminance` tag if the input has it, and otherwise defaults to treating
+(1, 1, 1) as 100 cd/m². It is also possible to override this using the
+`--luminance` (`-l`) flag, in two different ways:
+
+```shell
+# Specifies that the brightest pixel in the image happens to be 1500 cd/m².
+$ tools/exr_to_pq --luminance='max=1500' input.exr output.png
+
+# Specifies that (1, 1, 1) in the input file is 203 cd/m².
+$ tools/exr_to_pq --luminance='white=203' input.exr output.png
+```
+
# LUT generation
There are additionally two tools that can be used to generate look-up tables
diff --git a/tools/hdr/exr_to_pq.cc b/tools/hdr/exr_to_pq.cc
new file mode 100644
index 0000000000..6162b72221
--- /dev/null
+++ b/tools/hdr/exr_to_pq.cc
@@ -0,0 +1,155 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "lib/extras/codec.h"
+#include "lib/extras/dec/decode.h"
+#include "lib/extras/packed_image_convert.h"
+#include "lib/jxl/base/file_io.h"
+#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/enc_color_management.h"
+#include "tools/cmdline.h"
+
+namespace {
+
+struct LuminanceInfo {
+ enum class Kind { kWhite, kMaximum };
+ Kind kind = Kind::kWhite;
+ float luminance = 100.f;
+};
+
+bool ParseLuminanceInfo(const char* argument, LuminanceInfo* luminance_info) {
+ if (strncmp(argument, "white=", 6) == 0) {
+ luminance_info->kind = LuminanceInfo::Kind::kWhite;
+ argument += 6;
+ } else if (strncmp(argument, "max=", 4) == 0) {
+ luminance_info->kind = LuminanceInfo::Kind::kMaximum;
+ argument += 4;
+ } else {
+ fprintf(stderr,
+ "Invalid prefix for luminance info, expected white= or max=\n");
+ return false;
+ }
+ return jpegxl::tools::ParseFloat(argument, &luminance_info->luminance);
+}
+
+} // namespace
+
+int main(int argc, const char** argv) {
+ jxl::ThreadPoolInternal pool;
+
+ jpegxl::tools::CommandLineParser parser;
+ LuminanceInfo luminance_info;
+ auto luminance_option =
+ parser.AddOptionValue('l', "luminance", "<max|white=N>",
+ "luminance information (defaults to whiteLuminance "
+ "header if present, otherwise to white=100)",
+ &luminance_info, &ParseLuminanceInfo, 0);
+ const char* input_filename = nullptr;
+ auto input_filename_option = parser.AddPositionalOption(
+ "input", true, "input image", &input_filename, 0);
+ const char* output_filename = nullptr;
+ auto output_filename_option = parser.AddPositionalOption(
+ "output", true, "output image", &output_filename, 0);
+
+ if (!parser.Parse(argc, argv)) {
+ fprintf(stderr, "See -h for help.\n");
+ return EXIT_FAILURE;
+ }
+
+ if (parser.HelpFlagPassed()) {
+ parser.PrintHelp();
+ return EXIT_SUCCESS;
+ }
+
+ if (!parser.GetOption(input_filename_option)->matched()) {
+ fprintf(stderr, "Missing input filename.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+ if (!parser.GetOption(output_filename_option)->matched()) {
+ fprintf(stderr, "Missing output filename.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+
+ jxl::extras::PackedPixelFile ppf;
+ std::vector<uint8_t> input_bytes;
+ JXL_CHECK(jxl::ReadFile(input_filename, &input_bytes));
+ JXL_CHECK(jxl::extras::DecodeBytes(jxl::Span<const uint8_t>(input_bytes),
+ jxl::extras::ColorHints(),
+ jxl::SizeConstraints(), &ppf));
+
+ jxl::CodecInOut image;
+ JXL_CHECK(
+ jxl::extras::ConvertPackedPixelFileToCodecInOut(ppf, &pool, &image));
+ image.metadata.m.bit_depth.exponent_bits_per_sample = 0;
+ jxl::ColorEncoding linear_rec_2020 = image.Main().c_current();
+ linear_rec_2020.primaries = jxl::Primaries::k2100;
+ linear_rec_2020.tf.SetTransferFunction(jxl::TransferFunction::kLinear);
+ JXL_CHECK(linear_rec_2020.CreateICC());
+ JXL_CHECK(image.TransformTo(linear_rec_2020, jxl::GetJxlCms(), &pool));
+
+ float primaries_xyz[9];
+ const jxl::PrimariesCIExy primaries = image.Main().c_current().GetPrimaries();
+ const jxl::CIExy white_point = image.Main().c_current().GetWhitePoint();
+ JXL_CHECK(jxl::PrimariesToXYZ(primaries.r.x, primaries.r.y, primaries.g.x,
+ primaries.g.y, primaries.b.x, primaries.b.y,
+ white_point.x, white_point.y, primaries_xyz));
+
+ float max_value = 0.f;
+ float max_relative_luminance = 0.f;
+ float white_luminance = ppf.info.intensity_target != 0 &&
+ !parser.GetOption(luminance_option)->matched()
+ ? ppf.info.intensity_target
+ : luminance_info.kind == LuminanceInfo::Kind::kWhite
+ ? luminance_info.luminance
+ : 0.f;
+ bool out_of_gamut = false;
+ for (size_t y = 0; y < image.ysize(); ++y) {
+ const float* const rows[3] = {image.Main().color()->ConstPlaneRow(0, y),
+ image.Main().color()->ConstPlaneRow(1, y),
+ image.Main().color()->ConstPlaneRow(2, y)};
+ for (size_t x = 0; x < image.xsize(); ++x) {
+ if (!out_of_gamut &&
+ (rows[0][x] < 0 || rows[1][x] < 0 || rows[2][x] < 0)) {
+ out_of_gamut = true;
+ fprintf(stderr,
+ "WARNING: found colors outside of the Rec. 2020 gamut.\n");
+ }
+ max_value = std::max(
+ max_value, std::max(rows[0][x], std::max(rows[1][x], rows[2][x])));
+ const float luminance = primaries_xyz[1] * rows[0][x] +
+ primaries_xyz[4] * rows[1][x] +
+ primaries_xyz[7] * rows[2][x];
+ if (luminance_info.kind == LuminanceInfo::Kind::kMaximum &&
+ luminance > max_relative_luminance) {
+ max_relative_luminance = luminance;
+ white_luminance = luminance_info.luminance / luminance;
+ }
+ }
+ }
+ jxl::ScaleImage(1.f / max_value, image.Main().color());
+ white_luminance *= max_value;
+ image.metadata.m.SetIntensityTarget(white_luminance);
+ if (white_luminance > 10000) {
+ fprintf(stderr,
+ "WARNING: the image is too bright for PQ (would need (1, 1, 1) to "
+ "be %g cd/m^2).\n",
+ white_luminance);
+ } else {
+ fprintf(stderr,
+ "The resulting image should be compressed with "
+ "--intensity_target=%g.\n",
+ white_luminance);
+ }
+
+ jxl::ColorEncoding pq = image.Main().c_current();
+ pq.tf.SetTransferFunction(jxl::TransferFunction::kPQ);
+ JXL_CHECK(pq.CreateICC());
+ JXL_CHECK(image.TransformTo(pq, jxl::GetJxlCms(), &pool));
+ image.metadata.m.color_encoding = pq;
+ JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool));
+}