diff --git a/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs b/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs index ed30c36e7..deed04454 100644 --- a/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs +++ b/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs @@ -22,26 +22,60 @@ namespace SixLabors.ImageSharp.Processing /// The containing all the sums. public static Buffer2D CalculateIntegralImage(this Image source) where TPixel : unmanaged, IPixel + => CalculateIntegralImage(source.Frames.RootFrame); + + /// + /// Apply an image integral. + /// + /// The image on which to apply the integral. + /// The bounds within the image frame to calculate. + /// The type of the pixel. + /// The containing all the sums. + public static Buffer2D CalculateIntegralImage(this Image source, Rectangle bounds) + where TPixel : unmanaged, IPixel + => CalculateIntegralImage(source.Frames.RootFrame, bounds); + + /// + /// Apply an image integral. + /// + /// The image frame on which to apply the integral. + /// The type of the pixel. + /// The containing all the sums. + public static Buffer2D CalculateIntegralImage(this ImageFrame source) + where TPixel : unmanaged, IPixel + => source.CalculateIntegralImage(source.Bounds()); + + /// + /// Apply an image integral. + /// + /// The image frame on which to apply the integral. + /// The bounds within the image frame to calculate. + /// The type of the pixel. + /// The containing all the sums. + public static Buffer2D CalculateIntegralImage(this ImageFrame source, Rectangle bounds) + where TPixel : unmanaged, IPixel { 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 intImage = configuration.MemoryAllocator.Allocate2D(source.Width, source.Height); + Buffer2D intImage = configuration.MemoryAllocator.Allocate2D(interest.Width, interest.Height); ulong sumX0 = 0; - Buffer2D sourceBuffer = source.Frames.RootFrame.PixelBuffer; + Buffer2D sourceBuffer = source.PixelBuffer; - using (IMemoryOwner tempRow = configuration.MemoryAllocator.Allocate(source.Width)) + using (IMemoryOwner tempRow = configuration.MemoryAllocator.Allocate(interest.Width)) { Span tempSpan = tempRow.GetSpan(); - Span sourceRow = sourceBuffer.DangerousGetRowSpan(0); + Span sourceRow = sourceBuffer.DangerousGetRowSpan(startY).Slice(startX, tempSpan.Length); Span destRow = intImage.DangerousGetRowSpan(0); PixelOperations.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.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]; diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs index ecbec84e3..e7c5ad471 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs +++ b/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 /// The source area to process for the current processor instance. public AdaptiveThresholdProcessor(Configuration configuration, AdaptiveThresholdProcessor definition, Image source, Rectangle sourceRectangle) : base(configuration, source, sourceRectangle) - { - this.definition = definition; - } + => this.definition = definition; /// protected override void OnFrameApply(ImageFrame 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 lower = this.definition.Lower.ToPixel(); 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 sourceBuffer = source.PixelBuffer; - - // Using pooled 2d buffer for integer image table and temp memory to hold Rgb24 converted pixel data. - using (Buffer2D intImage = this.Configuration.MemoryAllocator.Allocate2D(width, height)) - { - Rgba32 rgb = default; - for (int x = startX; x < endX; x++) - { - ulong sum = 0; - for (int y = startY; y < endY; y++) - { - Span 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 intImage = source.CalculateIntegralImage(interest); + RowOperation operation = new(configuration, interest, source.PixelBuffer, intImage, upper, lower, thresholdLimit, clusterSize); + ParallelRowIterator.IterateRows( + configuration, + interest, + in operation); } - private readonly struct RowOperation : IRowOperation + private readonly struct RowOperation : IRowOperation { + private readonly Configuration configuration; private readonly Rectangle bounds; private readonly Buffer2D source; private readonly Buffer2D 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 source, Buffer2D 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; } /// [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(int y) + public void Invoke(int y, Span span) { - Rgba32 rgb = default; - Span pixelRow = this.source.DangerousGetRowSpan(y); + Span rowSpan = this.source.DangerousGetRowSpan(y).Slice(this.startX, span.Length); + PixelOperations.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; } } } diff --git a/tests/ImageSharp.Tests/Processing/Binarization/AdaptiveThresholdTests.cs b/tests/ImageSharp.Tests/Processing/Binarization/AdaptiveThresholdTests.cs index 5c725dbf0..c7378bac9 100644 --- a/tests/ImageSharp.Tests/Processing/Binarization/AdaptiveThresholdTests.cs +++ b/tests/ImageSharp.Tests/Processing/Binarization/AdaptiveThresholdTests.cs @@ -137,6 +137,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization Exception exception = Record.Exception(() => { using Image image = provider.GetImage(); + image.Mutate(img => img.AdaptiveThreshold(.5F)); + image.DebugSave(provider); }); Assert.Null(exception); diff --git a/tests/ImageSharp.Tests/Processing/IntegralImageTests.cs b/tests/ImageSharp.Tests/Processing/IntegralImageTests.cs index 330b95a6c..89d8e5330 100644 --- a/tests/ImageSharp.Tests/Processing/IntegralImageTests.cs +++ b/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 provider) + { + using Image image = provider.GetImage(); + + Rectangle interest = new(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2); + + // Act: + Buffer2D 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 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 provider) + { + using Image image = provider.GetImage(); + + Rectangle interest = new(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2); + + // Act: + Buffer2D integralBuffer = image.CalculateIntegralImage(interest); + + // Assert: + VerifySumValues(provider, integralBuffer, interest, (L8 pixel) => pixel.PackedValue); + } + + private static void VerifySumValues( + TestImageProvider provider, + Buffer2D integralBuffer, + System.Func getPixel) + where TPixel : unmanaged, IPixel + => VerifySumValues(provider, integralBuffer, integralBuffer.Bounds(), getPixel); + private static void VerifySumValues( TestImageProvider provider, Buffer2D integralBuffer, + Rectangle bounds, System.Func getPixel) where TPixel : unmanaged, IPixel { - Image image = provider.GetImage(); + // Image image = provider.GetImage(); + Buffer2DRegion image = provider.GetImage().GetRootFramePixelBuffer().GetRegion(bounds); // Check top-left corner Assert.Equal(getPixel(image[0, 0]), integralBuffer[0, 0]); diff --git a/tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_WithRectangle_Works_Rgba32_Bradley02.png b/tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_WithRectangle_Works_Rgba32_Bradley02.png index ea5a333e8..94d50e1ad 100644 --- a/tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_WithRectangle_Works_Rgba32_Bradley02.png +++ b/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 diff --git a/tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_Works_Rgba32_Bradley01.png b/tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_Works_Rgba32_Bradley01.png index 62660ef4b..5e53399d7 100644 --- a/tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_Works_Rgba32_Bradley01.png +++ b/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 diff --git a/tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_Works_Rgba32_Bradley02.png b/tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_Works_Rgba32_Bradley02.png index 7c40f64c0..97a594cf6 100644 --- a/tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_Works_Rgba32_Bradley02.png +++ b/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 diff --git a/tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_Works_Rgba32_ducky.png b/tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_Works_Rgba32_ducky.png index 467206ea6..5c19a8421 100644 --- a/tests/Images/External/ReferenceOutput/AdaptiveThresholdTests/AdaptiveThreshold_Works_Rgba32_ducky.png +++ b/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