Skip to content

Commit 6e065fa

Browse files
Merge pull request #2221 from SixLabors/bp/Issue2217
Fix AdaptiveThresholdProcessor throws IndexOutOfRangeException
2 parents af9bfe5 + 098a4ae commit 6e065fa

11 files changed

+144
-94
lines changed

src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,26 +22,60 @@ public static partial class ProcessingExtensions
2222
/// <returns>The <see cref="Buffer2D{T}"/> containing all the sums.</returns>
2323
public static Buffer2D<ulong> CalculateIntegralImage<TPixel>(this Image<TPixel> source)
2424
where TPixel : unmanaged, IPixel<TPixel>
25+
=> CalculateIntegralImage(source.Frames.RootFrame);
26+
27+
/// <summary>
28+
/// Apply an image integral. <See href="https://en.wikipedia.org/wiki/Summed-area_table"/>
29+
/// </summary>
30+
/// <param name="source">The image on which to apply the integral.</param>
31+
/// <param name="bounds">The bounds within the image frame to calculate.</param>
32+
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
33+
/// <returns>The <see cref="Buffer2D{T}"/> containing all the sums.</returns>
34+
public static Buffer2D<ulong> CalculateIntegralImage<TPixel>(this Image<TPixel> source, Rectangle bounds)
35+
where TPixel : unmanaged, IPixel<TPixel>
36+
=> CalculateIntegralImage(source.Frames.RootFrame, bounds);
37+
38+
/// <summary>
39+
/// Apply an image integral. <See href="https://en.wikipedia.org/wiki/Summed-area_table"/>
40+
/// </summary>
41+
/// <param name="source">The image frame on which to apply the integral.</param>
42+
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
43+
/// <returns>The <see cref="Buffer2D{T}"/> containing all the sums.</returns>
44+
public static Buffer2D<ulong> CalculateIntegralImage<TPixel>(this ImageFrame<TPixel> source)
45+
where TPixel : unmanaged, IPixel<TPixel>
46+
=> source.CalculateIntegralImage(source.Bounds());
47+
48+
/// <summary>
49+
/// Apply an image integral. <See href="https://en.wikipedia.org/wiki/Summed-area_table"/>
50+
/// </summary>
51+
/// <param name="source">The image frame on which to apply the integral.</param>
52+
/// <param name="bounds">The bounds within the image frame to calculate.</param>
53+
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
54+
/// <returns>The <see cref="Buffer2D{T}"/> containing all the sums.</returns>
55+
public static Buffer2D<ulong> CalculateIntegralImage<TPixel>(this ImageFrame<TPixel> source, Rectangle bounds)
56+
where TPixel : unmanaged, IPixel<TPixel>
2557
{
2658
Configuration configuration = source.GetConfiguration();
2759

28-
int endY = source.Height;
29-
int endX = source.Width;
60+
var interest = Rectangle.Intersect(bounds, source.Bounds());
61+
int startY = interest.Y;
62+
int startX = interest.X;
63+
int endY = interest.Height;
3064

31-
Buffer2D<ulong> intImage = configuration.MemoryAllocator.Allocate2D<ulong>(source.Width, source.Height);
65+
Buffer2D<ulong> intImage = configuration.MemoryAllocator.Allocate2D<ulong>(interest.Width, interest.Height);
3266
ulong sumX0 = 0;
33-
Buffer2D<TPixel> sourceBuffer = source.Frames.RootFrame.PixelBuffer;
67+
Buffer2D<TPixel> sourceBuffer = source.PixelBuffer;
3468

35-
using (IMemoryOwner<L8> tempRow = configuration.MemoryAllocator.Allocate<L8>(source.Width))
69+
using (IMemoryOwner<L8> tempRow = configuration.MemoryAllocator.Allocate<L8>(interest.Width))
3670
{
3771
Span<L8> tempSpan = tempRow.GetSpan();
38-
Span<TPixel> sourceRow = sourceBuffer.DangerousGetRowSpan(0);
72+
Span<TPixel> sourceRow = sourceBuffer.DangerousGetRowSpan(startY).Slice(startX, tempSpan.Length);
3973
Span<ulong> destRow = intImage.DangerousGetRowSpan(0);
4074

4175
PixelOperations<TPixel>.Instance.ToL8(configuration, sourceRow, tempSpan);
4276

4377
// First row
44-
for (int x = 0; x < endX; x++)
78+
for (int x = 0; x < tempSpan.Length; x++)
4579
{
4680
sumX0 += tempSpan[x].PackedValue;
4781
destRow[x] = sumX0;
@@ -52,7 +86,7 @@ public static Buffer2D<ulong> CalculateIntegralImage<TPixel>(this Image<TPixel>
5286
// All other rows
5387
for (int y = 1; y < endY; y++)
5488
{
55-
sourceRow = sourceBuffer.DangerousGetRowSpan(y);
89+
sourceRow = sourceBuffer.DangerousGetRowSpan(y + startY).Slice(startX, tempSpan.Length);
5690
destRow = intImage.DangerousGetRowSpan(y);
5791

5892
PixelOperations<TPixel>.Instance.ToL8(configuration, sourceRow, tempSpan);
@@ -62,7 +96,7 @@ public static Buffer2D<ulong> CalculateIntegralImage<TPixel>(this Image<TPixel>
6296
destRow[0] = sumX0 + previousDestRow[0];
6397

6498
// Process all other colmns
65-
for (int x = 1; x < endX; x++)
99+
for (int x = 1; x < tempSpan.Length; x++)
66100
{
67101
sumX0 += tempSpan[x].PackedValue;
68102
destRow[x] = sumX0 + previousDestRow[x];

src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs

Lines changed: 33 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Runtime.CompilerServices;
6-
using System.Runtime.InteropServices;
76
using SixLabors.ImageSharp.Advanced;
87
using SixLabors.ImageSharp.Memory;
98
using SixLabors.ImageSharp.PixelFormats;
@@ -27,133 +26,92 @@ internal class AdaptiveThresholdProcessor<TPixel> : ImageProcessor<TPixel>
2726
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
2827
public AdaptiveThresholdProcessor(Configuration configuration, AdaptiveThresholdProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
2928
: base(configuration, source, sourceRectangle)
30-
{
31-
this.definition = definition;
32-
}
29+
=> this.definition = definition;
3330

3431
/// <inheritdoc/>
3532
protected override void OnFrameApply(ImageFrame<TPixel> source)
3633
{
37-
var intersect = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
34+
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
3835

3936
Configuration configuration = this.Configuration;
4037
TPixel upper = this.definition.Upper.ToPixel<TPixel>();
4138
TPixel lower = this.definition.Lower.ToPixel<TPixel>();
4239
float thresholdLimit = this.definition.ThresholdLimit;
4340

44-
int startY = intersect.Y;
45-
int endY = intersect.Bottom;
46-
int startX = intersect.X;
47-
int endX = intersect.Right;
48-
49-
int width = intersect.Width;
50-
int height = intersect.Height;
51-
52-
// ClusterSize defines the size of cluster to used to check for average. Tweaked to support up to 4k wide pixels and not more. 4096 / 16 is 256 thus the '-1'
53-
byte clusterSize = (byte)Math.Truncate((width / 16f) - 1);
41+
// ClusterSize defines the size of cluster to used to check for average.
42+
// Tweaked to support up to 4k wide pixels and not more. 4096 / 16 is 256 thus the '-1'
43+
byte clusterSize = (byte)Math.Clamp(interest.Width / 16F, 0, 255);
5444

55-
Buffer2D<TPixel> sourceBuffer = source.PixelBuffer;
56-
57-
// Using pooled 2d buffer for integer image table and temp memory to hold Rgb24 converted pixel data.
58-
using (Buffer2D<ulong> intImage = this.Configuration.MemoryAllocator.Allocate2D<ulong>(width, height))
59-
{
60-
Rgba32 rgb = default;
61-
for (int x = startX; x < endX; x++)
62-
{
63-
ulong sum = 0;
64-
for (int y = startY; y < endY; y++)
65-
{
66-
Span<TPixel> row = sourceBuffer.DangerousGetRowSpan(y);
67-
ref TPixel rowRef = ref MemoryMarshal.GetReference(row);
68-
ref TPixel color = ref Unsafe.Add(ref rowRef, x);
69-
color.ToRgba32(ref rgb);
70-
71-
sum += (ulong)(rgb.R + rgb.G + rgb.B);
72-
73-
if (x - startX != 0)
74-
{
75-
intImage[x - startX, y - startY] = intImage[x - startX - 1, y - startY] + sum;
76-
}
77-
else
78-
{
79-
intImage[x - startX, y - startY] = sum;
80-
}
81-
}
82-
}
83-
84-
var operation = new RowOperation(intersect, source.PixelBuffer, intImage, upper, lower, thresholdLimit, clusterSize, startX, endX, startY);
85-
ParallelRowIterator.IterateRows(
86-
configuration,
87-
intersect,
88-
in operation);
89-
}
45+
using Buffer2D<ulong> intImage = source.CalculateIntegralImage(interest);
46+
RowOperation operation = new(configuration, interest, source.PixelBuffer, intImage, upper, lower, thresholdLimit, clusterSize);
47+
ParallelRowIterator.IterateRows<RowOperation, L8>(
48+
configuration,
49+
interest,
50+
in operation);
9051
}
9152

92-
private readonly struct RowOperation : IRowOperation
53+
private readonly struct RowOperation : IRowOperation<L8>
9354
{
55+
private readonly Configuration configuration;
9456
private readonly Rectangle bounds;
9557
private readonly Buffer2D<TPixel> source;
9658
private readonly Buffer2D<ulong> intImage;
9759
private readonly TPixel upper;
9860
private readonly TPixel lower;
9961
private readonly float thresholdLimit;
10062
private readonly int startX;
101-
private readonly int endX;
10263
private readonly int startY;
10364
private readonly byte clusterSize;
10465

10566
[MethodImpl(InliningOptions.ShortMethod)]
10667
public RowOperation(
68+
Configuration configuration,
10769
Rectangle bounds,
10870
Buffer2D<TPixel> source,
10971
Buffer2D<ulong> intImage,
11072
TPixel upper,
11173
TPixel lower,
11274
float thresholdLimit,
113-
byte clusterSize,
114-
int startX,
115-
int endX,
116-
int startY)
75+
byte clusterSize)
11776
{
77+
this.configuration = configuration;
11878
this.bounds = bounds;
79+
this.startX = bounds.X;
80+
this.startY = bounds.Y;
11981
this.source = source;
12082
this.intImage = intImage;
12183
this.upper = upper;
12284
this.lower = lower;
12385
this.thresholdLimit = thresholdLimit;
124-
this.startX = startX;
125-
this.endX = endX;
126-
this.startY = startY;
12786
this.clusterSize = clusterSize;
12887
}
12988

13089
/// <inheritdoc/>
13190
[MethodImpl(InliningOptions.ShortMethod)]
132-
public void Invoke(int y)
91+
public void Invoke(int y, Span<L8> span)
13392
{
134-
Rgba32 rgb = default;
135-
Span<TPixel> pixelRow = this.source.DangerousGetRowSpan(y);
93+
Span<TPixel> rowSpan = this.source.DangerousGetRowSpan(y).Slice(this.startX, span.Length);
94+
PixelOperations<TPixel>.Instance.ToL8(this.configuration, rowSpan, span);
13695

137-
for (int x = this.startX; x < this.endX; x++)
96+
int maxX = this.bounds.Width - 1;
97+
int maxY = this.bounds.Height - 1;
98+
for (int x = 0; x < rowSpan.Length; x++)
13899
{
139-
TPixel pixel = pixelRow[x];
140-
pixel.ToRgba32(ref rgb);
141-
142-
var x1 = Math.Max(x - this.startX - this.clusterSize + 1, 0);
143-
var x2 = Math.Min(x - this.startX + this.clusterSize + 1, this.bounds.Width - 1);
144-
var y1 = Math.Max(y - this.startY - this.clusterSize + 1, 0);
145-
var y2 = Math.Min(y - this.startY + this.clusterSize + 1, this.bounds.Height - 1);
100+
int x1 = Math.Clamp(x - this.clusterSize + 1, 0, maxX);
101+
int x2 = Math.Min(x + this.clusterSize + 1, maxX);
102+
int y1 = Math.Clamp(y - this.startY - this.clusterSize + 1, 0, maxY);
103+
int y2 = Math.Min(y - this.startY + this.clusterSize + 1, maxY);
146104

147-
var count = (uint)((x2 - x1) * (y2 - y1));
148-
var sum = (long)Math.Min(this.intImage[x2, y2] - this.intImage[x1, y2] - this.intImage[x2, y1] + this.intImage[x1, y1], long.MaxValue);
105+
uint count = (uint)((x2 - x1) * (y2 - y1));
106+
ulong sum = Math.Min(this.intImage[x2, y2] - this.intImage[x1, y2] - this.intImage[x2, y1] + this.intImage[x1, y1], ulong.MaxValue);
149107

150-
if ((rgb.R + rgb.G + rgb.B) * count <= sum * this.thresholdLimit)
108+
if (span[x].PackedValue * count <= sum * this.thresholdLimit)
151109
{
152-
this.source[x, y] = this.lower;
110+
rowSpan[x] = this.lower;
153111
}
154112
else
155113
{
156-
this.source[x, y] = this.upper;
114+
rowSpan[x] = this.upper;
157115
}
158116
}
159117
}

tests/ImageSharp.Tests/Processing/Binarization/AdaptiveThresholdTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ public void AdaptiveThreshold_SettingUpperLowerWithThresholdLimit_WithRectangle_
102102
[WithFile(TestImages.Png.Bradley01, PixelTypes.Rgba32)]
103103
[WithFile(TestImages.Png.Bradley02, PixelTypes.Rgba32)]
104104
[WithFile(TestImages.Png.Ducky, PixelTypes.Rgba32)]
105+
[WithFile(TestImages.Png.Issue2217, PixelTypes.Rgba32)]
105106
public void AdaptiveThreshold_Works<TPixel>(TestImageProvider<TPixel> provider)
106107
where TPixel : unmanaged, IPixel<TPixel>
107108
{

tests/ImageSharp.Tests/Processing/IntegralImageTests.cs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,30 @@ public void CalculateIntegralImage_Rgba32Works(TestImageProvider<Rgba32> provide
3232
});
3333
}
3434

35+
[Theory]
36+
[WithFile(TestImages.Png.Bradley01, PixelTypes.Rgba32)]
37+
[WithFile(TestImages.Png.Bradley02, PixelTypes.Rgba32)]
38+
[WithFile(TestImages.Png.Ducky, PixelTypes.Rgba32)]
39+
public void CalculateIntegralImage_WithBounds_Rgba32Works(TestImageProvider<Rgba32> provider)
40+
{
41+
using Image<Rgba32> image = provider.GetImage();
42+
43+
Rectangle interest = new(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2);
44+
45+
// Act:
46+
Buffer2D<ulong> integralBuffer = image.CalculateIntegralImage(interest);
47+
48+
// Assert:
49+
VerifySumValues(provider, integralBuffer, interest, (Rgba32 pixel) =>
50+
{
51+
L8 outputPixel = default;
52+
53+
outputPixel.FromRgba32(pixel);
54+
55+
return outputPixel.PackedValue;
56+
});
57+
}
58+
3559
[Theory]
3660
[WithFile(TestImages.Png.Bradley01, PixelTypes.L8)]
3761
[WithFile(TestImages.Png.Bradley02, PixelTypes.L8)]
@@ -43,16 +67,40 @@ public void CalculateIntegralImage_L8Works(TestImageProvider<L8> provider)
4367
Buffer2D<ulong> integralBuffer = image.CalculateIntegralImage();
4468

4569
// Assert:
46-
VerifySumValues(provider, integralBuffer, (L8 pixel) => { return pixel.PackedValue; });
70+
VerifySumValues(provider, integralBuffer, (L8 pixel) => pixel.PackedValue);
4771
}
4872

73+
[Theory]
74+
[WithFile(TestImages.Png.Bradley01, PixelTypes.L8)]
75+
[WithFile(TestImages.Png.Bradley02, PixelTypes.L8)]
76+
public void CalculateIntegralImage_WithBounds_L8Works(TestImageProvider<L8> provider)
77+
{
78+
using Image<L8> image = provider.GetImage();
79+
80+
Rectangle interest = new(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2);
81+
82+
// Act:
83+
Buffer2D<ulong> integralBuffer = image.CalculateIntegralImage(interest);
84+
85+
// Assert:
86+
VerifySumValues(provider, integralBuffer, interest, (L8 pixel) => pixel.PackedValue);
87+
}
88+
89+
private static void VerifySumValues<TPixel>(
90+
TestImageProvider<TPixel> provider,
91+
Buffer2D<ulong> integralBuffer,
92+
System.Func<TPixel, ulong> getPixel)
93+
where TPixel : unmanaged, IPixel<TPixel>
94+
=> VerifySumValues(provider, integralBuffer, integralBuffer.Bounds(), getPixel);
95+
4996
private static void VerifySumValues<TPixel>(
5097
TestImageProvider<TPixel> provider,
5198
Buffer2D<ulong> integralBuffer,
99+
Rectangle bounds,
52100
System.Func<TPixel, ulong> getPixel)
53101
where TPixel : unmanaged, IPixel<TPixel>
54102
{
55-
Image<TPixel> image = provider.GetImage();
103+
Buffer2DRegion<TPixel> image = provider.GetImage().GetRootFramePixelBuffer().GetRegion(bounds);
56104

57105
// Check top-left corner
58106
Assert.Equal(getPixel(image[0, 0]), integralBuffer[0, 0]);

tests/ImageSharp.Tests/TestImages.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ public static class Png
125125
// Discussion 1875: https://github.com/SixLabors/ImageSharp/discussions/1875
126126
public const string Issue1875 = "Png/raw-profile-type-exif.png";
127127

128+
// Issue 2217: https://github.com/SixLabors/ImageSharp/issues/2217
129+
public const string Issue2217 = "Png/issues/Issue_2217_AdaptiveThresholdProcessor.png";
130+
128131
// Issue 2209: https://github.com/SixLabors/ImageSharp/issues/2209
129132
public const string Issue2209IndexedWithTransparency = "Png/issues/Issue_2209.png";
130133

Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 2 additions & 2 deletions
Loading

0 commit comments

Comments
 (0)