Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix AdaptiveThresholdProcessor throws IndexOutOfRangeException #2221

Merged
merged 7 commits into from
Sep 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,60 @@ public static partial class ProcessingExtensions
/// <returns>The <see cref="Buffer2D{T}"/> containing all the sums.</returns>
public static Buffer2D<ulong> CalculateIntegralImage<TPixel>(this Image<TPixel> source)
where TPixel : unmanaged, IPixel<TPixel>
=> CalculateIntegralImage(source.Frames.RootFrame);

/// <summary>
/// Apply an image integral. <See href="https://en.wikipedia.org/wiki/Summed-area_table"/>
/// </summary>
/// <param name="source">The image on which to apply the integral.</param>
/// <param name="bounds">The bounds within the image frame to calculate.</param>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <returns>The <see cref="Buffer2D{T}"/> containing all the sums.</returns>
public static Buffer2D<ulong> CalculateIntegralImage<TPixel>(this Image<TPixel> source, Rectangle bounds)
where TPixel : unmanaged, IPixel<TPixel>
=> CalculateIntegralImage(source.Frames.RootFrame, bounds);

/// <summary>
/// Apply an image integral. <See href="https://en.wikipedia.org/wiki/Summed-area_table"/>
/// </summary>
/// <param name="source">The image frame on which to apply the integral.</param>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <returns>The <see cref="Buffer2D{T}"/> containing all the sums.</returns>
public static Buffer2D<ulong> CalculateIntegralImage<TPixel>(this ImageFrame<TPixel> source)
where TPixel : unmanaged, IPixel<TPixel>
=> source.CalculateIntegralImage(source.Bounds());

