From e1d39de2a6227a00a2406ef92c08a2ef2677c7c5 Mon Sep 17 00:00:00 2001 From: Simanto Rahman Date: Fri, 5 Oct 2018 00:41:07 -0230 Subject: [PATCH 01/22] Addition of Breadley AdaptiveThreshold --- .../Processing/AdaptiveThresholdExtensions.cs | 47 +++++++ .../AdaptiveThresholdProcessor.cs | 115 ++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs create mode 100644 src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs diff --git a/src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs b/src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs new file mode 100644 index 000000000..9a6d63342 --- /dev/null +++ b/src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs @@ -0,0 +1,47 @@ +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing +{ + /// + /// Extensions to perform AdaptiveThreshold through Mutator + /// + public static class AdaptiveThresholdExtensions + { + /// + /// Applies Bradley Adaptive Threshold to the image. + /// + /// The image this method extends. + /// The pixel format. + /// The . + public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source) + where TPixel : struct, IPixel + => source.ApplyProcessor(new AdaptiveThresholdProcessor()); + + /// + /// Applies Bradley Adaptive Threshold to the image. + /// + /// The image this method extends. + /// Upper (white) color for thresholding. + /// Lower (black) color for thresholding + /// /// The pixel format. + /// The . + public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, TPixel upper, TPixel lower) + where TPixel : struct, IPixel + => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower)); + + /// + /// Applies Bradley Adaptive Threshold to the image. + /// + /// The image this method extends. + /// Upper (white) color for thresholding. + /// Lower (black) color for thresholding + /// Rectangle region to apply the processor on. + /// The pixel format. + /// The . + public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, TPixel upper, TPixel lower, Rectangle rectangle) + where TPixel : struct, IPixel + => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower), rectangle); + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs new file mode 100644 index 000000000..b14de6679 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs @@ -0,0 +1,115 @@ +using System; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// Performs Bradley Adaptive Threshold filter against an image + /// + /// The pixel format of the image + internal class AdaptiveThresholdProcessor : IImageProcessor + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + public AdaptiveThresholdProcessor() + : this(NamedColors.White, NamedColors.Black) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Color for upper threshold + /// Color for lower threshold + public AdaptiveThresholdProcessor(TPixel upper, TPixel lower) + { + this.Upper = upper; + this.Lower = lower; + } + + /// + /// Gets or sets upper color limit for thresholding + /// + public TPixel Upper { get; set; } + + /// + /// Gets or sets lower color limit for threshold + /// + public TPixel Lower { get; set; } + + public unsafe void Apply(Image source, Rectangle sourceRectangle) + { + ushort xStart = (ushort)Math.Max(0, sourceRectangle.X); + ushort yStart = (ushort)Math.Max(0, sourceRectangle.Y); + ushort xEnd = (ushort)Math.Min(xStart + sourceRectangle.Width, source.Width); + ushort yEnd = (ushort)Math.Min(yStart + sourceRectangle.Height, source.Height); + + // Algorithm variables + uint sum, count; + ushort s = (ushort)Math.Truncate((xEnd / 16f) - 1); + uint[,] intImage = new uint[yEnd, xEnd]; + + // Trying to figure out how to do this + // Using (Buffer2D intImg = source.GetConfiguration().MemoryAllocator.Allocate2D) + Rgb24 rgb = default; + + for (ushort i = yStart; i < yEnd; i++) + { + Span span = source.GetPixelRowSpan(i); + + sum = 0; + + for (ushort j = xStart; j < xEnd; j++) + { + span[j].ToRgb24(ref rgb); + + sum += (uint)(rgb.R + rgb.G + rgb.B); + + if (i != 0) + { + intImage[i, j] = intImage[i - 1, j] + sum; + } + else + { + intImage[i, j] = sum; + } + } + } + + // How can I parallelize this? + ushort x1, x2, y1, y2; + + for (ushort i = yStart; i < yEnd; i++) + { + Span span = source.GetPixelRowSpan(i); + + for (ushort j = xStart; j < xEnd; j++) + { + x1 = (ushort)Math.Max(i - s + 1, 0); + x2 = (ushort)Math.Min(i + s + 1, yEnd - 1); + y1 = (ushort)Math.Max(j - s + 1, 0); + y2 = (ushort)Math.Min(j + s + 1, xEnd - 1); + + count = (ushort)((x2 - x1) * (y2 - y1)); + + sum = intImage[x2, y2] - intImage[x1, y2] - intImage[x2, y1] + intImage[x1, y1]; + + span[j].ToRgb24(ref rgb); + + if ((rgb.R + rgb.G + rgb.B) * count < sum * (1.0 - 0.15)) + { + span[j] = this.Lower; + } + else + { + span[j] = this.Upper; + } + } + } + } + } +} \ No newline at end of file From 152b8e680d584bec691e56c68f3100137c8a0e3c Mon Sep 17 00:00:00 2001 From: Simanto Rahman Date: Fri, 5 Oct 2018 12:35:15 -0230 Subject: [PATCH 02/22] # Added Rect.Intersect # Inherited from ImageProcessor # Minor changes to variables # Minor Tweaks --- .../AdaptiveThresholdProcessor.cs | 107 ++++++++++-------- 1 file changed, 59 insertions(+), 48 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs index b14de6679..46e2e67c0 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs @@ -1,6 +1,8 @@ using System; using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Memory; using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors @@ -9,7 +11,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Performs Bradley Adaptive Threshold filter against an image /// /// The pixel format of the image - internal class AdaptiveThresholdProcessor : IImageProcessor + internal class AdaptiveThresholdProcessor : ImageProcessor where TPixel : struct, IPixel { /// @@ -41,72 +43,81 @@ namespace SixLabors.ImageSharp.Processing.Processors /// public TPixel Lower { get; set; } - public unsafe void Apply(Image source, Rectangle sourceRectangle) + /// + protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) { - ushort xStart = (ushort)Math.Max(0, sourceRectangle.X); - ushort yStart = (ushort)Math.Max(0, sourceRectangle.Y); - ushort xEnd = (ushort)Math.Min(xStart + sourceRectangle.Width, source.Width); - ushort yEnd = (ushort)Math.Min(yStart + sourceRectangle.Height, source.Height); + var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); + ushort startY = (ushort)interest.Y; + ushort endY = (ushort)interest.Bottom; + ushort startX = (ushort)interest.X; + ushort endX = (ushort)interest.Right; - // Algorithm variables - uint sum, count; - ushort s = (ushort)Math.Truncate((xEnd / 16f) - 1); - uint[,] intImage = new uint[yEnd, xEnd]; + ushort width = (ushort)(endX - startX); + ushort height = (ushort)(endY - startY); - // Trying to figure out how to do this - // Using (Buffer2D intImg = source.GetConfiguration().MemoryAllocator.Allocate2D) - Rgb24 rgb = default; + // Algorithm variables // + ulong sum; + uint count; - for (ushort i = yStart; i < yEnd; i++) - { - Span span = source.GetPixelRowSpan(i); + // Tweaked to support upto 4k wide pixels + ushort s = (ushort)Math.Truncate((width / 16f) - 1); - sum = 0; + // Trying to figure out how to do this + using (Buffer2D intImage = configuration.MemoryAllocator.Allocate2D(width, height, AllocationOptions.Clean)) + { + Rgb24 rgb = default; - for (ushort j = xStart; j < xEnd; j++) + for (ushort i = startY; i < endY; i++) { - span[j].ToRgb24(ref rgb); + Span span = source.GetPixelRowSpan(i); - sum += (uint)(rgb.R + rgb.G + rgb.B); + sum = 0; - if (i != 0) + for (ushort j = startX; j < endX; j++) { - intImage[i, j] = intImage[i - 1, j] + sum; - } - else - { - intImage[i, j] = sum; + span[j].ToRgb24(ref rgb); + + sum += (uint)(rgb.R + rgb.G + rgb.B); + + if (i != 0) + { + intImage[i, j] = intImage[i - 1, j] + sum; + } + else + { + intImage[i, j] = sum; + } } } - } - // How can I parallelize this? - ushort x1, x2, y1, y2; + // How can I parallelize this? + ushort x1, x2, y1, y2; - for (ushort i = yStart; i < yEnd; i++) - { - Span span = source.GetPixelRowSpan(i); - - for (ushort j = xStart; j < xEnd; j++) + for (ushort i = startY; i < endY; i++) { - x1 = (ushort)Math.Max(i - s + 1, 0); - x2 = (ushort)Math.Min(i + s + 1, yEnd - 1); - y1 = (ushort)Math.Max(j - s + 1, 0); - y2 = (ushort)Math.Min(j + s + 1, xEnd - 1); + Span span = source.GetPixelRowSpan(i); - count = (ushort)((x2 - x1) * (y2 - y1)); + for (ushort j = startX; j < endX; j++) + { + x1 = (ushort)Math.Max(i - s + 1, 0); + x2 = (ushort)Math.Min(i + s + 1, endY - 1); + y1 = (ushort)Math.Max(j - s + 1, 0); + y2 = (ushort)Math.Min(j + s + 1, endX - 1); - sum = intImage[x2, y2] - intImage[x1, y2] - intImage[x2, y1] + intImage[x1, y1]; + count = (uint)((x2 - x1) * (y2 - y1)); - span[j].ToRgb24(ref rgb); + sum = intImage[x2, y2] - intImage[x1, y2] - intImage[x2, y1] + intImage[x1, y1]; - if ((rgb.R + rgb.G + rgb.B) * count < sum * (1.0 - 0.15)) - { - span[j] = this.Lower; - } - else - { - span[j] = this.Upper; + span[j].ToRgb24(ref rgb); + + if ((rgb.R + rgb.G + rgb.B) * count < sum * (1.0 - 0.15)) + { + span[j] = this.Lower; + } + else + { + span[j] = this.Upper; + } } } } From 95a7b0db1a127c728ff33d80c708392184d5c420 Mon Sep 17 00:00:00 2001 From: SimantoR Date: Sat, 13 Oct 2018 04:01:48 -0230 Subject: [PATCH 03/22] Added parallelism to loops --- .../AdaptiveThresholdProcessor.cs | 127 +++++++++++------- tests/Images/External | 2 +- 2 files changed, 76 insertions(+), 53 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs index 46e2e67c0..898e1f8fd 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs @@ -1,6 +1,11 @@ -using System; +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; using SixLabors.Primitives; @@ -46,80 +51,98 @@ namespace SixLabors.ImageSharp.Processing.Processors /// protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) { - var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); - ushort startY = (ushort)interest.Y; - ushort endY = (ushort)interest.Bottom; - ushort startX = (ushort)interest.X; - ushort endX = (ushort)interest.Right; + var intersect = Rectangle.Intersect(sourceRectangle, source.Bounds()); + ushort startY = (ushort)intersect.Y; + ushort endY = (ushort)intersect.Bottom; + ushort startX = (ushort)intersect.X; + ushort endX = (ushort)intersect.Right; ushort width = (ushort)(endX - startX); ushort height = (ushort)(endY - startY); - // Algorithm variables // - ulong sum; - uint count; - - // Tweaked to support upto 4k wide pixels - ushort s = (ushort)Math.Truncate((width / 16f) - 1); + // Tweaked to support upto 4k wide pixels and not more. 4096 / 16 is 256 thus the '-1' + byte s = (byte)Math.Truncate((width / 16f) - 1); - // Trying to figure out how to do this + // Using pooled 2d buffer for integer image table using (Buffer2D intImage = configuration.MemoryAllocator.Allocate2D(width, height, AllocationOptions.Clean)) { - Rgb24 rgb = default; - - for (ushort i = startY; i < endY; i++) - { - Span span = source.GetPixelRowSpan(i); - - sum = 0; + var workingnRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); - for (ushort j = startX; j < endX; j++) + ParallelHelper.IterateRows( + workingnRectangle, + configuration, + rows => { - span[j].ToRgb24(ref rgb); + ulong sum; - sum += (uint)(rgb.R + rgb.G + rgb.B); + Rgb24 rgb = default; - if (i != 0) + for (int i = rows.Min; i < rows.Max; i++) { - intImage[i, j] = intImage[i - 1, j] + sum; - } - else - { - intImage[i, j] = sum; + var row = source.GetPixelRowSpan(i); + + sum = 0; + + for (int j = startX; j < endX; j++) + { + row[j].ToRgb24(ref rgb); + sum += (ulong)(rgb.B + rgb.G + rgb.B); + + if (i != 0) + { + intImage[i, j] = intImage[i - 1, j] + sum; + } + else + { + intImage[i, j] = sum; + } + } } } - } + ); - // How can I parallelize this? - ushort x1, x2, y1, y2; + ParallelHelper.IterateRows( + workingnRectangle, + configuration, + rows => + { + ushort x1, x2, y1, y2; - for (ushort i = startY; i < endY; i++) - { - Span span = source.GetPixelRowSpan(i); + uint count; - for (ushort j = startX; j < endX; j++) - { - x1 = (ushort)Math.Max(i - s + 1, 0); - x2 = (ushort)Math.Min(i + s + 1, endY - 1); - y1 = (ushort)Math.Max(j - s + 1, 0); - y2 = (ushort)Math.Min(j + s + 1, endX - 1); + long sum; - count = (uint)((x2 - x1) * (y2 - y1)); + for (int i = rows.Min; i < rows.Max; i++) + { + var row = source.GetPixelRowSpan(i); - sum = intImage[x2, y2] - intImage[x1, y2] - intImage[x2, y1] + intImage[x1, y1]; + Rgb24 rgb = default; - span[j].ToRgb24(ref rgb); + for (int j = startX; j < endX; j++) + { + x1 = (ushort)Math.Max(i - s + 1, 0); + x2 = (ushort)Math.Min(i + s + 1, endY - 1); + y1 = (ushort)Math.Max(j - s + 1, 0); + y2 = (ushort)Math.Min(j + s + 1, endX - 1); - if ((rgb.R + rgb.G + rgb.B) * count < sum * (1.0 - 0.15)) - { - span[j] = this.Lower; - } - else - { - span[j] = this.Upper; + count = (uint)((x2 - x1) * (y2 - y1)); + + sum = (long)(intImage[x2, y2] - intImage[x1, y2] - intImage[x2, y1] + intImage[x1, y1]); + + row[j].ToRgb24(ref rgb); + + if ((rgb.R + rgb.G + rgb.B) * count < sum * (1.0 - 0.15)) + { + row[j] = this.Lower; + } + else + { + row[j] = this.Upper; + } + } } } - } + ); } } } diff --git a/tests/Images/External b/tests/Images/External index ee90e5f32..5f3cbd839 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit ee90e5f32218027744b5d40058b587cc1047b76f +Subproject commit 5f3cbd839fbbffae615d294d1dabafdcabc64cf9 From 2b6d93aabe3b958b6376c6ef598b056ec5d5e443 Mon Sep 17 00:00:00 2001 From: Simanto Rahman Date: Wed, 24 Oct 2018 13:38:07 -0230 Subject: [PATCH 04/22] Temporary fix to accomodate #744 --- .../AdaptiveThresholdProcessor.cs | 85 ++++++++++--------- 1 file changed, 46 insertions(+), 39 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs index 898e1f8fd..baecdf7f0 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Threading.Tasks; +using System.Buffers; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.ParallelUtils; @@ -34,8 +34,8 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Color for lower threshold public AdaptiveThresholdProcessor(TPixel upper, TPixel lower) { - this.Upper = upper; - this.Lower = lower; + this.Upper.PackFromRgba32(upper.ToRgba32()); + this.Lower.PackFromRgba32(lower.ToRgba32()); } /// @@ -75,26 +75,29 @@ namespace SixLabors.ImageSharp.Processing.Processors { ulong sum; - Rgb24 rgb = default; - for (int i = rows.Min; i < rows.Max; i++) { - var row = source.GetPixelRowSpan(i); - - sum = 0; - - for (int j = startX; j < endX; j++) + using (IMemoryOwner tmpPixels = configuration.MemoryAllocator.Allocate(width, AllocationOptions.None)) { - row[j].ToRgb24(ref rgb); - sum += (ulong)(rgb.B + rgb.G + rgb.B); + Span span = tmpPixels.GetSpan(); + PixelOperations.Instance.ToRgb24(source.GetPixelRowSpan(i), span, width); - if (i != 0) - { - intImage[i, j] = intImage[i - 1, j] + sum; - } - else + sum = 0; + + for (int j = startX; j < endX; j++) { - intImage[i, j] = sum; + ref Rgb24 rgb = ref span[(width * j) + i]; + + sum += (ulong)(rgb.B + rgb.G + rgb.B); + + if (i != 0) + { + intImage[i, j] = intImage[i - 1, j] + sum; + } + else + { + intImage[i, j] = sum; + } } } } @@ -114,30 +117,34 @@ namespace SixLabors.ImageSharp.Processing.Processors for (int i = rows.Min; i < rows.Max; i++) { - var row = source.GetPixelRowSpan(i); - - Rgb24 rgb = default; - - for (int j = startX; j < endX; j++) + using (IMemoryOwner tmpPixes = configuration.MemoryAllocator.Allocate(width)) { - x1 = (ushort)Math.Max(i - s + 1, 0); - x2 = (ushort)Math.Min(i + s + 1, endY - 1); - y1 = (ushort)Math.Max(j - s + 1, 0); - y2 = (ushort)Math.Min(j + s + 1, endX - 1); - - count = (uint)((x2 - x1) * (y2 - y1)); - - sum = (long)(intImage[x2, y2] - intImage[x1, y2] - intImage[x2, y1] + intImage[x1, y1]); + Span span = tmpPixes.GetSpan(); + PixelOperations.Instance.ToRgb24(source.GetPixelRowSpan(i), span, width); - row[j].ToRgb24(ref rgb); - - if ((rgb.R + rgb.G + rgb.B) * count < sum * (1.0 - 0.15)) - { - row[j] = this.Lower; - } - else + for (int j = startX; j < endX; j++) { - row[j] = this.Upper; + ref Rgb24 rgb = ref span[(width * j) + 1]; + + x1 = (ushort)Math.Max(i - s + 1, 0); + x2 = (ushort)Math.Min(i + s + 1, endY - 1); + y1 = (ushort)Math.Max(j - s + 1, 0); + y2 = (ushort)Math.Min(j + s + 1, endX - 1); + + count = (uint)((x2 - x1) * (y2 - y1)); + + sum = (long)(intImage[x2, y2] - intImage[x1, y2] - intImage[x2, y1] + intImage[x1, y1]) + + if ((rgb.R + rgb.G + rgb.B) * count < sum * (1.0 - 0.15)) + { + //row[j] = this.Lower; + rgb = this.Lower.ToRgba32().Rgb; + } + else + { + //row[j] = this.Upper; + rgb = this.Upper.ToRgba32().Rgb; + } } } } From bb5cc29ba2d8d993e10de6c528c90504eb03e228 Mon Sep 17 00:00:00 2001 From: Simanto Rahman Date: Wed, 24 Oct 2018 13:42:10 -0230 Subject: [PATCH 05/22] Few general changes without effecting the algorithm implementation --- .../AdaptiveThresholdProcessor.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs index baecdf7f0..50dfdfd9e 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs @@ -57,11 +57,13 @@ namespace SixLabors.ImageSharp.Processing.Processors ushort startX = (ushort)intersect.X; ushort endX = (ushort)intersect.Right; - ushort width = (ushort)(endX - startX); - ushort height = (ushort)(endY - startY); + ushort width = (ushort)intersect.Width; + ushort height = (ushort)intersect.Height; // Tweaked to support upto 4k wide pixels and not more. 4096 / 16 is 256 thus the '-1' - byte s = (byte)Math.Truncate((width / 16f) - 1); + byte clusterSize = (byte)((width / 16) - 1); + + float threshold = 0.85f; // Using pooled 2d buffer for integer image table using (Buffer2D intImage = configuration.MemoryAllocator.Allocate2D(width, height, AllocationOptions.Clean)) @@ -126,23 +128,21 @@ namespace SixLabors.ImageSharp.Processing.Processors { ref Rgb24 rgb = ref span[(width * j) + 1]; - x1 = (ushort)Math.Max(i - s + 1, 0); - x2 = (ushort)Math.Min(i + s + 1, endY - 1); - y1 = (ushort)Math.Max(j - s + 1, 0); - y2 = (ushort)Math.Min(j + s + 1, endX - 1); + x1 = (ushort)Math.Max(i - clusterSize + 1, 0); + x2 = (ushort)Math.Min(i + clusterSize + 1, endY - 1); + y1 = (ushort)Math.Max(j - clusterSize + 1, 0); + y2 = (ushort)Math.Min(j + clusterSize + 1, endX - 1); count = (uint)((x2 - x1) * (y2 - y1)); sum = (long)(intImage[x2, y2] - intImage[x1, y2] - intImage[x2, y1] + intImage[x1, y1]) - if ((rgb.R + rgb.G + rgb.B) * count < sum * (1.0 - 0.15)) + if ((rgb.R + rgb.G + rgb.B) * count < sum * threshold) { - //row[j] = this.Lower; rgb = this.Lower.ToRgba32().Rgb; } else { - //row[j] = this.Upper; rgb = this.Upper.ToRgba32().Rgb; } } From d8f3b397bb063b3695fe7dbe3e65643ed52d289d Mon Sep 17 00:00:00 2001 From: Simanto Rahman Date: Wed, 24 Oct 2018 13:57:29 -0230 Subject: [PATCH 06/22] Fixed few breaking changes --- .../AdaptiveThresholdProcessor.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs index 50dfdfd9e..d13b84b8a 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs @@ -19,6 +19,8 @@ namespace SixLabors.ImageSharp.Processing.Processors internal class AdaptiveThresholdProcessor : ImageProcessor where TPixel : struct, IPixel { + private readonly PixelOperations pixelOpInstance; + /// /// Initializes a new instance of the class. /// @@ -34,19 +36,20 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Color for lower threshold public AdaptiveThresholdProcessor(TPixel upper, TPixel lower) { - this.Upper.PackFromRgba32(upper.ToRgba32()); - this.Lower.PackFromRgba32(lower.ToRgba32()); + this.pixelOpInstance = PixelOperations.Instance; + this.Upper.FromRgba32(upper.ToRgba32()); + this.Lower.FromRgba32(lower.ToRgba32()); } /// /// Gets or sets upper color limit for thresholding /// - public TPixel Upper { get; set; } + public Rgb24 Upper { get; set; } /// /// Gets or sets lower color limit for threshold /// - public TPixel Lower { get; set; } + public Rgb24 Lower { get; set; } /// protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) @@ -82,7 +85,7 @@ namespace SixLabors.ImageSharp.Processing.Processors using (IMemoryOwner tmpPixels = configuration.MemoryAllocator.Allocate(width, AllocationOptions.None)) { Span span = tmpPixels.GetSpan(); - PixelOperations.Instance.ToRgb24(source.GetPixelRowSpan(i), span, width); + this.pixelOpInstance.ToRgb24(source.GetPixelRowSpan(i), span); sum = 0; @@ -122,7 +125,7 @@ namespace SixLabors.ImageSharp.Processing.Processors using (IMemoryOwner tmpPixes = configuration.MemoryAllocator.Allocate(width)) { Span span = tmpPixes.GetSpan(); - PixelOperations.Instance.ToRgb24(source.GetPixelRowSpan(i), span, width); + this.pixelOpInstance.ToRgb24(source.GetPixelRowSpan(i), span); for (int j = startX; j < endX; j++) { @@ -139,11 +142,11 @@ namespace SixLabors.ImageSharp.Processing.Processors if ((rgb.R + rgb.G + rgb.B) * count < sum * threshold) { - rgb = this.Lower.ToRgba32().Rgb; + rgb = this.Lower; } else { - rgb = this.Upper.ToRgba32().Rgb; + rgb = this.Upper; } } } From 8978bc34749b5a335b5aab51658de6a5966c05e7 Mon Sep 17 00:00:00 2001 From: Simanto Rahman Date: Wed, 24 Oct 2018 14:14:20 -0230 Subject: [PATCH 07/22] Missed an end of line by accident :p --- .../AdaptiveThresholdProcessor.cs | 92 +++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs index d13b84b8a..b6748ce00 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs @@ -77,81 +77,81 @@ namespace SixLabors.ImageSharp.Processing.Processors workingnRectangle, configuration, rows => - { - ulong sum; - - for (int i = rows.Min; i < rows.Max; i++) { - using (IMemoryOwner tmpPixels = configuration.MemoryAllocator.Allocate(width, AllocationOptions.None)) - { - Span span = tmpPixels.GetSpan(); - this.pixelOpInstance.ToRgb24(source.GetPixelRowSpan(i), span); + ulong sum; - sum = 0; - - for (int j = startX; j < endX; j++) + for (int i = rows.Min; i < rows.Max; i++) + { + using (IMemoryOwner tmpPixels = configuration.MemoryAllocator.Allocate(width, AllocationOptions.None)) { - ref Rgb24 rgb = ref span[(width * j) + i]; + Span span = tmpPixels.GetSpan(); + this.pixelOpInstance.ToRgb24(source.GetPixelRowSpan(i), span); - sum += (ulong)(rgb.B + rgb.G + rgb.B); + sum = 0; - if (i != 0) + for (int j = startX; j < endX; j++) { - intImage[i, j] = intImage[i - 1, j] + sum; - } - else - { - intImage[i, j] = sum; + ref Rgb24 rgb = ref span[(width * j) + i]; + + sum += (ulong)(rgb.B + rgb.G + rgb.B); + + if (i != 0) + { + intImage[i, j] = intImage[i - 1, j] + sum; + } + else + { + intImage[i, j] = sum; + } } } } } - } ); ParallelHelper.IterateRows( workingnRectangle, configuration, rows => - { - ushort x1, x2, y1, y2; + { + ushort x1, x2, y1, y2; - uint count; + uint count; - long sum; + long sum; - for (int i = rows.Min; i < rows.Max; i++) - { - using (IMemoryOwner tmpPixes = configuration.MemoryAllocator.Allocate(width)) + for (int i = rows.Min; i < rows.Max; i++) { - Span span = tmpPixes.GetSpan(); - this.pixelOpInstance.ToRgb24(source.GetPixelRowSpan(i), span); - - for (int j = startX; j < endX; j++) + using (IMemoryOwner tmpPixes = configuration.MemoryAllocator.Allocate(width)) { - ref Rgb24 rgb = ref span[(width * j) + 1]; + Span span = tmpPixes.GetSpan(); + this.pixelOpInstance.ToRgb24(source.GetPixelRowSpan(i), span); - x1 = (ushort)Math.Max(i - clusterSize + 1, 0); - x2 = (ushort)Math.Min(i + clusterSize + 1, endY - 1); - y1 = (ushort)Math.Max(j - clusterSize + 1, 0); - y2 = (ushort)Math.Min(j + clusterSize + 1, endX - 1); + for (int j = startX; j < endX; j++) + { + ref Rgb24 rgb = ref span[(width * j) + 1]; - count = (uint)((x2 - x1) * (y2 - y1)); + x1 = (ushort)Math.Max(i - clusterSize + 1, 0); + x2 = (ushort)Math.Min(i + clusterSize + 1, endY - 1); + y1 = (ushort)Math.Max(j - clusterSize + 1, 0); + y2 = (ushort)Math.Min(j + clusterSize + 1, endX - 1); - sum = (long)(intImage[x2, y2] - intImage[x1, y2] - intImage[x2, y1] + intImage[x1, y1]) + count = (uint)((x2 - x1) * (y2 - y1)); - if ((rgb.R + rgb.G + rgb.B) * count < sum * threshold) - { - rgb = this.Lower; - } - else - { - rgb = this.Upper; + sum = (long)(intImage[x2, y2] - intImage[x1, y2] - intImage[x2, y1] + intImage[x1, y1]); + + if ((rgb.R + rgb.G + rgb.B) * count < sum * threshold) + { + rgb = this.Lower; + } + else + { + rgb = this.Upper; + } } } } } - } ); } } From 6805f6d277eb2be7cd40acaab77185ce1ef78de5 Mon Sep 17 00:00:00 2001 From: Simanto Rahman Date: Thu, 25 Oct 2018 12:13:48 -0230 Subject: [PATCH 08/22] Used TempBuffer and fixed few logical errors --- .../AdaptiveThresholdProcessor.cs | 130 +++++++++--------- 1 file changed, 62 insertions(+), 68 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs index b6748ce00..0602777e3 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs @@ -37,19 +37,20 @@ namespace SixLabors.ImageSharp.Processing.Processors public AdaptiveThresholdProcessor(TPixel upper, TPixel lower) { this.pixelOpInstance = PixelOperations.Instance; - this.Upper.FromRgba32(upper.ToRgba32()); - this.Lower.FromRgba32(lower.ToRgba32()); + + this.Upper = upper; + this.Lower = lower; } /// /// Gets or sets upper color limit for thresholding /// - public Rgb24 Upper { get; set; } + public TPixel Upper { get; set; } /// /// Gets or sets lower color limit for threshold /// - public Rgb24 Lower { get; set; } + public TPixel Lower { get; set; } /// protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) @@ -69,90 +70,83 @@ namespace SixLabors.ImageSharp.Processing.Processors float threshold = 0.85f; // Using pooled 2d buffer for integer image table - using (Buffer2D intImage = configuration.MemoryAllocator.Allocate2D(width, height, AllocationOptions.Clean)) + using (Buffer2D intImage = configuration.MemoryAllocator.Allocate2D(width, height)) { - var workingnRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); + var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); - ParallelHelper.IterateRows( - workingnRectangle, + ParallelHelper.IterateRowsWithTempBuffer( + workingRectangle, configuration, - rows => + (rows, memory) => + { + ulong sum = 0; + + Span tmpSpan = memory.Span; + + for (int i = rows.Min; i < rows.Max; i++) { - ulong sum; + this.pixelOpInstance.ToRgb24(source.GetPixelRowSpan(i), tmpSpan); + + sum = 0; - for (int i = rows.Min; i < rows.Max; i++) + for (int j = startX; j < endX; j++) { - using (IMemoryOwner tmpPixels = configuration.MemoryAllocator.Allocate(width, AllocationOptions.None)) + ref Rgb24 rgb = ref tmpSpan[j]; + + sum += (ulong)(rgb.R + rgb.G + rgb.B); + + if (i != 0) + { + intImage[i, j] = intImage[i - 1, j] + sum; + } + else { - Span span = tmpPixels.GetSpan(); - this.pixelOpInstance.ToRgb24(source.GetPixelRowSpan(i), span); - - sum = 0; - - for (int j = startX; j < endX; j++) - { - ref Rgb24 rgb = ref span[(width * j) + i]; - - sum += (ulong)(rgb.B + rgb.G + rgb.B); - - if (i != 0) - { - intImage[i, j] = intImage[i - 1, j] + sum; - } - else - { - intImage[i, j] = sum; - } - } + intImage[i, j] = sum; } } } - ); + }); - ParallelHelper.IterateRows( - workingnRectangle, + ParallelHelper.IterateRowsWithTempBuffer( + workingRectangle, configuration, - rows => - { - ushort x1, x2, y1, y2; + (rows, memory) => + { + ushort x1, x2, y1, y2; + uint count = 0; + long sum = 0; - uint count; + Span tmpSpan = memory.Span; - long sum; + for (int i = rows.Min; i < rows.Max; i++) + { + Span originalSpan = source.GetPixelRowSpan(i); + this.pixelOpInstance.ToRgb24(originalSpan, tmpSpan); - for (int i = rows.Min; i < rows.Max; i++) + for (int j = startX; j < endX; j++) { - using (IMemoryOwner tmpPixes = configuration.MemoryAllocator.Allocate(width)) + ref Rgb24 rgb = ref tmpSpan[j]; + + x1 = (ushort)Math.Max(i - clusterSize + 1, 0); + x2 = (ushort)Math.Min(i + clusterSize + 1, endY - 1); + y1 = (ushort)Math.Max(j - clusterSize + 1, 0); + y2 = (ushort)Math.Min(j + clusterSize + 1, endX - 1); + + count = (uint)((x2 - x1) * (y2 - y1)); + + sum = (long)(intImage[x2, y2] - intImage[x1, y2] - intImage[x2, y1] + intImage[x1, y1]); + + if ((rgb.R + rgb.G + rgb.B) * count < sum * threshold) + { + originalSpan[j] = this.Lower; + } + else { - Span span = tmpPixes.GetSpan(); - this.pixelOpInstance.ToRgb24(source.GetPixelRowSpan(i), span); - - for (int j = startX; j < endX; j++) - { - ref Rgb24 rgb = ref span[(width * j) + 1]; - - x1 = (ushort)Math.Max(i - clusterSize + 1, 0); - x2 = (ushort)Math.Min(i + clusterSize + 1, endY - 1); - y1 = (ushort)Math.Max(j - clusterSize + 1, 0); - y2 = (ushort)Math.Min(j + clusterSize + 1, endX - 1); - - count = (uint)((x2 - x1) * (y2 - y1)); - - sum = (long)(intImage[x2, y2] - intImage[x1, y2] - intImage[x2, y1] + intImage[x1, y1]); - - if ((rgb.R + rgb.G + rgb.B) * count < sum * threshold) - { - rgb = this.Lower; - } - else - { - rgb = this.Upper; - } - } + originalSpan[j] = this.Upper; } } } - ); + }); } } } From b0541025b2804755edc7253aba3cdadf252d6fde Mon Sep 17 00:00:00 2001 From: Simanto Rahman Date: Thu, 25 Oct 2018 12:50:40 -0230 Subject: [PATCH 09/22] Added contructor to control threshold limit --- .../Processing/AdaptiveThresholdExtensions.cs | 42 ++++++++++++++++++- .../AdaptiveThresholdProcessor.cs | 31 +++++++++++--- 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs b/src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs index 9a6d63342..6c8795aa5 100644 --- a/src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs +++ b/src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs @@ -1,5 +1,5 @@ using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors; +using SixLabors.ImageSharp.Processing.Processors.Binarization; using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing @@ -19,18 +19,42 @@ namespace SixLabors.ImageSharp.Processing where TPixel : struct, IPixel => source.ApplyProcessor(new AdaptiveThresholdProcessor()); + /// + /// Applies Bradley Adaptive Threshold to the image. + /// + /// The image this method extends. + /// Threshold limit (0.0-1.0) to consider for binarization. + /// The pixel format. + /// The . + public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, float threshold) + where TPixel : struct, IPixel + => source.ApplyProcessor(new AdaptiveThresholdProcessor(threshold)); + /// /// Applies Bradley Adaptive Threshold to the image. /// /// The image this method extends. /// Upper (white) color for thresholding. /// Lower (black) color for thresholding - /// /// The pixel format. + /// The pixel format. /// The . public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, TPixel upper, TPixel lower) where TPixel : struct, IPixel => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower)); + /// + /// Applies Bradley Adaptive Threshold to the image. + /// + /// The image this method extends. + /// Upper (white) color for thresholding. + /// Lower (black) color for thresholding + /// Threshold limit (0.0-1.0) to consider for binarization. + /// The pixel format. + /// The . + public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, TPixel upper, TPixel lower, float threshold) + where TPixel : struct, IPixel + => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower, threshold)); + /// /// Applies Bradley Adaptive Threshold to the image. /// @@ -43,5 +67,19 @@ namespace SixLabors.ImageSharp.Processing public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, TPixel upper, TPixel lower, Rectangle rectangle) where TPixel : struct, IPixel => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower), rectangle); + + /// + /// Applies Bradley Adaptive Threshold to the image. + /// + /// The image this method extends. + /// Upper (white) color for thresholding. + /// Lower (black) color for thresholding + /// Threshold limit (0.0-1.0) to consider for binarization. + /// Rectangle region to apply the processor on. + /// The pixel format. + /// The . + public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, TPixel upper, TPixel lower, float threshold, Rectangle rectangle) + where TPixel : struct, IPixel + => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower, threshold), rectangle); } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs index 0602777e3..2bed73be0 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs @@ -10,7 +10,7 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; using SixLabors.Primitives; -namespace SixLabors.ImageSharp.Processing.Processors +namespace SixLabors.ImageSharp.Processing.Processors.Binarization { /// /// Performs Bradley Adaptive Threshold filter against an image @@ -25,7 +25,21 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Initializes a new instance of the class. /// public AdaptiveThresholdProcessor() - : this(NamedColors.White, NamedColors.Black) + : this(NamedColors.White, NamedColors.Black, 0.85f) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Threshold limit + public AdaptiveThresholdProcessor(float threshold) + : this(NamedColors.White, NamedColors.Black, threshold) + { + } + + public AdaptiveThresholdProcessor(TPixel upper, TPixel lower) + : this(upper, lower, 0.85f) { } @@ -34,12 +48,14 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// Color for upper threshold /// Color for lower threshold - public AdaptiveThresholdProcessor(TPixel upper, TPixel lower) + /// Threshold limit + public AdaptiveThresholdProcessor(TPixel upper, TPixel lower, float threshold) { this.pixelOpInstance = PixelOperations.Instance; this.Upper = upper; this.Lower = lower; + this.Threshold = threshold; } /// @@ -52,6 +68,11 @@ namespace SixLabors.ImageSharp.Processing.Processors /// public TPixel Lower { get; set; } + /// + /// Gets or sets the value for threshold limit + /// + public float Threshold { get; set; } + /// protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) { @@ -67,8 +88,6 @@ namespace SixLabors.ImageSharp.Processing.Processors // Tweaked to support upto 4k wide pixels and not more. 4096 / 16 is 256 thus the '-1' byte clusterSize = (byte)((width / 16) - 1); - float threshold = 0.85f; - // Using pooled 2d buffer for integer image table using (Buffer2D intImage = configuration.MemoryAllocator.Allocate2D(width, height)) { @@ -136,7 +155,7 @@ namespace SixLabors.ImageSharp.Processing.Processors sum = (long)(intImage[x2, y2] - intImage[x1, y2] - intImage[x2, y1] + intImage[x1, y1]); - if ((rgb.R + rgb.G + rgb.B) * count < sum * threshold) + if ((rgb.R + rgb.G + rgb.B) * count < sum * this.Threshold) { originalSpan[j] = this.Lower; } From 319fc9559ca9a7773aea2659322626f69029236b Mon Sep 17 00:00:00 2001 From: Simanto Rahman Date: Fri, 26 Oct 2018 19:22:03 -0230 Subject: [PATCH 10/22] Fixed several bugs produced during parallelism implementations --- .../AdaptiveThresholdProcessor.cs | 74 +++++++++---------- 1 file changed, 33 insertions(+), 41 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs index 2bed73be0..21c7f4f57 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs @@ -55,7 +55,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization this.Upper = upper; this.Lower = lower; - this.Threshold = threshold; + this.ThresholdLimit = threshold; } /// @@ -71,7 +71,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization /// /// Gets or sets the value for threshold limit /// - public float Threshold { get; set; } + public float ThresholdLimit { get; set; } /// protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) @@ -90,81 +90,73 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization // Using pooled 2d buffer for integer image table using (Buffer2D intImage = configuration.MemoryAllocator.Allocate2D(width, height)) + using (IMemoryOwner tmpBuffer = configuration.MemoryAllocator.Allocate(width * height)) { - var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); + Rectangle workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); - ParallelHelper.IterateRowsWithTempBuffer( + this.pixelOpInstance.ToRgb24(source.GetPixelSpan(), tmpBuffer.GetSpan()); + + ParallelHelper.IterateRows( workingRectangle, configuration, - (rows, memory) => + rows => { - ulong sum = 0; - - Span tmpSpan = memory.Span; - - for (int i = rows.Min; i < rows.Max; i++) + Span rgbSpan = tmpBuffer.GetSpan(); + uint sum; + for (int x = startX; x < endX; x++) { - this.pixelOpInstance.ToRgb24(source.GetPixelRowSpan(i), tmpSpan); - sum = 0; - - for (int j = startX; j < endX; j++) + for (int y = rows.Min; y < rows.Max; y++) { - ref Rgb24 rgb = ref tmpSpan[j]; - - sum += (ulong)(rgb.R + rgb.G + rgb.B); + ref Rgb24 rgb = ref rgbSpan[(width * y) + x]; + sum += (uint)(rgb.R + rgb.G + rgb.B); - if (i != 0) + if (x > 0) { - intImage[i, j] = intImage[i - 1, j] + sum; + intImage[x - startX, y - startY] = intImage[x - 1 - startX, y - startY] + sum; } else { - intImage[i, j] = sum; + intImage[x - startX, y - startY] = sum; } } } }); - ParallelHelper.IterateRowsWithTempBuffer( + ParallelHelper.IterateRows( workingRectangle, configuration, - (rows, memory) => + rows => { ushort x1, x2, y1, y2; - uint count = 0; + Span rgbSpan = tmpBuffer.GetSpan(); long sum = 0; + uint count = 0; - Span tmpSpan = memory.Span; - - for (int i = rows.Min; i < rows.Max; i++) + for (int x = startX; x < endX; x++) { - Span originalSpan = source.GetPixelRowSpan(i); - this.pixelOpInstance.ToRgb24(originalSpan, tmpSpan); - - for (int j = startX; j < endX; j++) + for (int y = rows.Min; y < rows.Max; y++) { - ref Rgb24 rgb = ref tmpSpan[j]; - - x1 = (ushort)Math.Max(i - clusterSize + 1, 0); - x2 = (ushort)Math.Min(i + clusterSize + 1, endY - 1); - y1 = (ushort)Math.Max(j - clusterSize + 1, 0); - y2 = (ushort)Math.Min(j + clusterSize + 1, endX - 1); + ref Rgb24 rgb = ref rgbSpan[(width * y) + x]; + x1 = (ushort)Math.Max(x - clusterSize + 1 - startX, 0); + x2 = (ushort)Math.Min(x + clusterSize + 1 - startX, endX - startX - 1); + y1 = (ushort)Math.Max(y - clusterSize + 1 - startY, 0); + y2 = (ushort)Math.Min(y + clusterSize + 1 - startY, endY - startY - 1); count = (uint)((x2 - x1) * (y2 - y1)); - sum = (long)(intImage[x2, y2] - intImage[x1, y2] - intImage[x2, y1] + intImage[x1, y1]); + sum = (long)(intImage[x2, y2] - intImage[x2, y1] - intImage[x1, y2] + intImage[x1, y1]); - if ((rgb.R + rgb.G + rgb.B) * count < sum * this.Threshold) + if ((rgb.R + rgb.G + rgb.B) * count <= sum * this.ThresholdLimit) { - originalSpan[j] = this.Lower; + source[x, y] = this.Lower; } else { - originalSpan[j] = this.Upper; + source[x, y] = this.Upper; } } - } + } }); } } From a239e60885030c15d3fffa505415657c159f5b4d Mon Sep 17 00:00:00 2001 From: Simanto Rahman Date: Tue, 30 Oct 2018 13:52:02 -0230 Subject: [PATCH 11/22] Algorithm behaves abnormally when applied with ParallelHelpers --- .../AdaptiveThresholdProcessor.cs | 118 +++++++++--------- 1 file changed, 57 insertions(+), 61 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs index 21c7f4f57..030768e69 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs @@ -76,7 +76,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization /// protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) { - var intersect = Rectangle.Intersect(sourceRectangle, source.Bounds()); + Rectangle intersect = Rectangle.Intersect(sourceRectangle, source.Bounds()); + + // Used ushort because the values should never exceed max ushort value ushort startY = (ushort)intersect.Y; ushort endY = (ushort)intersect.Bottom; ushort startX = (ushort)intersect.X; @@ -85,79 +87,73 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization ushort width = (ushort)intersect.Width; ushort height = (ushort)intersect.Height; - // Tweaked to support upto 4k wide pixels and not more. 4096 / 16 is 256 thus the '-1' - byte clusterSize = (byte)((width / 16) - 1); + // ClusterSize defines the size of cluster to used to check for average. Tweaked to support upto 4k wide pixels and not more. 4096 / 16 is 256 thus the '-1' + byte clusterSize = (byte)Math.Truncate((width / 16f) - 1); - // Using pooled 2d buffer for integer image table + // Using pooled 2d buffer for integer image table and temp memory to hold Rgb24 converted pixel data using (Buffer2D intImage = configuration.MemoryAllocator.Allocate2D(width, height)) - using (IMemoryOwner tmpBuffer = configuration.MemoryAllocator.Allocate(width * height)) + using (IMemoryOwner bulkRgbBuf = configuration.MemoryAllocator.Allocate(width * height)) { + // Defines the rectangle section of the image to work on Rectangle workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); - this.pixelOpInstance.ToRgb24(source.GetPixelSpan(), tmpBuffer.GetSpan()); + // TPixel span of the original image + Span pixelSpan = source.GetPixelSpan(); + + // RGB24 span of the converted pixel buffer + Span rgbSpan = bulkRgbBuf.GetSpan(); - ParallelHelper.IterateRows( - workingRectangle, - configuration, - rows => + // Bulk conversion to RGB24 + this.pixelOpInstance.ToRgb24(pixelSpan, rgbSpan); + + for (int x = startX; x < endX; x++) + { + ulong sum = 0; + for (int y = startY; y < endY; y++) { - Span rgbSpan = tmpBuffer.GetSpan(); - uint sum; - for (int x = startX; x < endX; x++) + ref Rgb24 rgb = ref rgbSpan[(width * y) + x]; + + sum += (ulong)(rgb.R + rgb.G + rgb.B); + + if (x != 0) + { + intImage[x - startX, y - startY] = intImage[x - startX - 1, y - startY] + sum; + } + else { - sum = 0; - for (int y = rows.Min; y < rows.Max; y++) - { - ref Rgb24 rgb = ref rgbSpan[(width * y) + x]; - sum += (uint)(rgb.R + rgb.G + rgb.B); - - if (x > 0) - { - intImage[x - startX, y - startY] = intImage[x - 1 - startX, y - startY] + sum; - } - else - { - intImage[x - startX, y - startY] = sum; - } - } + intImage[x - startX, y - startY] = sum; } - }); + } + } - ParallelHelper.IterateRows( - workingRectangle, - configuration, - rows => + ushort x1, x2, y1, y2; + uint count = 0; + + for (int x = startX; x < endX; x++) + { + long sum = 0; + for (int y = startY; y < endY; y++) { - ushort x1, x2, y1, y2; - Span rgbSpan = tmpBuffer.GetSpan(); - long sum = 0; - uint count = 0; + ref Rgb24 rgb = ref rgbSpan[(width * y) + x]; + + x1 = (ushort)Math.Max(x - startX - clusterSize + 1, 0); + x2 = (ushort)Math.Min(x - startX + clusterSize + 1, width - 1); + y1 = (ushort)Math.Max(y - startY - clusterSize + 1, 0); + y2 = (ushort)Math.Min(y - startY + clusterSize + 1, height - 1); + + count = (uint)((x2 - x1) * (y2 - y1)); + sum = (long)(intImage[x2, y2] - intImage[x1, y2] - intImage[x2, y1] + intImage[x1, y1]); - for (int x = startX; x < endX; x++) + if ((rgb.R + rgb.G + rgb.B) * count <= sum * this.ThresholdLimit) { - for (int y = rows.Min; y < rows.Max; y++) - { - ref Rgb24 rgb = ref rgbSpan[(width * y) + x]; - x1 = (ushort)Math.Max(x - clusterSize + 1 - startX, 0); - x2 = (ushort)Math.Min(x + clusterSize + 1 - startX, endX - startX - 1); - y1 = (ushort)Math.Max(y - clusterSize + 1 - startY, 0); - y2 = (ushort)Math.Min(y + clusterSize + 1 - startY, endY - startY - 1); - - count = (uint)((x2 - x1) * (y2 - y1)); - - sum = (long)(intImage[x2, y2] - intImage[x2, y1] - intImage[x1, y2] + intImage[x1, y1]); - - if ((rgb.R + rgb.G + rgb.B) * count <= sum * this.ThresholdLimit) - { - source[x, y] = this.Lower; - } - else - { - source[x, y] = this.Upper; - } - } - } - }); + pixelSpan[(width * y) + x] = this.Lower; + } + else + { + pixelSpan[(width * y) + x] = this.Upper; + } + } + } } } } From 1f52c9d77c83ec06edb3a4afda5c4c85a9b30744 Mon Sep 17 00:00:00 2001 From: Simanto Rahman Date: Tue, 30 Oct 2018 19:01:56 -0230 Subject: [PATCH 12/22] Fully working implementation --- .../AdaptiveThresholdProcessor.cs | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs index 030768e69..4fb97aa9f 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs @@ -92,29 +92,22 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization // Using pooled 2d buffer for integer image table and temp memory to hold Rgb24 converted pixel data using (Buffer2D intImage = configuration.MemoryAllocator.Allocate2D(width, height)) - using (IMemoryOwner bulkRgbBuf = configuration.MemoryAllocator.Allocate(width * height)) + using (IMemoryOwner tmpBuffer = configuration.MemoryAllocator.Allocate(width * height)) { // Defines the rectangle section of the image to work on Rectangle workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); - // TPixel span of the original image - Span pixelSpan = source.GetPixelSpan(); - - // RGB24 span of the converted pixel buffer - Span rgbSpan = bulkRgbBuf.GetSpan(); - - // Bulk conversion to RGB24 - this.pixelOpInstance.ToRgb24(pixelSpan, rgbSpan); + this.pixelOpInstance.ToRgb24(source.GetPixelSpan(), tmpBuffer.GetSpan()); for (int x = startX; x < endX; x++) { + Span rgbSpan = tmpBuffer.GetSpan(); ulong sum = 0; for (int y = startY; y < endY; y++) { ref Rgb24 rgb = ref rgbSpan[(width * y) + x]; - sum += (ulong)(rgb.R + rgb.G + rgb.B); - + sum += (ulong)(rgb.R + rgb.G + rgb.G); if (x != 0) { intImage[x - startX, y - startY] = intImage[x - startX - 1, y - startY] + sum; @@ -126,34 +119,41 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization } } - ushort x1, x2, y1, y2; - uint count = 0; - - for (int x = startX; x < endX; x++) - { - long sum = 0; - for (int y = startY; y < endY; y++) + ParallelHelper.IterateRows( + workingRectangle, + configuration, + rows => { - ref Rgb24 rgb = ref rgbSpan[(width * y) + x]; + Span rgbSpan = tmpBuffer.GetSpan(); + ushort x1, y1, x2, y2; + uint count = 0; + long sum = 0; - x1 = (ushort)Math.Max(x - startX - clusterSize + 1, 0); - x2 = (ushort)Math.Min(x - startX + clusterSize + 1, width - 1); - y1 = (ushort)Math.Max(y - startY - clusterSize + 1, 0); - y2 = (ushort)Math.Min(y - startY + clusterSize + 1, height - 1); - - count = (uint)((x2 - x1) * (y2 - y1)); - sum = (long)(intImage[x2, y2] - intImage[x1, y2] - intImage[x2, y1] + intImage[x1, y1]); - - if ((rgb.R + rgb.G + rgb.B) * count <= sum * this.ThresholdLimit) + for (int x = startX; x < endX; x++) { - pixelSpan[(width * y) + x] = this.Lower; + for (int y = rows.Min; y < rows.Max; y++) + { + ref Rgb24 rgb = ref rgbSpan[(width * y) + x]; + + x1 = (ushort)Math.Max(x - startX - clusterSize + 1, 0); + x2 = (ushort)Math.Min(x - startX + clusterSize + 1, width - 1); + y1 = (ushort)Math.Max(y - startY - clusterSize + 1, 0); + y2 = (ushort)Math.Min(y - startY + clusterSize + 1, height - 1); + + count = (uint)((x2 - x1) * (y2 - y1)); + sum = (long)(intImage[x2, y2] - intImage[x1, y2] - intImage[x2, y1] + intImage[x1, y1]); + + if ((rgb.R + rgb.G + rgb.B) * count <= sum * this.ThresholdLimit) + { + source[x, y] = this.Lower; + } + else + { + source[x, y] = this.Upper; + } + } } - else - { - pixelSpan[(width * y) + x] = this.Upper; - } - } - } + }); } } } From 31e5d8d35d3153906abaf30ff499282a05850a8a Mon Sep 17 00:00:00 2001 From: Simanto Rahman Date: Wed, 31 Oct 2018 12:45:56 -0230 Subject: [PATCH 13/22] Changed naming convention for threshold limit param --- .../Processing/AdaptiveThresholdExtensions.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs b/src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs index 6c8795aa5..33cf4b45b 100644 --- a/src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs +++ b/src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs @@ -23,12 +23,12 @@ namespace SixLabors.ImageSharp.Processing /// Applies Bradley Adaptive Threshold to the image. /// /// The image this method extends. - /// Threshold limit (0.0-1.0) to consider for binarization. + /// Threshold limit (0.0-1.0) to consider for binarization. /// The pixel format. /// The . - public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, float threshold) + public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, float thresholdLimit) where TPixel : struct, IPixel - => source.ApplyProcessor(new AdaptiveThresholdProcessor(threshold)); + => source.ApplyProcessor(new AdaptiveThresholdProcessor(thresholdLimit)); /// /// Applies Bradley Adaptive Threshold to the image. @@ -48,12 +48,12 @@ namespace SixLabors.ImageSharp.Processing /// The image this method extends. /// Upper (white) color for thresholding. /// Lower (black) color for thresholding - /// Threshold limit (0.0-1.0) to consider for binarization. + /// Threshold limit (0.0-1.0) to consider for binarization. /// The pixel format. /// The . - public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, TPixel upper, TPixel lower, float threshold) + public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, TPixel upper, TPixel lower, float thresholdLimit) where TPixel : struct, IPixel - => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower, threshold)); + => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower, thresholdLimit)); /// /// Applies Bradley Adaptive Threshold to the image. @@ -74,12 +74,12 @@ namespace SixLabors.ImageSharp.Processing /// The image this method extends. /// Upper (white) color for thresholding. /// Lower (black) color for thresholding - /// Threshold limit (0.0-1.0) to consider for binarization. + /// Threshold limit (0.0-1.0) to consider for binarization. /// Rectangle region to apply the processor on. /// The pixel format. /// The . - public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, TPixel upper, TPixel lower, float threshold, Rectangle rectangle) + public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, TPixel upper, TPixel lower, float thresholdLimit, Rectangle rectangle) where TPixel : struct, IPixel - => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower, threshold), rectangle); + => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower, thresholdLimit), rectangle); } } \ No newline at end of file From 8610f5c6b4f7f91e5d2a6f884361be03f6e3a246 Mon Sep 17 00:00:00 2001 From: Simanto Rahman Date: Wed, 31 Oct 2018 12:46:26 -0230 Subject: [PATCH 14/22] Fixed a minor bug --- .../AdaptiveThresholdProcessor.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs index 4fb97aa9f..2ad170e38 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs @@ -32,9 +32,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization /// /// Initializes a new instance of the class. /// - /// Threshold limit - public AdaptiveThresholdProcessor(float threshold) - : this(NamedColors.White, NamedColors.Black, threshold) + /// Threshold limit + public AdaptiveThresholdProcessor(float thresholdLimit) + : this(NamedColors.White, NamedColors.Black, thresholdLimit) { } @@ -48,14 +48,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization /// /// Color for upper threshold /// Color for lower threshold - /// Threshold limit - public AdaptiveThresholdProcessor(TPixel upper, TPixel lower, float threshold) + /// Threshold limit + public AdaptiveThresholdProcessor(TPixel upper, TPixel lower, float thresholdLimit) { this.pixelOpInstance = PixelOperations.Instance; this.Upper = upper; this.Lower = lower; - this.ThresholdLimit = threshold; + this.ThresholdLimit = thresholdLimit; } /// @@ -99,16 +99,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization this.pixelOpInstance.ToRgb24(source.GetPixelSpan(), tmpBuffer.GetSpan()); - for (int x = startX; x < endX; x++) + for (ushort x = startX; x < endX; x++) { Span rgbSpan = tmpBuffer.GetSpan(); ulong sum = 0; - for (int y = startY; y < endY; y++) + for (ushort y = startY; y < endY; y++) { ref Rgb24 rgb = ref rgbSpan[(width * y) + x]; sum += (ulong)(rgb.R + rgb.G + rgb.G); - if (x != 0) + if (x - startX != 0) { intImage[x - startX, y - startY] = intImage[x - startX - 1, y - startY] + sum; } @@ -129,9 +129,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization uint count = 0; long sum = 0; - for (int x = startX; x < endX; x++) + for (ushort x = startX; x < endX; x++) { - for (int y = rows.Min; y < rows.Max; y++) + for (ushort y = (ushort)rows.Min; y < (ushort)rows.Max; y++) { ref Rgb24 rgb = ref rgbSpan[(width * y) + x]; @@ -141,7 +141,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization y2 = (ushort)Math.Min(y - startY + clusterSize + 1, height - 1); count = (uint)((x2 - x1) * (y2 - y1)); - sum = (long)(intImage[x2, y2] - intImage[x1, y2] - intImage[x2, y1] + intImage[x1, y1]); + sum = (long)Math.Min(intImage[x2, y2] - intImage[x1, y2] - intImage[x2, y1] + intImage[x1, y1], long.MaxValue); if ((rgb.R + rgb.G + rgb.B) * count <= sum * this.ThresholdLimit) { From a5a0ecd0062741eb9625a1961bb24ce04d771c46 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 2 Apr 2020 12:05:51 +0200 Subject: [PATCH 15/22] Re-add external test images --- tests/Images/External | 1 + 1 file changed, 1 insertion(+) create mode 160000 tests/Images/External diff --git a/tests/Images/External b/tests/Images/External new file mode 160000 index 000000000..fe694a393 --- /dev/null +++ b/tests/Images/External @@ -0,0 +1 @@ +Subproject commit fe694a3938bea3565071a96cb1c90c4cbc586ff9 From 39d5a93d1439e28078b93c5e0ff43f0a497d3ff4 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 2 Apr 2020 12:31:35 +0200 Subject: [PATCH 16/22] Adjustments to changes from the upstream --- .../Processing/AdaptiveThresholdExtensions.cs | 49 ++--- .../AdaptiveThresholdProcessor.cs | 149 ++++------------ .../AdaptiveThresholdProcessor{TPixel}.cs | 168 ++++++++++++++++++ 3 files changed, 220 insertions(+), 146 deletions(-) create mode 100644 src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs diff --git a/src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs b/src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs index 33cf4b45b..4fc78a958 100644 --- a/src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs +++ b/src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs @@ -1,11 +1,12 @@ -using SixLabors.ImageSharp.PixelFormats; +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + using SixLabors.ImageSharp.Processing.Processors.Binarization; -using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing { /// - /// Extensions to perform AdaptiveThreshold through Mutator + /// Extensions to perform AdaptiveThreshold through Mutator. /// public static class AdaptiveThresholdExtensions { @@ -13,47 +14,39 @@ namespace SixLabors.ImageSharp.Processing /// Applies Bradley Adaptive Threshold to the image. /// /// The image this method extends. - /// The pixel format. /// The . - public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source) - where TPixel : struct, IPixel - => source.ApplyProcessor(new AdaptiveThresholdProcessor()); + public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source) + => source.ApplyProcessor(new AdaptiveThresholdProcessor()); /// /// Applies Bradley Adaptive Threshold to the image. /// /// The image this method extends. /// Threshold limit (0.0-1.0) to consider for binarization. - /// The pixel format. /// The . - public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, float thresholdLimit) - where TPixel : struct, IPixel - => source.ApplyProcessor(new AdaptiveThresholdProcessor(thresholdLimit)); + public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, float thresholdLimit) + => source.ApplyProcessor(new AdaptiveThresholdProcessor(thresholdLimit)); /// /// Applies Bradley Adaptive Threshold to the image. /// /// The image this method extends. /// Upper (white) color for thresholding. - /// Lower (black) color for thresholding - /// The pixel format. + /// Lower (black) color for thresholding. /// The . - public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, TPixel upper, TPixel lower) - where TPixel : struct, IPixel - => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower)); + public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, Color upper, Color lower) + => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower)); /// /// Applies Bradley Adaptive Threshold to the image. /// /// The image this method extends. /// Upper (white) color for thresholding. - /// Lower (black) color for thresholding + /// Lower (black) color for thresholding. /// Threshold limit (0.0-1.0) to consider for binarization. - /// The pixel format. /// The . - public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, TPixel upper, TPixel lower, float thresholdLimit) - where TPixel : struct, IPixel - => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower, thresholdLimit)); + public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, Color upper, Color lower, float thresholdLimit) + => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower, thresholdLimit)); /// /// Applies Bradley Adaptive Threshold to the image. @@ -62,11 +55,9 @@ namespace SixLabors.ImageSharp.Processing /// Upper (white) color for thresholding. /// Lower (black) color for thresholding /// Rectangle region to apply the processor on. - /// The pixel format. /// The . - public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, TPixel upper, TPixel lower, Rectangle rectangle) - where TPixel : struct, IPixel - => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower), rectangle); + public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, Color upper, Color lower, Rectangle rectangle) + => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower), rectangle); /// /// Applies Bradley Adaptive Threshold to the image. @@ -76,10 +67,8 @@ namespace SixLabors.ImageSharp.Processing /// Lower (black) color for thresholding /// Threshold limit (0.0-1.0) to consider for binarization. /// Rectangle region to apply the processor on. - /// The pixel format. /// The . - public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, TPixel upper, TPixel lower, float thresholdLimit, Rectangle rectangle) - where TPixel : struct, IPixel - => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower, thresholdLimit), rectangle); + public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, Color upper, Color lower, float thresholdLimit, Rectangle rectangle) + => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower, thresholdLimit), rectangle); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs index 2ad170e38..3558a9489 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs @@ -1,160 +1,77 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; -using System.Buffers; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Memory; -using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors.Binarization { /// - /// Performs Bradley Adaptive Threshold filter against an image + /// Performs Bradley Adaptive Threshold filter against an image. /// - /// The pixel format of the image - internal class AdaptiveThresholdProcessor : ImageProcessor - where TPixel : struct, IPixel + /// + /// Implements "Adaptive Thresholding Using the Integral Image", + /// see paper: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.420.7883&rep=rep1&type=pdf + /// + public class AdaptiveThresholdProcessor : IImageProcessor { - private readonly PixelOperations pixelOpInstance; - /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public AdaptiveThresholdProcessor() - : this(NamedColors.White, NamedColors.Black, 0.85f) + : this(Color.White, Color.Black, 0.85f) { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// Threshold limit + /// Threshold limit. public AdaptiveThresholdProcessor(float thresholdLimit) - : this(NamedColors.White, NamedColors.Black, thresholdLimit) + : this(Color.White, Color.Black, thresholdLimit) { } - public AdaptiveThresholdProcessor(TPixel upper, TPixel lower) + /// + /// Initializes a new instance of the class. + /// + /// Color for upper threshold. + /// Color for lower threshold. + public AdaptiveThresholdProcessor(Color upper, Color lower) : this(upper, lower, 0.85f) { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// Color for upper threshold - /// Color for lower threshold - /// Threshold limit - public AdaptiveThresholdProcessor(TPixel upper, TPixel lower, float thresholdLimit) + /// Color for upper threshold. + /// Color for lower threshold. + /// Threshold limit. + public AdaptiveThresholdProcessor(Color upper, Color lower, float thresholdLimit) { - this.pixelOpInstance = PixelOperations.Instance; - this.Upper = upper; this.Lower = lower; this.ThresholdLimit = thresholdLimit; } /// - /// Gets or sets upper color limit for thresholding + /// Gets or sets upper color limit for thresholding. /// - public TPixel Upper { get; set; } + public Color Upper { get; set; } /// - /// Gets or sets lower color limit for threshold + /// Gets or sets lower color limit for threshold. /// - public TPixel Lower { get; set; } + public Color Lower { get; set; } /// - /// Gets or sets the value for threshold limit + /// Gets or sets the value for threshold limit. /// public float ThresholdLimit { get; set; } - /// - protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) - { - Rectangle intersect = Rectangle.Intersect(sourceRectangle, source.Bounds()); - - // Used ushort because the values should never exceed max ushort value - ushort startY = (ushort)intersect.Y; - ushort endY = (ushort)intersect.Bottom; - ushort startX = (ushort)intersect.X; - ushort endX = (ushort)intersect.Right; - - ushort width = (ushort)intersect.Width; - ushort height = (ushort)intersect.Height; - - // ClusterSize defines the size of cluster to used to check for average. Tweaked to support upto 4k wide pixels and not more. 4096 / 16 is 256 thus the '-1' - byte clusterSize = (byte)Math.Truncate((width / 16f) - 1); - - // Using pooled 2d buffer for integer image table and temp memory to hold Rgb24 converted pixel data - using (Buffer2D intImage = configuration.MemoryAllocator.Allocate2D(width, height)) - using (IMemoryOwner tmpBuffer = configuration.MemoryAllocator.Allocate(width * height)) - { - // Defines the rectangle section of the image to work on - Rectangle workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); - - this.pixelOpInstance.ToRgb24(source.GetPixelSpan(), tmpBuffer.GetSpan()); - - for (ushort x = startX; x < endX; x++) - { - Span rgbSpan = tmpBuffer.GetSpan(); - ulong sum = 0; - for (ushort y = startY; y < endY; y++) - { - ref Rgb24 rgb = ref rgbSpan[(width * y) + x]; - - sum += (ulong)(rgb.R + rgb.G + rgb.G); - if (x - startX != 0) - { - intImage[x - startX, y - startY] = intImage[x - startX - 1, y - startY] + sum; - } - else - { - intImage[x - startX, y - startY] = sum; - } - } - } - - ParallelHelper.IterateRows( - workingRectangle, - configuration, - rows => - { - Span rgbSpan = tmpBuffer.GetSpan(); - ushort x1, y1, x2, y2; - uint count = 0; - long sum = 0; - - for (ushort x = startX; x < endX; x++) - { - for (ushort y = (ushort)rows.Min; y < (ushort)rows.Max; y++) - { - ref Rgb24 rgb = ref rgbSpan[(width * y) + x]; - - x1 = (ushort)Math.Max(x - startX - clusterSize + 1, 0); - x2 = (ushort)Math.Min(x - startX + clusterSize + 1, width - 1); - y1 = (ushort)Math.Max(y - startY - clusterSize + 1, 0); - y2 = (ushort)Math.Min(y - startY + clusterSize + 1, height - 1); - - count = (uint)((x2 - x1) * (y2 - y1)); - sum = (long)Math.Min(intImage[x2, y2] - intImage[x1, y2] - intImage[x2, y1] + intImage[x1, y1], long.MaxValue); - - if ((rgb.R + rgb.G + rgb.B) * count <= sum * this.ThresholdLimit) - { - source[x, y] = this.Lower; - } - else - { - source[x, y] = this.Upper; - } - } - } - }); - } - } + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new AdaptiveThresholdProcessor(configuration, this, source, sourceRectangle); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs new file mode 100644 index 000000000..130fc40f3 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs @@ -0,0 +1,168 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; + +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Binarization +{ + /// + /// Performs Bradley Adaptive Threshold filter against an image. + /// + internal class AdaptiveThresholdProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + private readonly AdaptiveThresholdProcessor definition; + private readonly PixelOperations pixelOpInstance; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// 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.pixelOpInstance = PixelOperations.Instance; + this.definition = definition; + } + + /// + protected override void OnFrameApply(ImageFrame source) + { + var intersect = 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; + + // Used ushort because the values should never exceed max ushort value. + ushort startY = (ushort)intersect.Y; + ushort endY = (ushort)intersect.Bottom; + ushort startX = (ushort)intersect.X; + ushort endX = (ushort)intersect.Right; + + ushort width = (ushort)intersect.Width; + ushort height = (ushort)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); + + // 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)) + using (IMemoryOwner tmpBuffer = this.Configuration.MemoryAllocator.Allocate(width * height)) + { + // Defines the rectangle section of the image to work on. + var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); + + this.pixelOpInstance.ToRgb24(this.Configuration, source.GetPixelSpan(), tmpBuffer.GetSpan()); + + for (ushort x = startX; x < endX; x++) + { + Span rgbSpan = tmpBuffer.GetSpan(); + ulong sum = 0; + for (ushort y = startY; y < endY; y++) + { + ref Rgb24 rgb = ref rgbSpan[(width * y) + x]; + + sum += (ulong)(rgb.R + rgb.G + rgb.G); + 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(workingRectangle, source, tmpBuffer, intImage, upper, lower, thresholdLimit, clusterSize, startX, endX, startY); + ParallelRowIterator.IterateRows( + configuration, + workingRectangle, + in operation); + } + } + + private readonly struct RowOperation : IRowOperation + { + private readonly Rectangle bounds; + private readonly ImageFrame source; + private readonly IMemoryOwner tmpBuffer; + private readonly Buffer2D intImage; + private readonly TPixel upper; + private readonly TPixel lower; + private readonly float thresholdLimit; + private readonly ushort startX; + private readonly ushort endX; + private readonly ushort startY; + private readonly byte clusterSize; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowOperation( + Rectangle bounds, + ImageFrame source, + IMemoryOwner tmpBuffer, + Buffer2D intImage, + TPixel upper, + TPixel lower, + float thresholdLimit, + byte clusterSize, + ushort startX, + ushort endX, + ushort startY) + { + this.bounds = bounds; + this.source = source; + this.tmpBuffer = tmpBuffer; + 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) + { + Span rgbSpan = this.tmpBuffer.GetSpan(); + ushort x1, y1, x2, y2; + + for (ushort x = this.startX; x < this.endX; x++) + { + ref Rgb24 rgb = ref rgbSpan[(this.bounds.Width * y) + x]; + + x1 = (ushort)Math.Max(x - this.startX - this.clusterSize + 1, 0); + x2 = (ushort)Math.Min(x - this.startX + this.clusterSize + 1, this.bounds.Width - 1); + y1 = (ushort)Math.Max(y - this.startY - this.clusterSize + 1, 0); + y2 = (ushort)Math.Min(y - this.startY + this.clusterSize + 1, this.bounds.Height - 1); + + 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); + + if ((rgb.R + rgb.G + rgb.B) * count <= sum * this.thresholdLimit) + { + this.source[x, y] = this.lower; + } + else + { + this.source[x, y] = this.upper; + } + } + } + } + } +} From 69639450979861eff73bc7cc17963f4e79a8f8fa Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 2 Apr 2020 15:18:53 +0200 Subject: [PATCH 17/22] Add tests for the AdaptiveThreshold processor --- .../Binarization/AdaptiveThresholdTests.cs | 77 ++++++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 3 + tests/Images/External | 2 +- tests/Images/Input/Png/Bradley01.png | Bin 0 -> 25266 bytes tests/Images/Input/Png/Bradley02.png | Bin 0 -> 26467 bytes 5 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 tests/ImageSharp.Tests/Processing/Binarization/AdaptiveThresholdTests.cs create mode 100644 tests/Images/Input/Png/Bradley01.png create mode 100644 tests/Images/Input/Png/Bradley02.png diff --git a/tests/ImageSharp.Tests/Processing/Binarization/AdaptiveThresholdTests.cs b/tests/ImageSharp.Tests/Processing/Binarization/AdaptiveThresholdTests.cs new file mode 100644 index 000000000..309716eb5 --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Binarization/AdaptiveThresholdTests.cs @@ -0,0 +1,77 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.Tga; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Binarization; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Processing.Binarization +{ + public class AdaptiveThresholdTests : BaseImageOperationsExtensionTest + { + [Fact] + public void AdaptiveThreshold_UsesDefaults_Works() + { + var expectedThresholdLimit = .85f; + Color expectedUpper = Color.White; + Color expectedLower = Color.Black; + this.operations.AdaptiveThreshold(); + AdaptiveThresholdProcessor p = this.Verify(); + Assert.Equal(expectedThresholdLimit, p.ThresholdLimit); + Assert.Equal(expectedUpper, p.Upper); + Assert.Equal(expectedLower, p.Lower); + } + + [Fact] + public void AdaptiveThreshold_SettingThresholdLimit_Works() + { + var expectedThresholdLimit = .65f; + this.operations.AdaptiveThreshold(expectedThresholdLimit); + AdaptiveThresholdProcessor p = this.Verify(); + Assert.Equal(expectedThresholdLimit, p.ThresholdLimit); + Assert.Equal(Color.White, p.Upper); + Assert.Equal(Color.Black, p.Lower); + } + + [Fact] + public void AdaptiveThreshold_SettingUpperLowerThresholds_Works() + { + Color expectedUpper = Color.HotPink; + Color expectedLower = Color.Yellow; + this.operations.AdaptiveThreshold(expectedUpper, expectedLower); + AdaptiveThresholdProcessor p = this.Verify(); + Assert.Equal(expectedUpper, p.Upper); + Assert.Equal(expectedLower, p.Lower); + } + + [Fact] + public void AdaptiveThreshold_SettingUpperLowerWithThresholdLimit_Works() + { + var expectedThresholdLimit = .77f; + Color expectedUpper = Color.HotPink; + Color expectedLower = Color.Yellow; + this.operations.AdaptiveThreshold(expectedUpper, expectedLower, expectedThresholdLimit); + AdaptiveThresholdProcessor p = this.Verify(); + Assert.Equal(expectedThresholdLimit, p.ThresholdLimit); + Assert.Equal(expectedUpper, p.Upper); + Assert.Equal(expectedLower, p.Lower); + } + + [Theory] + [WithFile(TestImages.Png.Bradley01, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.Bradley02, PixelTypes.Rgba32)] + public void AdaptiveThreshold_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage()) + { + image.Mutate(img => img.AdaptiveThreshold()); + image.DebugSave(provider); + image.CompareToReferenceOutput(ImageComparer.Exact, provider); + } + } + } +} diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 272998a89..e475a7712 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -83,6 +83,9 @@ namespace SixLabors.ImageSharp.Tests public const string Ducky = "Png/ducky.png"; public const string Rainbow = "Png/rainbow.png"; + public const string Bradley01 = "Png/Bradley01.png"; + public const string Bradley02 = "Png/Bradley02.png"; + // Issue 1014: https://github.com/SixLabors/ImageSharp/issues/1014 public const string Issue1014_1 = "Png/issues/Issue_1014_1.png"; public const string Issue1014_2 = "Png/issues/Issue_1014_2.png"; diff --git a/tests/Images/External b/tests/Images/External index fe694a393..c04c8b73a 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit fe694a3938bea3565071a96cb1c90c4cbc586ff9 +Subproject commit c04c8b73a99c1b198597ae640394d91ddd8e033b diff --git a/tests/Images/Input/Png/Bradley01.png b/tests/Images/Input/Png/Bradley01.png new file mode 100644 index 0000000000000000000000000000000000000000..b8c3c0b6f61c2f8126cf87402abdee1aa90d1a96 GIT binary patch literal 25266 zcmV({K+?a7P)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00002 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 lUbL#erNrK`Tm;&!d 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 JPDHLkV1h5aoCyE` literal 0 HcmV?d00001 diff --git a/tests/Images/Input/Png/Bradley02.png b/tests/Images/Input/Png/Bradley02.png new file mode 100644 index 0000000000000000000000000000000000000000..8aea767ab89b1d4f8f32a7f8f834d70ac63136ff GIT binary patch literal 26467 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 O0000eB literal 0 HcmV?d00001 From 75ac0eeb170ea6cb7be598cea853b807e89dc857 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 2 Apr 2020 17:25:46 +0200 Subject: [PATCH 18/22] Remove not needed tmp buffer --- .../AdaptiveThresholdProcessor{TPixel}.cs | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs index 130fc40f3..109631ab8 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs @@ -2,8 +2,8 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Buffers; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; @@ -58,20 +58,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization // 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)) - using (IMemoryOwner tmpBuffer = this.Configuration.MemoryAllocator.Allocate(width * height)) { // Defines the rectangle section of the image to work on. var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); - this.pixelOpInstance.ToRgb24(this.Configuration, source.GetPixelSpan(), tmpBuffer.GetSpan()); - + Rgba32 rgb = default; for (ushort x = startX; x < endX; x++) { - Span rgbSpan = tmpBuffer.GetSpan(); ulong sum = 0; for (ushort y = startY; y < endY; y++) { - ref Rgb24 rgb = ref rgbSpan[(width * y) + x]; + Span row = source.GetPixelRowSpan(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.G); if (x - startX != 0) @@ -85,7 +85,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization } } - var operation = new RowOperation(workingRectangle, source, tmpBuffer, intImage, upper, lower, thresholdLimit, clusterSize, startX, endX, startY); + var operation = new RowOperation(workingRectangle, source, intImage, upper, lower, thresholdLimit, clusterSize, startX, endX, startY); ParallelRowIterator.IterateRows( configuration, workingRectangle, @@ -97,7 +97,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization { private readonly Rectangle bounds; private readonly ImageFrame source; - private readonly IMemoryOwner tmpBuffer; private readonly Buffer2D intImage; private readonly TPixel upper; private readonly TPixel lower; @@ -111,7 +110,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization public RowOperation( Rectangle bounds, ImageFrame source, - IMemoryOwner tmpBuffer, Buffer2D intImage, TPixel upper, TPixel lower, @@ -123,7 +121,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization { this.bounds = bounds; this.source = source; - this.tmpBuffer = tmpBuffer; this.intImage = intImage; this.upper = upper; this.lower = lower; @@ -138,17 +135,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(int y) { - Span rgbSpan = this.tmpBuffer.GetSpan(); - ushort x1, y1, x2, y2; + Rgba32 rgb = default; for (ushort x = this.startX; x < this.endX; x++) { - ref Rgb24 rgb = ref rgbSpan[(this.bounds.Width * y) + x]; + TPixel pixel = this.source.PixelBuffer[x, y]; + pixel.ToRgba32(ref rgb); - x1 = (ushort)Math.Max(x - this.startX - this.clusterSize + 1, 0); - x2 = (ushort)Math.Min(x - this.startX + this.clusterSize + 1, this.bounds.Width - 1); - y1 = (ushort)Math.Max(y - this.startY - this.clusterSize + 1, 0); - y2 = (ushort)Math.Min(y - this.startY + this.clusterSize + 1, this.bounds.Height - 1); + var x1 = (ushort)Math.Max(x - this.startX - this.clusterSize + 1, 0); + var x2 = (ushort)Math.Min(x - this.startX + this.clusterSize + 1, this.bounds.Width - 1); + var y1 = (ushort)Math.Max(y - this.startY - this.clusterSize + 1, 0); + var y2 = (ushort)Math.Min(y - this.startY + this.clusterSize + 1, this.bounds.Height - 1); 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); From a468883110cf413ba6380c29e0bddb41c6356c74 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 2 Apr 2020 17:48:06 +0200 Subject: [PATCH 19/22] Changed startX and endX from ushort to int, Add test with rectangle --- .../AdaptiveThresholdProcessor{TPixel}.cs | 31 ++++++----- .../Binarization/AdaptiveThresholdTests.cs | 52 ++++++++++++++++++- tests/Images/External | 2 +- 3 files changed, 67 insertions(+), 18 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs index 109631ab8..6daf3a8ed 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs @@ -44,14 +44,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization TPixel lower = this.definition.Lower.ToPixel(); float thresholdLimit = this.definition.ThresholdLimit; - // Used ushort because the values should never exceed max ushort value. - ushort startY = (ushort)intersect.Y; - ushort endY = (ushort)intersect.Bottom; - ushort startX = (ushort)intersect.X; - ushort endX = (ushort)intersect.Right; + int startY = intersect.Y; + int endY = intersect.Bottom; + int startX = intersect.X; + int endX = intersect.Right; - ushort width = (ushort)intersect.Width; - ushort height = (ushort)intersect.Height; + 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); @@ -63,10 +62,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); Rgba32 rgb = default; - for (ushort x = startX; x < endX; x++) + for (int x = startX; x < endX; x++) { ulong sum = 0; - for (ushort y = startY; y < endY; y++) + for (int y = startY; y < endY; y++) { Span row = source.GetPixelRowSpan(y); ref TPixel rowRef = ref MemoryMarshal.GetReference(row); @@ -101,9 +100,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization private readonly TPixel upper; private readonly TPixel lower; private readonly float thresholdLimit; - private readonly ushort startX; - private readonly ushort endX; - private readonly ushort startY; + private readonly int startX; + private readonly int endX; + private readonly int startY; private readonly byte clusterSize; [MethodImpl(InliningOptions.ShortMethod)] @@ -115,9 +114,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization TPixel lower, float thresholdLimit, byte clusterSize, - ushort startX, - ushort endX, - ushort startY) + int startX, + int endX, + int startY) { this.bounds = bounds; this.source = source; @@ -137,7 +136,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization { Rgba32 rgb = default; - for (ushort x = this.startX; x < this.endX; x++) + for (int x = this.startX; x < this.endX; x++) { TPixel pixel = this.source.PixelBuffer[x, y]; pixel.ToRgba32(ref rgb); diff --git a/tests/ImageSharp.Tests/Processing/Binarization/AdaptiveThresholdTests.cs b/tests/ImageSharp.Tests/Processing/Binarization/AdaptiveThresholdTests.cs index 309716eb5..f992ac35b 100644 --- a/tests/ImageSharp.Tests/Processing/Binarization/AdaptiveThresholdTests.cs +++ b/tests/ImageSharp.Tests/Processing/Binarization/AdaptiveThresholdTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Binarization; @@ -15,10 +14,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization [Fact] public void AdaptiveThreshold_UsesDefaults_Works() { + // arrange var expectedThresholdLimit = .85f; Color expectedUpper = Color.White; Color expectedLower = Color.Black; + + // act this.operations.AdaptiveThreshold(); + + // assert AdaptiveThresholdProcessor p = this.Verify(); Assert.Equal(expectedThresholdLimit, p.ThresholdLimit); Assert.Equal(expectedUpper, p.Upper); @@ -28,8 +32,13 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization [Fact] public void AdaptiveThreshold_SettingThresholdLimit_Works() { + // arrange var expectedThresholdLimit = .65f; + + // act this.operations.AdaptiveThreshold(expectedThresholdLimit); + + // assert AdaptiveThresholdProcessor p = this.Verify(); Assert.Equal(expectedThresholdLimit, p.ThresholdLimit); Assert.Equal(Color.White, p.Upper); @@ -39,9 +48,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization [Fact] public void AdaptiveThreshold_SettingUpperLowerThresholds_Works() { + // arrange Color expectedUpper = Color.HotPink; Color expectedLower = Color.Yellow; + + // act this.operations.AdaptiveThreshold(expectedUpper, expectedLower); + + // assert AdaptiveThresholdProcessor p = this.Verify(); Assert.Equal(expectedUpper, p.Upper); Assert.Equal(expectedLower, p.Lower); @@ -50,16 +64,39 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization [Fact] public void AdaptiveThreshold_SettingUpperLowerWithThresholdLimit_Works() { + // arrange var expectedThresholdLimit = .77f; Color expectedUpper = Color.HotPink; Color expectedLower = Color.Yellow; + + // act this.operations.AdaptiveThreshold(expectedUpper, expectedLower, expectedThresholdLimit); + + // assert AdaptiveThresholdProcessor p = this.Verify(); Assert.Equal(expectedThresholdLimit, p.ThresholdLimit); Assert.Equal(expectedUpper, p.Upper); Assert.Equal(expectedLower, p.Lower); } + [Fact] + public void AdaptiveThreshold_SettingUpperLowerWithThresholdLimit_WithRectangle_Works() + { + // arrange + var expectedThresholdLimit = .77f; + Color expectedUpper = Color.HotPink; + Color expectedLower = Color.Yellow; + + // act + this.operations.AdaptiveThreshold(expectedUpper, expectedLower, expectedThresholdLimit, this.rect); + + // assert + AdaptiveThresholdProcessor p = this.Verify(this.rect); + Assert.Equal(expectedThresholdLimit, p.ThresholdLimit); + Assert.Equal(expectedUpper, p.Upper); + Assert.Equal(expectedLower, p.Lower); + } + [Theory] [WithFile(TestImages.Png.Bradley01, PixelTypes.Rgba32)] [WithFile(TestImages.Png.Bradley02, PixelTypes.Rgba32)] @@ -73,5 +110,18 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization image.CompareToReferenceOutput(ImageComparer.Exact, provider); } } + + [Theory] + [WithFile(TestImages.Png.Bradley02, PixelTypes.Rgba32)] + public void AdaptiveThreshold_WithRectangle_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage()) + { + image.Mutate(img => img.AdaptiveThreshold(Color.White, Color.Black, new Rectangle(60, 90, 200, 30))); + image.DebugSave(provider); + image.CompareToReferenceOutput(ImageComparer.Exact, provider); + } + } } } diff --git a/tests/Images/External b/tests/Images/External index c04c8b73a..6fdc6d19b 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit c04c8b73a99c1b198597ae640394d91ddd8e033b +Subproject commit 6fdc6d19b101dc1c00a297d3e92257df60c413d0 From 1c926703a87f93dc77b55fa1304074a1ae59e459 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 2 Apr 2020 19:59:24 +0200 Subject: [PATCH 20/22] Using pixel row span to access the pixels --- .../Binarization/AdaptiveThresholdProcessor{TPixel}.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs index 6daf3a8ed..7b3e10b0d 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs @@ -135,10 +135,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization public void Invoke(int y) { Rgba32 rgb = default; + Span pixelRow = this.source.GetPixelRowSpan(y); for (int x = this.startX; x < this.endX; x++) { - TPixel pixel = this.source.PixelBuffer[x, y]; + TPixel pixel = pixelRow[x]; pixel.ToRgba32(ref rgb); var x1 = (ushort)Math.Max(x - this.startX - this.clusterSize + 1, 0); From 50aa77e3a002df178afd823d4b391e9b5ea10db8 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 2 Apr 2020 23:15:48 +0200 Subject: [PATCH 21/22] Review changes --- .../AdaptiveThresholdProcessor{TPixel}.cs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs index 7b3e10b0d..dd8833ad9 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs @@ -18,7 +18,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization where TPixel : unmanaged, IPixel { private readonly AdaptiveThresholdProcessor definition; - private readonly PixelOperations pixelOpInstance; /// /// Initializes a new instance of the class. @@ -30,7 +29,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization public AdaptiveThresholdProcessor(Configuration configuration, AdaptiveThresholdProcessor definition, Image source, Rectangle sourceRectangle) : base(configuration, source, sourceRectangle) { - this.pixelOpInstance = PixelOperations.Instance; this.definition = definition; } @@ -58,9 +56,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization // 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)) { - // Defines the rectangle section of the image to work on. - var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); - Rgba32 rgb = default; for (int x = startX; x < endX; x++) { @@ -84,10 +79,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization } } - var operation = new RowOperation(workingRectangle, source, intImage, upper, lower, thresholdLimit, clusterSize, startX, endX, startY); + var operation = new RowOperation(intersect, source, intImage, upper, lower, thresholdLimit, clusterSize, startX, endX, startY); ParallelRowIterator.IterateRows( configuration, - workingRectangle, + intersect, in operation); } } @@ -142,10 +137,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization TPixel pixel = pixelRow[x]; pixel.ToRgba32(ref rgb); - var x1 = (ushort)Math.Max(x - this.startX - this.clusterSize + 1, 0); - var x2 = (ushort)Math.Min(x - this.startX + this.clusterSize + 1, this.bounds.Width - 1); - var y1 = (ushort)Math.Max(y - this.startY - this.clusterSize + 1, 0); - var y2 = (ushort)Math.Min(y - this.startY + this.clusterSize + 1, this.bounds.Height - 1); + 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); 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); From ee016f61157986fe50c50ff13390d6c485972050 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 3 Apr 2020 11:48:15 +0200 Subject: [PATCH 22/22] Minor formatting change --- src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs b/src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs index 4fc78a958..3279d96e3 100644 --- a/src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs +++ b/src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs @@ -53,7 +53,7 @@ namespace SixLabors.ImageSharp.Processing /// /// The image this method extends. /// Upper (white) color for thresholding. - /// Lower (black) color for thresholding + /// Lower (black) color for thresholding. /// Rectangle region to apply the processor on. /// The . public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, Color upper, Color lower, Rectangle rectangle) @@ -64,7 +64,7 @@ namespace SixLabors.ImageSharp.Processing /// /// The image this method extends. /// Upper (white) color for thresholding. - /// Lower (black) color for thresholding + /// Lower (black) color for thresholding. /// Threshold limit (0.0-1.0) to consider for binarization. /// Rectangle region to apply the processor on. /// The .