Blob Blame History Raw
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));
+}