From 765c892df0b1443d4192963afdc883cd08fe209d Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 25 Jul 2026 00:03:38 +0400 Subject: [PATCH] Implement Butteraugli core functions --- .../Jxl/Processing/Butteraugli/Butteraugli.cs | 2333 +++++++++++++++++ .../Butteraugli/ButteraugliComparator.cs | 46 + .../Butteraugli/ButteraugliParameters.cs | 12 +- .../Formats/Jxl/Processing/JxlQuantizer.cs | 1 - .../Jxl/Processing/JxlWeightsSeparable5.cs | 8 +- 5 files changed, 2390 insertions(+), 10 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs new file mode 100644 index 000000000..eb25583ec --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -0,0 +1,2333 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; + +/// +/// Implementation of Google Butteraugli, an advanced +/// image comparison system. Unlike PSNR or SSIM, +/// Butteraugli compares images the way humans may +/// spot differences instead of pixelwise. +/// +internal static class Butteraugli +{ + // NOTE: The meaning of those constants + // is not perfectly understood. + public const float WMfMalta = 37.0819870399f; + public const float Norm1Mf = 130262059.556f; + public const float WMfMaltaX = 8246.75321353f; + public const float Norm1MfX = 1009002.70582f; + + public const float WHfMalta = 18.7237414387f; + public const float Norm1Hf = 4498534.45232f; + public const float WHfMaltaX = 6923.99476109f; + public const float Norm1HfX = 8051.15833247f; + + public const float WUhfMalta = 1.10039032555f; + public const float Norm1Uhf = 71.7800275169f; + public const float WUhfMaltaX = 173.5f; + public const float Norm1UhfX = 5.0f; + + private const float IntensityTargetNormalizationHack = 0.79079917404f; + + private static readonly float InternalGoodQualityThreshold = + 17.83f * IntensityTargetNormalizationHack; + + private static readonly float GlobalScale = + 1.0f / InternalGoodQualityThreshold; + + public static ReadOnlySpan Wmul => + [ + 400.0, 1.50815703118, 0, + 2150.0, 10.6195433239, 16.2176043152, + 29.2353797994, 0.844626970982, 0.703646627719, + ]; + + public static ReadOnlySpan ComputeKernel(float sigma) + { + const float m = 2.25f; // Accuracy increases when m is increased + float scaler = -1.0f / (2.0f * sigma * sigma); + int diff = Math.Max(1, (int)(m * MathF.Abs(sigma))); + + // Use new because there's only up to 3 elements + float[] kernel = new float[(2 * diff) + 1]; + + for (int i = -diff; i <= diff; i++) + { + kernel[i + diff] = MathF.Exp(scaler * i * i); + } + + return kernel; + } + + public static void ConvolveBorderColumn( + JxlImageF input, + ReadOnlySpan kernel, + int x, + Span rowOut) + { + int offset = kernel.Length / 2; + + int minX = x < offset ? 0 : x - offset; + 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]; + } + + float scale = 1.0f / weight; + + for (int y = 0; y < input.YSize; y++) + { + Span rowIn = input.GetRow(y); + + float sum = 0.0f; + + for (int j = minX; j <= maxX; j++) + { + sum += rowIn[j] * kernel[j - x + offset]; + } + + rowOut[y] = sum * scale; + } + } + + public static bool ConvolutionWithTranspose( + JxlImageF input, + ReadOnlySpan kernel, + JxlImageF output) + { + if (output.XSize != input.YSize) + { + return false; + } + + if (output.YSize != input.XSize) + { + return false; + } + + int len = kernel.Length; + int offset = len / 2; + + float weightNoBorder = 0.0f; + + for (int j = 0; j < len; j++) + { + weightNoBorder += kernel[j]; + } + + float scaleNoBorder = 1.0f / weightNoBorder; + + int border1 = Math.Min(input.XSize, offset); + int border2 = input.XSize > offset ? input.XSize - offset : 0; + + Span scaledKernel = stackalloc float[(len / 2) + 1]; + + for (int i = 0; i <= len / 2; i++) + { + scaledKernel[i] = kernel[i] * scaleNoBorder; + } + + // Middle + switch (len) + { + case 7: + { + float sk0 = scaledKernel[0]; + float sk1 = scaledKernel[1]; + float sk2 = scaledKernel[2]; + float sk3 = scaledKernel[3]; + + for (int y = 0; y < input.YSize; y++) + { + Span rowIn = input.GetRow(y); + + for (int x = border1; x < border2; x++) + { + int i = x - border1; + + float sum0 = (rowIn[i + 0] + rowIn[i + 6]) * sk0; + float sum1 = (rowIn[i + 1] + rowIn[i + 5]) * sk1; + float sum2 = (rowIn[i + 2] + rowIn[i + 4]) * sk2; + float sum = (rowIn[i + 3] * sk3) + sum0 + sum1 + sum2; + + output.GetRow(x)[y] = sum; + } + } + + break; + } + + case 13: + { + for (int y = 0; y < input.YSize; y++) + { + Span rowIn = input.GetRow(y); + + for (int x = border1; x < border2; x++) + { + int i = x - border1; + + float sum0 = (rowIn[i + 0] + rowIn[i + 12]) * scaledKernel[0]; + float sum1 = (rowIn[i + 1] + rowIn[i + 11]) * scaledKernel[1]; + float sum2 = (rowIn[i + 2] + rowIn[i + 10]) * scaledKernel[2]; + float sum3 = (rowIn[i + 3] + rowIn[i + 9]) * scaledKernel[3]; + + sum0 += (rowIn[i + 4] + rowIn[i + 8]) * scaledKernel[4]; + sum1 += (rowIn[i + 5] + rowIn[i + 7]) * scaledKernel[5]; + + float sum = rowIn[i + 6] * scaledKernel[6]; + + output.GetRow(x)[y] = sum + sum0 + sum1 + sum2 + sum3; + } + } + + break; + } + + case 15: + { + for (int y = 0; y < input.YSize; y++) + { + Span rowIn = input.GetRow(y); + + for (int x = border1; x < border2; x++) + { + int i = x - border1; + + float sum0 = (rowIn[i + 0] + rowIn[i + 14]) * scaledKernel[0]; + float sum1 = (rowIn[i + 1] + rowIn[i + 13]) * scaledKernel[1]; + float sum2 = (rowIn[i + 2] + rowIn[i + 12]) * scaledKernel[2]; + float sum3 = (rowIn[i + 3] + rowIn[i + 11]) * scaledKernel[3]; + + sum0 += (rowIn[i + 4] + rowIn[i + 10]) * scaledKernel[4]; + sum1 += (rowIn[i + 5] + rowIn[i + 9]) * scaledKernel[5]; + sum2 += (rowIn[i + 6] + rowIn[i + 8]) * scaledKernel[6]; + + float sum = rowIn[i + 7] * scaledKernel[7]; + + output.GetRow(x)[y] = sum + sum0 + sum1 + sum2 + sum3; + } + } + + break; + } + + case 33: + { + for (int y = 0; y < input.YSize; y++) + { + Span rowIn = input.GetRow(y); + + for (int x = border1; x < border2; x++) + { + int i = x - border1; + + float sum0 = (rowIn[i + 0] + rowIn[i + 32]) * scaledKernel[0]; + float sum1 = (rowIn[i + 1] + rowIn[i + 31]) * scaledKernel[1]; + float sum2 = (rowIn[i + 2] + rowIn[i + 30]) * scaledKernel[2]; + float sum3 = (rowIn[i + 3] + rowIn[i + 29]) * scaledKernel[3]; + + sum0 += (rowIn[i + 4] + rowIn[i + 28]) * scaledKernel[4]; + sum1 += (rowIn[i + 5] + rowIn[i + 27]) * scaledKernel[5]; + sum2 += (rowIn[i + 6] + rowIn[i + 26]) * scaledKernel[6]; + sum3 += (rowIn[i + 7] + rowIn[i + 25]) * scaledKernel[7]; + + sum0 += (rowIn[i + 8] + rowIn[i + 24]) * scaledKernel[8]; + sum1 += (rowIn[i + 9] + rowIn[i + 23]) * scaledKernel[9]; + sum2 += (rowIn[i + 10] + rowIn[i + 22]) * scaledKernel[10]; + sum3 += (rowIn[i + 11] + rowIn[i + 21]) * scaledKernel[11]; + + sum0 += (rowIn[i + 12] + rowIn[i + 20]) * scaledKernel[12]; + sum1 += (rowIn[i + 13] + rowIn[i + 19]) * scaledKernel[13]; + sum2 += (rowIn[i + 14] + rowIn[i + 18]) * scaledKernel[14]; + sum3 += (rowIn[i + 15] + rowIn[i + 17]) * scaledKernel[15]; + + float sum = rowIn[i + 16] * scaledKernel[16]; + + output.GetRow(x)[y] = sum + sum0 + sum1 + sum2 + sum3; + } + } + + break; + } + + default: + throw new NotSupportedException($"Kernel size {len} not implemented."); + } + + // Left border + for (int x = 0; x < border1; x++) + { + ConvolveBorderColumn(input, kernel, x, output.GetRow(x)); + } + + // Right border + for (int x = border2; x < input.XSize; x++) + { + ConvolveBorderColumn(input, kernel, x, output.GetRow(x)); + } + + return true; + } + + private static bool Blur( + JxlImageF input, + float sigma, + in ButteraugliParameters parameters, + ButteraugliBlurTemp temp, + JxlImageF output) + { + ReadOnlySpan kernel = ComputeKernel(sigma); + + // Separable5 does an in-place convolution, so this fast path is not safe + // if input aliases output. + if (kernel.Length == 5 && !ReferenceEquals(input, output)) + { + float sumWeights = 0.0f; + + foreach (float w in kernel) + { + sumWeights += w; + } + + float scale = 1.0f / sumWeights; + + float w0 = kernel[2] * scale; + float w1 = kernel[1] * scale; + float w2 = kernel[0] * scale; + + JxlWeightsSeparable5 weights = default; + FillRep4(ref weights.Horizontal, w0, w1, w2); + FillRep4(ref weights.Vertical, w0, w1, w2); + + if (!Separable5(input, input.GetRectangle(), weights, null, output)) + { + return false; + } + + return true; + } + + if (!temp.GetTransposed(input, out JxlImageF tempT)) + { + return false; + } + + if (!ConvolutionWithTranspose(input, kernel, tempT)) + { + return false; + } + + if (!ConvolutionWithTranspose(tempT, kernel, output)) + { + return false; + } + + return true; + } + + /// + /// Equivalent to HWY_REP4. + /// + private static void FillRep4(ref InlineArray12 values, float a, float b, float c) + { + for (int i = 0; i < 4; i++) + { + values[i] = a; + values[4 + i] = b; + values[8 + i] = c; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector MaximumClamp(Vector value, float maximum) + { + Vector multiplier = new(0.724216145665f); + Vector maximumValue = new(maximum); + Vector ifPositive = ((value - maximumValue) * multiplier) + maximumValue; + Vector ifNegative = ((value + maximumValue) * multiplier) - maximumValue; + Vector positiveOrValue = Vector.ConditionalSelect(Vector.GreaterThan(value, maximumValue), ifPositive, value); + Vector result = Vector.ConditionalSelect(Vector.LessThan(value, Vector.Negate(maximumValue)), ifNegative, positiveOrValue); + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector RemoveRangeAroundZero(Vector x, float width) + { + Vector w = new(width); + + return Vector.ConditionalSelect( + Vector.GreaterThan(x, w), + x - w, + Vector.ConditionalSelect( + Vector.LessThan(x, -w), + x + w, + Vector.Zero)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector AmplifyRangeAroundZero(Vector x, float width) + { + Vector w = new(width); + + return Vector.ConditionalSelect( + Vector.GreaterThan(x, w), + x + w, + Vector.ConditionalSelect( + Vector.LessThan(x, -w), + x - w, + x + x)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void XybLowFrequencyToValues( + Vector x, + Vector y, + Vector bArg, + out Vector valX, + out Vector valY, + out Vector valB) + { + Vector xMul = new(33.832837186260f); + Vector yMul = new(14.458268100570f); + Vector bMul = new(49.87984651440f); + Vector yToBMul = new(-0.362267051518f); + + Vector b = (yToBMul * y) + bArg; + + valB = b * bMul; + valX = x * xMul; + valY = y * yMul; + } + + public static void XybLowFrequencyToValues(JxlImage3F xybLf) + { + int lanes = Vector.Count; + + for (int y = 0; y < xybLf.YSize; y++) + { + Span rowX = xybLf.PlaneRow(0, y); + Span rowY = xybLf.PlaneRow(1, y); + Span rowB = xybLf.PlaneRow(2, y); + + for (int x = 0; x < xybLf.XSize; x += lanes) + { + Vector valX = new(rowX.Slice(x, lanes)); + Vector valY = new(rowY.Slice(x, lanes)); + Vector valB = new(rowB.Slice(x, lanes)); + + XybLowFrequencyToValues( + valX, + valY, + valB, + out valX, + out valY, + out valB); + + valX.CopyTo(rowX.Slice(x, lanes)); + valY.CopyTo(rowY.Slice(x, lanes)); + valB.CopyTo(rowB.Slice(x, lanes)); + } + } + } + + public static bool SuppressXByY(JxlImageF inY, JxlImageF inOutX) + { + if (!SameSize(inOutX, inY)) + { + return false; + } + + int xSize = inY.XSize; + int ySize = inY.YSize; + int lanes = Vector.Count; + + const float suppress = 46.0f; + const float s = 0.653020556257f; + + Vector sv = new(s); + Vector oneMinusS = new(1.0f - s); + Vector ywv = new(suppress); + + for (int y = 0; y < ySize; y++) + { + ReadOnlySpan rowY = inY.GetRow(y); + Span rowX = inOutX.GetRow(y); + + for (int x = 0; x < xSize; x += lanes) + { + Vector vx = new(rowX.Slice(x, lanes)); + Vector vy = new(rowY.Slice(x, lanes)); + + Vector scaler = + ((ywv / ((vy * vy) + ywv)) * oneMinusS) + sv; + + (scaler * vx).CopyTo(rowX.Slice(x, lanes)); + } + } + + return true; + } + + public static void Subtract(JxlPlane a, JxlPlane b, JxlPlane c) + { + int lanes = Vector.Count; + + for (int y = 0; y < a.YSize; y++) + { + ReadOnlySpan rowA = a.GetRow(y); + ReadOnlySpan rowB = b.GetRow(y); + Span rowC = c.GetRow(y); + + for (int x = 0; x < a.XSize; x += lanes) + { + Vector va = new(rowA.Slice(x, lanes)); + Vector vb = new(rowB.Slice(x, lanes)); + + (va - vb).CopyTo(rowC.Slice(x, lanes)); + } + } + } + + public static bool SeparateLFAndMF( + in ButteraugliParameters parameters, + JxlImage3F xyb, + JxlImage3F lf, + JxlImage3F mf, + ButteraugliBlurTemp blurTemp) + { + const float sigmaLf = 7.15593339443f; + + for (int i = 0; i < 3; i++) + { + if (!Blur( + xyb.Plane(i), + sigmaLf, + parameters, + blurTemp, + lf.Plane(i))) + { + return false; + } + + Subtract( + xyb.Plane(i), + lf.Plane(i), + mf.Plane(i)); + } + + XybLowFrequencyToValues(lf); + + return true; + } + + public static bool SeparateMfAndHf( + Configuration configuration, + in ButteraugliParameters parameters, + JxlImage3F mf, + JxlImageF[] hf, + BlurTemp 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); + + int lanes = Vector.Count; + + for (int i = 0; i < 3; i++) + { + if (i == 2) + { + if (!Blur(mf.Plane(i), sigmaHf, parameters, blurTemp, mf.Plane(i))) + { + return false; + } + + break; + } + + for (int y = 0; y < ySize; y++) + { + Span rowMf = mf.PlaneRow(i, y); + Span rowHf = hf[i].GetRow(y); + + for (int x = 0; x < xSize; x += lanes) + { + new Vector(rowMf.Slice(x, lanes)) + .CopyTo(rowHf.Slice(x, lanes)); + } + } + + if (!Blur(mf.Plane(i), sigmaHf, parameters, blurTemp, mf.Plane(i))) + { + return false; + } + + if (i == 0) + { + const float removeMfRange = 0.29f; + + for (int y = 0; y < ySize; y++) + { + Span rowMf = mf.PlaneRow(0, y); + Span rowHf = hf[0].GetRow(y); + + for (int x = 0; x < xSize; x += lanes) + { + Vector mfv = new(rowMf.Slice(x, lanes)); + Vector hfv = new Vector(rowHf.Slice(x, lanes)) - mfv; + + mfv = RemoveRangeAroundZero(mfv, removeMfRange); + + mfv.CopyTo(rowMf.Slice(x, lanes)); + hfv.CopyTo(rowHf.Slice(x, lanes)); + } + } + } + else + { + const float addMfRange = 0.1f; + + for (int y = 0; y < ySize; y++) + { + Span rowMf = mf.PlaneRow(1, y); + Span rowHf = hf[1].GetRow(y); + + for (int x = 0; x < xSize; x += lanes) + { + Vector mfv = new(rowMf.Slice(x, lanes)); + Vector hfv = new Vector(rowHf.Slice(x, lanes)) - mfv; + + mfv = AmplifyRangeAroundZero(mfv, addMfRange); + + mfv.CopyTo(rowMf.Slice(x, lanes)); + hfv.CopyTo(rowHf.Slice(x, lanes)); + } + } + } + } + + return SuppressXByY(hf[1], hf[0]); + } + + public static bool SeparateHFAndUHF( + in ButteraugliParameters parameters, + JxlImageF[] hf, + JxlImageF[] uhf, + JxlBlurTemp 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); + + int lanes = Vector.Count; + + for (int i = 0; i < 2; i++) + { + for (int y = 0; y < ySize; y++) + { + Span rowUhf = uhf[i].GetRow(y); + Span rowHf = hf[i].GetRow(y); + + for (int x = 0; x < xSize; x += lanes) + { + new Vector(rowHf.Slice(x, lanes)).CopyTo(rowUhf.Slice(x, lanes)); + } + } + + if (!Blur(hf[i], sigmaUhf, parameters, blurTemp, hf[i])) + { + return false; + } + + if (i == 0) + { + const float removeHfRange = 1.5f; + const float removeUhfRange = 0.04f; + + for (int y = 0; y < ySize; y++) + { + Span rowUhf = uhf[0].GetRow(y); + Span rowHf = hf[0].GetRow(y); + + for (int x = 0; x < xSize; x += lanes) + { + Vector hfv = new(rowHf.Slice(x, lanes)); + Vector uhfv = new Vector(rowUhf.Slice(x, lanes)) - hfv; + + hfv = RemoveRangeAroundZero(hfv, removeHfRange); + uhfv = RemoveRangeAroundZero(uhfv, removeUhfRange); + + hfv.CopyTo(rowHf.Slice(x, lanes)); + uhfv.CopyTo(rowUhf.Slice(x, lanes)); + } + } + } + else + { + const float addHfRange = 0.132f; + const float maxClampHf = 28.4691806922f; + const float maxClampUhf = 5.19175294647f; + const float mulYHf = 2.155f; + const float mulYUhf = 2.69313763794f; + + Vector mulHf = new(mulYHf); + Vector mulUhf = new(mulYUhf); + + for (int y = 0; y < ySize; y++) + { + Span rowUhf = uhf[1].GetRow(y); + Span rowHf = hf[1].GetRow(y); + + for (int x = 0; x < xSize; x += lanes) + { + Vector hfv = new(rowHf.Slice(x, lanes)); + hfv = MaximumClamp(hfv, maxClampHf); + + Vector uhfv = new Vector(rowUhf.Slice(x, lanes)) - hfv; + + uhfv = MaximumClamp(uhfv, maxClampUhf); + uhfv *= mulUhf; + + uhfv.CopyTo(rowUhf.Slice(x, lanes)); + + hfv *= mulHf; + hfv = AmplifyRangeAroundZero(hfv, addHfRange); + + hfv.CopyTo(rowHf.Slice(x, lanes)); + } + } + } + } + + return true; + } + + public static void DeallocateHFAndUHF(JxlImageF[] hf, JxlImageF[] uhf) + { + for (int i = 0; i < 2; i++) + { + hf[i] = new JxlImageF(); + uhf[i] = new JxlImageF(); + } + } + + public static bool SeparateFrequencies( + Configuration configuration, + in ButteraugliParameters parameters, + BlurTemp blurTemp, + JxlImage3F xyb, + PsychoImage ps) + { + ps.Lf = JxlImage3F.Create( + configuration, + xyb.XSize, + xyb.YSize); + + ps.Mf = JxlImage3F.Create( + configuration, + xyb.XSize, + xyb.YSize); + + if (!SeparateLFAndMF( + parameters, + xyb, + ps.Lf, + ps.Mf, + blurTemp)) + { + return false; + } + + if (!SeparateMfAndHf( + parameters, + ps.Mf, + ps.Hf, + blurTemp)) + { + return false; + } + + if (!SeparateHFAndUHF( + parameters, + ps.Hf, + ps.Uhf, + blurTemp)) + { + return false; + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector Sum( + Vector a, + Vector b, + Vector c, + Vector d) + => (a + b) + (c + d); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector Sum( + Vector a, + Vector b, + Vector c, + Vector d, + Vector e) + => Sum(a, b, c, d + e); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector Sum( + Vector a, + Vector b, + Vector c, + Vector d, + Vector e, + Vector f, + Vector g) + => Sum(a, b, c, Sum(d, e, f, g)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector Sum( + Vector a, + Vector b, + Vector c, + Vector d, + Vector e, + Vector f, + Vector g, + Vector h, + Vector i) + => (Sum(a, b, c, d) + Sum(e, f, g, h)) + i; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector MaltaUnitLF( + ReadOnlySpan row, + int index, + int xs) + { + // helper + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static Vector Load(ReadOnlySpan row, int index) + => new(row.Slice(index, Vector.Count)); + + int xs3 = 3 * xs; + + Vector center = Load(row, index); + + Vector sumYConst = Sum( + Load(row, index - 4), + Load(row, index - 2), + center, + Load(row, index + 2), + Load(row, index + 4)); + + Vector retval = sumYConst * sumYConst; + + Vector sum; + + sum = Sum( + Load(row, index - xs3 - xs), + Load(row, index - xs - xs), + center, + Load(row, index + xs + xs), + Load(row, index + xs3 + xs)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 - 3), + Load(row, index - xs - xs - 2), + center, + Load(row, index + xs + xs + 2), + Load(row, index + xs3 + 3)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 + 3), + Load(row, index - xs - xs + 2), + center, + Load(row, index + xs + xs - 2), + Load(row, index + xs3 - 3)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 - xs + 1), + Load(row, index - xs - xs + 1), + center, + Load(row, index + xs + xs - 1), + Load(row, index + xs3 + xs - 1)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 - xs - 1), + Load(row, index - xs - xs - 1), + center, + Load(row, index + xs + xs + 1), + Load(row, index + xs3 + xs + 1)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - 4 - xs), + Load(row, index - 2 - xs), + center, + Load(row, index + 2 + xs), + Load(row, index + 4 + xs)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - 4 + xs), + Load(row, index - 2 + xs), + center, + Load(row, index + 2 - xs), + Load(row, index + 4 - xs)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 - 2), + Load(row, index - xs - xs - 1), + center, + Load(row, index + xs + xs + 1), + Load(row, index + xs3 + 2)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 + 2), + Load(row, index - xs - xs + 1), + center, + Load(row, index + xs + xs - 1), + Load(row, index + xs3 - 2)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs - xs - 3), + Load(row, index - xs - 2), + center, + Load(row, index + xs + 2), + Load(row, index + xs + xs + 3)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs - xs + 3), + Load(row, index - xs + 2), + center, + Load(row, index + xs - 2), + Load(row, index + xs + xs - 3)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index + xs + xs - 4), + Load(row, index + xs - 2), + center, + Load(row, index - xs + 2), + Load(row, index - xs - xs + 4)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs - xs - 4), + Load(row, index - xs - 2), + center, + Load(row, index + xs + 2), + Load(row, index + xs + xs + 4)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 - xs - 2), + Load(row, index - xs - xs - 1), + center, + Load(row, index + xs + xs + 1), + Load(row, index + xs3 + xs + 2)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 - xs + 2), + Load(row, index - xs - xs + 1), + center, + Load(row, index + xs + xs - 1), + Load(row, index + xs3 + xs - 2)); + retval = (sum * sum) + retval; + + return retval; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector MaltaUnit( + ReadOnlySpan row, + int index, + int xs) + { + // helper + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static Vector Load(ReadOnlySpan row, int index) + => new(row.Slice(index, Vector.Count)); + + int xs3 = 3 * xs; + + Vector center = Load(row, index); + + Vector sumYConst = Sum( + Load(row, index - 4), + Load(row, index - 3), + Load(row, index - 2), + Load(row, index - 1), + center, + Load(row, index + 1), + Load(row, index + 2), + Load(row, index + 3), + Load(row, index + 4)); + + Vector retval = sumYConst * sumYConst; + + Vector sum; + + sum = Sum( + Load(row, index - xs3 - xs), + Load(row, index - xs3), + Load(row, index - xs - xs), + Load(row, index - xs), + center, + Load(row, index + xs), + Load(row, index + xs + xs), + Load(row, index + xs3), + Load(row, index + xs3 + xs)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 - 3), + Load(row, index - xs - xs - 2), + Load(row, index - xs - 1), + center, + Load(row, index + xs + 1), + Load(row, index + xs + xs + 2), + Load(row, index + xs3 + 3)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 + 3), + Load(row, index - xs - xs + 2), + Load(row, index - xs + 1), + center, + Load(row, index + xs - 1), + Load(row, index + xs + xs - 2), + Load(row, index + xs3 - 3)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 - xs + 1), + Load(row, index - xs3 + 1), + Load(row, index - xs - xs + 1), + Load(row, index - xs), + center, + Load(row, index + xs), + Load(row, index + xs + xs - 1), + Load(row, index + xs3 - 1), + Load(row, index + xs3 + xs - 1)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 - xs - 1), + Load(row, index - xs3 - 1), + Load(row, index - xs - xs - 1), + Load(row, index - xs), + center, + Load(row, index + xs), + Load(row, index + xs + xs + 1), + Load(row, index + xs3 + 1), + Load(row, index + xs3 + xs + 1)); + retval += sum * sum; + + sum = Sum( + Load(row, index - 4 - xs), + Load(row, index - 3 - xs), + Load(row, index - 2 - xs), + Load(row, index - 1), + center, + Load(row, index + 1), + Load(row, index + 2 + xs), + Load(row, index + 3 + xs), + Load(row, index + 4 + xs)); + retval += sum * sum; + + sum = Sum( + Load(row, index - 4 + xs), + Load(row, index - 3 + xs), + Load(row, index - 2 + xs), + Load(row, index - 1), + center, + Load(row, index + 1), + Load(row, index + 2 - xs), + Load(row, index + 3 - xs), + Load(row, index + 4 - xs)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 - 2), + Load(row, index - xs - xs - 1), + Load(row, index - xs - 1), + center, + Load(row, index + xs + 1), + Load(row, index + xs + xs + 1), + Load(row, index + xs3 + 2)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 + 2), + Load(row, index - xs - xs + 1), + Load(row, index - xs + 1), + center, + Load(row, index + xs - 1), + Load(row, index + xs + xs - 1), + Load(row, index + xs3 - 2)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs - xs - 3), + Load(row, index - xs - 2), + Load(row, index - xs - 1), + center, + Load(row, index + xs + 1), + Load(row, index + xs + 2), + Load(row, index + xs + xs + 3)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs - xs + 3), + Load(row, index - xs + 2), + Load(row, index - xs + 1), + center, + Load(row, index + xs - 1), + Load(row, index + xs - 2), + Load(row, index + xs + xs - 3)); + retval += sum * sum; + + sum = Sum( + Load(row, index + xs - 4), + Load(row, index + xs - 3), + Load(row, index + xs - 2), + Load(row, index - 1), + center, + Load(row, index + 1), + Load(row, index - xs + 2), + Load(row, index - xs + 3), + Load(row, index - xs + 4)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs - 4), + Load(row, index - xs - 3), + Load(row, index - xs - 2), + Load(row, index - 1), + center, + Load(row, index + 1), + Load(row, index + xs + 2), + Load(row, index + xs + 3), + Load(row, index + xs + 4)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 - xs - 1), + Load(row, index - xs3 - 1), + Load(row, index - xs - xs - 1), + Load(row, index - xs), + center, + Load(row, index + xs), + Load(row, index + xs + xs + 1), + Load(row, index + xs3 + 1), + Load(row, index + xs3 + xs + 1)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 - xs + 1), + Load(row, index - xs3 + 1), + Load(row, index - xs - xs + 1), + Load(row, index - xs), + center, + Load(row, index + xs), + Load(row, index + xs + xs - 1), + Load(row, index + xs3 - 1), + Load(row, index + xs3 + xs - 1)); + retval += sum * sum; + + return retval; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float PaddedMaltaUnit( + JxlImageF diffs, + int x0, + int y0, + bool isLF) + { + if (x0 >= 4 && + y0 >= 4 && + x0 < diffs.XSize - 4 && + y0 < diffs.YSize - 4) + { + return isLF + ? MaltaUnitLF( + diffs.GetRow(y0), + x0, + diffs.PixelsPerRow)[0] + : MaltaUnit( + diffs.GetRow(y0), + x0, + diffs.PixelsPerRow)[0]; + } + + Span borderImage = stackalloc float[12 * 9]; + + for (int dy = 0; dy < 9; dy++) + { + int y = y0 + dy - 4; + + if (y < 0 || y >= diffs.YSize) + { + borderImage.Slice(dy * 12, 12).Clear(); + continue; + } + + ReadOnlySpan rowDiffs = diffs.GetRow(y); + + for (int dx = 0; dx < 9; dx++) + { + int x = x0 + dx - 4; + + borderImage[(dy * 12) + dx] = + x < 0 || x >= diffs.XSize + ? 0.0f + : rowDiffs[x]; + } + + borderImage.Slice((dy * 12) + 9, 3).Clear(); + } + + return isLF + ? MaltaUnitLF( + diffs.GetRow(y0), + x0, + diffs.PixelsPerRow)[0] + : MaltaUnit( + borderImage, + (4 * 12) + 4, + 12)[0]; + } + + public static bool MaltaDiffMap( + bool isLf, + JxlImageF lum0, + JxlImageF lum1, + float w0Gt1, + float w0Lt1, + float norm1, + float len, + float mulli, + JxlImageF diffs, + JxlImageF blockDiffAc) + { + if (!SameSize(lum0, lum1) || !SameSize(lum0, diffs)) + { + return false; + } + + int width = lum0.XSize; + int height = lum0.YSize; + + const float weight0 = 0.5f; + const float weight1 = 0.33f; + + float norm2_0Gt1 = + (float)(mulli * MathF.Sqrt(weight0 * w0Gt1) / ((len * 2) + 1) * norm1); + + float norm2_0Lt1 = + (float)(mulli * MathF.Sqrt(weight1 * w0Lt1) / ((len * 2) + 1) * norm1); + + for (int y = 0; y < height; y++) + { + ReadOnlySpan row0 = lum0.GetRow(y); + ReadOnlySpan row1 = lum1.GetRow(y); + Span rowDiffs = diffs.GetRow(y); + + for (int x = 0; x < width; x++) + { + float absVal = 0.5f * + (MathF.Abs(row0[x]) + MathF.Abs(row1[x])); + + float diff = row0[x] - row1[x]; + + float scaler = norm2_0Gt1 / ((float)norm1 + absVal); + + rowDiffs[x] = scaler * diff; + + float scaler2 = norm2_0Lt1 / ((float)norm1 + absVal); + + float fabs0 = MathF.Abs(row0[x]); + + float tooSmall = 0.55f * fabs0; + float tooBig = 1.05f * fabs0; + + if (row0[x] < 0) + { + if (row1[x] > -tooSmall) + { + rowDiffs[x] -= scaler2 * (row1[x] + tooSmall); + } + else if (row1[x] < -tooBig) + { + rowDiffs[x] += scaler2 * (-row1[x] - tooBig); + } + } + else + { + if (row1[x] < tooSmall) + { + rowDiffs[x] += scaler2 * (tooSmall - row1[x]); + } + else if (row1[x] > tooBig) + { + rowDiffs[x] -= scaler2 * (row1[x] - tooBig); + } + } + } + } + + int y0 = 0; + + for (; y0 < 4; y0++) + { + Span row = blockDiffAc.GetRow(y0); + + for (int x = 0; x < width; x++) + { + row[x] += PaddedMaltaUnit(diffs, x, y0, isLf); + } + } + + int lanes = Vector.Count; + int alignedX = Math.Max(4, lanes); + + int stride = diffs.PixelsPerRow; + + for (; y0 < height - 4; y0++) + { + ReadOnlySpan input = diffs.GetRow(y0); + Span output = blockDiffAc.GetRow(y0); + ref float outputReference = ref MemoryMarshal.GetReference(output); + + int x = 0; + + for (; x < alignedX; x++) + { + output[x] += PaddedMaltaUnit(diffs, x, y0, isLf); + } + + for (; x + lanes + 4 <= width; x += lanes) + { + Vector value = Vector.LoadUnsafe(ref Unsafe.Add(ref outputReference, x)); + + Vector malta = isLf + ? MaltaUnitLF(input, x, stride) + : MaltaUnit(input, x, stride); + + (value + malta).CopyTo(output[x..]); + } + + for (; x < width; x++) + { + output[x] += PaddedMaltaUnit(diffs, x, y0, isLf); + } + } + + for (; y0 < height; y0++) + { + Span row = blockDiffAc.GetRow(y0); + + for (int x = 0; x < width; x++) + { + row[x] += PaddedMaltaUnit(diffs, x, y0, isLf); + } + } + + return true; + } + + public static bool MaltaDiffMap( + JxlImageF lum0, + JxlImageF lum1, + float w0Gt1, + float w0Lt1, + float norm1, + JxlImageF diffs, + JxlImageF blockDiffAc) + { + const float len = 3.75f; + const float mulli = 0.39905817637f; + + return MaltaDiffMap( + false, + lum0, + lum1, + w0Gt1, + w0Lt1, + norm1, + len, + mulli, + diffs, + blockDiffAc); + } + + public static bool MaltaDiffMapLf( + JxlImageF lum0, + JxlImageF lum1, + float w0Gt1, + float w0Lt1, + float norm1, + JxlImageF diffs, + JxlImageF blockDiffAc) + { + const float len = 3.75f; + const float mulli = 0.611612573796f; + + return MaltaDiffMap( + true, + lum0, + lum1, + w0Gt1, + w0Lt1, + norm1, + len, + mulli, + diffs, + blockDiffAc); + } + + public static void CombineChannelsForMasking( + JxlImageF[] hf, + JxlImageF[] uhf, + JxlImageF output) + { + // Only X and Y components are involved in masking. + ReadOnlySpan muls = + [ + 2.5f, + 0.4f, + 0.4f + ]; + + int width = hf[0].XSize; + int height = hf[0].YSize; + + for (int y = 0; y < height; y++) + { + ReadOnlySpan rowYHf = hf[1].GetRow(y); + ReadOnlySpan rowYUhf = uhf[1].GetRow(y); + ReadOnlySpan rowXHf = hf[0].GetRow(y); + ReadOnlySpan rowXUhf = uhf[0].GetRow(y); + + Span row = output.GetRow(y); + + for (int x = 0; x < width; x++) + { + float xDiff = (rowXUhf[x] + rowXHf[x]) * muls[0]; + float yDiff = (rowYUhf[x] * muls[1]) + (rowYHf[x] * muls[2]); + + row[x] = MathF.Sqrt((xDiff * xDiff) + (yDiff * yDiff)); + } + } + } + + public static void DiffPrecompute( + JxlImageF xyb, + float mul, + float biasArg, + JxlImageF output) + { + int width = xyb.XSize; + int height = xyb.YSize; + + float bias = mul * biasArg; + float sqrtBias = MathF.Sqrt(bias); + + for (int y = 0; y < height; y++) + { + ReadOnlySpan rowIn = xyb.GetRow(y); + Span rowOut = output.GetRow(y); + + for (int x = 0; x < width; x++) + { + rowOut[x] = + MathF.Sqrt( + (mul * MathF.Abs(rowIn[x])) + bias) + - sqrtBias; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreMin3( + float value, + ref float min0, + ref float min1, + ref float min2) + { + if (value < min2) + { + if (value < min0) + { + min2 = min1; + min1 = min0; + min0 = value; + } + else if (value < min1) + { + min2 = min1; + min1 = value; + } + else + { + min2 = value; + } + } + } + + public static void FuzzyErosion(JxlImageF from, JxlImageF to) + { + int width = from.XSize; + int height = from.YSize; + + const int step = 3; + + for (int y = 0; y < height; y++) + { + Span output = to.GetRow(y); + + for (int x = 0; x < width; x++) + { + float min0 = from.GetRow(y)[x]; + float min1 = 2 * min0; + float min2 = min1; + + if (x >= step) + { + StoreMin3( + from.GetRow(y)[x - step], + ref min0, + ref min1, + ref min2); + + if (y >= step) + { + StoreMin3( + from.GetRow(y - step)[x - step], + ref min0, + ref min1, + ref min2); + } + + if (y < height - step) + { + StoreMin3( + from.GetRow(y + step)[x - step], + ref min0, + ref min1, + ref min2); + } + } + + if (x < width - step) + { + StoreMin3( + from.GetRow(y)[x + step], + ref min0, + ref min1, + ref min2); + + if (y >= step) + { + StoreMin3( + from.GetRow(y - step)[x + step], + ref min0, + ref min1, + ref min2); + } + + if (y < height - step) + { + StoreMin3( + from.GetRow(y + step)[x + step], + ref min0, + ref min1, + ref min2); + } + } + + if (y >= step) + { + StoreMin3( + from.GetRow(y - step)[x], + ref min0, + ref min1, + ref min2); + } + + if (y < height - step) + { + StoreMin3( + from.GetRow(y + step)[x], + ref min0, + ref min1, + ref min2); + } + + output[x] = + (0.45f * min0) + + (0.3f * min1) + + (0.25f * min2); + } + } + } + + public static bool Mask( + Configuration configuration, + JxlImageF mask0, + JxlImageF mask1, + in ButteraugliParameters parameters, + JxlBlurTemp blurTemp, + JxlImageF? diffAc, + out JxlImageF mask) + { + int width = mask0.XSize; + int height = mask0.YSize; + + mask = new(configuration, width, height); + + const float mul = 6.19424080439f; + const float bias = 12.61050594197f; + const float radius = 2.7f; + + JxlImageF diff0 = new(configuration, width, height); + JxlImageF diff1 = new(configuration, width, height); + JxlImageF blurred0 = new(configuration, width, height); + JxlImageF blurred1 = new(configuration, width, height); + + DiffPrecompute(mask0, mul, bias, diff0); + DiffPrecompute(mask1, mul, bias, diff1); + + if (!Blur(diff0, radius, parameters, blurTemp, blurred0)) + { + return false; + } + + FuzzyErosion(blurred0, diff0); + + if (!Blur(diff1, radius, parameters, blurTemp, blurred1)) + { + return false; + } + + for (int y = 0; y < height; y++) + { + Span maskRow = mask.GetRow(y); + Span diffRow = diffAc is not null ? diffAc.GetRow(y) : []; + + ReadOnlySpan diff0Row = diff0.GetRow(y); + ReadOnlySpan blur0Row = blurred0.GetRow(y); + ReadOnlySpan blur1Row = blurred1.GetRow(y); + + for (int x = 0; x < width; x++) + { + maskRow[x] = diff0Row[x]; + + if (diffRow != null) + { + const float maskToErrorMul = 10.0f; + float diff = blur0Row[x] - blur1Row[x]; + diffRow[x] += maskToErrorMul * diff * diff; + } + } + } + + return true; + } + + public static bool MaskPsychoImage( + Configuration configuration, + ButteraugliPsychoImage pi0, + ButteraugliPsychoImage pi1, + int width, + int height, + in ButteraugliParameters parameters, + BlurTemp blurTemp, + JxlImageF mask, + JxlImageF? diffAc) + { + JxlImageF mask0 = new(configuration, width, height); + JxlImageF mask1 = new(configuration, width, height); + + CombineChannelsForMasking( + pi0.Hf, + pi0.Uhf, + mask0); + + CombineChannelsForMasking( + pi1.Hf, + pi1.Uhf, + mask1); + + return Mask( + mask0, + mask1, + parameters, + blurTemp, + mask, + diffAc); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MaskY(float delta) + { + const float offset = 0.829591754942f; + const float scaler = 0.451936922203f; + const float mul = 2.5485944793f; + + float c = mul / ((scaler * delta) + offset); + float result = GlobalScale * (1.0f + c); + + return result * result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MaskDcY(float delta) + { + const float offset = 0.20025578522f; + const float scaler = 3.87449418804f; + const float mul = 0.505054525019f; + + float c = mul / ((scaler * delta) + offset); + float result = GlobalScale * (1.0f + c); + + return result * result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MaskColor( + ReadOnlySpan color, + float mask) + => (color[0] * mask) + (color[1] * mask) + (color[2] * mask); + + public static bool CombineChannelsToDiffmap( + JxlImageF mask, + JxlImage3F blockDiffDc, + JxlImage3F blockDiffAc, + float xmul, + JxlImageF result) + { + if (!SameSize(mask, result)) + { + return false; + } + + Span diffDc = stackalloc float[3]; + Span diffAc = stackalloc float[3]; + + int xsize = mask.XSize; + int ysize = mask.YSize; + + for (int y = 0; y < ysize; ++y) + { + Span rowOut = result.GetRow(y); + + for (int x = 0; x < xsize; ++x) + { + float val = mask.GetRow(y)[x]; + + float maskVal = MaskY(val); + float dcMaskVal = MaskDcY(val); + + for (int i = 0; i < 3; ++i) + { + diffDc[i] = blockDiffDc.PlaneRow(i, y)[x]; + diffAc[i] = blockDiffAc.PlaneRow(i, y)[x]; + } + + diffAc[0] *= xmul; + diffDc[0] *= xmul; + + rowOut[x] = MathF.Sqrt( + MaskColor(diffDc, dcMaskVal) + + MaskColor(diffAc, maskVal)); + } + } + + return true; + } + + public static void L2Diff( + JxlImageF i0, + JxlImageF i1, + float w, + JxlImageF diffmap) + { + if (w == 0) + { + return; + } + + for (int y = 0; y < i0.YSize; ++y) + { + ReadOnlySpan row0 = i0.GetRow(y); + ReadOnlySpan row1 = i1.GetRow(y); + Span rowDiff = diffmap.GetRow(y); + + for (int x = 0; x < i0.XSize; ++x) + { + float diff = row0[x] - row1[x]; + rowDiff[x] += diff * diff * w; + } + } + } + + public static void SetL2Diff( + JxlImageF i0, + JxlImageF i1, + float w, + JxlImageF diffmap) + { + if (w == 0) + { + return; + } + + for (int y = 0; y < i0.YSize; ++y) + { + ReadOnlySpan row0 = i0.GetRow(y); + ReadOnlySpan row1 = i1.GetRow(y); + Span rowDiff = diffmap.GetRow(y); + + for (int x = 0; x < i0.XSize; ++x) + { + float diff = row0[x] - row1[x]; + rowDiff[x] = diff * diff * w; + } + } + } + + public static void L2DiffAsymmetric( + JxlImageF i0, + JxlImageF i1, + float w0gt1, + float w0lt1, + JxlImageF diffmap) + { + if (w0gt1 == 0 && w0lt1 == 0) + { + return; + } + + Vector vw0gt1 = new(w0gt1 * 0.8f); + Vector vw0lt1 = new(w0lt1 * 0.8f); + + int lanes = Vector.Count; + + for (int y = 0; y < i0.YSize; ++y) + { + ReadOnlySpan row0 = i0.GetRow(y); + ReadOnlySpan row1 = i1.GetRow(y); + Span rowDiff = diffmap.GetRow(y); + + for (int x = 0; x < i0.XSize; x += lanes) + { + Vector val0 = new(row0[x..]); + Vector val1 = new(row1[x..]); + + // Primary symmetric quadratic objective. + Vector diff = val0 - val1; + + Vector total = + (diff * diff * vw0gt1) + new Vector(rowDiff[x..]); + + Vector fabs0 = Vector.Abs(val0); + + Vector tooSmall = fabs0 * new Vector(0.4f); + + Vector tooBig = fabs0; + + Vector ifNeg = + Vector.ConditionalSelect( + Vector.GreaterThan(val1, -tooSmall), + val1 + tooSmall, + Vector.ConditionalSelect( + Vector.LessThan(val1, -tooBig), + -val1 - tooBig, + Vector.Zero)); + + Vector ifPos = + Vector.ConditionalSelect( + Vector.LessThan(val1, tooSmall), + tooSmall - val1, + Vector.ConditionalSelect( + Vector.GreaterThan(val1, tooBig), + val1 - tooBig, + Vector.Zero)); + + Vector v = + Vector.ConditionalSelect( + Vector.LessThan(val0, Vector.Zero), + ifNeg, + ifPos); + + total += vw0lt1 * v * v; + + total.CopyTo(rowDiff[x..]); + } + } + } + + public static void OpsinAbsorbance( + bool clamp, + Vector in0, + Vector in1, + Vector in2, + out Vector out0, + out Vector out1, + out Vector out2) + { + Vector mix0 = new(0.29956550340058319f); + Vector mix1 = new(0.63373087833825936f); + Vector mix2 = new(0.077705617820981968f); + Vector mix3 = new(1.7557483643287353f); + + Vector mix4 = new(0.22158691104574774f); + Vector mix5 = new(0.69391388044116142f); + Vector mix6 = new(0.0987313588422f); + Vector mix7 = new(1.7557483643287353f); + + Vector mix8 = new(0.02f); + Vector mix9 = new(0.02f); + Vector mix10 = new(0.20480129041026129f); + Vector mix11 = new(12.226454707163354f); + + out0 = (mix0 * in0) + ((mix1 * in1) + ((mix2 * in2) + mix3)); + out1 = (mix4 * in0) + ((mix5 * in1) + ((mix6 * in2) + mix7)); + out2 = (mix8 * in0) + ((mix9 * in1) + ((mix10 * in2) + mix11)); + + if (clamp) + { + out0 = Vector.Max(out0, mix3); + out1 = Vector.Max(out1, mix7); + out2 = Vector.Max(out2, mix11); + } + } + + public static bool OpsinDynamicsImage( + JxlImage3F rgb, + in ButteraugliParameters parameters, + JxlImage3F blurred, + BlurTemp blurTemp, + JxlImage3F xyb) + { + if (blurred == null) + { + return false; + } + + const double sigma = 1.2; + + if (!Blur(rgb.Plane(0), sigma, parameters, blurTemp, blurred.Plane(0))) + { + return false; + } + + if (!Blur(rgb.Plane(1), sigma, parameters, blurTemp, blurred.Plane(1))) + { + return false; + } + + if (!Blur(rgb.Plane(2), sigma, parameters, blurTemp, blurred.Plane(2))) + { + return false; + } + + Vector intensityMultiplier = new((float)parameters.IntensityTarget); + + int lanes = Vector.Count; + + Vector minValue = new(1e-4f); + + for (int y = 0; y < rgb.YSize; ++y) + { + ReadOnlySpan rowR = rgb.PlaneRow(0, y); + ReadOnlySpan rowG = rgb.PlaneRow(1, y); + ReadOnlySpan rowB = rgb.PlaneRow(2, y); + + ReadOnlySpan blurredR = blurred.PlaneRow(0, y); + ReadOnlySpan blurredG = blurred.PlaneRow(1, y); + ReadOnlySpan blurredB = blurred.PlaneRow(2, y); + + Span outX = xyb.PlaneRow(0, y); + Span outY = xyb.PlaneRow(1, y); + Span outB = xyb.PlaneRow(2, y); + + for (int x = 0; x < rgb.XSize; x += lanes) + { + Vector sensitivity0; + Vector sensitivity1; + Vector sensitivity2; + { + OpsinAbsorbance( + true, + new Vector(blurredR[x..]) * intensityMultiplier, + new Vector(blurredG[x..]) * intensityMultiplier, + new Vector(blurredB[x..]) * intensityMultiplier, + out Vector pre0, + out Vector pre1, + out Vector pre2); + + pre0 = Vector.Max(pre0, minValue); + pre1 = Vector.Max(pre1, minValue); + pre2 = Vector.Max(pre2, minValue); + + sensitivity0 = Gamma(pre0) / pre0; + sensitivity1 = Gamma(pre1) / pre1; + sensitivity2 = Gamma(pre2) / pre2; + + sensitivity0 = Vector.Max(sensitivity0, minValue); + sensitivity1 = Vector.Max(sensitivity1, minValue); + sensitivity2 = Vector.Max(sensitivity2, minValue); + } + + OpsinAbsorbance( + false, + new Vector(rowR[x..]) * intensityMultiplier, + new Vector(rowG[x..]) * intensityMultiplier, + new Vector(rowB[x..]) * intensityMultiplier, + out Vector cur0, + out Vector cur1, + out Vector cur2); + + cur0 *= sensitivity0; + cur1 *= sensitivity1; + cur2 *= sensitivity2; + + Vector min01 = new(1.7557483643287353f); + Vector min2 = new(12.226454707163354f); + + cur0 = Vector.Max(cur0, min01); + cur1 = Vector.Max(cur1, min01); + cur2 = Vector.Max(cur2, min2); + + (cur0 - cur1).CopyTo(outX[x..]); + (cur0 + cur1).CopyTo(outY[x..]); + cur2.CopyTo(outB[x..]); + } + } + + return true; + } + + public static bool ButteraugliDiffmapInPlace( + Configuration configuration, + JxlImage3F image0, + JxlImage3F image1, + in ButteraugliParameters parameters, + JxlImageF diffmap) + { + int xSize = image0.XSize; + int ySize = image0.YSize; + + using var blurTemp = new JxlBlurTemp(); + + using (JxlImage3F temp = new(configuration, xSize, ySize)) + { + if (!OpsinDynamicsImage(image0, parameters, temp, blurTemp, image0)) + { + return false; + } + + if (!OpsinDynamicsImage(image1, parameters, temp, blurTemp, image1)) + { + return false; + } + } + + using JxlPlane blockDiffDc = JxlImageF.Create(configuration, xSize, ySize); + blockDiffDc.ZeroFill(); + + // LF/DC + using (JxlImage3F lf0 = new(configuration, xSize, ySize)) + using (JxlImage3F lf1 = new(configuration, xSize, ySize)) + { + if (!SeparateLFAndMF(parameters, image0, lf0, image0, blurTemp)) + { + return false; + } + + if (!SeparateLFAndMF(parameters, image1, lf1, image1, blurTemp)) + { + return false; + } + + for (int c = 0; c < 3; c++) + { + L2Diff( + lf0.Plane(c), + lf1.Plane(c), + Wmul[6 + c], + blockDiffDc); + } + } + + JxlImageF[] hf0 = new JxlImageF[2]; + JxlImageF[] hf1 = new JxlImageF[2]; + + if (!SeparateMfAndHf(parameters, image0, hf0, blurTemp)) + { + return false; + } + + if (!SeparateMfAndHf(parameters, image1, hf1, blurTemp)) + { + return false; + } + + using JxlImageF blockDiffAc = new(configuration, xSize, ySize); + blockDiffAc.ZeroFill(); + + using (JxlImageF diffs = new(configuration, xSize, ySize)) + { + if (!MaltaDiffMap( + true, + image0.Plane(1), + image1.Plane(1), + WMfMalta, + WMfMalta, + Norm1Mf, + diffs, + blockDiffAc)) + { + return false; + } + + if (!MaltaDiffMap( + true, + image0.Plane(0), + image1.Plane(0), + WMfMaltaX, + WMfMaltaX, + Norm1MfX, + diffs, + blockDiffAc)) + { + return false; + } + } + + for (int c = 0; c < 3; c++) + { + L2Diff( + image0.Plane(c), + image1.Plane(c), + Wmul[3 + c], + blockDiffAc); + } + + // Free MF images + image0.Dispose(); + image1.Dispose(); + + JxlImageF[] uhf0 = new JxlImageF[2]; + JxlImageF[] uhf1 = new JxlImageF[2]; + + if (!SeparateHFAndUHF(parameters, hf0, uhf0, blurTemp)) + { + return false; + } + + if (!SeparateHFAndUHF(parameters, hf1, uhf1, blurTemp)) + { + return false; + } + + float hfAsymmetry = parameters.HfAsymmetry; + + using (JxlImageF diffs = new(configuration, xSize, ySize)) + { + MaltaDiffMap( + false, + uhf0[1], + uhf1[1], + WUhfMalta * hfAsymmetry, + WUhfMalta / hfAsymmetry, + Norm1Uhf, + diffs, + blockDiffAc); + + MaltaDiffMap( + false, + uhf0[0], + uhf1[0], + wUhfMaltaX * hfAsymmetry, + wUhfMaltaX / hfAsymmetry, + norm1UhfX, + diffs, + blockDiffAc); + + float sqrtAsym = MathF.Sqrt(hfAsymmetry); + + MaltaDiffMap( + true, + hf0[1], + hf1[1], + WHfMalta * sqrtAsym, + WHfMalta / sqrtAsym, + Norm1Hf, + diffs, + blockDiffAc); + + MaltaDiffMap( + true, + hf0[0], + hf1[0], + WHfMaltaX * sqrtAsym, + WHfMaltaX / sqrtAsym, + Norm1HfX, + diffs, + blockDiffAc); + } + + for (int c = 0; c < 2; c++) + { + L2DiffAsymmetric( + hf0[c], + hf1[c], + Wmul[c] * hfAsymmetry, + Wmul[c] / hfAsymmetry, + blockDiffAc); + } + + // Mask + using JxlImageF mask = new(configuration, xSize, ySize); + using JxlImageF mask0 = new(configuration, xSize, ySize); + using JxlImageF mask1 = new(configuration, xSize, ySize); + + CombineChannelsForMasking(hf0, uhf0, mask0); + CombineChannelsForMasking(hf1, uhf1, mask1); + + DeallocateHFAndUHF(hf0, uhf0); + DeallocateHFAndUHF(hf1, uhf1); + + if (!Mask(mask0, mask1, parameters, blurTemp, mask, blockDiffAc)) + { + return false; + } + + for (int y = 0; y < ySize; y++) + { + ReadOnlySpan dc = blockDiffDc.GetRow(y); + ReadOnlySpan ac = blockDiffAc.GetRow(y); + Span output = diffmap.GetRow(y); + ReadOnlySpan maskRow = mask.GetRow(y); + + for (int x = 0; x < xSize; x++) + { + float m = maskRow[x]; + + output[x] = + MathF.Sqrt( + (dc[x] * (float)MaskDcY(m)) + + (ac[x] * (float)MaskY(m))); + } + } + + return true; + } + + // Calculate a 2x2 subsampled image for purposes of recursive butteraugli at + // multiresolution. + public static JxlImage3F SubSample2x(Configuration configuration, JxlImage3F input) + { + int xs = (input.XSize + 1) / 2; + int ys = (input.YSize + 1) / 2; + + JxlImage3F retval = new(Configuration, xs, ys); + + for (int c = 0; c < 3; ++c) + { + for (int y = 0; y < ys; ++y) + { + for (int x = 0; x < xs; ++x) + { + retval.PlaneRow(c, y)[x] = 0.0f; + } + } + } + + for (int c = 0; c < 3; ++c) + { + for (int y = 0; y < input.YSize; ++y) + { + ReadOnlySpan srcRow = input.PlaneRow(c, y); + + for (int x = 0; x < input.XSize; ++x) + { + retval.PlaneRow(c, y / 2)[x / 2] += + 0.25f * srcRow[x]; + } + } + + if ((input.XSize & 1) != 0) + { + for (int y = 0; y < retval.YSize; ++y) + { + int lastColumn = retval.XSize - 1; + retval.PlaneRow(c, y)[lastColumn] *= 2.0f; + } + } + + if ((input.YSize & 1) != 0) + { + for (int x = 0; x < retval.XSize; ++x) + { + int lastRow = retval.YSize - 1; + retval.PlaneRow(c, lastRow)[x] *= 2.0f; + } + } + } + + return retval; + } + + public static void AddSupersampled2x(JxlImageF src, float w, JxlImageF dest) + { + const float heuristicMixingValue = 0.3f; + + for (int y = 0; y < dest.YSize; ++y) + { + Span destRow = dest.GetRow(y); + + for (int x = 0; x < dest.XSize; ++x) + { + destRow[x] *= 1.0f - (heuristicMixingValue * w); + destRow[x] += w * src.GetRow(y / 2)[x / 2]; + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs new file mode 100644 index 000000000..be5837534 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; + +internal sealed class ButteraugliComparator +{ + private int xSize; + private int ySize; + private ButteraugliParameters parameters; + private readonly ButteraugliPsychoImage pi0; + private readonly JxlImage3F temp; + private bool tempInUse; + private readonly ButteraugliBlurTemp blurTemp; + + /// + /// 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) + { + throw new NotImplementedException(); + } + + /// + /// Same as Diffmap but OpsinDynamicsImage() was already applied. + /// + public bool DiffmapOpsinDynamicsImage(JxlImage3F xyb1, JxlImageF result) + { + throw new NotImplementedException(); + } + + /// + /// Same as above but the frequency decomposition was already applied. + /// + public bool DiffmapPsychoImage(ButteraugliPsychoImage pi1, JxlImageF diffmap) + { + throw new NotImplementedException(); + } + + public bool Mask(JxlImageF mask) + { + throw new NotImplementedException(); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliParameters.cs index 9b5fd5761..7af303c8b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliParameters.cs @@ -9,18 +9,18 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; internal struct ButteraugliParameters() { /// - /// Multiplier for penalizing new HF artifacts more than + /// Gets or sets the multiplier for penalizing new HF artifacts more than /// blurring away features. Value of 1.0 represents neutral. /// - public float HfAsymmetry = 1f; + public float HfAsymmetry { get; set; } = 1f; /// - /// Multiplier for the psychovisual difference in the X channel. + /// Gets or sets the multiplier for the psychovisual difference in the X channel. /// - public float XMultiplier = 1f; + public float XMultiplier { get; set; } = 1f; /// - /// Number of nits that correspond to 1.0f input values. + /// Gets or sets the number of nits that correspond to 1.0f input values. /// - public float IntensityTarget = 80f; + public float IntensityTarget { get; set; } = 80f; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs index 5a1405b35..423095286 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs @@ -4,7 +4,6 @@ using System.Buffers; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; -using SixLabors.ImageSharp.Formats.Jxl.IO; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs index 249d1ec9a..7fdec43b0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs @@ -3,9 +3,11 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; -internal sealed class JxlWeightsSeparable5 +internal struct JxlWeightsSeparable5 { - public InlineArray12 Horizontal { get; set; } + // Don't make these a property so we can ref into them. - public InlineArray12 Vertical { get; set; } + public InlineArray12 Horizontal; + + public InlineArray12 Vertical; }