Skip to content

Commit fd5cbce

Browse files
Marek MaślankaMaslankaMarek
authored andcommitted
Add pixel difference statistics and changed region counts in image diff view
- Calculate changed pixel count and percentage in ImageDifferenceDetector - Display pixel difference statistics in image diff toolbar - Show total number of detected change regions when outline mode is active
1 parent 1019469 commit fd5cbce

3 files changed

Lines changed: 83 additions & 13 deletions

File tree

src/Models/DiffResult.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,17 @@ public class ImageDiff
8282
public string OldImageSize => Old != null ? $"{Old.PixelSize.Width} x {Old.PixelSize.Height}" : "0 x 0";
8383
public string NewImageSize => New != null ? $"{New.PixelSize.Width} x {New.PixelSize.Height}" : "0 x 0";
8484

85-
public IReadOnlyList<Avalonia.Rect> ChangeOutlines => _changeOutlines ??= ImageDifferenceDetector.DetectChangeBoxes(Old, New);
86-
public int ChangeCount => ChangeOutlines.Count;
85+
public ImageDiffDetectionResult DetectionResult => _detectionResult ??= ImageDifferenceDetector.Detect(Old, New);
8786

88-
private IReadOnlyList<Avalonia.Rect> _changeOutlines = null;
87+
public IReadOnlyList<Avalonia.Rect> ChangeOutlines => DetectionResult.ChangeBoxes;
88+
public int ChangeCount => DetectionResult.ChangeBoxes.Count;
89+
public long ChangedPixelCount => DetectionResult.ChangedPixels;
90+
public double ChangedPixelPercentage => DetectionResult.ChangedPercentage;
91+
92+
public string DiffPixelStatsText => $"{ChangedPixelCount:N0} px ({ChangedPixelPercentage:F2}%)";
93+
public string DiffAreaStatsText => ChangeCount == 1 ? "1 area" : $"{ChangeCount} areas";
94+
95+
private ImageDiffDetectionResult _detectionResult = null;
8996
}
9097

9198
public class EmptyFile

src/Models/ImageDifferenceDetector.cs

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,72 @@
77

88
namespace SourceGit.Models
99
{
10+
public class ImageDiffDetectionResult
11+
{
12+
public long ChangedPixels { get; set; } = 0;
13+
public long TotalPixels { get; set; } = 0;
14+
public double ChangedPercentage => TotalPixels > 0 ? (double)ChangedPixels / TotalPixels * 100.0 : 0.0;
15+
public List<Rect> ChangeBoxes { get; set; } = [];
16+
}
17+
1018
public static class ImageDifferenceDetector
1119
{
12-
public static List<Rect> DetectChangeBoxes(Bitmap oldBmp, Bitmap newBmp)
20+
public static ImageDiffDetectionResult Detect(Bitmap oldBmp, Bitmap newBmp)
1321
{
22+
var result = new ImageDiffDetectionResult();
23+
1424
if (oldBmp == null && newBmp == null)
15-
return [];
25+
return result;
1626

1727
if (oldBmp == null)
18-
return [new Rect(0, 0, newBmp.PixelSize.Width, newBmp.PixelSize.Height)];
28+
{
29+
var w = newBmp.PixelSize.Width;
30+
var h = newBmp.PixelSize.Height;
31+
result.TotalPixels = (long)w * h;
32+
result.ChangedPixels = result.TotalPixels;
33+
result.ChangeBoxes = [new Rect(0, 0, w, h)];
34+
return result;
35+
}
1936

2037
if (newBmp == null)
21-
return [new Rect(0, 0, oldBmp.PixelSize.Width, oldBmp.PixelSize.Height)];
38+
{
39+
var w = oldBmp.PixelSize.Width;
40+
var h = oldBmp.PixelSize.Height;
41+
result.TotalPixels = (long)w * h;
42+
result.ChangedPixels = result.TotalPixels;
43+
result.ChangeBoxes = [new Rect(0, 0, w, h)];
44+
return result;
45+
}
2246

2347
var oldBytes = GetRgbaBytes(oldBmp, out var w1, out var h1);
2448
var newBytes = GetRgbaBytes(newBmp, out var w2, out var h2);
2549

2650
if (oldBytes == null && newBytes == null)
27-
return [];
51+
return result;
52+
2853
if (oldBytes == null)
29-
return [new Rect(0, 0, w2, h2)];
54+
{
55+
result.TotalPixels = (long)w2 * h2;
56+
result.ChangedPixels = result.TotalPixels;
57+
result.ChangeBoxes = [new Rect(0, 0, w2, h2)];
58+
return result;
59+
}
60+
3061
if (newBytes == null)
31-
return [new Rect(0, 0, w1, h1)];
62+
{
63+
result.TotalPixels = (long)w1 * h1;
64+
result.ChangedPixels = result.TotalPixels;
65+
result.ChangeBoxes = [new Rect(0, 0, w1, h1)];
66+
return result;
67+
}
3268

3369
int maxW = Math.Max(w1, w2);
3470
int maxH = Math.Max(h1, h2);
3571

3672
if (maxW <= 0 || maxH <= 0)
37-
return [];
73+
return result;
74+
75+
result.TotalPixels = (long)maxW * maxH;
3876

3977
const int cellSize = 16;
4078
int gridW = (maxW + cellSize - 1) / cellSize;
@@ -43,6 +81,7 @@ public static List<Rect> DetectChangeBoxes(Bitmap oldBmp, Bitmap newBmp)
4381
var cellBounds = new Rect[gridW, gridH];
4482
var hasDiff = new bool[gridW, gridH];
4583
bool anyDiff = false;
84+
long diffPixelCount = 0;
4685

4786
for (int cy = 0; cy < gridH; cy++)
4887
{
@@ -107,6 +146,7 @@ public static List<Rect> DetectChangeBoxes(Bitmap oldBmp, Bitmap newBmp)
107146

108147
if (diffPixel)
109148
{
149+
diffPixelCount++;
110150
cellHasDiff = true;
111151
if (x < minX) minX = x;
112152
if (x > maxX) maxX = x;
@@ -125,8 +165,10 @@ public static List<Rect> DetectChangeBoxes(Bitmap oldBmp, Bitmap newBmp)
125165
}
126166
}
127167

168+
result.ChangedPixels = diffPixelCount;
169+
128170
if (!anyDiff)
129-
return [];
171+
return result;
130172

131173
var visited = new bool[gridW, gridH];
132174
var clusters = new List<Rect>();
@@ -193,7 +235,8 @@ public static List<Rect> DetectChangeBoxes(Bitmap oldBmp, Bitmap newBmp)
193235
}
194236
}
195237

196-
return clusters;
238+
result.ChangeBoxes = clusters;
239+
return result;
197240
}
198241

