From 58c803edcd9e5f8e104666737e71f6e10134733b Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 5 Sep 2026 00:07:40 +0400 Subject: [PATCH] Complete Butteraugli Just the tests for Butteraugli are remaining --- .../Jxl/Processing/Butteraugli/Butteraugli.cs | 558 ++++++++++++++---- .../Butteraugli/ButteraugliBlurTemp.cs | 24 + .../Butteraugli/ButteraugliComparator.cs | 284 ++++++++- .../Butteraugli/ButteraugliPsychoImage.cs | 23 + .../Processing/Image/JxlImageOperations.cs | 9 + .../Formats/Jxl/Processing/JxlToc.cs | 11 +- 6 files changed, 776 insertions(+), 133 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliBlurTemp.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index 99f1cf2d88..f65a66df92 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; @@ -43,6 +44,26 @@ internal static class Butteraugli private const float GlobalScale = 1.0f / InternalGoodQualityThreshold; +#pragma warning disable // Just indentation warnings + /// + /// Underlying data for . + /// + private static readonly float[,] HeatmapData = + { + {0, 0, 0}, {0, 0, 1}, + {0, 1, 1}, {0, 1, 0}, // Good level + {1, 1, 0}, {1, 0, 0}, // Bad level + {1, 0, 1}, {0.5f, 0.5f, 1.0f}, + {1.0f, 0.5f, 0.5f}, // Pastel colors for the very bad quality range. + {1.0f, 1.0f, 0.5f}, {1, 1, 1}, + {1, 1, 1}, // Last color repeated to have a solid range of white. + }; +#pragma warning restore + + private static readonly DenseMatrix Heatmap; + + static Butteraugli() => Heatmap = new(HeatmapData); + public static ReadOnlySpan Wmul => [ 400.0f, 1.50815703118f, 0f, @@ -72,7 +93,7 @@ internal static class Butteraugli } public static void ConvolveBorderColumn( - JxlImageF input, + JxlPlane input, ReadOnlySpan kernel, int x, Span rowOut) @@ -83,6 +104,7 @@ internal static class Butteraugli int maxX = Math.Min(input.XSize - 1, x + offset); float weight = 0.0f; + for (int j = minX; j <= maxX; j++) { weight += kernel[j - x + offset]; @@ -106,9 +128,9 @@ internal static class Butteraugli } public static bool ConvolutionWithTranspose( - JxlImageF input, + JxlPlane input, ReadOnlySpan kernel, - JxlImageF output) + JxlPlane output) { if (output.XSize != input.YSize) { @@ -286,11 +308,11 @@ internal static class Butteraugli } private static bool Blur( - JxlImageF input, + Configuration configuration, + JxlPlane input, float sigma, - in ButteraugliParameters parameters, ButteraugliBlurTemp temp, - JxlImageF output) + JxlPlane output) { ReadOnlySpan kernel = ComputeKernel(sigma); @@ -323,10 +345,7 @@ internal static class Butteraugli return true; } - if (!temp.GetTransposed(input, out JxlImageF tempT)) - { - return false; - } + JxlPlane tempT = temp.GetTransposed(configuration, input); if (!ConvolutionWithTranspose(input, kernel, tempT)) { @@ -447,9 +466,9 @@ internal static class Butteraugli } } - public static bool SuppressXByY(JxlImageF inY, JxlImageF inOutX) + public static bool SuppressXByY(JxlPlane inY, JxlPlane inOutX) { - if (!SameSize(inOutX, inY)) + if (!JxlImageOperations.SameSize(inOutX, inY)) { return false; } @@ -506,7 +525,7 @@ internal static class Butteraugli } public static bool SeparateLFAndMF( - in ButteraugliParameters parameters, + Configuration configuration, JxlImage3F xyb, JxlImage3F lf, JxlImage3F mf, @@ -517,9 +536,9 @@ internal static class Butteraugli for (int i = 0; i < 3; i++) { if (!Blur( + configuration, xyb.Plane(i), sigmaLf, - parameters, blurTemp, lf.Plane(i))) { @@ -539,18 +558,17 @@ internal static class Butteraugli public static bool SeparateMfAndHf( Configuration configuration, - in ButteraugliParameters parameters, JxlImage3F mf, - ref InlineArray2 hf, - BlurTemp blurTemp) + ref InlineArray2> hf, + ButteraugliBlurTemp blurTemp) { const float sigmaHf = 3.22489901262f; int xSize = mf.XSize; int ySize = mf.YSize; - hf[0] = new JxlImageF(configuration, xSize, ySize); - hf[1] = new JxlImageF(configuration, xSize, ySize); + hf[0] = JxlPlane.Create(configuration, xSize, ySize); + hf[1] = JxlPlane.Create(configuration, xSize, ySize); int lanes = Vector.Count; @@ -558,7 +576,7 @@ internal static class Butteraugli { if (i == 2) { - if (!Blur(mf.Plane(i), sigmaHf, parameters, blurTemp, mf.Plane(i))) + if (!Blur(configuration, mf.Plane(i), sigmaHf, blurTemp, mf.Plane(i))) { return false; } @@ -578,7 +596,7 @@ internal static class Butteraugli } } - if (!Blur(mf.Plane(i), sigmaHf, parameters, blurTemp, mf.Plane(i))) + if (!Blur(configuration, mf.Plane(i), sigmaHf, blurTemp, mf.Plane(i))) { return false; } @@ -631,18 +649,18 @@ internal static class Butteraugli } public static bool SeparateHFAndUHF( - in ButteraugliParameters parameters, - JxlImageF[] hf, - JxlImageF[] uhf, - JxlBlurTemp blurTemp) + Configuration configuration, + InlineArray2> hf, + InlineArray2> uhf, + ButteraugliBlurTemp blurTemp) { const float sigmaUhf = 1.56416327805f; int xSize = hf[0].XSize; int ySize = hf[0].YSize; - uhf[0] = new JxlImageF(xSize, ySize); - uhf[1] = new JxlImageF(xSize, ySize); + uhf[0] = JxlPlane.Create(configuration, xSize, ySize); + uhf[1] = JxlPlane.Create(configuration, xSize, ySize); int lanes = Vector.Count; @@ -659,7 +677,7 @@ internal static class Butteraugli } } - if (!Blur(hf[i], sigmaUhf, parameters, blurTemp, hf[i])) + if (!Blur(configuration, hf[i], sigmaUhf, blurTemp, hf[i])) { return false; } @@ -727,34 +745,36 @@ internal static class Butteraugli return true; } - public static void DeallocateHFAndUHF(InlineArray2 hf, InlineArray2 uhf) + public static void DeallocateHFAndUHF(InlineArray2> hf, InlineArray2> uhf) { for (int i = 0; i < 2; i++) { - hf[i] = new JxlImageF(); - uhf[i] = new JxlImageF(); + hf[i].Dispose(); + uhf[i].Dispose(); + + hf[i] = new JxlPlane(); + uhf[i] = new JxlPlane(); } } public static bool SeparateFrequencies( Configuration configuration, - in ButteraugliParameters parameters, - BlurTemp blurTemp, + ButteraugliBlurTemp blurTemp, JxlImage3F xyb, - PsychoImage ps) + ButteraugliPsychoImage ps) { - ps.Lf = JxlImage3F.Create( + ps.Lf = new JxlImage3F( configuration, xyb.XSize, xyb.YSize); - ps.Mf = JxlImage3F.Create( + ps.Mf = new JxlImage3F( configuration, xyb.XSize, xyb.YSize); if (!SeparateLFAndMF( - parameters, + configuration, xyb, ps.Lf, ps.Mf, @@ -764,16 +784,16 @@ internal static class Butteraugli } if (!SeparateMfAndHf( - parameters, + configuration, ps.Mf, - ps.Hf, + ref ps.Hf, blurTemp)) { return false; } if (!SeparateHFAndUHF( - parameters, + configuration, ps.Hf, ps.Uhf, blurTemp)) @@ -1177,7 +1197,7 @@ internal static class Butteraugli [MethodImpl(MethodImplOptions.AggressiveInlining)] private static float PaddedMaltaUnit( - JxlImageF diffs, + JxlPlane diffs, int x0, int y0, bool isLF) @@ -1238,17 +1258,43 @@ internal static class Butteraugli public static bool MaltaDiffMap( bool isLf, - JxlImageF lum0, - JxlImageF lum1, + JxlPlane lum0, + JxlPlane lum1, + float w0Gt1, + float w0Lt1, + float norm1, + JxlPlane diffs, + JxlPlane blockDiffAc) + { + if (isLf) + { + const float len = 3.75f; + const float mulli = 0.39905817637f; + + return MaltaDiffMap(true, lum0, lum1, w0Gt1, w0Lt1, norm1, len, mulli, diffs, blockDiffAc); + } + else + { + const float len = 3.75f; + const float mulli = 0.611612573796f; + + return MaltaDiffMap(false, lum0, lum1, w0Gt1, w0Lt1, norm1, len, mulli, diffs, blockDiffAc); + } + } + + public static bool MaltaDiffMap( + bool isLf, + JxlPlane lum0, + JxlPlane lum1, float w0Gt1, float w0Lt1, float norm1, float len, float mulli, - JxlImageF diffs, - JxlImageF blockDiffAc) + JxlPlane diffs, + JxlPlane blockDiffAc) { - if (!SameSize(lum0, lum1) || !SameSize(lum0, diffs)) + if (!JxlImageOperations.SameSize(lum0, lum1) || !JxlImageOperations.SameSize(lum0, diffs)) { return false; } @@ -1400,13 +1446,13 @@ internal static class Butteraugli } public static bool MaltaDiffMapLf( - JxlImageF lum0, - JxlImageF lum1, + JxlPlane lum0, + JxlPlane lum1, float w0Gt1, float w0Lt1, float norm1, - JxlImageF diffs, - JxlImageF blockDiffAc) + JxlPlane diffs, + JxlPlane blockDiffAc) { const float len = 3.75f; const float mulli = 0.611612573796f; @@ -1425,9 +1471,9 @@ internal static class Butteraugli } public static void CombineChannelsForMasking( - InlineArray2 hf, - InlineArray2 uhf, - JxlImageF output) + InlineArray2> hf, + InlineArray2> uhf, + JxlPlane output) { // Only X and Y components are involved in masking. ReadOnlySpan muls = @@ -1614,8 +1660,7 @@ internal static class Butteraugli Configuration configuration, JxlImageF mask0, JxlImageF mask1, - in ButteraugliParameters parameters, - JxlBlurTemp blurTemp, + ButteraugliBlurTemp blurTemp, JxlImageF? diffAc, out JxlImageF mask) { @@ -1636,14 +1681,14 @@ internal static class Butteraugli DiffPrecompute(mask0, mul, bias, diff0); DiffPrecompute(mask1, mul, bias, diff1); - if (!Blur(diff0, radius, parameters, blurTemp, blurred0)) + if (!Blur(configuration, diff0, radius, blurTemp, blurred0)) { return false; } FuzzyErosion(blurred0, diff0); - if (!Blur(diff1, radius, parameters, blurTemp, blurred1)) + if (!Blur(configuration, diff1, radius, blurTemp, blurred1)) { return false; } @@ -1661,7 +1706,7 @@ internal static class Butteraugli { maskRow[x] = diff0Row[x]; - if (diffRow != null) + if (diffRow.Length > 0) { const float maskToErrorMul = 10.0f; float diff = blur0Row[x] - blur1Row[x]; @@ -1673,16 +1718,15 @@ internal static class Butteraugli return true; } - public static bool MaskPsychoImage( + public static bool MaskButteraugliPsychoImage( Configuration configuration, ButteraugliPsychoImage pi0, ButteraugliPsychoImage pi1, int width, int height, - in ButteraugliParameters parameters, - BlurTemp blurTemp, + ButteraugliBlurTemp blurTemp, JxlImageF mask, - JxlImageF? diffAc) + out JxlImageF? diffAc) { JxlImageF mask0 = new(configuration, width, height); JxlImageF mask1 = new(configuration, width, height); @@ -1698,12 +1742,12 @@ internal static class Butteraugli mask1); return Mask( + configuration, mask0, mask1, - parameters, blurTemp, mask, - diffAc); + out diffAc); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -1741,11 +1785,11 @@ internal static class Butteraugli public static bool CombineChannelsToDiffmap( JxlImageF mask, JxlImage3F blockDiffDc, - JxlImage3F blockDiffAc, + JxlImage3 blockDiffAc, float xmul, JxlImageF result) { - if (!SameSize(mask, result)) + if (!JxlImageOperations.SameSize(mask, result)) { return false; } @@ -1786,10 +1830,10 @@ internal static class Butteraugli } public static void L2Diff( - JxlImageF i0, - JxlImageF i1, + JxlPlane i0, + JxlPlane i1, float w, - JxlImageF diffmap) + JxlPlane diffmap) { if (w == 0) { @@ -1811,10 +1855,10 @@ internal static class Butteraugli } public static void SetL2Diff( - JxlImageF i0, - JxlImageF i1, + JxlPlane i0, + JxlPlane i1, float w, - JxlImageF diffmap) + JxlPlane diffmap) { if (w == 0) { @@ -1836,11 +1880,11 @@ internal static class Butteraugli } public static void L2DiffAsymmetric( - JxlImageF i0, - JxlImageF i1, + JxlPlane i0, + JxlPlane i1, float w0gt1, float w0lt1, - JxlImageF diffmap) + JxlPlane diffmap) { if (w0gt1 == 0 && w0lt1 == 0) { @@ -1942,11 +1986,27 @@ internal static class Butteraugli } } + // A simple HDR-compatible gamma function + private static Vector Gamma(Vector v) + { + Vector kRetMul = Vector.Create(19.245013259874995f * JxlMath.InverseLog2E); + Vector kRetAdd = Vector.Create(-23.16046239805755f); + + // if (value < 0) value = 0; + v = Vector.ConditionalSelect(Vector.LessThan(v, Vector.Zero), Vector.Zero, v); + + Vector biased = v + Vector.Create(9.9710635769299145f); + Vector log = Vector.Log2(biased); + + return (kRetMul * log) + kRetAdd; + } + public static bool OpsinDynamicsImage( + Configuration configuration, JxlImage3F rgb, in ButteraugliParameters parameters, JxlImage3F blurred, - BlurTemp blurTemp, + ButteraugliBlurTemp blurTemp, JxlImage3F xyb) { if (blurred == null) @@ -1954,19 +2014,19 @@ internal static class Butteraugli return false; } - const double sigma = 1.2; + const float sigma = 1.2f; - if (!Blur(rgb.Plane(0), sigma, parameters, blurTemp, blurred.Plane(0))) + if (!Blur(configuration, rgb.Plane(0), sigma, blurTemp, blurred.Plane(0))) { return false; } - if (!Blur(rgb.Plane(1), sigma, parameters, blurTemp, blurred.Plane(1))) + if (!Blur(configuration, rgb.Plane(1), sigma, blurTemp, blurred.Plane(1))) { return false; } - if (!Blur(rgb.Plane(2), sigma, parameters, blurTemp, blurred.Plane(2))) + if (!Blur(configuration, rgb.Plane(2), sigma, blurTemp, blurred.Plane(2))) { return false; } @@ -2058,16 +2118,16 @@ internal static class Butteraugli int xSize = image0.XSize; int ySize = image0.YSize; - using var blurTemp = new JxlBlurTemp(); + using ButteraugliBlurTemp blurTemp = new(); using (JxlImage3F temp = new(configuration, xSize, ySize)) { - if (!OpsinDynamicsImage(image0, parameters, temp, blurTemp, image0)) + if (!OpsinDynamicsImage(configuration, image0, parameters, temp, blurTemp, image0)) { return false; } - if (!OpsinDynamicsImage(image1, parameters, temp, blurTemp, image1)) + if (!OpsinDynamicsImage(configuration, image1, parameters, temp, blurTemp, image1)) { return false; } @@ -2080,12 +2140,12 @@ internal static class Butteraugli using (JxlImage3F lf0 = new(configuration, xSize, ySize)) using (JxlImage3F lf1 = new(configuration, xSize, ySize)) { - if (!SeparateLFAndMF(parameters, image0, lf0, image0, blurTemp)) + if (!SeparateLFAndMF(configuration, image0, lf0, image0, blurTemp)) { return false; } - if (!SeparateLFAndMF(parameters, image1, lf1, image1, blurTemp)) + if (!SeparateLFAndMF(configuration, image1, lf1, image1, blurTemp)) { return false; } @@ -2100,15 +2160,15 @@ internal static class Butteraugli } } - InlineArray2 hf0 = default; - InlineArray2 hf1 = default; + InlineArray2> hf0 = default; + InlineArray2> hf1 = default; - if (!SeparateMfAndHf(parameters, image0, hf0, blurTemp)) + if (!SeparateMfAndHf(configuration, image0, ref hf0, blurTemp)) { return false; } - if (!SeparateMfAndHf(parameters, image1, hf1, blurTemp)) + if (!SeparateMfAndHf(configuration, image1, ref hf1, blurTemp)) { return false; } @@ -2158,15 +2218,15 @@ internal static class Butteraugli image0.Dispose(); image1.Dispose(); - InlineArray2 uhf0 = default; - InlineArray2 uhf1 = default; + InlineArray2> uhf0 = default; + InlineArray2> uhf1 = default; - if (!SeparateHFAndUHF(parameters, hf0, uhf0, blurTemp)) + if (!SeparateHFAndUHF(configuration, hf0, uhf0, blurTemp)) { return false; } - if (!SeparateHFAndUHF(parameters, hf1, uhf1, blurTemp)) + if (!SeparateHFAndUHF(configuration, hf1, uhf1, blurTemp)) { return false; } @@ -2175,7 +2235,7 @@ internal static class Butteraugli using (JxlImageF diffs = new(configuration, xSize, ySize)) { - MaltaDiffMap( + _ = MaltaDiffMap( false, uhf0[1], uhf1[1], @@ -2185,19 +2245,19 @@ internal static class Butteraugli diffs, blockDiffAc); - MaltaDiffMap( + _ = MaltaDiffMap( false, uhf0[0], uhf1[0], - wUhfMaltaX * hfAsymmetry, - wUhfMaltaX / hfAsymmetry, - norm1UhfX, + WUhfMaltaX * hfAsymmetry, + WUhfMaltaX / hfAsymmetry, + Norm1UhfX, diffs, blockDiffAc); float sqrtAsym = MathF.Sqrt(hfAsymmetry); - MaltaDiffMap( + _ = MaltaDiffMap( true, hf0[1], hf1[1], @@ -2207,7 +2267,7 @@ internal static class Butteraugli diffs, blockDiffAc); - MaltaDiffMap( + _ = MaltaDiffMap( true, hf0[0], hf1[0], @@ -2239,15 +2299,15 @@ internal static class Butteraugli DeallocateHFAndUHF(hf0, uhf0); DeallocateHFAndUHF(hf1, uhf1); - if (!Mask(mask0, mask1, parameters, blurTemp, mask, blockDiffAc)) + if (!Mask(configuration, mask0, mask1, blurTemp, mask, out JxlImageF newBlockDiffAc)) { return false; } for (int y = 0; y < ySize; y++) { - ReadOnlySpan dc = blockDiffDc.GetRow(y); - ReadOnlySpan ac = blockDiffAc.GetRow(y); + ReadOnlySpan dc = newBlockDiffAc.GetRow(y); + ReadOnlySpan ac = newBlockDiffAc.GetRow(y); Span output = diffmap.GetRow(y); ReadOnlySpan maskRow = mask.GetRow(y); @@ -2255,10 +2315,7 @@ internal static class Butteraugli { float m = maskRow[x]; - output[x] = - MathF.Sqrt( - (dc[x] * (float)MaskDcY(m)) + - (ac[x] * (float)MaskY(m))); + output[x] = MathF.Sqrt((dc[x] * (float)MaskDcY(m)) + (ac[x] * (float)MaskY(m))); } } @@ -2272,7 +2329,7 @@ internal static class Butteraugli int xs = (input.XSize + 1) / 2; int ys = (input.YSize + 1) / 2; - JxlImage3F retval = new(Configuration, xs, ys); + JxlImage3F retval = new(configuration, xs, ys); for (int c = 0; c < 3; ++c) { @@ -2293,8 +2350,7 @@ internal static class Butteraugli for (int x = 0; x < input.XSize; ++x) { - retval.PlaneRow(c, y / 2)[x / 2] += - 0.25f * srcRow[x]; + retval.PlaneRow(c, y / 2)[x / 2] += 0.25f * srcRow[x]; } } @@ -2335,4 +2391,286 @@ internal static class Butteraugli } } } + + private static void ScoreToRgb(float score, float goodThreshold, float badThreshold, ref InlineArray3 rgb) + { + if (score < goodThreshold) + { + score = (score / goodThreshold) * 0.3f; + } + else if (score < badThreshold) + { + score = 0.3f + ((score - goodThreshold) / (badThreshold - goodThreshold) * 0.15f); + } + else + { + score = 0.45f + ((score - badThreshold) / (badThreshold * 12) * 0.5f); + } + + int tableSize = HeatmapData.GetLength(0); + score = Math.Clamp(score * (tableSize - 1), 0f, tableSize - 2); + + int ix = (int)score; + ix = Math.Clamp(ix, 0, tableSize - 2); // handle NaN + float mix = score - ix; + + for (int i = 0; i < 3; ++i) + { + float v = (mix * Heatmap[ix + 1, i]) + ((1 - mix) * Heatmap[ix, i]); + rgb[i] = MathF.Pow(v, 0.5f); + } + } + + public static JxlImage3F CreateHeatMapImage(Configuration configuration, JxlImageF distmap, float goodThreshold, float badThreshold) + { + // Do not dispose (this is the return value; it is caller's responsibility to dispose this + // or keep it) + JxlImage3F heatmap = new(configuration, distmap.XSize, distmap.YSize); + + for (int y = 0; y < distmap.YSize; y++) + { + Span row_distmap = distmap.GetRow(y); + Span row_h0 = heatmap.PlaneRow(0, y); + Span row_h1 = heatmap.PlaneRow(1, y); + Span row_h2 = heatmap.PlaneRow(2, y); + + for (int x = 0; x < distmap.XSize; ++x) + { + float d = row_distmap[x]; + InlineArray3 rgb = default; + ScoreToRgb(d, goodThreshold, badThreshold, ref rgb); + row_h0[x] = rgb[0]; + row_h1[x] = rgb[1]; + row_h2[x] = rgb[2]; + } + } + + return heatmap; + } + + public static float ButteraugliFuzzyInverse(float seek) + { + float pos = 0f; + + for (float range = 1.0f; range >= 1e-10f; range *= 0.5f) + { + float cur = ButteraugliFuzzyClass(pos); + + if (cur < seek) + { + pos -= range; + } + else + { + pos += range; + } + } + + // Normalization (pos) can be printed if seek == 1.0, for example + // when debugging. + return pos; + } + + public static float ButteraugliFuzzyClass(float score) + { + const float fuzzyWidthUp = 4.8f; + const float fuzzyWidthDown = 4.8f; + const float m0 = 2.0f; + const float scaler = 0.7777f; + + float val; + + if (score < 1.0) + { + val = m0 / (1.0f + MathF.Exp((score - 1.0f) * fuzzyWidthDown)); + val -= 1.0f; + val *= 2.0f - scaler; + val += scaler; + } + else + { + val = m0 / (1.0f + MathF.Exp((score - 1.0f) * fuzzyWidthUp)); + val *= scaler; + } + + return val; + } + + public static bool ButteraugliInterfaceInPlace(Configuration configuration, JxlImage3F rgb0, JxlImage3F rgb1, ButteraugliParameters parameters, JxlImageF diffmap, out double diffvalue) + { + diffvalue = 0; + + int xsize = rgb0.XSize; + int ysize = rgb0.YSize; + + if ((xsize & ysize) == 0) // equivalent to xsize == 0 || ysize == 0, minus one branch + { + throw new InvalidOperationException("Zero-sized image"); + } + + if (!JxlImageOperations.SameSize(rgb0, rgb1)) + { + throw new InvalidOperationException("Size mismatch"); + } + + const int max = 8; + + if (xsize < max || ysize < max) + { + bool ok = ButteraugliDiffmapSmall(configuration, max, rgb0, rgb1, parameters, out diffmap); + diffvalue = ButteraugliScoreFromDiffmap(diffmap, parameters); + return ok; + } + + JxlImageF subDiffmap = new(); + + if (xsize >= 15 && ysize >= 15) + { + using JxlImage3F rgb0Sub = SubSample2x(configuration, rgb0); + using JxlImage3F rgb1Sub = SubSample2x(configuration, rgb1); + + if (!ButteraugliDiffmapInPlace(configuration, rgb0Sub, rgb1Sub, parameters, diffmap)) + { + return false; + } + } + + if (!ButteraugliDiffmapInPlace(configuration, rgb0, rgb1, parameters, diffmap)) + { + return false; + } + + if (xsize >= 15 && ysize >= 15) + { + AddSupersampled2x(subDiffmap, 0.5f, diffmap); + } + + diffvalue = ButteraugliScoreFromDiffmap(diffmap, parameters); + return true; + } + + public static bool ButteraugliInterface(Configuration configuration, JxlImage3F rgb0, JxlImage3F rgb1, ButteraugliParameters parameters, JxlImageF diffmap, out double diffValue) + { + diffValue = 0; + + if (!ButteraugliDiffmap(configuration, rgb0, rgb1, parameters, out diffmap)) + { + return false; + } + + diffValue = ButteraugliScoreFromDiffmap(diffmap, parameters); + return true; + } + + public static bool ButteraugliInterface(Configuration configuration, JxlImage3F rgb0, JxlImage3F rgb1, float hfAsymmetry, float xmul, JxlImageF diffmap, out double diffValue) + { + ButteraugliParameters parameters = new() + { + HfAsymmetry = hfAsymmetry, + XMultiplier = xmul + }; + + return ButteraugliInterface(configuration, rgb0, rgb1, parameters, diffmap, out diffValue); + } + + public static bool ButteraugliDiffmap(Configuration configuration, JxlImage3F rgb0, JxlImage3F rgb1, ButteraugliParameters parameters, out JxlImageF diffmap) + { + diffmap = new(); + + int xsize = rgb0.XSize; + int ysize = rgb0.YSize; + + if ((xsize & ysize) == 0) // equivalent to xsize == 0 || ysize == 0, minus one branch + { + throw new InvalidOperationException("Zero-sized image"); + } + + if (!JxlImageOperations.SameSize(rgb0, rgb1)) + { + throw new InvalidOperationException("Size mismatch"); + } + + const int max = 8; + + if (xsize < max || ysize < max) + { + if (!ButteraugliDiffmapSmall(configuration, max, rgb0, rgb1, parameters, out diffmap)) + { + return false; + } + } + + ButteraugliComparator butteraugli = ButteraugliComparator.Make(configuration, rgb0, parameters); + + return butteraugli.Diffmap(configuration, rgb1, diffmap); + } + + public static bool ButteraugliDiffmapSmall(Configuration configuration, int max, JxlImage3F rgb0, JxlImage3F rgb1, ButteraugliParameters parameters, out JxlImageF diffmap) + { + int xsize = rgb0.XSize; + int ysize = rgb0.YSize; + + int xborder = xsize < max ? (max - xsize) / 2 : 0; + int yborder = ysize < max ? (max - ysize) / 2 : 0; + int xscaled = Math.Max(max, xsize); + int yscaled = Math.Max(max, ysize); + + using JxlImage3F scaled0 = new(configuration, xscaled, yscaled); + using JxlImage3F scaled1 = new(configuration, xscaled, yscaled); + + for (int i = 0; i < 3; ++i) + { + for (int y = 0; y < yscaled; y++) + { + for (int x = 0; x < xscaled; x++) + { + int x2 = Math.Min(xsize - 1, x > xborder ? x - xborder : 0); + int y2 = Math.Min(ysize - 1, y > yborder ? y - yborder : 0); + + scaled0.PlaneRow(i, y)[x] = rgb0.PlaneRow(i, y2)[x2]; + scaled1.PlaneRow(i, y)[x] = rgb1.PlaneRow(i, y2)[x2]; + } + } + } + + JxlImageF diffmapScaled = new(); + bool ok = ButteraugliDiffmap(configuration, scaled0, scaled1, parameters, out diffmapScaled); + + diffmap = new(configuration, xsize, ysize); + + for (int y = 0; y < ysize; y++) + { + Span diffmapRow = diffmap.GetRow(y); + Span diffmapScaledRow = diffmapScaled.GetRow(y + yborder); + + for (int x = 0; x < xsize; x++) + { + diffmapRow[x] = diffmapScaledRow[x + xborder]; + } + } + + return ok; + } + + public static float ButteraugliScoreFromDiffmap(JxlImageF diffmap, ButteraugliParameters parameters) + { + float retval = 0.0f; + + for (int y = 0; y < diffmap.YSize; ++y) + { + Span row = diffmap.GetRow(y); + for (int x = 0; x < diffmap.XSize; ++x) + { + retval = Math.Max(retval, row[x]); + } + } + + return retval; + } + + public static bool MaltaDiffMap(JxlPlane lum0, JxlPlane lum1, float w0Gt1, float w0Lt1, float norm1, JxlPlane diffs, JxlImage3 blockDiffAc, int c) + => MaltaDiffMap(isLf: false, lum0, lum1, w0Gt1, w0Lt1, norm1, diffs, blockDiffAc.Plane(c)); + + public static bool MaltaDiffMapLf(JxlPlane lum0, JxlPlane lum1, float w0Gt1, float w0Lt1, float norm1, JxlPlane diffs, JxlImage3 blockDiffAc, int c) + => MaltaDiffMap(isLf: true, lum0, lum1, w0Gt1, w0Lt1, norm1, diffs, blockDiffAc.Plane(c)); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliBlurTemp.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliBlurTemp.cs new file mode 100644 index 0000000000..26aa8372ce --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliBlurTemp.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; + +internal sealed class ButteraugliBlurTemp : IDisposable +{ + public JxlPlane TransposedTemp { get; set; } = new(); + + public JxlPlane GetTransposed(Configuration configuration, JxlPlane input) + { + if (this.TransposedTemp.XSize == 0) + { + // Yes, YSize and XSize are swapped + this.TransposedTemp = JxlPlane.Create(configuration, input.YSize, input.XSize); + } + + return this.TransposedTemp; + } + + public void Dispose() => this.TransposedTemp.Dispose(); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs index be5837534b..d290268da6 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs @@ -1,46 +1,300 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; -internal sealed class ButteraugliComparator +internal class ButteraugliComparator : IDisposable { - private int xSize; - private int ySize; + private readonly int xSize; + private readonly int ySize; private ButteraugliParameters parameters; - private readonly ButteraugliPsychoImage pi0; - private readonly JxlImage3F temp; - private bool tempInUse; - private readonly ButteraugliBlurTemp blurTemp; + private readonly ButteraugliPsychoImage pi0 = new(); + private JxlImage3F? temp; + private readonly ButteraugliBlurTemp blurTemp = new(); + private ButteraugliComparator? sub; + + public ButteraugliComparator(int xsize, int ysize, ButteraugliParameters parameters) + { + this.xSize = xsize; + this.ySize = ysize; + this.parameters = parameters; + } + + public JxlImage3F Temp => this.temp ??= new(); + + public static ButteraugliComparator Make(Configuration configuration, JxlImage3F rgb0, ButteraugliParameters parameters) + { + int xSize = rgb0.XSize; + int ySize = rgb0.YSize; + + ButteraugliComparator result = new(xSize, ySize, parameters) + { + temp = new JxlImage3F(configuration, xSize, ySize) + }; + + if (xSize < 8 || ySize < 8) + { + return result; + } + + JxlImage3F xyb0 = new(configuration, xSize, ySize); + + if (!Butteraugli.OpsinDynamicsImage(configuration, rgb0, parameters, result.Temp, result.blurTemp, xyb0)) + { + throw new InvalidOperationException("OpsinDynamicsImage failed"); + } + + result.ReleaseTemp(); + + if (!Butteraugli.SeparateFrequencies(configuration, result.blurTemp, xyb0, result.pi0)) + { + throw new InvalidOperationException("Could not separate frequencies"); + } + + JxlImage3F subsampledRgb0 = Butteraugli.SubSample2x(configuration, rgb0); + result.sub = Make(configuration, subsampledRgb0, parameters); + + return result; + } + + public void ReleaseTemp() + { + this.temp?.Dispose(); + this.temp = null; + } /// /// Computes the butteraugli map between the original image given in the constructor and the distorted image given here. /// - public bool Diffmap(JxlImage3F rgb1, JxlImageF result) + public virtual bool Diffmap(Configuration configuration, JxlImage3F rgb1, JxlImageF result) { - throw new NotImplementedException(); + if (this.xSize < 8 || this.ySize < 8) + { + result.Clear(); + return true; + } + + JxlImage3F xyb1 = new(configuration, this.xSize, this.ySize); + + if (!Butteraugli.OpsinDynamicsImage(configuration, rgb1, this.parameters, this.Temp, this.blurTemp, xyb1)) + { + return false; + } + + this.ReleaseTemp(); + + if (!this.DiffmapOpsinDynamicsImage(configuration, xyb1, out result)) + { + return false; + } + + if (this.sub is not null) + { + if (this.sub.xSize < 8 || this.sub.ySize < 8) + { + return true; + } + + JxlImage3F subXyb = new(configuration, this.sub.xSize, this.sub.ySize); + JxlImage3F subsampledRgb1 = Butteraugli.SubSample2x(configuration, rgb1); + + if (!Butteraugli.OpsinDynamicsImage(configuration, subsampledRgb1, this.parameters, this.sub.Temp, this.sub.blurTemp, subXyb)) + { + return false; + } + + this.sub.ReleaseTemp(); + + if (!this.DiffmapOpsinDynamicsImage(configuration, subXyb, out JxlImageF subResult)) + { + return false; + } + + Butteraugli.AddSupersampled2x(subResult, 0.5f, result); + } + + return true; } /// /// Same as Diffmap but OpsinDynamicsImage() was already applied. /// - public bool DiffmapOpsinDynamicsImage(JxlImage3F xyb1, JxlImageF result) + public bool DiffmapOpsinDynamicsImage(Configuration configuration, JxlImage3F xyb1, out JxlImageF result) { - throw new NotImplementedException(); + result = new(); + + if (this.xSize < 8 || this.ySize < 8) + { + result.Clear(); + return true; + } + + ButteraugliPsychoImage pi1 = new(); + + if (!Butteraugli.SeparateFrequencies(configuration, this.blurTemp, xyb1, pi1)) + { + return false; + } + + result = new(configuration, this.xSize, this.ySize); + return this.DiffmapPsychoImage(configuration, pi1, result); } /// /// Same as above but the frequency decomposition was already applied. /// - public bool DiffmapPsychoImage(ButteraugliPsychoImage pi1, JxlImageF diffmap) + public bool DiffmapPsychoImage(Configuration configuration, ButteraugliPsychoImage pi1, JxlImageF diffmap) { - throw new NotImplementedException(); + if (this.xSize < 8 || this.ySize < 8) + { + diffmap.Clear(); + return true; + } + + float hfAsymmetry = this.parameters.HfAsymmetry; + float xmul = this.parameters.XMultiplier; + + JxlImageF diffs = new(configuration, this.xSize, this.ySize); + JxlImage3 blockDiffAc = new(configuration, this.xSize, this.ySize); + + JxlImageOperations.ZeroFillImage(blockDiffAc); + + if (!Butteraugli.MaltaDiffMap( + this.pi0.Uhf[1], + pi1.Uhf[1], + Butteraugli.WUhfMalta * hfAsymmetry, + Butteraugli.WUhfMalta / hfAsymmetry, + Butteraugli.Norm1Uhf, + diffs, + blockDiffAc, + 1)) + { + return false; + } + + if (!Butteraugli.MaltaDiffMap( + this.pi0.Uhf[0], + pi1.Uhf[0], + Butteraugli.WUhfMaltaX * hfAsymmetry, + Butteraugli.WUhfMaltaX / hfAsymmetry, + Butteraugli.Norm1UhfX, + diffs, + blockDiffAc, + 0)) + { + return false; + } + + if (!Butteraugli.MaltaDiffMapLf( + this.pi0.Hf[1], + pi1.Hf[1], + Butteraugli.WHfMalta * MathF.Sqrt(hfAsymmetry), + Butteraugli.WHfMalta / MathF.Sqrt(hfAsymmetry), + Butteraugli.Norm1Hf, + diffs, + blockDiffAc, + 1)) + { + return false; + } + + if (!Butteraugli.MaltaDiffMapLf( + this.pi0.Hf[0], + pi1.Hf[0], + Butteraugli.WHfMaltaX * MathF.Sqrt(hfAsymmetry), + Butteraugli.WHfMaltaX / MathF.Sqrt(hfAsymmetry), + Butteraugli.Norm1HfX, + diffs, + blockDiffAc, + 0)) + { + return false; + } + + if (!Butteraugli.MaltaDiffMapLf( + this.pi0.Mf!.Plane(1), + pi1.Mf!.Plane(1), + Butteraugli.WHfMalta, + Butteraugli.WMfMalta, + Butteraugli.Norm1Mf, + diffs, + blockDiffAc, + 1)) + { + return false; + } + + if (!Butteraugli.MaltaDiffMapLf( + this.pi0.Mf!.Plane(0), + pi1.Mf!.Plane(0), + Butteraugli.WHfMaltaX, + Butteraugli.WMfMaltaX, + Butteraugli.Norm1MfX, + diffs, + blockDiffAc, + 0)) + { + return false; + } + + JxlImage3F blockDiffDc = new(configuration, this.xSize, this.ySize); + + for (int c = 0; c < 3; c++) + { + if (c < 2) + { + Butteraugli.L2DiffAsymmetric( + this.pi0.Hf[c], + pi1.Hf[c], + Butteraugli.Wmul[c] * hfAsymmetry, + Butteraugli.Wmul[c] / hfAsymmetry, + blockDiffAc.Plane(c)); + } + + Butteraugli.L2Diff( + this.pi0.Mf.Plane(c), + pi1.Mf.Plane(c), + Butteraugli.Wmul[3 + c], + blockDiffAc.Plane(c)); + + Butteraugli.SetL2Diff( + this.pi0.Lf!.Plane(c), + pi1.Lf!.Plane(c), + Butteraugli.Wmul[6 + c], + blockDiffDc.Plane(c)); + } + + JxlImageF mask = new(); + + if (!Butteraugli.MaskButteraugliPsychoImage( + configuration, + this.pi0, + pi1, + this.xSize, + this.ySize, + this.blurTemp, + mask, + out JxlImageF? diffAc)) + { + return false; + } + + diffAc?.BytesSpan.CopyTo(blockDiffAc.Plane(1).BytesSpan); + + return Butteraugli.CombineChannelsToDiffmap(mask, blockDiffDc, blockDiffAc, xmul, diffmap); } - public bool Mask(JxlImageF mask) + public virtual bool Mask(Configuration configuration, JxlImageF mask) + => Butteraugli.MaskButteraugliPsychoImage(configuration, this.pi0, this.pi0, this.xSize, this.ySize, this.blurTemp, mask, out _); + + public void Dispose() { - throw new NotImplementedException(); + this.temp?.Dispose(); + this.temp = null; + this.blurTemp.Dispose(); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs new file mode 100644 index 0000000000..de1166cf8e --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; + +// Public fields because InlineArray2 values +// cannot be mutated if it's a property +#pragma warning disable SA1401 // Fields should be private + +internal sealed class ButteraugliPsychoImage +{ + public InlineArray2> Uhf; // XY + + public InlineArray2> Hf; // XY + + public JxlImage3F? Mf { get; set; } // XYB + + public JxlImage3F? Lf { get; set; } // XYB +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs index 0a4ff8f5d7..b4e98c5ca4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs @@ -23,6 +23,15 @@ internal static class JxlImageOperations /// True if width and height is equal. public static bool SameSize(JxlPlaneBase a, JxlPlaneBase b) => a.XSize == b.XSize && a.YSize == b.YSize; + /// + /// Returns true if first image has same width and height as the second image. + /// + /// First image + /// Second image + /// True if width and height is equal. + public static bool SameSize(JxlImage3 a, JxlImage3 b) + where T : unmanaged => a.XSize == b.XSize && a.YSize == b.YSize; + /// /// Copies everything from one plane to another. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs index 21fa5d23b2..b6dfd4e39f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs @@ -31,6 +31,7 @@ internal static class JxlToc } private const int BitsPerByte = 8; + private const int MaxTocEntries = 65536; public static bool ReadToc( @@ -98,10 +99,7 @@ internal static class JxlToc } } - if (!reader.JumpToByteBoundary()) - { - return false; - } + reader.JumpToByteBoundary(); if (!CheckBitBudget(tocEntries)) { @@ -113,10 +111,7 @@ internal static class JxlToc sizes[i] = JxlU32Coder.Read(TocDistribution, reader); } - if (!reader.JumpToByteBoundary()) - { - return false; - } + reader.JumpToByteBoundary(); return CheckBitBudget(0); }