Compare commits

...
Sign in to create a new pull request.

4 commits

Author SHA1 Message Date
Dominik 'Rathann' Mierzejewski
5963802d87 fix missing include on aarch64 2025-08-12 15:03:54 +02:00
Dominik 'Rathann' Mierzejewski
4ee22d2808 update to 0.7.2 (resolves rhbz#2384428)
- fixes CVE-2024-11403 and CVE-2024-11498
2025-08-12 13:34:39 +02:00
Sérgio M. Basto
95c02f0c1b fix epel8 builds 2023-03-20 16:28:14 +00:00
Robert-André Mauchin
bbdca6f5b2 Update to 0.7.0
EPEL9 build

Close: rhbz#2143289
2022-11-19 17:33:15 +01:00
6 changed files with 881 additions and 12 deletions

4
.gitignore vendored
View file

@ -4,3 +4,7 @@
/third_party-0.5.tar.gz
/jpegxl-0.6.1.tar.gz
/third_party-0.6.1.tar.gz
/jpegxl-0.7.0.tar.gz
/third_party-0.7.0.tar.gz
/jpegxl-0.7.2.tar.gz
/third_party-0.7.2.tar.gz

View file

@ -0,0 +1,579 @@
From 795b363120bbcfdbb2e2e4fa85146ac6d385137d Mon Sep 17 00:00:00 2001
From: Jon Sneyers <jon@cloudinary.com>
Date: Mon, 29 Aug 2022 08:37:24 +0200
Subject: [PATCH] SSIMULACRA 2 (#1646)
---
tools/CMakeLists.txt | 2 +
tools/ssimulacra2.cc | 424 ++++++++++++++++++++++++++++++++++++++
tools/ssimulacra2.h | 32 +++
tools/ssimulacra2_main.cc | 70 +++++++
4 files changed, 528 insertions(+)
create mode 100644 tools/ssimulacra2.cc
create mode 100644 tools/ssimulacra2.h
create mode 100644 tools/ssimulacra2_main.cc
diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt
index 739d4bcede..0d6a28f55a 100644
--- a/tools/CMakeLists.txt
+++ b/tools/CMakeLists.txt
@@ -171,6 +171,7 @@ if(JPEGXL_ENABLE_DEVTOOLS)
texture_to_cube
generate_lut_template
ssimulacra_main
+ ssimulacra2
xyb_range
jxl_from_tree
)
@@ -178,6 +179,7 @@ if(JPEGXL_ENABLE_DEVTOOLS)
add_executable(fuzzer_corpus fuzzer_corpus.cc)
add_executable(ssimulacra_main ssimulacra_main.cc ssimulacra.cc)
+ add_executable(ssimulacra2 ssimulacra2_main.cc ssimulacra2.cc)
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)
diff --git a/tools/ssimulacra2.cc b/tools/ssimulacra2.cc
new file mode 100644
index 0000000000..8bc31b87e7
--- /dev/null
+++ b/tools/ssimulacra2.cc
@@ -0,0 +1,424 @@
+// 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.
+
+/*
+SSIMULACRA 2 - Structural SIMilarity Unveiling Local And Compression Related
+Artifacts
+
+Perceptual metric developed by Jon Sneyers (Cloudinary) in July 2022.
+Design:
+- XYB color space (X+0.5, Y, Y-B+1.0)
+- SSIM map
+- 'blockiness/ringing' map (distorted has edges where original is smooth)
+- 'smoothing' map (distorted is smooth where original has edges)
+- error maps are computed at 6 scales (1:1 to 1:32) for each component (X,Y,B)
+- for all 6*3*3=54 maps, two norms are computed: 1-norm (mean) and 4-norm
+- a weighted sum of these 54*2=108 norms leads to the final score
+- weights were tuned based on a large set of subjective scores for images
+ compressed with JPEG, JPEG 2000, JPEG XL, WebP, AVIF, and HEIC.
+*/
+
+#include "tools/ssimulacra2.h"
+
+#include <stdio.h>
+
+#include <cmath>
+
+#include "lib/jxl/enc_color_management.h"
+#include "lib/jxl/enc_xyb.h"
+#include "lib/jxl/gauss_blur.h"
+#include "lib/jxl/image_ops.h"
+
+namespace {
+
+using jxl::Image3F;
+using jxl::ImageF;
+
+static const float kC1 = 0.0001f;
+static const float kC2 = 0.0003f;
+static const int kNumScales = 6;
+
+Image3F Downsample(const Image3F& in, size_t fx, size_t fy) {
+ const size_t out_xsize = (in.xsize() + fx - 1) / fx;
+ const size_t out_ysize = (in.ysize() + fy - 1) / fy;
+ Image3F out(out_xsize, out_ysize);
+ const float normalize = 1.0f / (fx * fy);
+ for (size_t c = 0; c < 3; ++c) {
+ for (size_t oy = 0; oy < out_ysize; ++oy) {
+ float* JXL_RESTRICT row_out = out.PlaneRow(c, oy);
+ for (size_t ox = 0; ox < out_xsize; ++ox) {
+ float sum = 0.0f;
+ for (size_t iy = 0; iy < fy; ++iy) {
+ for (size_t ix = 0; ix < fx; ++ix) {
+ const size_t x = std::min(ox * fx + ix, in.xsize() - 1);
+ const size_t y = std::min(oy * fy + iy, in.ysize() - 1);
+ sum += in.PlaneRow(c, y)[x];
+ }
+ }
+ row_out[ox] = sum * normalize;
+ }
+ }
+ }
+ return out;
+}
+
+void Multiply(const Image3F& a, const Image3F& b, Image3F* mul) {
+ for (size_t c = 0; c < 3; ++c) {
+ for (size_t y = 0; y < a.ysize(); ++y) {
+ const float* JXL_RESTRICT in1 = a.PlaneRow(c, y);
+ const float* JXL_RESTRICT in2 = b.PlaneRow(c, y);
+ float* JXL_RESTRICT out = mul->PlaneRow(c, y);
+ for (size_t x = 0; x < a.xsize(); ++x) {
+ out[x] = in1[x] * in2[x];
+ }
+ }
+ }
+}
+
+// Temporary storage for Gaussian blur, reused for multiple images.
+class Blur {
+ public:
+ Blur(const size_t xsize, const size_t ysize)
+ : rg_(jxl::CreateRecursiveGaussian(1.5)), temp_(xsize, ysize) {}
+
+ void operator()(const ImageF& in, ImageF* JXL_RESTRICT out) {
+ jxl::ThreadPool* null_pool = nullptr;
+ FastGaussian(rg_, in, null_pool, &temp_, out);
+ }
+
+ Image3F operator()(const Image3F& in) {
+ Image3F out(in.xsize(), in.ysize());
+ operator()(in.Plane(0), &out.Plane(0));
+ operator()(in.Plane(1), &out.Plane(1));
+ operator()(in.Plane(2), &out.Plane(2));
+ return out;
+ }
+
+ // Allows reusing across scales.
+ void ShrinkTo(const size_t xsize, const size_t ysize) {
+ temp_.ShrinkTo(xsize, ysize);
+ }
+
+ private:
+ hwy::AlignedUniquePtr<jxl::RecursiveGaussian> rg_;
+ ImageF temp_;
+};
+
+double tothe4th(double x) {
+ x *= x;
+ x *= x;
+ return x;
+}
+void SSIMMap(const Image3F& m1, const Image3F& m2, const Image3F& s11,
+ const Image3F& s22, const Image3F& s12, double* plane_averages) {
+ const double onePerPixels = 1.0 / (m1.ysize() * m1.xsize());
+ for (size_t c = 0; c < 3; ++c) {
+ double sum1[2] = {0.0};
+ for (size_t y = 0; y < m1.ysize(); ++y) {
+ const float* JXL_RESTRICT row_m1 = m1.PlaneRow(c, y);
+ const float* JXL_RESTRICT row_m2 = m2.PlaneRow(c, y);
+ const float* JXL_RESTRICT row_s11 = s11.PlaneRow(c, y);
+ const float* JXL_RESTRICT row_s22 = s22.PlaneRow(c, y);
+ const float* JXL_RESTRICT row_s12 = s12.PlaneRow(c, y);
+ for (size_t x = 0; x < m1.xsize(); ++x) {
+ float mu1 = row_m1[x];
+ float mu2 = row_m2[x];
+ float mu11 = mu1 * mu1;
+ float mu22 = mu2 * mu2;
+ float mu12 = mu1 * mu2;
+ float num_m = 2 * mu12 + kC1;
+ float num_s = 2 * (row_s12[x] - mu12) + kC2;
+ float denom_m = mu11 + mu22 + kC1;
+ float denom_s = (row_s11[x] - mu11) + (row_s22[x] - mu22) + kC2;
+ double d = 1.0 - ((num_m * num_s) / (denom_m * denom_s));
+ d = std::max(d, 0.0);
+ sum1[0] += d;
+ sum1[1] += tothe4th(d);
+ }
+ }
+ plane_averages[c * 2] = onePerPixels * sum1[0];
+ plane_averages[c * 2 + 1] = sqrt(sqrt(onePerPixels * sum1[1]));
+ }
+}
+
+void EdgeDiffMap(const Image3F& img1, const Image3F& mu1, const Image3F& img2,
+ const Image3F& mu2, double* plane_averages) {
+ const double onePerPixels = 1.0 / (img1.ysize() * img1.xsize());
+ for (size_t c = 0; c < 3; ++c) {
+ double sum1[4] = {0.0};
+ for (size_t y = 0; y < img1.ysize(); ++y) {
+ const float* JXL_RESTRICT row1 = img1.PlaneRow(c, y);
+ const float* JXL_RESTRICT row2 = img2.PlaneRow(c, y);
+ const float* JXL_RESTRICT rowm1 = mu1.PlaneRow(c, y);
+ const float* JXL_RESTRICT rowm2 = mu2.PlaneRow(c, y);
+ for (size_t x = 0; x < img1.xsize(); ++x) {
+ double d1 = (1.0 + std::abs(row2[x] - rowm2[x])) /
+ (1.0 + std::abs(row1[x] - rowm1[x])) -
+ 1.0;
+ // d1 > 0: distorted has an edge where original is smooth
+ // (indicating ringing, color banding, blockiness, etc)
+ // d1 < 0: original has an edge where distorted is smooth
+ // (indicating smoothing, blurring, smearing, etc)
+ double artifact = std::max(d1, 0.0);
+ sum1[0] += artifact;
+ sum1[1] += tothe4th(artifact);
+ double detail_lost = std::max(-d1, 0.0);
+ sum1[2] += detail_lost;
+ sum1[3] += tothe4th(detail_lost);
+ }
+ }
+ plane_averages[c * 4] = onePerPixels * sum1[0];
+ plane_averages[c * 4 + 1] = sqrt(sqrt(onePerPixels * sum1[1]));
+ plane_averages[c * 4 + 2] = onePerPixels * sum1[2];
+ plane_averages[c * 4 + 3] = sqrt(sqrt(onePerPixels * sum1[3]));
+ }
+}
+
+// Add 0.5 to X and turn B into 1 + B-Y
+// (SSIM expects non-negative ranges)
+void MakePositiveXYB(jxl::Image3F& img) {
+ for (size_t y = 0; y < img.ysize(); ++y) {
+ const float* JXL_RESTRICT rowY = img.PlaneRow(1, y);
+ float* JXL_RESTRICT rowB = img.PlaneRow(2, y);
+ float* JXL_RESTRICT rowX = img.PlaneRow(0, y);
+ for (size_t x = 0; x < img.xsize(); ++x) {
+ rowB[x] += 1.0f - rowY[x];
+ rowX[x] += 0.5f;
+ }
+ }
+}
+
+void AlphaBlend(jxl::ImageBundle& img, float bg) {
+ for (size_t y = 0; y < img.ysize(); ++y) {
+ float* JXL_RESTRICT r = img.color()->PlaneRow(0, y);
+ float* JXL_RESTRICT g = img.color()->PlaneRow(1, y);
+ float* JXL_RESTRICT b = img.color()->PlaneRow(2, y);
+ const float* JXL_RESTRICT a = img.alpha()->Row(y);
+ for (size_t x = 0; x < img.xsize(); ++x) {
+ r[x] = a[x] * r[x] + (1.f - a[x]) * bg;
+ g[x] = a[x] * g[x] + (1.f - a[x]) * bg;
+ b[x] = a[x] * b[x] + (1.f - a[x]) * bg;
+ }
+ }
+}
+
+} // namespace
+
+/*
+The final score is based on a weighted sum of 108 sub-scores:
+- for 6 scales (1:1 to 1:32)
+- for 3 components (X + 0.5, Y, B - Y + 1.0)
+- using 2 norms (the 1-norm and the 4-norm)
+- over 3 error maps:
+ - SSIM
+ - "ringing" (distorted edges where there are no orig edges)
+ - "blurring" (orig edges where there are no distorted edges)
+
+The weights were obtained by running Nelder-Mead simplex search,
+optimizing to minimize MSE and maximize Kendall and Pearson correlation
+for training data consisting of 17611 subjective quality scores,
+validated on separate validation data consisting of 4292 scores.
+*/
+double Msssim::Score() const {
+ double ssim = 0.0;
+
+ constexpr double weight[108] = {-0.4887721343447775,
+ 0.023424809409418046,
+ -2.030114616889109,
+ 0.09295054976135864,
+ -0.6104037165108389,
+ -1.5043796645843681,
+ 0.7157171902116809,
+ 0.021100488642748494,
+ 0.020230927541050825,
+ 0.2133671136286971,
+ 2.907924945735136,
+ 0.022228357764969564,
+ -0.5071214811492599,
+ 0.02509144129036578,
+ 0.022718837619484678,
+ 0.06120691522596422,
+ 0.30589062196252637,
+ 1.0329845093332668,
+ 0.014860334913185502,
+ 0.01765439482987108,
+ 0.015223496420421201,
+ 0.3352969449271075,
+ 0.022351073746802674,
+ 4.920455953428159,
+ 0.024538210211043743,
+ 0.019983582316880577,
+ 0.03292085152661295,
+ 0.030445280321387935,
+ 10.0,
+ 0.018398131276209817,
+ 0.01980139702737216,
+ 0.01896441623922851,
+ 0.022574971070788208,
+ 0.01961469275632899,
+ 0.017925070068654958,
+ 0.022549199991725777,
+ 0.042694824314348456,
+ -1.1040485020844724,
+ -0.8328856967713545,
+ 0.010486287554451912,
+ -0.15815014109963288,
+ 0.5276802305511856,
+ -0.16586473934630663,
+ -1.6713862869185236,
+ -0.5685528568072895,
+ 0.015496263249374809,
+ 0.3313776030981479,
+ 0.5841917745193543,
+ 0.9532541813860999,
+ 1.6364826627594853,
+ -1.4297400383069743,
+ 0.054491944876914444,
+ 0.17913906220820508,
+ 0.44577334447914807,
+ 0.5681719372906395,
+ 3.32951901196007,
+ 0.020829537529045927,
+ -0.5816250842771562,
+ -0.45135245825393433,
+ -0.7025952973955096,
+ 0.4838975315421883,
+ 8.711509549878194,
+ 0.791270929375639,
+ 0.581356024160242,
+ 0.3922969596921355,
+ -0.7387562349132657,
+ -1.2614352370812663,
+ -0.8858225265098523,
+ 0.019758617519097244,
+ 0.14685779225447715,
+ -1.8664586782764085,
+ 0.17359675572073996,
+ 0.03043962244431553,
+ 1.68973994737285,
+ 0.14241547168885893,
+ -0.006681229849861969,
+ 0.035286585384673774,
+ -0.1867844252373887,
+ -0.05325949297845933,
+ -0.6188421202762768,
+ 0.019256498690954693,
+ 0.011261488314097567,
+ -0.8734726170866911,
+ 0.2611408152583321,
+ 0.018228313151663178,
+ 0.9920747746816749,
+ -1.9939450259663953,
+ -0.010909816645386039,
+ 0.8744010916528506,
+ 0.3484037761057752,
+ 0.01700115030331162,
+ 0.0200509230497925,
+ 0.9049230270226147,
+ -0.03502086486907441,
+ -0.13860433328031307,
+ -0.5008190311548453,
+ 0.13113827477880657,
+ 3.814948743048878,
+ 3.369522386056538,
+ 0.002328237048341175,
+ 0.9372362537216947,
+ 0.9359420764223362,
+ -1.0601842054652018,
+ 0.01621181769404767,
+ -0.4876602083027073,
+ 0.1933464422710256,
+ -2.10812470816676,
+ 0.018569908820821213};
+ size_t i = 0;
+ for (size_t c = 0; c < 3; ++c) {
+ for (size_t scale = 0; scale < scales.size(); ++scale) {
+ for (size_t n = 0; n < 2; n++) {
+#ifdef SSIMULACRA2_OUTPUT_RAW_SCORES_FOR_WEIGHT_TUNING
+ printf("%.12f,%.12f,%.12f,", scales[scale].avg_ssim[c * 2 + n],
+ scales[scale].avg_edgediff[c * 4 + n],
+ scales[scale].avg_edgediff[c * 4 + 2 + n]);
+#endif
+ ssim += weight[i++] * std::abs(scales[scale].avg_ssim[c * 2 + n]);
+ ssim += weight[i++] * std::abs(scales[scale].avg_edgediff[c * 4 + n]);
+ ssim +=
+ weight[i++] * std::abs(scales[scale].avg_edgediff[c * 4 + n + 2]);
+ }
+ }
+ }
+
+ ssim *= 272.58355078216107;
+ ssim += -0.12953872587232876;
+
+ if (ssim > 0) {
+ ssim = 100.0 - 10.0 * pow(ssim, 0.6940375837127916);
+ } else {
+ ssim = 100.0;
+ }
+ return ssim;
+}
+
+Msssim ComputeSSIMULACRA2(const jxl::ImageBundle& orig,
+ const jxl::ImageBundle& dist, float bg) {
+ Msssim msssim;
+
+ jxl::Image3F img1(orig.xsize(), orig.ysize());
+ jxl::Image3F img2(img1.xsize(), img1.ysize());
+
+ if (orig.HasAlpha()) {
+ jxl::ImageBundle orig2 = orig.Copy();
+ AlphaBlend(orig2, bg);
+ jxl::ToXYB(orig2, nullptr, &img1, jxl::GetJxlCms(), nullptr);
+ } else {
+ jxl::ToXYB(orig, nullptr, &img1, jxl::GetJxlCms(), nullptr);
+ }
+ if (dist.HasAlpha()) {
+ jxl::ImageBundle dist2 = dist.Copy();
+ AlphaBlend(dist2, bg);
+ jxl::ToXYB(dist2, nullptr, &img2, jxl::GetJxlCms(), nullptr);
+ } else {
+ jxl::ToXYB(dist, nullptr, &img2, jxl::GetJxlCms(), nullptr);
+ }
+ MakePositiveXYB(img1);
+ MakePositiveXYB(img2);
+
+ Image3F mul(img1.xsize(), img1.ysize());
+ Blur blur(img1.xsize(), img1.ysize());
+
+ for (int scale = 0; scale < kNumScales; scale++) {
+ if (img1.xsize() < 8 || img1.ysize() < 8) {
+ break;
+ }
+ if (scale) {
+ img1 = Downsample(img1, 2, 2);
+ img2 = Downsample(img2, 2, 2);
+ }
+ mul.ShrinkTo(img1.xsize(), img2.ysize());
+ blur.ShrinkTo(img1.xsize(), img2.ysize());
+
+ Multiply(img1, img1, &mul);
+ Image3F sigma1_sq = blur(mul);
+
+ Multiply(img2, img2, &mul);
+ Image3F sigma2_sq = blur(mul);
+
+ Multiply(img1, img2, &mul);
+ Image3F sigma12 = blur(mul);
+
+ Image3F mu1 = blur(img1);
+ Image3F mu2 = blur(img2);
+
+ MsssimScale sscale;
+ SSIMMap(mu1, mu2, sigma1_sq, sigma2_sq, sigma12, sscale.avg_ssim);
+ EdgeDiffMap(img1, mu1, img2, mu2, sscale.avg_edgediff);
+ msssim.scales.push_back(sscale);
+ }
+ return msssim;
+}
+
+Msssim ComputeSSIMULACRA2(const jxl::ImageBundle& orig,
+ const jxl::ImageBundle& distorted) {
+ return ComputeSSIMULACRA2(orig, distorted, 0.5f);
+}
diff --git a/tools/ssimulacra2.h b/tools/ssimulacra2.h
new file mode 100644
index 0000000000..36d1193112
--- /dev/null
+++ b/tools/ssimulacra2.h
@@ -0,0 +1,32 @@
+// 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_SSIMULACRA2_H_
+#define TOOLS_SSIMULACRA2_H_
+
+#include <vector>
+
+#include "lib/jxl/image_bundle.h"
+
+struct MsssimScale {
+ double avg_ssim[3 * 2];
+ double avg_edgediff[3 * 4];
+};
+
+struct Msssim {
+ std::vector<MsssimScale> scales;
+
+ double Score() const;
+};
+
+// Computes the SSIMULACRA 2 score between reference image 'orig' and
+// distorted image 'distorted'. In case of alpha transparency, assume
+// a gray background if intensity 'bg' (in range 0..1).
+Msssim ComputeSSIMULACRA2(const jxl::ImageBundle &orig,
+ const jxl::ImageBundle &distorted, float bg);
+Msssim ComputeSSIMULACRA2(const jxl::ImageBundle &orig,
+ const jxl::ImageBundle &distorted);
+
+#endif // TOOLS_SSIMULACRA2_H_
diff --git a/tools/ssimulacra2_main.cc b/tools/ssimulacra2_main.cc
new file mode 100644
index 0000000000..35e284b5dc
--- /dev/null
+++ b/tools/ssimulacra2_main.cc
@@ -0,0 +1,70 @@
+// 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 "lib/extras/codec.h"
+#include "lib/jxl/color_management.h"
+#include "lib/jxl/enc_color_management.h"
+#include "tools/ssimulacra2.h"
+
+int PrintUsage(char** argv) {
+ fprintf(stderr, "Usage: %s orig.png distorted.png\n", argv[0]);
+ fprintf(stderr,
+ "Returns a score in range -inf..100, which correlates to subjective "
+ "visual quality:\n");
+ fprintf(stderr,
+ " 30 = low quality (p10 worst output of mozjpeg -quality 30)\n");
+ fprintf(stderr,
+ " 50 = medium quality (average output of cjxl -q 40 or mozjpeg "
+ "-quality 40,\n");
+ fprintf(stderr,
+ " p10 output of cjxl -q 50 or mozjpeg "
+ "-quality 60)\n");
+ fprintf(stderr,
+ " 70 = high quality (average output of cjxl -q 70 or mozjpeg "
+ "-quality 70,\n");
+ fprintf(stderr,
+ " p10 output of cjxl -q 75 or mozjpeg "
+ "-quality 80)\n");
+ fprintf(stderr,
+ " 90 = very high quality (impossible to distinguish from "
+ "original at 1:1,\n");
+ fprintf(stderr,
+ " average output of cjxl -q 90 or "
+ "mozjpeg -quality 90)\n");
+ return 1;
+}
+
+int main(int argc, char** argv) {
+ if (argc != 3) return PrintUsage(argv);
+
+ jxl::CodecInOut io1;
+ jxl::CodecInOut io2;
+ JXL_CHECK(SetFromFile(argv[1], jxl::extras::ColorHints(), &io1));
+
+ if (io1.xsize() < 8 || io1.ysize() < 8) {
+ fprintf(stderr, "Minimum image size is 8x8 pixels\n");
+ return 1;
+ }
+
+ JXL_CHECK(SetFromFile(argv[2], jxl::extras::ColorHints(), &io2));
+ if (io1.xsize() != io2.xsize() || io1.ysize() != io2.ysize()) {
+ fprintf(stderr, "Image size mismatch\n");
+ return 1;
+ }
+
+ if (!io1.Main().HasAlpha()) {
+ Msssim msssim = ComputeSSIMULACRA2(io1.Main(), io2.Main());
+ printf("%.8f\n", msssim.Score());
+ } else {
+ // in case of alpha transparency: blend against dark and bright backgrounds
+ // and return the worst of both scores
+ Msssim msssim0 = ComputeSSIMULACRA2(io1.Main(), io2.Main(), 0.1f);
+ Msssim msssim1 = ComputeSSIMULACRA2(io1.Main(), io2.Main(), 0.9f);
+ printf("%.8f\n", std::min(msssim0.Score(), msssim1.Score()));
+ }
+ return 0;
+}

