From 0520ab33b99bc77bdbd115dc085dc48f50edc5ca Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 8 Sep 2026 23:59:20 +0400 Subject: [PATCH] Add encoder image comparators --- .../Comparator/ButteraugliJxlComparator.cs | 83 +++++++++ .../Encoder/Comparator/JxlComparator.cs | 27 +++ .../JxlComparatorScoreComputation.cs | 175 ++++++++++++++++++ .../Encoder/Comparator/NullComparator.cs | 23 +++ 4 files changed, 308 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/ButteraugliJxlComparator.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/NullComparator.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/ButteraugliJxlComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/ButteraugliJxlComparator.cs new file mode 100644 index 0000000000..919446bfca --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/ButteraugliJxlComparator.cs @@ -0,0 +1,83 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics.Tensors; +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Comparator; + +/// +/// Compares images using Google Butteraugli. +/// +internal sealed class ButteraugliJxlComparator(ButteraugliParameters parameters, JxlCmsInterface cms) + : JxlComparator +{ + private int width; + private int height; + private float intensityTarget; + private ButteraugliComparator? comparator; + + public override float GoodQualityScore => Butteraugli.Butteraugli.ButteraugliFuzzyInverse(1.5f); + + public override float BadQualityScore => Butteraugli.Butteraugli.ButteraugliFuzzyInverse(0.5f); + + public override void CompareWith(Configuration configuration, JxlImageBundle actual, JxlImageF? diffMap, out float score) + { + DebugGuard.NotNull(this.comparator, nameof(this.comparator)); + + if (this.width != actual.XSize || this.height != actual.YSize) + { + throw new InvalidOperationException("Image must have same size"); + } + + JxlImageMetadata metadata = actual.Metadata!; + JxlImageBundle store = new(metadata); + + JxlTransformsEncoder.TransformIfNeeded(configuration, actual, JxlColorEncoding.LinearSRgb(actual.IsGray), cms, store, out JxlImageBundle actualLinearSRgb); + + JxlImageF tempDiffmap = new(configuration, this.width, this.height); + JxlImage3F scaledActualLinearSRgb = actualLinearSRgb.Color!; + + if (this.intensityTarget != 0 && actual.Metadata!.IntensityTarget != this.intensityTarget) + { + JxlImage3F scaledActualLinearSRgbStore = new(configuration, this.width, this.height); + scaledActualLinearSRgb = scaledActualLinearSRgbStore; + + float scale = actual.Metadata!.IntensityTarget / this.intensityTarget; + for (int c = 0; c < 3; ++c) + { + for (int y = 0; y < this.height; ++y) + { + Span souceRow = actualLinearSRgb.Color!.PlaneRow(c, y); + Span scaledRow = scaledActualLinearSRgb.PlaneRow(c, y); + TensorPrimitives.Multiply(souceRow, scale, scaledRow); + } + } + } + + if (!this.comparator.Diffmap(configuration, scaledActualLinearSRgb, tempDiffmap)) + { + throw new InvalidOperationException("Diffmap failed"); + } + + score = Butteraugli.Butteraugli.ButteraugliScoreFromDiffmap(tempDiffmap, parameters); + diffMap?.Swap(tempDiffmap); + } + + public override void SetReferenceImage(Configuration configuration, JxlImageBundle reference) + { + JxlImageMetadata metadata = reference.Metadata!; + JxlImageBundle store = new(metadata); + + JxlTransformsEncoder.TransformIfNeeded(configuration, reference, JxlColorEncoding.LinearSRgb(reference.IsGray), cms, store, out JxlImageBundle refLinearSRgb); + this.comparator = ButteraugliComparator.Make(configuration, refLinearSRgb.Color!, parameters); + + this.width = reference.XSize; + this.height = reference.YSize; + this.intensityTarget = reference.Metadata!.IntensityTarget; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs new file mode 100644 index 0000000000..920e929099 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparator.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Comparator; + +/// +/// Compares images. +/// +internal abstract class JxlComparator +{ + public abstract float GoodQualityScore { get; } + + public abstract float BadQualityScore { get; } + + // Sets the reference image, the first to compare. + public abstract void SetReferenceImage(Configuration configuration, JxlImageBundle reference); + + // Sets the actual image, the second to compare. + public abstract void CompareWith( + Configuration configuration, + JxlImageBundle actual, + JxlImageF? diffMap, + out float score); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs new file mode 100644 index 0000000000..8dec22e152 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/JxlComparatorScoreComputation.cs @@ -0,0 +1,175 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics.Tensors; +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Comparator; + +internal static class JxlComparatorScoreComputation +{ + private static void AlphaBlend(JxlImage3F input, int c, float backgroundLinear, JxlImageF alpha, JxlImage3F output) + { + float background = (float)JxlGammaCorrect.LinearToSRgb8Direct(backgroundLinear); + + for (int y = 0; y < output.YSize; y++) + { + ReadOnlySpan rowA = alpha.GetRow(y); + ReadOnlySpan rowI = input.PlaneRow(c, y); + Span rowO = output.PlaneRow(c, y); + + for (int x = 0; x < output.XSize; x++) + { + float a = rowA[x]; + + if (a <= 0.0f) + { + rowO[x] = backgroundLinear; + } + else if (a >= 1.0f) + { + rowO[x] = rowI[x]; + } + else + { + float wFg = a; + float wBg = 1.0f - wFg; + float fg = wFg * (float)JxlGammaCorrect.LinearToSRgb8Direct(rowI[x]); + float bg = wBg * background; + + rowO[x] = (float)JxlGammaCorrect.SRgb8ToLinearDirect(fg + bg); + } + } + } + } + + private static void AlphaBlend(float backgroundLinear, JxlImageBundle image) + { + if (!image.ContainsAlpha) + { + return; + } + + for (int c = 0; c < 3; c++) + { + AlphaBlend(image.Color!, c, backgroundLinear, image.Alpha!, image.Color!); + } + } + + private static void ComputeScoreImpl(Configuration configuration, JxlImageBundle rgb0, JxlImageBundle rgb1, JxlComparator comparator, JxlImageF? diffMap, out float score) + { + comparator.SetReferenceImage(rgb0); + comparator.CompareWith(configuration, rgb1, diffMap, out score); + } + + public static void ComputeScore( + Configuration configuration, + JxlImageBundle rgb0, + JxlImageBundle rgb1, + JxlComparator comparator, + JxlCmsInterface cms, + out float score, + JxlImageF? diffMap = null, + bool ignoreAlpha = false) + { + JxlImageMetadata metadata0 = rgb0.Metadata.Copy(); + + JxlImageBundle store0 = new(metadata0); + + JxlTransformsEncoder.TransformIfNeeded( + rgb0, + JxlColorEncoding.LinearSRgb(rgb0.IsGray), + cms, + pool, + store0, + out JxlImageBundle linearSrgb0); + + JxlImageMetadata metadata1 = rgb1.Metadata.Copy(); + + JxlImageBundle store1 = new(metadata1); + + JxlTransformsEncoder.TransformIfNeeded( + rgb1, + JxlColorEncoding.LinearSRgb(rgb1.IsGray), + cms, + pool, + store1, + out JxlImageBundle linearSrgb1); + + if (ignoreAlpha || (!rgb0.ContainsAlpha && !rgb1.ContainsAlpha)) + { + ComputeScoreImpl( + configuration, + linearSrgb0, + linearSrgb1, + comparator, + diffMap, + out score); + } + + JxlImageF diffMapBlack = new(); + float distBlack; + { + const float black = 0.0f; + + JxlImageBundle blendedBlack0 = linearSrgb0.Copy(configuration); + JxlImageBundle blendedBlack1 = linearSrgb1.Copy(configuration); + + AlphaBlend(black, blendedBlack0); + AlphaBlend(black, blendedBlack1); + + ComputeScoreImpl( + configuration, + blendedBlack0, + blendedBlack1, + comparator, + diffMapBlack, + out distBlack); + } + + JxlImageF diffMapWhite = new(); + float distWhite; + { + const float white = 1.0f; + + JxlImageBundle blendedWhite0 = linearSrgb0.Copy(configuration); + JxlImageBundle blendedWhite1 = linearSrgb1.Copy(configuration); + + AlphaBlend(white, blendedWhite0); + AlphaBlend(white, blendedWhite1); + + ComputeScoreImpl( + configuration, + blendedWhite0, + blendedWhite1, + comparator, + diffMapWhite, + out distWhite); + } + + if (diffMap is not null) + { + int xSize = rgb0.XSize; + int ySize = rgb0.YSize; + + JxlImageF outputDiffMap = new( + configuration, + xSize, + ySize); + + for (int y = 0; y < ySize; y++) + { + ReadOnlySpan rowBlack = diffMapBlack.GetRow(y); + ReadOnlySpan rowWhite = diffMapWhite.GetRow(y); + Span rowOut = diffMap.GetRow(y); + + TensorPrimitives.Max(rowBlack, rowWhite, rowOut); + } + } + + score = MathF.Max(distBlack, distWhite); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/NullComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/NullComparator.cs new file mode 100644 index 0000000000..810c4c59a0 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Comparator/NullComparator.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Comparator; + +/// +/// Performs no image comparison at all. +/// +internal sealed class NullComparator : JxlComparator +{ + public override float GoodQualityScore => 0; + + public override float BadQualityScore => 0; + + public override void CompareWith(Configuration configuration, JxlImageBundle actual, JxlImageF? diffMap, out float score) => score = 0; + + public override void SetReferenceImage(Configuration configuration, JxlImageBundle reference) + { + } +}