Browse Source

Merge branch 'main' into js/remove-obsolete-code

pull/2189/head
James Jackson-South 4 years ago
committed by GitHub
parent
commit
65a2ea3202
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 52
      src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs
  2. 108
      src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs
  3. 1
      tests/ImageSharp.Tests/Processing/Binarization/AdaptiveThresholdTests.cs
  4. 52
      tests/ImageSharp.Tests/Processing/IntegralImageTests.cs
  5. 3
      tests/ImageSharp.Tests/TestImages.cs
  6. 4
      tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_WithRectangle_Works_Rgba32_Bradley02.png
  7. 4
      tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_Works_Rgba32_Bradley01.png
  8. 4
      tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_Works_Rgba32_Bradley02.png
  9. 3
      tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_Works_Rgba32_Issue_2217_AdaptiveThresholdProcessor.png
  10. 4
      tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_Works_Rgba32_ducky.png
  11. 3
      tests/Images/Input/Png/issues/Issue_2217_AdaptiveThresholdProcessor.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> /// <returns>The <see cref="Buffer2D{T}"/> containing all the sums.</returns>
public static Buffer2D<ulong> CalculateIntegralImage<TPixel>(this Image<TPixel> source) public static Buffer2D<ulong> CalculateIntegralImage<TPixel>(this Image<TPixel> source)
where TPixel : unmanaged, IPixel<TPixel> 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(); Configuration configuration = source.GetConfiguration();
int endY = source.Height; var interest = Rectangle.Intersect(bounds, source.Bounds());
int endX = source.Width; 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; 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<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); Span<ulong> destRow = intImage.DangerousGetRowSpan(0);
PixelOperations<TPixel>.Instance.ToL8(configuration, sourceRow, tempSpan); PixelOperations<TPixel>.Instance.ToL8(configuration, sourceRow, tempSpan);
// First row // First row
for (int x = 0; x < endX; x++) for (int x = 0; x < tempSpan.Length; x++)
{ {
sumX0 += tempSpan[x].PackedValue; sumX0 += tempSpan[x].PackedValue;
destRow[x] = sumX0; destRow[x] = sumX0;
@ -52,7 +86,7 @@ namespace SixLabors.ImageSharp.Processing
// All other rows // All other rows
for (int y = 1; y < endY; y++) for (int y = 1; y < endY; y++)
{ {
sourceRow = sourceBuffer.DangerousGetRowSpan(y); sourceRow = sourceBuffer.DangerousGetRowSpan(y + startY).Slice(startX, tempSpan.Length);
destRow = intImage.DangerousGetRowSpan(y); destRow = intImage.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToL8(configuration, sourceRow, tempSpan); PixelOperations<TPixel>.Instance.ToL8(configuration, sourceRow, tempSpan);
@ -62,7 +96,7 @@ namespace SixLabors.ImageSharp.Processing
destRow[0] = sumX0 + previousDestRow[0]; destRow[0] = sumX0 + previousDestRow[0];
// Process all other colmns // Process all other colmns
for (int x = 1; x < endX; x++) for (int x = 1; x < tempSpan.Length; x++)
{ {
sumX0 += tempSpan[x].PackedValue; sumX0 += tempSpan[x].PackedValue;
destRow[x] = sumX0 + previousDestRow[x]; destRow[x] = sumX0 + previousDestRow[x];

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

@ -3,7 +3,6 @@
using System; using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; 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> /// <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) public AdaptiveThresholdProcessor(Configuration configuration, AdaptiveThresholdProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle) : base(configuration, source, sourceRectangle)
{ => this.definition = definition;
this.definition = definition;
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) 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; Configuration configuration = this.Configuration;
TPixel upper = this.definition.Upper.ToPixel<TPixel>(); TPixel upper = this.definition.Upper.ToPixel<TPixel>();
TPixel lower = this.definition.Lower.ToPixel<TPixel>(); TPixel lower = this.definition.Lower.ToPixel<TPixel>();
float thresholdLimit = this.definition.ThresholdLimit; float thresholdLimit = this.definition.ThresholdLimit;
int startY = intersect.Y; // ClusterSize defines the size of cluster to used to check for average.
int endY = intersect.Bottom; // Tweaked to support up to 4k wide pixels and not more. 4096 / 16 is 256 thus the '-1'
int startX = intersect.X; byte clusterSize = (byte)Math.Clamp(interest.Width / 16F, 0, 255);
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 Buffer2D<ulong> intImage = source.CalculateIntegralImage(interest);
RowOperation operation = new(configuration, interest, source.PixelBuffer, intImage, upper, lower, thresholdLimit, clusterSize);
// Using pooled 2d buffer for integer image table and temp memory to hold Rgb24 converted pixel data. ParallelRowIterator.IterateRows<RowOperation, L8>(
using (Buffer2D<ulong> intImage = this.Configuration.MemoryAllocator.Allocate2D<ulong>(width, height)) configuration,
{ interest,
Rgba32 rgb = default; in operation);
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);
}
} }
private readonly struct RowOperation : IRowOperation private readonly struct RowOperation : IRowOperation<L8>
{ {
private readonly Configuration configuration;
private readonly Rectangle bounds; private readonly Rectangle bounds;
private readonly Buffer2D<TPixel> source; private readonly Buffer2D<TPixel> source;
private readonly Buffer2D<ulong> intImage; private readonly Buffer2D<ulong> intImage;
@ -98,62 +60,58 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization
private readonly TPixel lower; private readonly TPixel lower;
private readonly float thresholdLimit; private readonly float thresholdLimit;
private readonly int startX; private readonly int startX;
private readonly int endX;
private readonly int startY; private readonly int startY;
private readonly byte clusterSize; private readonly byte clusterSize;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public RowOperation( public RowOperation(
Configuration configuration,
Rectangle bounds, Rectangle bounds,
Buffer2D<TPixel> source, Buffer2D<TPixel> source,
Buffer2D<ulong> intImage, Buffer2D<ulong> intImage,
TPixel upper, TPixel upper,
TPixel lower, TPixel lower,
float thresholdLimit, float thresholdLimit,
byte clusterSize, byte clusterSize)
int startX,
int endX,
int startY)
{ {
this.configuration = configuration;
this.bounds = bounds; this.bounds = bounds;
this.startX = bounds.X;
this.startY = bounds.Y;
this.source = source; this.source = source;
this.intImage = intImage; this.intImage = intImage;
this.upper = upper; this.upper = upper;
this.lower = lower; this.lower = lower;
this.thresholdLimit = thresholdLimit; this.thresholdLimit = thresholdLimit;
this.startX = startX;
this.endX = endX;
this.startY = startY;
this.clusterSize = clusterSize; this.clusterSize = clusterSize;
} }
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y) public void Invoke(int y, Span<L8> span)
{ {
Rgba32 rgb = default; Span<TPixel> rowSpan = this.source.DangerousGetRowSpan(y).Slice(this.startX, span.Length);
Span<TPixel> pixelRow = this.source.DangerousGetRowSpan(y); 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]; int x1 = Math.Clamp(x - this.clusterSize + 1, 0, maxX);
pixel.ToRgba32(ref rgb); int x2 = Math.Min(x + this.clusterSize + 1, maxX);
int y1 = Math.Clamp(y - this.startY - this.clusterSize + 1, 0, maxY);
var x1 = Math.Max(x - this.startX - this.clusterSize + 1, 0); int y2 = Math.Min(y - this.startY + this.clusterSize + 1, maxY);
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);
var count = (uint)((x2 - x1) * (y2 - y1)); uint 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); 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 else
{ {
this.source[x, y] = this.upper; rowSpan[x] = this.upper;
} }
} }
} }

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