View file

@ -0,0 +1,234 @@
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));
+}

View file

@ -0,0 +1,11 @@
diff -up libjxl-0.7.2/lib/jxl/fast_dct-inl.h.orig libjxl-0.7.2/lib/jxl/fast_dct-inl.h
--- libjxl-0.7.2/lib/jxl/fast_dct-inl.h.orig 2024-11-27 18:45:37.000000000 +0100
+++ libjxl-0.7.2/lib/jxl/fast_dct-inl.h 2025-08-12 14:55:41.725697497 +0200
@@ -10,6 +10,7 @@
#define LIB_JXL_FAST_DCT_INL_H_
#endif
+#include <cmath>
#include <hwy/aligned_allocator.h>
#include <hwy/highway.h>

View file

@ -1,7 +1,11 @@
# epel 8 need this other already have it
%undefine __cmake_in_source_build
# Uncomment for special build to rebuild aom on bumped soname.
#global new_soname 1
# %%global new_soname 0
%global sover_old 0.6
%global sover 0.6
%global sover 0.7
%global gdk_pixbuf_moduledir $(pkgconf gdk-pixbuf-2.0 --variable=gdk_pixbuf_moduledir)
@ -15,7 +19,7 @@ This package contains a reference implementation of JPEG XL (encoder and
decoder).}
Name: jpegxl
Version: 0.6.1
Version: 0.7.2
Release: %autorelease %{?new_soname:-p -e 0~sonamebump}
Summary: JPEG XL image format reference implementation
@ -23,19 +27,25 @@ Summary: JPEG XL image format reference implementation
# lodepng: zlib
# sjpeg: ASL 2.0
# skcms: BSD
License: BSD and ASL 2.0 and zlib
License: BSD-3-Clause AND Apache-2.0 AND Zlib
URL: https://jpeg.org/jpegxl/
VCS: https://github.com/libjxl/libjxl
Source0: %vcs/archive/v%{version}/%{name}-%{version}.tar.gz
# exr_to_pq
Patch0: https://github.com/libjxl/libjxl/commit/873890998bb151ba54a865cdbd61df22af29774c.patch
# Ssimulacra 2
Patch1: https://github.com/libjxl/libjxl/commit/795b363120bbcfdbb2e2e4fa85146ac6d385137d.patch
# fix missing include on aarch64
Patch2: %{name}-0.7.2-missing-include.patch
# git clone https://github.com/libjxl/libjxl
# cd libjxl/
# git checkout v%%{version}
# git submodule init ; git submodule update
# rm -r third_party/brotli/ third_party/difftest_ng/ third_party/googletest/
# rm -r third_party/brotli/ third_party/googletest/
# rm -r third_party/HEVCSoftware/ third_party/highway/
# rm -r third_party/IQA-optimization/ third_party/lcms/
# rm -r third_party/skcms/profiles/ third_party/vmaf/ third_party/testdata/
# rm -r third_party/lcms/ third_party/libpng/
# rm -r third_party/skcms/profiles/ third_party/zlib
# tar -zcvf ../third_party-%%{version}.tar.gz third_party/
Source1: third_party-%{version}.tar.gz
@ -54,6 +64,7 @@ BuildRequires: pkgconfig(gimp-2.0)
%endif
BuildRequires: (pkgconfig(glut) or pkgconfig(freeglut))
BuildRequires: gtest-devel
BuildRequires: gflags-devel
BuildRequires: pkgconfig(libhwy)
BuildRequires: pkgconfig(libbrotlicommon)
BuildRequires: pkgconfig(libjpeg)
@ -63,12 +74,12 @@ BuildRequires: pkgconfig(OpenEXR)
BuildRequires: pkgconfig(Qt5)
BuildRequires: pkgconfig(Qt5X11Extras)
BuildRequires: pkgconfig(zlib)
# epel 8 need this other already have it
BuildRequires: python3-devel
%if 0%{?new_soname}
BuildRequires: libjxl < %{version}
%endif
# Header-only library to be directly included in the project's source tree
Provides: bundled(lodepng) = 0-0.1.20210522git48e5364
# No official release
Provides: bundled(sjpeg) = 0-0.1.20210522git868ab55
# Build system is Bazel, which is not packaged by Fedora
@ -87,6 +98,15 @@ Obsoletes: jpegxl-utils < 0.3.7-5
%description -n libjxl-utils
%{common_description}
%package -n libjxl-devtools
Summary: Development tools for JPEG-XL
Requires: libjxl%{?_isa} = %{version}-%{release}
%description -n libjxl-devtools
%{common_description}
Development tools for JPEG-XL
%package doc
Summary: Documentation for JPEG-XL
BuildArch: noarch
@ -151,7 +171,10 @@ rm -rf third_party/
-DJPEGXL_FORCE_SYSTEM_GTEST:BOOL=ON \
-DJPEGXL_FORCE_SYSTEM_HWY:BOOL=ON \
-DJPEGXL_WARNINGS_AS_ERRORS:BOOL=OFF \
-DBUILD_SHARED_LIBS:BOOL=OFF
-DBUILD_SHARED_LIBS:BOOL=ON \
-DBUNDLE_LIBPNG_DEFAULT:BOOL=OFF \
-DBUNDLE_GFLAGS_DEFAULT:BOOL=OFF \
-DJPEGXL_ENABLE_DEVTOOLS=ON
%cmake_build -- all doc
%install
@ -168,9 +191,27 @@ cp -p %{_libdir}/libjxl.so.%{sover_old}* \
%doc CONTRIBUTING.md CONTRIBUTORS README.md
%{_bindir}/cjxl
%{_bindir}/djxl
%{_bindir}/cjpeg_hdr
%{_bindir}/jxlinfo
%{_mandir}/man1/cjxl.1*
%{_mandir}/man1/djxl.1*
%files -n libjxl-devtools
%{_bindir}/fuzzer_corpus
%{_bindir}/butteraugli_main
%{_bindir}/decode_and_encode
%{_bindir}/display_to_hlg
%{_bindir}/exr_to_pq
%{_bindir}/pq_to_hlg
%{_bindir}/render_hlg
%{_bindir}/tone_map
%{_bindir}/texture_to_cube
%{_bindir}/generate_lut_template
%{_bindir}/ssimulacra_main
%{_bindir}/ssimulacra2
%{_bindir}/xyb_range
%{_bindir}/jxl_from_tree
%files doc
%doc doc/*.md
%doc %{_vpath_builddir}/html

View file

@ -1,2 +1,2 @@
SHA512 (jpegxl-0.6.1.tar.gz) = 302935d722160b0b288ac63301f9e95caf82eccf6ad76c4f4da6316a0314ee3562115932b1ceacb0d02708de0a07788992d3478cae73af0b90193f5769f9fb52
SHA512 (third_party-0.6.1.tar.gz) = 7e416daf97e1cc54da00ec9b58636151308371d223fe29fcc22f6e9087852fd6fe9784a4e2f3e191a85a6f907a2732520ee7c8fd387292e19b5cc0b8a9d9392b
SHA512 (jpegxl-0.7.2.tar.gz) = f7a5321b6c2ce5b9dfef759df3419728c0a5bc399d030d1a7636953bb3f432be1bfd264f38b674e789ab77accb16ac6b03d5e78f6f3c86c91a782e61bf2747ae
SHA512 (third_party-0.7.2.tar.gz) = 27923ec78bec22d2580404f05c15005ff2a3efe951f97e0a3281be78298e99b8eee065a6d836eb1a77e70dbd0241dd82b376e015c2c54d334491e9017428a3ef