/// <summary>
/// Apply an image integral. <See href="https://en.wikipedia.org/wiki/Summed-area_table"/>
/// </summary>
/// <param name="source">The image frame on which to apply the integral.</param>
/// <param name="bounds">The bounds within the image frame to calculate.</param>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <returns>The <see cref="Buffer2D{T}"/> containing all the sums.</returns>
public static Buffer2D<ulong> CalculateIntegralImage<TPixel>(this ImageFrame<TPixel> source, Rectangle bounds)
where TPixel : unmanaged, IPixel<TPixel>
{
Configuration configuration = source.GetConfiguration();

int endY = source.Height;
int endX = source.Width;
var interest = Rectangle.Intersect(bounds, source.Bounds());
int startY = interest.Y;
int startX = interest.X;
int endY = interest.Height;

Buffer2D<ulong> intImage = configuration.MemoryAllocator.Allocate2D<ulong>(source.Width, source.Height);
Buffer2D<ulong> intImage = configuration.MemoryAllocator.Allocate2D<ulong>(interest.Width, interest.Height);
ulong sumX0 = 0;
Buffer2D<TPixel> sourceBuffer = source.Frames.RootFrame.PixelBuffer;
Buffer2D<TPixel> sourceBuffer = source.PixelBuffer;

using (IMemoryOwner<L8> tempRow = configuration.MemoryAllocator.Allocate<L8>(source.Width))
using (IMemoryOwner<L8> tempRow = configuration.MemoryAllocator.Allocate<L8>(interest.Width))
{
Span<L8> tempSpan = tempRow.GetSpan();
Span<TPixel> sourceRow = sourceBuffer.DangerousGetRowSpan(0);
Span<TPixel> sourceRow = sourceBuffer.DangerousGetRowSpan(startY).Slice(startX, tempSpan.Length);
Span<ulong> destRow = intImage.DangerousGetRowSpan(0);

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

// First row
for (int x = 0; x < endX; x++)
for (int x = 0; x < tempSpan.Length; x++)
{
sumX0 += tempSpan[x].PackedValue;
destRow[x] = sumX0;
Expand All @@ -52,7 +86,7 @@ public static Buffer2D<ulong> CalculateIntegralImage<TPixel>(this Image<TPixel>
// All other rows
for (int y = 1; y < endY; y++)
{
sourceRow = sourceBuffer.DangerousGetRowSpan(y);
sourceRow = sourceBuffer.DangerousGetRowSpan(y + startY).Slice(startX, tempSpan.Length);
destRow = intImage.DangerousGetRowSpan(y);

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

// Process all other colmns
for (int x = 1; x < endX; x++)
for (int x = 1; x < tempSpan.Length; x++)
{
sumX0 += tempSpan[x].PackedValue;
destRow[x] = sumX0 + previousDestRow[x];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
Expand All @@ -27,133 +26,92 @@ internal class AdaptiveThresholdProcessor<TPixel> : ImageProcessor<TPixel>
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
public AdaptiveThresholdProcessor(Configuration configuration, AdaptiveThresholdProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle)
{
this.definition = definition;
}
=> this.definition = definition;

/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
var intersect = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());

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

int startY = intersect.Y;
int endY = intersect.Bottom;
int startX = intersect.X;
int endX = intersect.Right;

int width = intersect.Width;
int height = intersect.Height;

// 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'
byte clusterSize = (byte)Math.Truncate((width / 16f) - 1);
// 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'
byte clusterSize = (byte)Math.Clamp(interest.Width / 16F, 0, 255);

Buffer2D<TPixel> sourceBuffer = source.PixelBuffer;

// Using pooled 2d buffer for integer image table and temp memory to hold Rgb24 converted pixel data.
using (Buffer2D<ulong> intImage = this.Configuration.MemoryAllocator.Allocate2D<ulong>(width, height))
{
Rgba32 rgb = default;
for (int x = startX; x < endX; x++)
{
ulong sum = 0;
for (int y = startY; y < endY; y++)
{
Span<TPixel> row = sourceBuffer.DangerousGetRowSpan(y);
ref TPixel rowRef = ref MemoryMarshal.GetReference(row);
ref TPixel color = ref Unsafe.Add(ref rowRef, x);
color.ToRgba32(ref rgb);

sum += (ulong)(rgb.R + rgb.G + rgb.B);

if (x - startX != 0)
{
intImage[x - startX, y - startY] = intImage[x - startX - 1, y - startY] + sum;
}
else
{
intImage[x - startX, y - startY] = sum;
}
}
}

var operation = new RowOperation(intersect, source.PixelBuffer, intImage, upper, lower, thresholdLimit, clusterSize, startX, endX, startY);
ParallelRowIterator.IterateRows(
configuration,
intersect,
in operation);
}
using Buffer2D<ulong> intImage = source.CalculateIntegralImage(interest);
RowOperation operation = new(configuration, interest, source.PixelBuffer, intImage, upper, lower, thresholdLimit, clusterSize);
ParallelRowIterator.IterateRows<RowOperation, L8>(
configuration,
interest,
in operation);
}

private readonly struct RowOperation : IRowOperation
private readonly struct RowOperation : IRowOperation<L8>
{
private readonly Configuration configuration;
private readonly Rectangle bounds;
private readonly Buffer2D<TPixel> source;
private readonly Buffer2D<ulong> intImage;
private readonly TPixel upper;
private readonly TPixel lower;
private readonly float thresholdLimit;
private readonly int startX;
private readonly int endX;
private readonly int startY;
private readonly byte clusterSize;

[MethodImpl(InliningOptions.ShortMethod)]
public RowOperation(
Configuration configuration,
Rectangle bounds,
Buffer2D<TPixel> source,
Buffer2D<ulong> intImage,
TPixel upper,
TPixel lower,
float thresholdLimit,
byte clusterSize,
int startX,
int endX,
int startY)
byte clusterSize)
{
this.configuration = configuration;
this.bounds = bounds;
this.startX = bounds.X;
this.startY = bounds.Y;
this.source = source;
this.intImage = intImage;
this.upper = upper;
this.lower = lower;
this.thresholdLimit = thresholdLimit;
this.startX = startX;
this.endX = endX;
this.startY = startY;
this.clusterSize = clusterSize;
}

/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y)
public void Invoke(int y, Span<L8> span)
{
Rgba32 rgb = default;
Span<TPixel> pixelRow = this.source.DangerousGetRowSpan(y);
Span<TPixel> rowSpan = this.source.DangerousGetRowSpan(y).Slice(this.startX, span.Length);
PixelOperations<TPixel>.Instance.ToL8(this.configuration, rowSpan, span);

for (int x = this.startX; x < this.endX; x++)
int maxX = this.bounds.Width - 1;
int maxY = this.bounds.Height - 1;
for (int x = 0; x < rowSpan.Length; x++)
{
TPixel pixel = pixelRow[x];
pixel.ToRgba32(ref rgb);

var x1 = Math.Max(x - this.startX - this.clusterSize + 1, 0);
var x2 = Math.Min(x - this.startX + this.clusterSize + 1, this.bounds.Width - 1);
var y1 = Math.Max(y - this.startY - this.clusterSize + 1, 0);
var y2 = Math.Min(y - this.startY + this.clusterSize + 1, this.bounds.Height - 1);
int x1 = Math.Clamp(x - this.clusterSize + 1, 0, maxX);
int x2 = Math.Min(x + this.clusterSize + 1, maxX);
int y1 = Math.Clamp(y - this.startY - this.clusterSize + 1, 0, maxY);
int y2 = Math.Min(y - this.startY + this.clusterSize + 1, maxY);

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

if ((rgb.R + rgb.G + rgb.B) * count <= sum * this.thresholdLimit)
if (span[x].PackedValue * count <= sum * this.thresholdLimit)
{
this.source[x, y] = this.lower;
rowSpan[x] = this.lower;
}
else
{
this.source[x, y] = this.upper;
rowSpan[x] = this.upper;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public void AdaptiveThreshold_SettingUpperLowerWithThresholdLimit_WithRectangle_
[WithFile(TestImages.Png.Bradley01, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Bradley02, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Ducky, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Issue2217, PixelTypes.Rgba32)]
public void AdaptiveThreshold_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
Expand Down
52 changes: 50 additions & 2 deletions tests/ImageSharp.Tests/Processing/IntegralImageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,30 @@ public void CalculateIntegralImage_Rgba32Works(TestImageProvider<Rgba32> provide
});
}

[Theory]
[WithFile(TestImages.Png.Bradley01, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Bradley02, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Ducky, PixelTypes.Rgba32)]
public void CalculateIntegralImage_WithBounds_Rgba32Works(TestImageProvider<Rgba32> provider)
{
using Image<Rgba32> image = provider.GetImage();

Rectangle interest = new(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2);

// Act:
Buffer2D<ulong> integralBuffer = image.CalculateIntegralImage(interest);

// Assert:
VerifySumValues(provider, integralBuffer, interest, (Rgba32 pixel) =>
{
L8 outputPixel = default;

outputPixel.FromRgba32(pixel);

return outputPixel.PackedValue;
});
}

[Theory]
[WithFile(TestImages.Png.Bradley01, PixelTypes.L8)]
[WithFile(TestImages.Png.Bradley02, PixelTypes.L8)]
Expand All @@ -43,16 +67,40 @@ public void CalculateIntegralImage_L8Works(TestImageProvider<L8> provider)
Buffer2D<ulong> integralBuffer = image.CalculateIntegralImage();

// Assert:
VerifySumValues(provider, integralBuffer, (L8 pixel) => { return pixel.PackedValue; });
VerifySumValues(provider, integralBuffer, (L8 pixel) => pixel.PackedValue);
}

[Theory]
[WithFile(TestImages.Png.Bradley01, PixelTypes.L8)]
[WithFile(TestImages.Png.Bradley02, PixelTypes.L8)]
public void CalculateIntegralImage_WithBounds_L8Works(TestImageProvider<L8> provider)
{
using Image<L8> image = provider.GetImage();

Rectangle interest = new(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2);

// Act:
Buffer2D<ulong> integralBuffer = image.CalculateIntegralImage(interest);

// Assert:
VerifySumValues(provider, integralBuffer, interest, (L8 pixel) => pixel.PackedValue);
}

private static void VerifySumValues<TPixel>(
TestImageProvider<TPixel> provider,
Buffer2D<ulong> integralBuffer,
System.Func<TPixel, ulong> getPixel)
where TPixel : unmanaged, IPixel<TPixel>
=> VerifySumValues(provider, integralBuffer, integralBuffer.Bounds(), getPixel);

private static void VerifySumValues<TPixel>(
TestImageProvider<TPixel> provider,
Buffer2D<ulong> integralBuffer,
Rectangle bounds,
System.Func<TPixel, ulong> getPixel)
where TPixel : unmanaged, IPixel<TPixel>
{
Image<TPixel> image = provider.GetImage();
Buffer2DRegion<TPixel> image = provider.GetImage().GetRootFramePixelBuffer().GetRegion(bounds);

// Check top-left corner
Assert.Equal(getPixel(image[0, 0]), integralBuffer[0, 0]);
Expand Down
3 changes: 3 additions & 0 deletions tests/ImageSharp.Tests/TestImages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ public static class Png
// Discussion 1875: https://github.com/SixLabors/ImageSharp/discussions/1875
public const string Issue1875 = "Png/raw-profile-type-exif.png";

// Issue 2217: https://github.com/SixLabors/ImageSharp/issues/2217
public const string Issue2217 = "Png/issues/Issue_2217_AdaptiveThresholdProcessor.png";

// Issue 2209: https://github.com/SixLabors/ImageSharp/issues/2209
public const string Issue2209IndexedWithTransparency = "Png/issues/Issue_2209.png";

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.