diff options
Diffstat (limited to 'tools/hdr')
-rw-r--r-- | tools/hdr/README.md | 16 | ||||
-rw-r--r-- | tools/hdr/display_to_hlg.cc | 25 | ||||
-rw-r--r-- | tools/hdr/exr_to_pq.cc | 158 | ||||
-rw-r--r-- | tools/hdr/generate_lut_template.cc | 11 | ||||
-rw-r--r-- | tools/hdr/image_utils.h | 35 | ||||
-rw-r--r-- | tools/hdr/local_tone_map.cc | 541 | ||||
-rw-r--r-- | tools/hdr/pq_to_hlg.cc | 23 | ||||
-rw-r--r-- | tools/hdr/render_hlg.cc | 23 | ||||
-rw-r--r-- | tools/hdr/texture_to_cube.cc | 11 | ||||
-rw-r--r-- | tools/hdr/tone_map.cc | 28 |
10 files changed, 823 insertions, 48 deletions
diff --git a/tools/hdr/README.md b/tools/hdr/README.md index 227b22b..85eb1bd 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/display_to_hlg.cc b/tools/hdr/display_to_hlg.cc index a2caef2..8fa8fde 100644 --- a/tools/hdr/display_to_hlg.cc +++ b/tools/hdr/display_to_hlg.cc @@ -9,13 +9,15 @@ #include "lib/extras/codec.h" #include "lib/extras/hlg.h" #include "lib/extras/tone_mapping.h" -#include "lib/jxl/base/thread_pool_internal.h" -#include "lib/jxl/enc_color_management.h" +#include "lib/jxl/base/span.h" #include "tools/args.h" #include "tools/cmdline.h" +#include "tools/file_io.h" +#include "tools/hdr/image_utils.h" +#include "tools/thread_pool_internal.h" int main(int argc, const char** argv) { - jxl::ThreadPoolInternal pool; + jpegxl::tools::ThreadPoolInternal pool; jpegxl::tools::CommandLineParser parser; float max_nits = 0; @@ -64,9 +66,11 @@ int main(int argc, const char** argv) { return EXIT_FAILURE; } + std::vector<uint8_t> encoded; + JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded)); jxl::CodecInOut image; - JXL_CHECK(jxl::SetFromFile(input_filename, jxl::extras::ColorHints(), &image, - &pool)); + JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), jxl::extras::ColorHints(), + &image, &pool)); image.metadata.m.SetIntensityTarget(max_nits); JXL_CHECK(jxl::HlgInverseOOTF( &image.Main(), jxl::GetHlgGamma(max_nits, surround_nits), &pool)); @@ -75,11 +79,12 @@ int main(int argc, const char** argv) { jxl::ColorEncoding hlg; hlg.SetColorSpace(jxl::ColorSpace::kRGB); - hlg.primaries = jxl::Primaries::k2100; - hlg.white_point = jxl::WhitePoint::kD65; - hlg.tf.SetTransferFunction(jxl::TransferFunction::kHLG); + JXL_CHECK(hlg.SetPrimariesType(jxl::Primaries::k2100)); + JXL_CHECK(hlg.SetWhitePointType(jxl::WhitePoint::kD65)); + hlg.Tf().SetTransferFunction(jxl::TransferFunction::kHLG); JXL_CHECK(hlg.CreateICC()); - JXL_CHECK(image.TransformTo(hlg, jxl::GetJxlCms(), &pool)); + JXL_CHECK(jpegxl::tools::TransformCodecInOutTo(image, hlg, &pool)); image.metadata.m.color_encoding = hlg; - JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool)); + JXL_CHECK(jxl::Encode(image, output_filename, &encoded, &pool)); + JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded)); } diff --git a/tools/hdr/exr_to_pq.cc b/tools/hdr/exr_to_pq.cc new file mode 100644 index 0000000..c7ce1b7 --- /dev/null +++ b/tools/hdr/exr_to_pq.cc @@ -0,0 +1,158 @@ +// 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/cms/jxl_cms_internal.h" +#include "lib/jxl/image_bundle.h" +#include "tools/cmdline.h" +#include "tools/file_io.h" +#include "tools/hdr/image_utils.h" +#include "tools/thread_pool_internal.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) { + jpegxl::tools::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(jpegxl::tools::ReadFile(input_filename, &input_bytes)); + JXL_CHECK(jxl::extras::DecodeBytes(jxl::Bytes(input_bytes), + jxl::extras::ColorHints(), &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(); + JXL_CHECK(linear_rec_2020.SetPrimariesType(jxl::Primaries::k2100)); + linear_rec_2020.Tf().SetTransferFunction(jxl::TransferFunction::kLinear); + JXL_CHECK(linear_rec_2020.CreateICC()); + JXL_CHECK( + jpegxl::tools::TransformCodecInOutTo(image, linear_rec_2020, &pool)); + + float primaries_xyz[9]; + const jxl::PrimariesCIExy p = image.Main().c_current().GetPrimaries(); + const jxl::CIExy wp = image.Main().c_current().GetWhitePoint(); + JXL_CHECK(jxl::PrimariesToXYZ(p.r.x, p.r.y, p.g.x, p.g.y, p.b.x, p.b.y, wp.x, + wp.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(jpegxl::tools::TransformCodecInOutTo(image, pq, &pool)); + image.metadata.m.color_encoding = pq; + std::vector<uint8_t> encoded; + JXL_CHECK(jxl::Encode(image, output_filename, &encoded, &pool)); + JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded)); +} diff --git a/tools/hdr/generate_lut_template.cc b/tools/hdr/generate_lut_template.cc index 626d54f..da8ecee 100644 --- a/tools/hdr/generate_lut_template.cc +++ b/tools/hdr/generate_lut_template.cc @@ -7,12 +7,13 @@ #include <stdlib.h> #include "lib/extras/codec.h" -#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/image_metadata.h" #include "tools/args.h" #include "tools/cmdline.h" +#include "tools/thread_pool_internal.h" int main(int argc, const char** argv) { - jxl::ThreadPoolInternal pool; + jpegxl::tools::ThreadPoolInternal pool; jpegxl::tools::CommandLineParser parser; size_t N = 64; @@ -55,6 +56,8 @@ int main(int argc, const char** argv) { jxl::CodecInOut output; output.metadata.m.bit_depth.bits_per_sample = 16; output.SetFromImage(std::move(image), jxl::ColorEncoding::SRGB()); - JXL_CHECK(jxl::EncodeToFile(output, jxl::ColorEncoding::SRGB(), 16, - output_filename, &pool)); + std::vector<uint8_t> encoded; + JXL_CHECK(jxl::Encode(output, jxl::ColorEncoding::SRGB(), 16, output_filename, + &encoded, &pool)); + JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded)); } diff --git a/tools/hdr/image_utils.h b/tools/hdr/image_utils.h new file mode 100644 index 0000000..901c2b6 --- /dev/null +++ b/tools/hdr/image_utils.h @@ -0,0 +1,35 @@ +// 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. + +#ifndef TOOLS_HDR_IMAGE_UTILS_H_ +#define TOOLS_HDR_IMAGE_UTILS_H_ + +#include <jxl/cms.h> +#include <jxl/cms_interface.h> + +#include "lib/jxl/base/status.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/image_bundle.h" + +namespace jpegxl { +namespace tools { + +static inline jxl::Status TransformCodecInOutTo( + jxl::CodecInOut& io, const jxl::ColorEncoding& c_desired, + jxl::ThreadPool* pool) { + const JxlCmsInterface& cms = *JxlGetDefaultCms(); + if (io.metadata.m.have_preview) { + JXL_RETURN_IF_ERROR(io.preview_frame.TransformTo(c_desired, cms, pool)); + } + for (jxl::ImageBundle& ib : io.frames) { + JXL_RETURN_IF_ERROR(ib.TransformTo(c_desired, cms, pool)); + } + return true; +} + +} // namespace tools +} // namespace jpegxl + +#endif // TOOLS_HDR_IMAGE_UTILS_H_ diff --git a/tools/hdr/local_tone_map.cc b/tools/hdr/local_tone_map.cc new file mode 100644 index 0000000..b6582a6 --- /dev/null +++ b/tools/hdr/local_tone_map.cc @@ -0,0 +1,541 @@ +// 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 <jxl/cms.h> +#include <stdio.h> +#include <stdlib.h> + +#include "lib/extras/codec.h" +#include "lib/extras/tone_mapping.h" +#include "lib/jxl/convolve.h" +#include "lib/jxl/enc_gamma_correct.h" +#include "lib/jxl/image_bundle.h" +#include "tools/args.h" +#include "tools/cmdline.h" +#include "tools/thread_pool_internal.h" + +namespace jxl { +namespace { + +constexpr WeightsSeparable5 kPyramidFilter = { + {HWY_REP4(.375f), HWY_REP4(.25f), HWY_REP4(.0625f)}, + {HWY_REP4(.375f), HWY_REP4(.25f), HWY_REP4(.0625f)}}; + +template <typename Tin, typename Tout> +void Subtract(const Image3<Tin>& image1, const Image3<Tin>& image2, + Image3<Tout>* out) { + const size_t xsize = image1.xsize(); + const size_t ysize = image1.ysize(); + JXL_CHECK(xsize == image2.xsize()); + JXL_CHECK(ysize == image2.ysize()); + + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < ysize; ++y) { + const Tin* const JXL_RESTRICT row1 = image1.ConstPlaneRow(c, y); + const Tin* const JXL_RESTRICT row2 = image2.ConstPlaneRow(c, y); + Tout* const JXL_RESTRICT row_out = out->PlaneRow(c, y); + for (size_t x = 0; x < xsize; ++x) { + row_out[x] = row1[x] - row2[x]; + } + } + } +} + +// Adds `what` of the size of `rect` to `to` in the position of `rect`. +template <typename Tin, typename Tout> +void AddTo(const Rect& rect, const Image3<Tin>& what, Image3<Tout>* to) { + const size_t xsize = what.xsize(); + const size_t ysize = what.ysize(); + JXL_ASSERT(xsize == rect.xsize()); + JXL_ASSERT(ysize == rect.ysize()); + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < ysize; ++y) { + const Tin* JXL_RESTRICT row_what = what.ConstPlaneRow(c, y); + Tout* JXL_RESTRICT row_to = rect.PlaneRow(to, c, y); + for (size_t x = 0; x < xsize; ++x) { + row_to[x] += row_what[x]; + } + } + } +} + +template <typename T> +Plane<T> Product(const Plane<T>& a, const Plane<T>& b) { + Plane<T> c(a.xsize(), a.ysize()); + for (size_t y = 0; y < a.ysize(); ++y) { + const T* const JXL_RESTRICT row_a = a.Row(y); + const T* const JXL_RESTRICT row_b = b.Row(y); + T* const JXL_RESTRICT row_c = c.Row(y); + for (size_t x = 0; x < a.xsize(); ++x) { + row_c[x] = row_a[x] * row_b[x]; + } + } + return c; +} + +// Expects sRGB input. +// Will call consumer(x, y, contrast) for each pixel. +template <typename Consumer> +void Contrast(const jxl::Image3F& image, const Consumer& consumer, + ThreadPool* const pool) { + static constexpr WeightsSymmetric3 kLaplacianWeights = { + {HWY_REP4(-4)}, {HWY_REP4(1)}, {HWY_REP4(0)}}; + ImageF grayscale(image.xsize(), image.ysize()); + static constexpr float kLuminances[3] = {0.2126, 0.7152, 0.0722}; + for (size_t y = 0; y < image.ysize(); ++y) { + const float* const JXL_RESTRICT input_rows[3] = { + image.PlaneRow(0, y), image.PlaneRow(1, y), image.PlaneRow(2, y)}; + float* const JXL_RESTRICT row = grayscale.Row(y); + + for (size_t x = 0; x < image.xsize(); ++x) { + row[x] = LinearToSrgb8Direct( + kLuminances[0] * Srgb8ToLinearDirect(input_rows[0][x]) + + kLuminances[1] * Srgb8ToLinearDirect(input_rows[1][x]) + + kLuminances[2] * Srgb8ToLinearDirect(input_rows[2][x])); + } + } + + ImageF laplacian(image.xsize(), image.ysize()); + Symmetric3(grayscale, Rect(grayscale), kLaplacianWeights, pool, &laplacian); + for (size_t y = 0; y < image.ysize(); ++y) { + const float* const JXL_RESTRICT row = laplacian.ConstRow(y); + for (size_t x = 0; x < image.xsize(); ++x) { + consumer(x, y, std::abs(row[x])); + } + } +} + +template <typename Consumer> +void Saturation(const jxl::Image3F& image, const Consumer& consumer) { + for (size_t y = 0; y < image.ysize(); ++y) { + const float* const JXL_RESTRICT rows[3] = { + image.PlaneRow(0, y), image.PlaneRow(1, y), image.PlaneRow(2, y)}; + for (size_t x = 0; x < image.xsize(); ++x) { + // TODO(sboukortt): experiment with other methods of computing the + // saturation, e.g. C*/L* in LUV/LCh. + const float mean = (1.f / 3) * (rows[0][x] + rows[1][x] + rows[2][x]); + const float deviations[3] = {rows[0][x] - mean, rows[1][x] - mean, + rows[2][x] - mean}; + consumer(x, y, + std::sqrt((1.f / 3) * (deviations[0] * deviations[0] + + deviations[1] * deviations[1] + + deviations[2] * deviations[2]))); + } + } +} + +template <typename Consumer> +void MidToneness(const jxl::Image3F& image, const float sigma, + const Consumer& consumer) { + const float inv_sigma_squared = 1.f / (sigma * sigma); + const auto Gaussian = [inv_sigma_squared](const float x) { + return std::exp(-.5f * (x - .5f) * (x - .5f) * inv_sigma_squared); + }; + for (size_t y = 0; y < image.ysize(); ++y) { + const float* const JXL_RESTRICT rows[3] = { + image.PlaneRow(0, y), image.PlaneRow(1, y), image.PlaneRow(2, y)}; + for (size_t x = 0; x < image.xsize(); ++x) { + consumer( + x, y, + Gaussian(rows[0][x]) * Gaussian(rows[1][x]) * Gaussian(rows[2][x])); + } + } +} + +ImageF ComputeWeights(const jxl::Image3F& image, const float contrast_weight, + const float saturation_weight, + const float midtoneness_weight, + const float midtoneness_sigma, ThreadPool* const pool) { + ImageF log_weights(image.xsize(), image.ysize()); + ZeroFillImage(&log_weights); + + if (contrast_weight > 0) { + Contrast( + image, + [&log_weights, contrast_weight](const size_t x, const size_t y, + const float weight) { + log_weights.Row(y)[x] = contrast_weight * std::log(weight); + }, + pool); + } + + if (saturation_weight > 0) { + Saturation(image, [&log_weights, saturation_weight]( + const size_t x, const size_t y, const float weight) { + log_weights.Row(y)[x] += saturation_weight * std::log(weight); + }); + } + + if (midtoneness_weight > 0) { + MidToneness(image, midtoneness_sigma, + [&log_weights, midtoneness_weight]( + const size_t x, const size_t y, const float weight) { + log_weights.Row(y)[x] += + midtoneness_weight * std::log(weight); + }); + } + + ImageF weights = std::move(log_weights); + + for (size_t y = 0; y < weights.ysize(); ++y) { + float* const JXL_RESTRICT row = weights.Row(y); + for (size_t x = 0; x < weights.xsize(); ++x) { + row[x] = std::exp(row[x]); + } + } + + return weights; +} + +std::vector<ImageF> ComputeWeights(const std::vector<Image3F>& images, + const float contrast_weight, + const float saturation_weight, + const float midtoneness_weight, + const float midtoneness_sigma, + ThreadPool* const pool) { + std::vector<ImageF> weights; + weights.reserve(images.size()); + for (const Image3F& image : images) { + if (image.xsize() != images.front().xsize() || + image.ysize() != images.front().ysize()) { + return {}; + } + weights.push_back(ComputeWeights(image, contrast_weight, saturation_weight, + midtoneness_weight, midtoneness_sigma, + pool)); + } + + std::vector<float*> rows(images.size()); + for (size_t y = 0; y < images.front().ysize(); ++y) { + for (size_t i = 0; i < images.size(); ++i) { + rows[i] = weights[i].Row(y); + } + for (size_t x = 0; x < images.front().xsize(); ++x) { + float sum = 1e-9f; + for (size_t i = 0; i < images.size(); ++i) { + sum += rows[i][x]; + } + const float ratio = 1.f / sum; + for (size_t i = 0; i < images.size(); ++i) { + rows[i][x] *= ratio; + } + } + } + + return weights; +} + +ImageF Downsample(const ImageF& image, ThreadPool* const pool) { + ImageF filtered(image.xsize(), image.ysize()); + Separable5(image, Rect(image), kPyramidFilter, pool, &filtered); + ImageF result(DivCeil(image.xsize(), 2), DivCeil(image.ysize(), 2)); + for (size_t y = 0; y < result.ysize(); ++y) { + const float* const JXL_RESTRICT filtered_row = filtered.ConstRow(2 * y); + float* const JXL_RESTRICT row = result.Row(y); + for (size_t x = 0; x < result.xsize(); ++x) { + row[x] = filtered_row[2 * x]; + } + } + return result; +} + +Image3F Downsample(const Image3F& image, ThreadPool* const pool) { + return Image3F(Downsample(image.Plane(0), pool), + Downsample(image.Plane(1), pool), + Downsample(image.Plane(2), pool)); +} + +Image3F PadImageMirror(const Image3F& in, const size_t xborder, + const size_t yborder) { + size_t xsize = in.xsize(); + size_t ysize = in.ysize(); + Image3F out(xsize + 2 * xborder, ysize + 2 * yborder); + if (xborder > xsize || yborder > ysize) { + for (size_t c = 0; c < 3; c++) { + for (int32_t y = 0; y < static_cast<int32_t>(out.ysize()); y++) { + float* row_out = out.PlaneRow(c, y); + const float* row_in = in.PlaneRow( + c, Mirror(y - static_cast<int32_t>(yborder), in.ysize())); + for (int32_t x = 0; x < static_cast<int32_t>(out.xsize()); x++) { + int32_t xin = Mirror(x - static_cast<int32_t>(xborder), in.xsize()); + row_out[x] = row_in[xin]; + } + } + } + return out; + } + CopyImageTo(Rect(in), in, Rect(xborder, yborder, xsize, ysize), &out); + for (size_t c = 0; c < 3; c++) { + // Horizontal pad. + for (size_t y = 0; y < ysize; y++) { + for (size_t x = 0; x < xborder; x++) { + out.PlaneRow(c, y + yborder)[x] = + in.ConstPlaneRow(c, y)[xborder - x - 1]; + out.PlaneRow(c, y + yborder)[x + xsize + xborder] = + in.ConstPlaneRow(c, y)[xsize - 1 - x]; + } + } + // Vertical pad. + for (size_t y = 0; y < yborder; y++) { + memcpy(out.PlaneRow(c, y), out.ConstPlaneRow(c, 2 * yborder - 1 - y), + out.xsize() * sizeof(float)); + memcpy(out.PlaneRow(c, y + ysize + yborder), + out.ConstPlaneRow(c, ysize + yborder - 1 - y), + out.xsize() * sizeof(float)); + } + } + return out; +} + +Image3F Upsample(const Image3F& image, const bool odd_width, + const bool odd_height, ThreadPool* const pool) { + const Image3F padded = PadImageMirror(image, 1, 1); + Image3F upsampled(2 * padded.xsize(), 2 * padded.ysize()); + ZeroFillImage(&upsampled); + for (int c = 0; c < 3; ++c) { + for (size_t y = 0; y < padded.ysize(); ++y) { + const float* const JXL_RESTRICT padded_row = padded.ConstPlaneRow(c, y); + float* const JXL_RESTRICT row = upsampled.PlaneRow(c, 2 * y); + for (size_t x = 0; x < padded.xsize(); ++x) { + row[2 * x] = 4 * padded_row[x]; + } + } + } + Image3F filtered(upsampled.xsize(), upsampled.ysize()); + for (int c = 0; c < 3; ++c) { + Separable5(upsampled.Plane(c), Rect(upsampled), kPyramidFilter, pool, + &filtered.Plane(c)); + } + Image3F result(2 * image.xsize() - (odd_width ? 1 : 0), + 2 * image.ysize() - (odd_height ? 1 : 0)); + CopyImageTo(Rect(2, 2, result.xsize(), result.ysize()), filtered, + Rect(result), &result); + return result; +} + +std::vector<ImageF> GaussianPyramid(ImageF image, int num_levels, + ThreadPool* pool) { + std::vector<ImageF> pyramid(num_levels); + for (int i = 0; i < num_levels - 1; ++i) { + ImageF downsampled = Downsample(image, pool); + pyramid[i] = std::move(image); + image = std::move(downsampled); + } + pyramid[num_levels - 1] = std::move(image); + return pyramid; +} + +std::vector<Image3F> LaplacianPyramid(Image3F image, int num_levels, + ThreadPool* pool) { + std::vector<Image3F> pyramid(num_levels); + for (int i = 0; i < num_levels - 1; ++i) { + Image3F downsampled = Downsample(image, pool); + const bool odd_width = image.xsize() % 2 != 0; + const bool odd_height = image.ysize() % 2 != 0; + Subtract(image, Upsample(downsampled, odd_width, odd_height, pool), &image); + pyramid[i] = std::move(image); + image = std::move(downsampled); + } + pyramid[num_levels - 1] = std::move(image); + return pyramid; +} + +Image3F ReconstructFromLaplacianPyramid(std::vector<Image3F> pyramid, + ThreadPool* const pool) { + Image3F result = std::move(pyramid.back()); + pyramid.pop_back(); + for (auto it = pyramid.rbegin(); it != pyramid.rend(); ++it) { + const bool odd_width = it->xsize() % 2 != 0; + const bool odd_height = it->ysize() % 2 != 0; + result = Upsample(result, odd_width, odd_height, pool); + AddTo(Rect(result), *it, &result); + } + return result; +} + +// Exposure fusion algorithm as described in: +// https://mericam.github.io/exposure_fusion/ +// +// That is, given n images of identical size: for each pixel coordinate, one +// weight per input image is computed, indicating how much each input image will +// contribute to the result. There are therefore n weight maps, the sum of which +// is 1 at every pixel. +// +// Those weights are then applied at various scales rather than directly at full +// resolution. To understand how, it helps to familiarize oneself with Laplacian +// and Gaussian pyramids, as described in "The Laplacian Pyramid as a Compact +// Image Code" by P. Burt and E. Adelson: +// http://persci.mit.edu/pub_pdfs/pyramid83.pdf +// +// A Gaussian pyramid of k levels is a sequence of k images in which the first +// image is the original image and each following level is a low-pass-filtered +// version of the previous one. A Laplacian pyramid is obtained from a Gaussian +// pyramid by: +// +// laplacian_pyramid[i] = gaussian_pyramid[i] − gaussian_pyramid[i + 1]. +// (The last item of the Laplacian pyramid is just the last one from the +// Gaussian pyramid without subtraction.) +// +// From there, the original image can be reconstructed by adding all the images +// from the Laplacian pyramid together. (If desired, the Gaussian pyramid can be +// reconstructed as well by storing the cumulative sums starting from the end.) +// +// Having established that, the application of the weight images is done by +// constructing a Laplacian pyramid for each input image, as well as a Gaussian +// pyramid for each weight image, and then constructing a Laplacian pyramid such +// that: +// +// pyramid[i] = sum(laplacian_pyramids[j][i] .* weight_gaussian_pyramids[j][i] +// for j in 1..n) +// +// And then reconstructing an image from the pyramid thus obtained. +Image3F ExposureFusion(std::vector<Image3F> images, int num_levels, + const float contrast_weight, + const float saturation_weight, + const float midtoneness_weight, + const float midtoneness_sigma, ThreadPool* const pool) { + std::vector<ImageF> weights = + ComputeWeights(images, contrast_weight, saturation_weight, + midtoneness_weight, midtoneness_sigma, pool); + + std::vector<Image3F> pyramid(num_levels); + for (size_t i = 0; i < images.size(); ++i) { + const std::vector<ImageF> weight_pyramid = + GaussianPyramid(std::move(weights[i]), num_levels, pool); + const std::vector<Image3F> image_pyramid = + LaplacianPyramid(std::move(images[i]), num_levels, pool); + + for (int k = 0; k < num_levels; ++k) { + Image3F product(Product(weight_pyramid[k], image_pyramid[k].Plane(0)), + Product(weight_pyramid[k], image_pyramid[k].Plane(1)), + Product(weight_pyramid[k], image_pyramid[k].Plane(2))); + if (pyramid[k].xsize() == 0) { + pyramid[k] = std::move(product); + } else { + AddTo(Rect(product), product, &pyramid[k]); + } + } + } + + return ReconstructFromLaplacianPyramid(std::move(pyramid), pool); +} + +} // namespace +} // namespace jxl + +int main(int argc, const char** argv) { + jpegxl::tools::ThreadPoolInternal pool; + + jpegxl::tools::CommandLineParser parser; + float max_nits = 0; + parser.AddOptionValue('m', "max_nits", "nits", + "maximum luminance in the image", &max_nits, + &jpegxl::tools::ParseFloat, 0); + float preserve_saturation = .1f; + parser.AddOptionValue( + 's', "preserve_saturation", "0..1", + "to what extent to try and preserve saturation over luminance", + &preserve_saturation, &jpegxl::tools::ParseFloat, 0); + int64_t num_levels = -1; + parser.AddOptionValue('l', "num_levels", "1..", + "number of levels in the pyramid", &num_levels, + &jpegxl::tools::ParseInt64, 0); + float contrast_weight = 0.f; + parser.AddOptionValue('c', "contrast_weight", "0..", + "importance of contrast when computing weights", + &contrast_weight, &jpegxl::tools::ParseFloat, 0); + float saturation_weight = .2f; + parser.AddOptionValue('a', "saturation_weight", "0..", + "importance of saturation when computing weights", + &saturation_weight, &jpegxl::tools::ParseFloat, 0); + float midtoneness_weight = 1.f; + parser.AddOptionValue('t', "midtoneness_weight", "0..", + "importance of \"midtoneness\" when computing weights", + &midtoneness_weight, &jpegxl::tools::ParseFloat, 0); + float midtoneness_sigma = .2f; + parser.AddOptionValue('g', "midtoneness_sigma", "0..", + "spread of the function that computes midtoneness", + &midtoneness_sigma, &jpegxl::tools::ParseFloat, 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::CodecInOut image; + jxl::extras::ColorHints color_hints; + color_hints.Add("color_space", "RGB_D65_202_Rel_PeQ"); + std::vector<uint8_t> encoded; + JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded)); + JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), color_hints, &image, &pool)); + + if (max_nits > 0) { + image.metadata.m.SetIntensityTarget(max_nits); + } else { + max_nits = image.metadata.m.IntensityTarget(); + } + + std::vector<jxl::Image3F> input_images; + + if (max_nits <= 4 * jxl::kDefaultIntensityTarget) { + jxl::CodecInOut sRGB_image; + jxl::Image3F color(image.xsize(), image.ysize()); + CopyImageTo(*image.Main().color(), &color); + sRGB_image.SetFromImage(std::move(color), image.Main().c_current()); + JXL_CHECK(sRGB_image.Main().TransformTo(jxl::ColorEncoding::SRGB(), + *JxlGetDefaultCms(), &pool)); + input_images.push_back(std::move(*sRGB_image.Main().color())); + } + + for (int i = 0; i < 4; ++i) { + const float target = std::ldexp(jxl::kDefaultIntensityTarget, 2 - i); + if (target >= max_nits) continue; + jxl::CodecInOut tone_mapped_image; + jxl::Image3F color(image.xsize(), image.ysize()); + CopyImageTo(*image.Main().color(), &color); + tone_mapped_image.SetFromImage(std::move(color), image.Main().c_current()); + tone_mapped_image.metadata.m.SetIntensityTarget( + image.metadata.m.IntensityTarget()); + JXL_CHECK(jxl::ToneMapTo({0, target}, &tone_mapped_image, &pool)); + JXL_CHECK(jxl::GamutMap(&tone_mapped_image, preserve_saturation, &pool)); + JXL_CHECK(tone_mapped_image.Main().TransformTo(jxl::ColorEncoding::SRGB(), + *JxlGetDefaultCms(), &pool)); + input_images.push_back(std::move(*tone_mapped_image.Main().color())); + } + + if (num_levels < 1) { + num_levels = jxl::FloorLog2Nonzero(std::min(image.xsize(), image.ysize())); + } + + jxl::Image3F fused = jxl::ExposureFusion( + std::move(input_images), num_levels, contrast_weight, saturation_weight, + midtoneness_weight, midtoneness_sigma, &pool); + + jxl::CodecInOut output; + output.SetFromImage(std::move(fused), jxl::ColorEncoding::SRGB()); + + JXL_CHECK(jxl::Encode(output, output_filename, &encoded, &pool)); + JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded)); +} diff --git a/tools/hdr/pq_to_hlg.cc b/tools/hdr/pq_to_hlg.cc index 3b2125b..ea47a6b 100644 --- a/tools/hdr/pq_to_hlg.cc +++ b/tools/hdr/pq_to_hlg.cc @@ -9,13 +9,13 @@ #include "lib/extras/codec.h" #include "lib/extras/hlg.h" #include "lib/extras/tone_mapping.h" -#include "lib/jxl/base/thread_pool_internal.h" -#include "lib/jxl/enc_color_management.h" #include "tools/args.h" #include "tools/cmdline.h" +#include "tools/hdr/image_utils.h" +#include "tools/thread_pool_internal.h" int main(int argc, const char** argv) { - jxl::ThreadPoolInternal pool; + jpegxl::tools::ThreadPoolInternal pool; jpegxl::tools::CommandLineParser parser; float max_nits = 0; @@ -56,10 +56,14 @@ int main(int argc, const char** argv) { jxl::CodecInOut image; jxl::extras::ColorHints color_hints; color_hints.Add("color_space", "RGB_D65_202_Rel_PeQ"); - JXL_CHECK(jxl::SetFromFile(input_filename, color_hints, &image, &pool)); + std::vector<uint8_t> encoded; + JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded)); + JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), color_hints, &image, &pool)); if (max_nits > 0) { image.metadata.m.SetIntensityTarget(max_nits); } + const jxl::Primaries original_primaries = + image.Main().c_current().GetPrimariesType(); JXL_CHECK(jxl::ToneMapTo({0, 1000}, &image, &pool)); JXL_CHECK(jxl::HlgInverseOOTF(&image.Main(), 1.2f, &pool)); JXL_CHECK(jxl::GamutMap(&image, preserve_saturation, &pool)); @@ -70,11 +74,12 @@ int main(int argc, const char** argv) { jxl::ColorEncoding hlg; hlg.SetColorSpace(jxl::ColorSpace::kRGB); - hlg.primaries = jxl::Primaries::k2100; - hlg.white_point = jxl::WhitePoint::kD65; - hlg.tf.SetTransferFunction(jxl::TransferFunction::kHLG); + JXL_CHECK(hlg.SetPrimariesType(original_primaries)); + JXL_CHECK(hlg.SetWhitePointType(jxl::WhitePoint::kD65)); + hlg.Tf().SetTransferFunction(jxl::TransferFunction::kHLG); JXL_CHECK(hlg.CreateICC()); - JXL_CHECK(image.TransformTo(hlg, jxl::GetJxlCms(), &pool)); + JXL_CHECK(jpegxl::tools::TransformCodecInOutTo(image, hlg, &pool)); image.metadata.m.color_encoding = hlg; - JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool)); + JXL_CHECK(jxl::Encode(image, output_filename, &encoded, &pool)); + JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded)); } diff --git a/tools/hdr/render_hlg.cc b/tools/hdr/render_hlg.cc index c8a2395..cca43b1 100644 --- a/tools/hdr/render_hlg.cc +++ b/tools/hdr/render_hlg.cc @@ -9,13 +9,13 @@ #include "lib/extras/codec.h" #include "lib/extras/hlg.h" #include "lib/extras/tone_mapping.h" -#include "lib/jxl/base/thread_pool_internal.h" -#include "lib/jxl/enc_color_management.h" #include "tools/args.h" #include "tools/cmdline.h" +#include "tools/hdr/image_utils.h" +#include "tools/thread_pool_internal.h" int main(int argc, const char** argv) { - jxl::ThreadPoolInternal pool; + jpegxl::tools::ThreadPoolInternal pool; jpegxl::tools::CommandLineParser parser; float target_nits = 0; @@ -71,7 +71,9 @@ int main(int argc, const char** argv) { jxl::CodecInOut image; jxl::extras::ColorHints color_hints; color_hints.Add("color_space", "RGB_D65_202_Rel_HLG"); - JXL_CHECK(jxl::SetFromFile(input_filename, color_hints, &image, &pool)); + std::vector<uint8_t> encoded; + JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded)); + JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), color_hints, &image, &pool)); // Ensures that conversions to linear by JxlCms will not apply the OOTF as we // apply it ourselves to control the subsequent gamut mapping. image.metadata.m.SetIntensityTarget(301); @@ -82,13 +84,12 @@ int main(int argc, const char** argv) { image.metadata.m.SetIntensityTarget(target_nits); jxl::ColorEncoding c_out = image.metadata.m.color_encoding; - if (pq) { - c_out.tf.SetTransferFunction(jxl::TransferFunction::kPQ); - } else { - c_out.tf.SetTransferFunction(jxl::TransferFunction::k709); - } + jxl::cms::TransferFunction tf = + pq ? jxl::TransferFunction::kPQ : jxl::TransferFunction::kSRGB; + c_out.Tf().SetTransferFunction(tf); JXL_CHECK(c_out.CreateICC()); - JXL_CHECK(image.TransformTo(c_out, jxl::GetJxlCms(), &pool)); + JXL_CHECK(jpegxl::tools::TransformCodecInOutTo(image, c_out, &pool)); image.metadata.m.color_encoding = c_out; - JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool)); + JXL_CHECK(jxl::Encode(image, output_filename, &encoded, &pool)); + JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded)); } diff --git a/tools/hdr/texture_to_cube.cc b/tools/hdr/texture_to_cube.cc index a5e5af7..0d9f731 100644 --- a/tools/hdr/texture_to_cube.cc +++ b/tools/hdr/texture_to_cube.cc @@ -7,12 +7,13 @@ #include <stdlib.h> #include "lib/extras/codec.h" -#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/image_bundle.h" #include "tools/args.h" #include "tools/cmdline.h" +#include "tools/thread_pool_internal.h" int main(int argc, const char** argv) { - jxl::ThreadPoolInternal pool; + jpegxl::tools::ThreadPoolInternal pool; jpegxl::tools::CommandLineParser parser; const char* input_filename = nullptr; @@ -42,8 +43,10 @@ int main(int argc, const char** argv) { } jxl::CodecInOut image; - JXL_CHECK(jxl::SetFromFile(input_filename, jxl::extras::ColorHints(), &image, - &pool)); + std::vector<uint8_t> encoded; + JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded)); + JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), jxl::extras::ColorHints(), + &image, &pool)); JXL_CHECK(image.xsize() == image.ysize() * image.ysize()); const unsigned N = image.ysize(); diff --git a/tools/hdr/tone_map.cc b/tools/hdr/tone_map.cc index 1ef3823..67fea48 100644 --- a/tools/hdr/tone_map.cc +++ b/tools/hdr/tone_map.cc @@ -8,13 +8,14 @@ #include "lib/extras/codec.h" #include "lib/extras/tone_mapping.h" -#include "lib/jxl/base/thread_pool_internal.h" -#include "lib/jxl/enc_color_management.h" #include "tools/args.h" #include "tools/cmdline.h" +#include "tools/file_io.h" +#include "tools/hdr/image_utils.h" +#include "tools/thread_pool_internal.h" int main(int argc, const char** argv) { - jxl::ThreadPoolInternal pool; + jpegxl::tools::ThreadPoolInternal pool; jpegxl::tools::CommandLineParser parser; float max_nits = 0; @@ -69,7 +70,9 @@ int main(int argc, const char** argv) { jxl::CodecInOut image; jxl::extras::ColorHints color_hints; color_hints.Add("color_space", "RGB_D65_202_Rel_PeQ"); - JXL_CHECK(jxl::SetFromFile(input_filename, color_hints, &image, &pool)); + std::vector<uint8_t> encoded; + JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded)); + JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), color_hints, &image, &pool)); if (max_nits > 0) { image.metadata.m.SetIntensityTarget(max_nits); } @@ -77,13 +80,18 @@ int main(int argc, const char** argv) { JXL_CHECK(jxl::GamutMap(&image, preserve_saturation, &pool)); jxl::ColorEncoding c_out = image.metadata.m.color_encoding; - if (pq) { - c_out.tf.SetTransferFunction(jxl::TransferFunction::kPQ); - } else { - c_out.tf.SetTransferFunction(jxl::TransferFunction::k709); + jxl::cms::TransferFunction tf = + pq ? jxl::TransferFunction::kPQ : jxl::TransferFunction::kSRGB; + + if (jxl::extras::CodecFromPath(output_filename) == jxl::extras::Codec::kEXR) { + tf = jxl::TransferFunction::kLinear; + image.metadata.m.SetFloat16Samples(); } + c_out.Tf().SetTransferFunction(tf); + JXL_CHECK(c_out.CreateICC()); - JXL_CHECK(image.TransformTo(c_out, jxl::GetJxlCms(), &pool)); + JXL_CHECK(jpegxl::tools::TransformCodecInOutTo(image, c_out, &pool)); image.metadata.m.color_encoding = c_out; - JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool)); + JXL_CHECK(jxl::Encode(image, output_filename, &encoded, &pool)); + JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded)); } |