@ -102,6 +102,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization
[WithFile(TestImages.Png.Bradley01, PixelTypes.Rgba32)] [WithFile(TestImages.Png.Bradley01, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Bradley02, PixelTypes.Rgba32)] [WithFile(TestImages.Png.Bradley02, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Ducky, PixelTypes.Rgba32)] [WithFile(TestImages.Png.Ducky, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Issue2217, PixelTypes.Rgba32)]
public void AdaptiveThreshold_Works<TPixel>(TestImageProvider<TPixel> provider) public void AdaptiveThreshold_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {

52
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] [Theory]
[WithFile(TestImages.Png.Bradley01, PixelTypes.L8)] [WithFile(TestImages.Png.Bradley01, PixelTypes.L8)]
[WithFile(TestImages.Png.Bradley02, PixelTypes.L8)] [WithFile(TestImages.Png.Bradley02, PixelTypes.L8)]
@ -43,16 +67,40 @@ namespace SixLabors.ImageSharp.Tests.Processing
Buffer2D<ulong> integralBuffer = image.CalculateIntegralImage(); Buffer2D<ulong> integralBuffer = image.CalculateIntegralImage();
// Assert: // 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>( private static void VerifySumValues<TPixel>(
TestImageProvider<TPixel> provider, TestImageProvider<TPixel> provider,
Buffer2D<ulong> integralBuffer, Buffer2D<ulong> integralBuffer,
Rectangle bounds,
System.Func<TPixel, ulong> getPixel) System.Func<TPixel, ulong> getPixel)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Image<TPixel> image = provider.GetImage(); Buffer2DRegion<TPixel> image = provider.GetImage().GetRootFramePixelBuffer().GetRegion(bounds);
// Check top-left corner // Check top-left corner
Assert.Equal(getPixel(image[0, 0]), integralBuffer[0, 0]); Assert.Equal(getPixel(image[0, 0]), integralBuffer[0, 0]);

