From 440d8cf11633164995870127abe3b27845325937 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 16 Nov 2020 14:56:11 +0100 Subject: [PATCH 1/4] Use GetPixelRowSpan to access pixels, fixes #1429 --- ...eHistogramEqualizationProcessor{TPixel}.cs | 70 ++++++++----------- 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs index b5b07d7a8..95626ce1e 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs @@ -86,42 +86,39 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization new Rectangle(0, 0, sourceWidth, tileYStartPositions.Count), in operation); - ref TPixel pixelsBase = ref source.GetPixelReference(0, 0); - // Fix left column - ProcessBorderColumn(ref pixelsBase, cdfData, 0, sourceWidth, sourceHeight, this.Tiles, tileHeight, xStart: 0, xEnd: halfTileWidth, luminanceLevels); + ProcessBorderColumn(source, cdfData, 0, sourceHeight, this.Tiles, tileHeight, xStart: 0, xEnd: halfTileWidth, luminanceLevels); // Fix right column int rightBorderStartX = ((this.Tiles - 1) * tileWidth) + halfTileWidth; - ProcessBorderColumn(ref pixelsBase, cdfData, this.Tiles - 1, sourceWidth, sourceHeight, this.Tiles, tileHeight, xStart: rightBorderStartX, xEnd: sourceWidth, luminanceLevels); + ProcessBorderColumn(source, cdfData, this.Tiles - 1, sourceHeight, this.Tiles, tileHeight, xStart: rightBorderStartX, xEnd: sourceWidth, luminanceLevels); // Fix top row - ProcessBorderRow(ref pixelsBase, cdfData, 0, sourceWidth, this.Tiles, tileWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels); + ProcessBorderRow(source, cdfData, 0, sourceWidth, this.Tiles, tileWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels); // Fix bottom row int bottomBorderStartY = ((this.Tiles - 1) * tileHeight) + halfTileHeight; - ProcessBorderRow(ref pixelsBase, cdfData, this.Tiles - 1, sourceWidth, this.Tiles, tileWidth, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels); + ProcessBorderRow(source, cdfData, this.Tiles - 1, sourceWidth, this.Tiles, tileWidth, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels); // Left top corner - ProcessCornerTile(ref pixelsBase, cdfData, sourceWidth, 0, 0, xStart: 0, xEnd: halfTileWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels); + ProcessCornerTile(source, cdfData, 0, 0, xStart: 0, xEnd: halfTileWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels); // Left bottom corner - ProcessCornerTile(ref pixelsBase, cdfData, sourceWidth, 0, this.Tiles - 1, xStart: 0, xEnd: halfTileWidth, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels); + ProcessCornerTile(source, cdfData, 0, this.Tiles - 1, xStart: 0, xEnd: halfTileWidth, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels); // Right top corner - ProcessCornerTile(ref pixelsBase, cdfData, sourceWidth, this.Tiles - 1, 0, xStart: rightBorderStartX, xEnd: sourceWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels); + ProcessCornerTile(source, cdfData, this.Tiles - 1, 0, xStart: rightBorderStartX, xEnd: sourceWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels); // Right bottom corner - ProcessCornerTile(ref pixelsBase, cdfData, sourceWidth, this.Tiles - 1, this.Tiles - 1, xStart: rightBorderStartX, xEnd: sourceWidth, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels); + ProcessCornerTile(source, cdfData, this.Tiles - 1, this.Tiles - 1, xStart: rightBorderStartX, xEnd: sourceWidth, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels); } } /// /// Processes the part of a corner tile which was previously left out. It consists of 1 / 4 of a tile and does not need interpolation. /// - /// The output pixels base reference. + /// The source image. /// The lookup table to remap the grey values. - /// The source image width. /// The x-position in the CDF lookup map. /// The y-position in the CDF lookup map. /// X start position. @@ -133,9 +130,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization /// or 65536 for 16-bit grayscale images. /// private static void ProcessCornerTile( - ref TPixel pixelsBase, + ImageFrame source, CdfTileData cdfData, - int sourceWidth, int cdfX, int cdfY, int xStart, @@ -146,12 +142,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization { for (int dy = yStart; dy < yEnd; dy++) { - int dyOffSet = dy * sourceWidth; + Span rowSpan = source.GetPixelRowSpan(dy); for (int dx = xStart; dx < xEnd; dx++) { - ref TPixel pixel = ref Unsafe.Add(ref pixelsBase, dyOffSet + dx); + TPixel pixel = rowSpan[dx]; float luminanceEqualized = cdfData.RemapGreyValue(cdfX, cdfY, GetLuminance(pixel, luminanceLevels)); - pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); + rowSpan[dx].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); } } } @@ -159,10 +155,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization /// /// Processes a border column of the image which is half the size of the tile width. /// - /// The output pixels reference. + /// The source image. /// The pre-computed lookup tables to remap the grey values for each tiles. /// The X index of the lookup table to use. - /// The source image width. /// The source image height. /// The number of vertical tiles. /// The height of a tile. @@ -173,10 +168,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization /// or 65536 for 16-bit grayscale images. /// private static void ProcessBorderColumn( - ref TPixel pixelBase, + ImageFrame source, CdfTileData cdfData, int cdfX, - int sourceWidth, int sourceHeight, int tileCount, int tileHeight, @@ -194,12 +188,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization int tileY = 0; for (int dy = y; dy < yLimit; dy++) { - int dyOffSet = dy * sourceWidth; + Span rowSpan = source.GetPixelRowSpan(dy); for (int dx = xStart; dx < xEnd; dx++) { - ref TPixel pixel = ref Unsafe.Add(ref pixelBase, dyOffSet + dx); + TPixel pixel = rowSpan[dx]; float luminanceEqualized = InterpolateBetweenTwoTiles(pixel, cdfData, cdfX, cdfY, cdfX, cdfY + 1, tileY, tileHeight, luminanceLevels); - pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); + rowSpan[dx].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); } tileY++; @@ -213,7 +207,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization /// /// Processes a border row of the image which is half of the size of the tile height. /// - /// The output pixels base reference. + /// The source image. /// The pre-computed lookup tables to remap the grey values for each tiles. /// The Y index of the lookup table to use. /// The source image width. @@ -226,7 +220,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization /// or 65536 for 16-bit grayscale images. /// private static void ProcessBorderRow( - ref TPixel pixelBase, + ImageFrame source, CdfTileData cdfData, int cdfY, int sourceWidth, @@ -244,14 +238,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization { for (int dy = yStart; dy < yEnd; dy++) { - int dyOffSet = dy * sourceWidth; + Span rowSpan = source.GetPixelRowSpan(dy); int tileX = 0; int xLimit = Math.Min(x + tileWidth, sourceWidth - 1); for (int dx = x; dx < xLimit; dx++) { - ref TPixel pixel = ref Unsafe.Add(ref pixelBase, dyOffSet + dx); + TPixel pixel = rowSpan[dx]; float luminanceEqualized = InterpolateBetweenTwoTiles(pixel, cdfData, cdfX, cdfY, cdfX + 1, cdfY, tileX, tileWidth, luminanceLevels); - pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); + rowSpan[dx].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); tileX++; } } @@ -410,8 +404,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(in RowInterval rows) { - ref TPixel sourceBase = ref this.source.GetPixelReference(0, 0); - for (int index = rows.Min; index < rows.Max; index++) { (int y, int cdfY) tileYStartPosition = this.tileYStartPositions[index]; @@ -427,11 +419,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization int xEnd = Math.Min(x + this.tileWidth, this.sourceWidth); for (int dy = y; dy < yEnd; dy++) { - int dyOffSet = dy * this.sourceWidth; + Span rowSpan = this.source.GetPixelRowSpan(dy); int tileX = 0; for (int dx = x; dx < xEnd; dx++) { - ref TPixel pixel = ref Unsafe.Add(ref sourceBase, dyOffSet + dx); + TPixel pixel = rowSpan[dx]; float luminanceEqualized = InterpolateBetweenFourTiles( pixel, this.cdfData, @@ -445,7 +437,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization this.tileHeight, this.luminanceLevels); - pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); + rowSpan[dx].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); tileX++; } @@ -597,15 +589,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(in RowInterval rows) { - ref TPixel sourceBase = ref this.source.GetPixelReference(0, 0); - for (int index = rows.Min; index < rows.Max; index++) { int cdfX = 0; int cdfY = this.tileYStartPositions[index].cdfY; int y = this.tileYStartPositions[index].y; int endY = Math.Min(y + this.tileHeight, this.sourceHeight); - ref int cdfMinBase = ref MemoryMarshal.GetReference(this.cdfMinBuffer2D.GetRowSpan(cdfY)); + Span cdfMinSpan = this.cdfMinBuffer2D.GetRowSpan(cdfY); using IMemoryOwner histogramBuffer = this.allocator.Allocate(this.luminanceLevels); Span histogram = histogramBuffer.GetSpan(); @@ -620,10 +610,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization int xlimit = Math.Min(x + this.tileWidth, this.sourceWidth); for (int dy = y; dy < endY; dy++) { - int dyOffset = dy * this.sourceWidth; + Span rowSpan = this.source.GetPixelRowSpan(dy); for (int dx = x; dx < xlimit; dx++) { - int luminance = GetLuminance(Unsafe.Add(ref sourceBase, dyOffset + dx), this.luminanceLevels); + int luminance = GetLuminance(rowSpan[dx], this.luminanceLevels); histogram[luminance]++; } } @@ -633,7 +623,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization this.processor.ClipHistogram(histogram, this.processor.ClipLimit); } - Unsafe.Add(ref cdfMinBase, cdfX) = this.processor.CalculateCdf(ref cdfBase, ref histogramBase, histogram.Length - 1); + cdfMinSpan[cdfX] += this.processor.CalculateCdf(ref cdfBase, ref histogramBase, histogram.Length - 1); cdfX++; } From 9d8ed6c852a45f07c5c8121b701cc1b53e8a927b Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 16 Nov 2020 15:06:11 +0100 Subject: [PATCH 2/4] Use GetPixelRowSpan to access pixel data in global hist equalization --- .../GlobalHistogramEqualizationProcessor{TPixel}.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs index 488426f93..2aaf8cdcd 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs @@ -116,13 +116,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization public void Invoke(int y) { ref int histogramBase = ref MemoryMarshal.GetReference(this.histogramBuffer.GetSpan()); - ref TPixel pixelBase = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y)); + Span pixelRow = this.source.GetPixelRowSpan(y); int levels = this.luminanceLevels; for (int x = 0; x < this.bounds.Width; x++) { // TODO: We should bulk convert here. - var vector = Unsafe.Add(ref pixelBase, x).ToVector4(); + var vector = pixelRow[x].ToVector4(); int luminance = ImageMaths.GetBT709Luminance(ref vector, levels); Interlocked.Increment(ref Unsafe.Add(ref histogramBase, luminance)); } @@ -165,18 +165,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization public void Invoke(int y) { ref int cdfBase = ref MemoryMarshal.GetReference(this.cdfBuffer.GetSpan()); - ref TPixel pixelBase = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y)); + Span pixelRow = this.source.GetPixelRowSpan(y); int levels = this.luminanceLevels; float noOfPixelsMinusCdfMin = this.numberOfPixelsMinusCdfMin; for (int x = 0; x < this.bounds.Width; x++) { // TODO: We should bulk convert here. - ref TPixel pixel = ref Unsafe.Add(ref pixelBase, x); + TPixel pixel = pixelRow[x]; var vector = pixel.ToVector4(); int luminance = ImageMaths.GetBT709Luminance(ref vector, levels); float luminanceEqualized = Unsafe.Add(ref cdfBase, luminance) / noOfPixelsMinusCdfMin; - pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, vector.W)); + pixelRow[x].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, vector.W)); } } } From 3558e7ef97ddc1e3f8a1b8d7a43241799c97d779 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 16 Nov 2020 16:04:13 +0100 Subject: [PATCH 3/4] Use ref TPixel pixel = ref rowSpan[dx]; --- ...tiveHistogramEqualizationProcessor{TPixel}.cs | 16 ++++++++-------- ...obalHistogramEqualizationProcessor{TPixel}.cs | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs index 95626ce1e..14687426d 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs @@ -145,9 +145,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization Span rowSpan = source.GetPixelRowSpan(dy); for (int dx = xStart; dx < xEnd; dx++) { - TPixel pixel = rowSpan[dx]; + ref TPixel pixel = ref rowSpan[dx]; float luminanceEqualized = cdfData.RemapGreyValue(cdfX, cdfY, GetLuminance(pixel, luminanceLevels)); - rowSpan[dx].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); + pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); } } } @@ -191,9 +191,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization Span rowSpan = source.GetPixelRowSpan(dy); for (int dx = xStart; dx < xEnd; dx++) { - TPixel pixel = rowSpan[dx]; + ref TPixel pixel = ref rowSpan[dx]; float luminanceEqualized = InterpolateBetweenTwoTiles(pixel, cdfData, cdfX, cdfY, cdfX, cdfY + 1, tileY, tileHeight, luminanceLevels); - rowSpan[dx].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); + pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); } tileY++; @@ -243,9 +243,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization int xLimit = Math.Min(x + tileWidth, sourceWidth - 1); for (int dx = x; dx < xLimit; dx++) { - TPixel pixel = rowSpan[dx]; + ref TPixel pixel = ref rowSpan[dx]; float luminanceEqualized = InterpolateBetweenTwoTiles(pixel, cdfData, cdfX, cdfY, cdfX + 1, cdfY, tileX, tileWidth, luminanceLevels); - rowSpan[dx].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); + pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); tileX++; } } @@ -423,7 +423,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization int tileX = 0; for (int dx = x; dx < xEnd; dx++) { - TPixel pixel = rowSpan[dx]; + ref TPixel pixel = ref rowSpan[dx]; float luminanceEqualized = InterpolateBetweenFourTiles( pixel, this.cdfData, @@ -437,7 +437,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization this.tileHeight, this.luminanceLevels); - rowSpan[dx].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); + pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); tileX++; } diff --git a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs index 2aaf8cdcd..74d293566 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs @@ -172,11 +172,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization for (int x = 0; x < this.bounds.Width; x++) { // TODO: We should bulk convert here. - TPixel pixel = pixelRow[x]; + ref TPixel pixel = ref pixelRow[x]; var vector = pixel.ToVector4(); int luminance = ImageMaths.GetBT709Luminance(ref vector, levels); float luminanceEqualized = Unsafe.Add(ref cdfBase, luminance) / noOfPixelsMinusCdfMin; - pixelRow[x].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, vector.W)); + pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, vector.W)); } } } From fc4944a2c09f66df6e4eb2727c07b9ed1efd6acf Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 16 Nov 2020 16:36:25 +0100 Subject: [PATCH 4/4] Add histogram equalization benchmark --- .../Processing/HistogramEqualization.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/ImageSharp.Benchmarks/Processing/HistogramEqualization.cs diff --git a/tests/ImageSharp.Benchmarks/Processing/HistogramEqualization.cs b/tests/ImageSharp.Benchmarks/Processing/HistogramEqualization.cs new file mode 100644 index 000000000..081d3e8e3 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Processing/HistogramEqualization.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Normalization; +using SixLabors.ImageSharp.Tests; + +namespace SixLabors.ImageSharp.Benchmarks.Processing +{ + [Config(typeof(Config.ShortClr))] + public class HistogramEqualization : BenchmarkBase + { + private Image image; + + [GlobalSetup] + public void ReadImages() + { + if (this.image == null) + { + this.image = Image.Load(File.OpenRead(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.HistogramEqImage))); + } + } + + [GlobalCleanup] + public void Cleanup() + { + this.image.Dispose(); + } + + [Benchmark(Description = "Global Histogram Equalization")] + public void GlobalHistogramEqualization() + { + this.image.Mutate(img => img.HistogramEqualization(new HistogramEqualizationOptions() + { + LuminanceLevels = 256, + Method = HistogramEqualizationMethod.Global + })); + } + + [Benchmark(Description = "AdaptiveHistogramEqualization (Tile interpolation)")] + public void AdaptiveHistogramEqualization() + { + this.image.Mutate(img => img.HistogramEqualization(new HistogramEqualizationOptions() + { + LuminanceLevels = 256, + Method = HistogramEqualizationMethod.AdaptiveTileInterpolation + })); + } + } +}