199242
private static byte[] GetRgbaBytes(Bitmap bitmap, out int width, out int height)

src/Views/ImageDiffView.axaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,28 @@
5151
<StackPanel Orientation="Horizontal" Spacing="4" VerticalAlignment="Center">
5252
<Path Width="12" Height="12" Data="{StaticResource Icons.Eye}" Fill="{DynamicResource Brush.FG1}"/>
5353
<TextBlock Text="{DynamicResource Text.Diff.Image.HighlightChanges}" FontSize="10" VerticalAlignment="Center"/>
54+
<Border Background="{DynamicResource Brush.Badge}"
55+
CornerRadius="6"
56+
Height="14"
57+
Padding="4,0"
58+
VerticalAlignment="Center"
59+
IsVisible="{Binding Source={x:Static vm:Preferences.Instance}, Path=HighlightImageDiffChanges}">
60+
<TextBlock Text="{Binding ChangeCount}" FontSize="9" Foreground="{DynamicResource Brush.BadgeFG}" VerticalAlignment="Center"/>
61+
</Border>
5462
</StackPanel>
5563
</ToggleButton>
64+
65+
<Rectangle Width="1" Height="14" Fill="{DynamicResource Brush.Border1}" Margin="4,0"/>
66+
67+
<!-- Difference statistics (pixels and changed areas) -->
68+
<Border Background="{DynamicResource Brush.Badge}" CornerRadius="4" Padding="6,3" VerticalAlignment="Center">
69+
<StackPanel Orientation="Horizontal" Spacing="4" VerticalAlignment="Center">
70+
<Path Width="11" Height="11" Data="{StaticResource Icons.Diff}" Fill="{DynamicResource Brush.FG2}" VerticalAlignment="Center"/>
71+
<TextBlock Text="{Binding DiffPixelStatsText}" FontSize="10" Foreground="{DynamicResource Brush.FG1}" VerticalAlignment="Center"/>
72+
<TextBlock Text="{Binding DiffAreaStatsText}" FontSize="10" Foreground="{DynamicResource Brush.Accent}" FontWeight="Bold" VerticalAlignment="Center"
73+
IsVisible="{Binding Source={x:Static vm:Preferences.Instance}, Path=HighlightImageDiffChanges}"/>
74+
</StackPanel>
75+
</Border>
5676
</StackPanel>
5777
</Border>
5878

0 commit comments

Comments
 (0)