Skip to content

Commit ab159a7

Browse files
committed
Merge remote-tracking branch 'origin/main' into copilot/copy-skia-to-maui
2 parents 7223663 + 4f16bb8 commit ab159a7

File tree

11,628 files changed

+225385
-154
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

11,628 files changed

+225385
-154
lines changed

docs/docs/pixel-comparer.md

Lines changed: 124 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Console.WriteLine($"Total pixels: {result.TotalPixels}");
1919
Console.WriteLine($"Error pixels: {result.ErrorPixelCount}");
2020
Console.WriteLine($"Error percentage: {result.ErrorPixelPercentage:P2}");
2121
Console.WriteLine($"Absolute error: {result.AbsoluteError}");
22+
Console.WriteLine($"RMSE: {result.RootMeanSquaredError:F4}");
23+
Console.WriteLine($"PSNR: {result.PeakSignalToNoiseRatio:F2} dB");
2224
```
2325

2426
### Generate a difference mask
@@ -35,11 +37,12 @@ The mask is a black-and-white image where **white pixels** indicate differences
3537

3638
## How It Works
3739

38-
The comparer normalizes both images to BGRA8888 format, then walks through every pixel and sums the per-channel (red, green, blue) absolute differences:
40+
The comparer normalizes both images to BGRA8888 unpremultiplied format, then walks through every pixel and computes per-channel differences:
3941

40-
1. For each pixel, compute `|R₁ − R₂| + |G₁ − G₂| + |B₁ − B₂|`
41-
2. If the sum is greater than zero, that pixel is counted as an error
42-
3. The total per-pixel sums are accumulated into `AbsoluteError`
42+
1. For each pixel, compute per-channel differences: `ΔR = |R₁ − R₂|`, `ΔG = |G₁ − G₂|`, `ΔB = |B₁ − B₂|`
43+
2. If the sum `ΔR + ΔG + ΔB` is greater than zero, that pixel is counted as an error
44+
3. The per-pixel sums are accumulated into `AbsoluteError`
45+
4. The per-channel squared differences (`ΔR² + ΔG² + ΔB²`) are accumulated into `SumSquaredError`, which drives the MSE, RMSE, NRMSE, and PSNR metrics
4346

4447
Both images must have the same dimensions; otherwise an `InvalidOperationException` is thrown.
4548

@@ -53,10 +56,19 @@ The [`SKPixelComparisonResult`](xref:SkiaSharp.Extended.SKPixelComparisonResult)
5356
| `ErrorPixelCount` | `int` | Number of pixels with any difference |
5457
| `ErrorPixelPercentage` | `double` | `ErrorPixelCount / TotalPixels` |
5558
| `AbsoluteError` | `int` | Sum of all per-channel differences |
59+
| `SumSquaredError` | `long` | Sum of all per-channel squared differences |
60+
| `ChannelCount` | `int` | Number of channels compared (3 for RGB, 4 for RGBA) |
61+
| `MeanAbsoluteError` | `double` | Average absolute error per channel (range: 0–255) |
62+
| `MeanSquaredError` | `double` | Average squared error per channel (range: 0–65025) |
63+
| `RootMeanSquaredError` | `double` | Square root of MSE (range: 0–255) |
64+
| `NormalizedRootMeanSquaredError` | `double` | RMSE divided by 255 (range: 0–1) |
65+
| `PeakSignalToNoiseRatio` | `double` | PSNR in dB (∞ for identical images) |
66+
67+
The **RMSE** metric is commonly used in visual testing tools (such as .NET MAUI's `VisualTestUtils.MagickNet`) to quantify image differences as a single number. The **NormalizedRootMeanSquaredError** maps this to a 0–1 range, where 0 means identical and 1 means maximum difference—useful for threshold-based pass/fail testing.
5668

5769
## Mask-Based Comparison
5870

59-
When comparing images that have expected minor differences (e.g., anti-aliasing, compression artifacts), you can supply a tolerance mask. The mask image uses per-channel thresholds—a difference is only counted if it exceeds the corresponding channel value in the mask pixel:
71+
When comparing images that have expected minor differences (e.g., anti-aliasing, compression artifacts), you can supply a tolerance mask. By default, the mask uses per-channel thresholds — each channel's difference is checked independently against the corresponding mask channel value:
6072

6173
```csharp
6274
using var expected = SKImage.FromEncodedData("expected.png");
@@ -66,7 +78,90 @@ using var mask = SKImage.FromEncodedData("tolerance-mask.png");
6678
var result = SKPixelComparer.Compare(expected, actual, mask);
6779
```
6880

69-
For example, if a mask pixel has RGB values of `(10, 10, 10)`, differences of up to 10 per channel at that location are ignored. This lets you define region-specific tolerances.
81+
For example, if a mask pixel has RGB values of `(10, 10, 10)`, each channel is independently checked — a red difference of 12 would be counted but a green difference of 8 would be ignored. The mask must have the same dimensions as the images being compared; otherwise an `InvalidOperationException` is thrown.
82+
83+
## Tolerance-Based Comparison
84+
85+
For a simpler approach than mask-based comparison, you can specify a uniform per-pixel tolerance threshold (similar to ImageMagick's "fuzz" parameter). By default, tolerance is applied **per channel** — each channel (R, G, B) is checked independently, and only channels that exceed the tolerance contribute to error metrics:
86+
87+
```csharp
88+
// Ignore channels where the individual difference is 10 or less
89+
var result = SKPixelComparer.Compare(expected, actual, tolerance: 10);
90+
91+
Console.WriteLine($"Pixels exceeding tolerance: {result.ErrorPixelCount}");
92+
```
93+
94+
When a pixel falls within tolerance, it is completely excluded from **all** metrics — not just `ErrorPixelCount`, but also `AbsoluteError`, `SumSquaredError`, and all derived metrics (MAE, MSE, RMSE, NRMSE, PSNR).
95+
96+
A tolerance of `0` is equivalent to the standard comparison. In per-channel mode (the default), the maximum meaningful tolerance is 255. In summed mode (`TolerancePerChannel = false`), the maximum is 765 (255 × 3 channels), or 1020 (255 × 4) when `CompareAlpha` is enabled. A negative tolerance throws `ArgumentOutOfRangeException`.
97+
98+
### Summed Tolerance Mode
99+
100+
By default, `TolerancePerChannel` is `true` and each channel is checked independently. You can switch to summed mode where the total difference (`|ΔR| + |ΔG| + |ΔB|`) is compared against the tolerance:
101+
102+
```csharp
103+
// Summed: ignore pixels where the total RGB difference is 15 or less
104+
var options = new SKPixelComparerOptions { TolerancePerChannel = false };
105+
var result = SKPixelComparer.Compare(expected, actual, tolerance: 15, options);
106+
```
107+
108+
In summed mode, the entire pixel is either counted as an error (all channels contribute) or excluded (none do).
109+
110+
The same option is available for mask-based comparison. When `TolerancePerChannel` is `false`, the sum of channel differences is checked against the sum of the mask's channel values — the pixel is either fully counted or fully excluded:
111+
112+
```csharp
113+
// Mask with sum-based semantics
114+
var options = new SKPixelComparerOptions { TolerancePerChannel = false };
115+
var result = SKPixelComparer.Compare(expected, actual, mask, options);
116+
```
117+
118+
## Comparison Options
119+
120+
For more control over comparison behavior, use [`SKPixelComparerOptions`](xref:SkiaSharp.Extended.SKPixelComparerOptions):
121+
122+
```csharp
123+
var options = new SKPixelComparerOptions
124+
{
125+
TolerancePerChannel = true, // Check each channel independently (default: true)
126+
CompareAlpha = true // Include alpha channel in comparison (default: false)
127+
};
128+
129+
var result = SKPixelComparer.Compare(expected, actual, options);
130+
```
131+
132+
| Property | Type | Default | Description |
133+
| :------- | :--- | :------ | :---------- |
134+
| `TolerancePerChannel` | `bool` | `true` | When `true`, each channel is checked independently against the tolerance. When `false`, the sum of per-channel differences is used. |
135+
| `CompareAlpha` | `bool` | `false` | When `true`, the alpha channel is included in all error calculations and metrics. |
136+
137+
Options can be used with all comparison modes:
138+
139+
```csharp
140+
// Base comparison with alpha
141+
var result = SKPixelComparer.Compare(expected, actual, options);
142+
143+
// Tolerance-based with options
144+
var result = SKPixelComparer.Compare(expected, actual, tolerance: 10, options);
145+
146+
// Mask-based with options
147+
var result = SKPixelComparer.Compare(expected, actual, mask, options);
148+
```
149+
150+
### Alpha Channel Comparison
151+
152+
By default, only RGB channels are compared. Set `CompareAlpha = true` to include the alpha channel in difference detection and error metrics. When enabled:
153+
154+
- Alpha differences contribute to `AbsoluteError`, `SumSquaredError`, and all derived metrics
155+
- The `ChannelCount` in the result is set to 4 (instead of 3), so MAE and MSE are normalized per 4 channels
156+
- Tolerance and mask thresholds apply to the alpha channel as well
157+
158+
```csharp
159+
var options = new SKPixelComparerOptions { CompareAlpha = true };
160+
161+
var result = SKPixelComparer.Compare(expected, actual, options);
162+
Console.WriteLine($"Channels compared: {result.ChannelCount}"); // 4
163+
Console.WriteLine($"MAE: {result.MeanAbsoluteError}"); // Normalized over RGBA
164+
```
70165

71166
## Input Overloads
72167

@@ -79,7 +174,21 @@ All comparison methods accept multiple input types for convenience:
79174
| `SKBitmap` | `Compare(bitmapA, bitmapB)` |
80175
| `SKPixmap` | `Compare(pixmapA, pixmapB)` |
81176

82-
The same overloads are available for `GenerateDifferenceMask` (without mask support) and for the three-argument masked `Compare`.
177+
The same input type overloads are available for `GenerateDifferenceMask`, `GenerateDifferenceImage`, mask-based `Compare`, and tolerance-based `Compare`.
178+
179+
### Generate a difference image
180+
181+
Unlike the binary black-and-white mask, `GenerateDifferenceImage` produces a full-color visualization of per-channel differences:
182+
183+
```csharp
184+
using var diff = SKPixelComparer.GenerateDifferenceImage("expected.png", "actual.png");
185+
186+
// Each pixel's R, G, B values represent the absolute channel differences
187+
using var data = diff.Encode(SKEncodedImageFormat.Png, 100);
188+
File.WriteAllBytes("diff-image.png", data.ToArray());
189+
```
190+
191+
This is similar to the difference visualization produced by ImageMagick's `compare` command and is useful for understanding the magnitude and distribution of differences across channels.
83192

84193
## Usage Patterns
85194

@@ -111,11 +220,14 @@ public void UI_MatchesBaseline_WithTolerance()
111220
using var actual = CaptureScreenshot();
112221
using var expected = SKImage.FromEncodedData("baselines/screen.png");
113222

114-
var result = SKPixelComparer.Compare(expected, actual);
223+
// Option 1: Use per-pixel tolerance to ignore minor differences
224+
var result = SKPixelComparer.Compare(expected, actual, tolerance: 10);
225+
Assert.Equal(0, result.ErrorPixelCount);
115226

116-
// Allow up to 0.5% pixel difference
117-
Assert.True(result.ErrorPixelPercentage < 0.005,
118-
$"Too many differing pixels: {result.ErrorPixelPercentage:P2}");
227+
// Option 2: Use RMSE threshold (like .NET MAUI VisualTestUtils)
228+
var result2 = SKPixelComparer.Compare(expected, actual);
229+
Assert.True(result2.NormalizedRootMeanSquaredError < 0.005,
230+
$"NRMSE too high: {result2.NormalizedRootMeanSquaredError:F4}");
119231
}
120232
```
121233

@@ -136,3 +248,4 @@ if (result.ErrorPixelCount > 0)
136248

137249
- [API Reference — SKPixelComparer](xref:SkiaSharp.Extended.SKPixelComparer) — Full method documentation
138250
- [API Reference — SKPixelComparisonResult](xref:SkiaSharp.Extended.SKPixelComparisonResult) — Result class documentation
251+
- [API Reference — SKPixelComparerOptions](xref:SkiaSharp.Extended.SKPixelComparerOptions) — Options class documentation

0 commit comments

Comments
 (0)