Browse Source

Fix sampling and optimize.

pull/2221/head
James Jackson-South 3 years ago
parent
commit
dac8f712f0
  1. 52
      src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs
  2. 104
      src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs
  3. 2
      tests/ImageSharp.Tests/Processing/Binarization/AdaptiveThresholdTests.cs
  4. 53
      tests/ImageSharp.Tests/Processing/IntegralImageTests.cs
  5. 4
      tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_WithRectangle_Works_Rgba32_Bradley02.png
  6. 4
      tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_Works_Rgba32_Bradley01.png
  7. 4
      tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_Works_Rgba32_Bradley02.png
  8. 4
      tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_Works_Rgba32_ducky.png

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

@ -22,26 +22,60 @@ namespace SixLabors.ImageSharp.Processing
/// <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;
@ -52,7 +86,7 @@ namespace SixLabors.ImageSharp.Processing
// 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);
@ -62,7 +96,7 @@ namespace SixLabors.ImageSharp.Processing
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];

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

@ -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;
@ -27,70 +26,33 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization
/// <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);
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);
// 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);
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;
@ -98,64 +60,58 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization
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);
int maxX = this.bounds.Width - 1;
int maxY = this.bounds.Height - 1;
for (int x = this.startX; x < this.endX; x++)
for (int x = 0; x < rowSpan.Length; x++)
{
TPixel pixel = pixelRow[x];
pixel.ToRgba32(ref rgb);
int x1 = Math.Min(Math.Max(x - this.startX - this.clusterSize + 1, 0), maxX);
int x2 = Math.Min(x - this.startX + this.clusterSize + 1, maxX);
int y1 = Math.Min(Math.Max(y - this.startY - this.clusterSize + 1, 0), maxY);
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);
uint count = (uint)((x2 - x1) * (y2 - y1));
long sum = (long)Math.Min(this.intImage[x2, y2] - this.intImage[x1, y2] - this.intImage[x2, y1] + this.intImage[x1, y1], long.MaxValue);
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;
}
}
}

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

@ -137,6 +137,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization
Exception exception = Record.Exception(() =>
{
using Image<TPixel> image = provider.GetImage();
image.Mutate(img => img.AdaptiveThreshold(.5F));
image.DebugSave(provider);
});
Assert.Null(exception);

53
tests/ImageSharp.Tests/Processing/IntegralImageTests.cs

@ -32,6 +32,30 @@ namespace SixLabors.ImageSharp.Tests.Processing
});
}
[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)]
@ -43,16 +67,41 @@ namespace SixLabors.ImageSharp.Tests.Processing
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();
// 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]);

4
tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_WithRectangle_Works_Rgba32_Bradley02.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:36f60abb0ade0320779e242716c61b6dbabc8243a125f0a3145be35e233e117c
size 24542
oid sha256:5745f61e9b8cd49066b347605deee6dcde17690b9dc0f675466df6b2db706bd6
size 22348

4
tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_Works_Rgba32_Bradley01.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4c4a92f0ecd0f2ec06b12091b14f2d421605ef178092bf4f7f7cb4e661270945
size 52876
oid sha256:7a767913020c3924f0a7ae95b20c064993a2fcdc3007610df6abe6f34c194ef8
size 1644

4
tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_Works_Rgba32_Bradley02.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c6d99bcaefa9e344e602465d08714f628b165e7783f73ddb3316e31c3f679825
size 5760
oid sha256:e02f5e94b9251be80250926678a2d8bc05318f40c3eff98204e74312ffbca138
size 2239

4
tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_Works_Rgba32_ducky.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6826d39280ffd36f075e52cd055975748fedec25a4b58c148b623a6dc6a517f4
size 2040
oid sha256:fdd84a24f616d7f06f78ebca01540b59cf1cf8564f442548fe4c8ede6dc1d412
size 757

Loading…
Cancel
Save