3
tests/ImageSharp.Tests/TestImages.cs

@ -125,6 +125,9 @@ namespace SixLabors.ImageSharp.Tests
// Discussion 1875: https://github.com/SixLabors/ImageSharp/discussions/1875 // Discussion 1875: https://github.com/SixLabors/ImageSharp/discussions/1875
public const string Issue1875 = "Png/raw-profile-type-exif.png"; 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 // Issue 2209: https://github.com/SixLabors/ImageSharp/issues/2209
public const string Issue2209IndexedWithTransparency = "Png/issues/Issue_2209.png"; public const string Issue2209IndexedWithTransparency = "Png/issues/Issue_2209.png";

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

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:36f60abb0ade0320779e242716c61b6dbabc8243a125f0a3145be35e233e117c oid sha256:5745f61e9b8cd49066b347605deee6dcde17690b9dc0f675466df6b2db706bd6
size 24542 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 version https://git-lfs.github.com/spec/v1
oid sha256:4c4a92f0ecd0f2ec06b12091b14f2d421605ef178092bf4f7f7cb4e661270945 oid sha256:7a767913020c3924f0a7ae95b20c064993a2fcdc3007610df6abe6f34c194ef8
size 52876 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 version https://git-lfs.github.com/spec/v1
oid sha256:c6d99bcaefa9e344e602465d08714f628b165e7783f73ddb3316e31c3f679825 oid sha256:e02f5e94b9251be80250926678a2d8bc05318f40c3eff98204e74312ffbca138
size 5760 size 2239

3
tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_Works_Rgba32_Issue_2217_AdaptiveThresholdProcessor.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b9e47d328e9d6d37a5bf090d2f8d748b80d78be936df06f4b3afec0fd9712f39
size 412

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

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

3
tests/Images/Input/Png/issues/Issue_2217_AdaptiveThresholdProcessor.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0516beb3860c464e9d7bb2e9da678f0ef9bdb5643eeb1675323d5693546c6646
size 251
Loading…
Cancel
Save