diff --git a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs
new file mode 100644
index 000000000..fc84e4a1f
--- /dev/null
+++ b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs
@@ -0,0 +1,286 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.Primitives;
+using SixLabors.Shapes;
+
+namespace SixLabors.ImageSharp.Processing
+{
+ ///
+ /// Provides an implementation of a brush for painting gradients between multiple color positions in 2D coordinates.
+ /// It works similarly with the class in System.Drawing.Drawing2D of the same name.
+ ///
+ public sealed class PathGradientBrush : IBrush
+ {
+ private readonly Polygon path;
+
+ private readonly IList edges;
+
+ private readonly Color centerColor;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Line segments of a polygon that represents the gradient area.
+ /// Array of colors that correspond to each point in the polygon.
+ /// Color at the center of the gradient area to which the other colors converge.
+ public PathGradientBrush(ILineSegment[] lines, Color[] colors, Color centerColor)
+ {
+ if (lines == null)
+ {
+ throw new ArgumentNullException(nameof(lines));
+ }
+
+ if (lines.Length < 3)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(lines),
+ "There must be at least 3 lines to construct a path gradient brush.");
+ }
+
+ if (colors == null)
+ {
+ throw new ArgumentNullException(nameof(colors));
+ }
+
+ if (!colors.Any())
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(colors),
+ "One or more color is needed to construct a path gradient brush.");
+ }
+
+ this.path = new Polygon(lines);
+ this.centerColor = centerColor;
+
+ Color ColorAt(int index) => colors[index % colors.Length];
+
+ this.edges = this.path.LineSegments.Select(s => new Path(s))
+ .Select((path, i) => new Edge(path, ColorAt(i), ColorAt(i + 1))).ToList();
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Line segments of a polygon that represents the gradient area.
+ /// Array of colors that correspond to each point in the polygon.
+ public PathGradientBrush(ILineSegment[] lines, Color[] colors)
+ : this(lines, colors, CalculateCenterColor(colors))
+ {
+ }
+
+ ///
+ public BrushApplicator CreateApplicator(
+ ImageFrame source,
+ RectangleF region,
+ GraphicsOptions options)
+ where TPixel : struct, IPixel
+ {
+ return new PathGradientBrushApplicator(source, this.path, this.edges, this.centerColor, options);
+ }
+
+ private static Color CalculateCenterColor(Color[] colors)
+ {
+ if (colors == null)
+ {
+ throw new ArgumentNullException(nameof(colors));
+ }
+
+ if (!colors.Any())
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(colors),
+ "One or more color is needed to construct a path gradient brush.");
+ }
+
+ return new Color(colors.Select(c => c.ToVector4()).Aggregate((p1, p2) => p1 + p2) / colors.Length);
+ }
+
+ private static float DistanceBetween(PointF p1, PointF p2) => ((Vector2)(p2 - p1)).Length();
+
+ private struct Intersection
+ {
+ public Intersection(PointF point, float distance)
+ {
+ this.Point = point;
+ this.Distance = distance;
+ }
+
+ public PointF Point { get; }
+
+ public float Distance { get; }
+ }
+
+ ///
+ /// An edge of the polygon that represents the gradient area.
+ ///
+ private class Edge
+ {
+ private readonly Path path;
+
+ private readonly float length;
+
+ private readonly PointF[] buffer;
+
+ public Edge(Path path, Color startColor, Color endColor)
+ {
+ this.path = path;
+
+ Vector2[] points = path.LineSegments.SelectMany(s => s.Flatten()).Select(p => (Vector2)p).ToArray();
+
+ this.Start = points.First();
+ this.StartColor = startColor.ToVector4();
+
+ this.End = points.Last();
+ this.EndColor = endColor.ToVector4();
+
+ this.length = DistanceBetween(this.End, this.Start);
+ this.buffer = new PointF[this.path.MaxIntersections];
+ }
+
+ public PointF Start { get; }
+
+ public Vector4 StartColor { get; }
+
+ public PointF End { get; }
+
+ public Vector4 EndColor { get; }
+
+ public Intersection? FindIntersection(PointF start, PointF end)
+ {
+ int intersections = this.path.FindIntersections(start, end, this.buffer);
+
+ if (intersections == 0)
+ {
+ return null;
+ }
+
+ return this.buffer.Take(intersections)
+ .Select(p => new Intersection(point: p, distance: ((Vector2)(p - start)).LengthSquared()))
+ .Aggregate((min, current) => min.Distance > current.Distance ? current : min);
+ }
+
+ public Vector4 ColorAt(float distance)
+ {
+ float ratio = this.length > 0 ? distance / this.length : 0;
+
+ return Vector4.Lerp(this.StartColor, this.EndColor, ratio);
+ }
+
+ public Vector4 ColorAt(PointF point) => this.ColorAt(DistanceBetween(point, this.Start));
+ }
+
+ ///
+ /// The path gradient brush applicator.
+ ///
+ private class PathGradientBrushApplicator : BrushApplicator
+ where TPixel : struct, IPixel
+ {
+ private readonly Path path;
+
+ private readonly PointF center;
+
+ private readonly Vector4 centerColor;
+
+ private readonly float maxDistance;
+
+ private readonly IList edges;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The source image.
+ /// A polygon that represents the gradient area.
+ /// Edges of the polygon.
+ /// Color at the center of the gradient area to which the other colors converge.
+ /// The options.
+ public PathGradientBrushApplicator(
+ ImageFrame source,
+ Path path,
+ IList edges,
+ Color centerColor,
+ GraphicsOptions options)
+ : base(source, options)
+ {
+ this.path = path;
+ this.edges = edges;
+
+ PointF[] points = path.LineSegments.Select(s => s.EndPoint).ToArray();
+
+ this.center = points.Aggregate((p1, p2) => p1 + p2) / points.Length;
+ this.centerColor = centerColor.ToVector4();
+
+ this.maxDistance = points.Select(p => (Vector2)(p - this.center)).Select(d => d.Length()).Max();
+ }
+
+ ///
+ internal override TPixel this[int x, int y]
+ {
+ get
+ {
+ var point = new PointF(x, y);
+
+ if (point == this.center)
+ {
+ return new Color(this.centerColor).ToPixel();
+ }
+
+ if (!this.path.Contains(point))
+ {
+ return Color.Transparent.ToPixel();
+ }
+
+ Vector2 direction = Vector2.Normalize(point - this.center);
+
+ PointF end = point + (PointF)(direction * this.maxDistance);
+
+ (Edge edge, Intersection? info) = this.FindIntersection(point, end);
+
+ PointF intersection = info.Value.Point;
+
+ Vector4 edgeColor = edge.ColorAt(intersection);
+
+ float length = DistanceBetween(intersection, this.center);
+ float ratio = length > 0 ? DistanceBetween(intersection, point) / length : 0;
+
+ Vector4 color = Vector4.Lerp(edgeColor, this.centerColor, ratio);
+
+ return new Color(color).ToPixel();
+ }
+ }
+
+ private (Edge edge, Intersection? info) FindIntersection(PointF start, PointF end)
+ {
+ (Edge edge, Intersection? info) closest = default;
+
+ foreach (Edge edge in this.edges)
+ {
+ Intersection? intersection = edge.FindIntersection(start, end);
+
+ if (!intersection.HasValue)
+ {
+ continue;
+ }
+
+ if (closest.info == null || closest.info.Value.Distance > intersection.Value.Distance)
+ {
+ closest = (edge, intersection);
+ }
+ }
+
+ return closest;
+ }
+
+ ///
+ public override void Dispose()
+ {
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs
index 08222d4ca..4cda4030f 100644
--- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs
+++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs
@@ -73,10 +73,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
var tileYStartPositions = new List<(int y, int cdfY)>();
int cdfY = 0;
- for (int y = halfTileHeight; y < sourceHeight - halfTileHeight; y += tileHeight)
+ int yStart = halfTileHeight;
+ for (int tile = 0; tile < tileCount - 1; tile++)
{
- tileYStartPositions.Add((y, cdfY));
+ tileYStartPositions.Add((yStart, cdfY));
cdfY++;
+ yStart += tileHeight;
}
Parallel.For(
@@ -92,7 +94,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
ref TPixel sourceBase = ref source.GetPixelReference(0, 0);
int cdfX = 0;
- for (int x = halfTileWidth; x < sourceWidth - halfTileWidth; x += tileWidth)
+ int x = halfTileWidth;
+ for (int tile = 0; tile < tileCount - 1; tile++)
{
int tileY = 0;
int yEnd = Math.Min(y + tileHeight, sourceHeight);
@@ -125,24 +128,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
}
cdfX++;
+ x += tileWidth;
}
});
ref TPixel pixelsBase = ref source.GetPixelReference(0, 0);
// Fix left column
- ProcessBorderColumn(ref pixelsBase, cdfData, 0, sourceWidth, sourceHeight, tileWidth, tileHeight, xStart: 0, xEnd: halfTileWidth, luminanceLevels);
+ ProcessBorderColumn(ref pixelsBase, cdfData, 0, sourceWidth, 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, tileWidth, tileHeight, xStart: rightBorderStartX, xEnd: sourceWidth, luminanceLevels);
+ ProcessBorderColumn(ref pixelsBase, cdfData, this.Tiles - 1, sourceWidth, sourceHeight, this.Tiles, tileHeight, xStart: rightBorderStartX, xEnd: sourceWidth, luminanceLevels);
// Fix top row
- ProcessBorderRow(ref pixelsBase, cdfData, 0, sourceWidth, tileWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels);
+ ProcessBorderRow(ref pixelsBase, 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, tileWidth, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels);
+ ProcessBorderRow(ref pixelsBase, 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);
@@ -206,7 +210,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// The X index of the lookup table to use.
/// The source image width.
/// The source image height.
- /// The width of a tile.
+ /// The number of vertical tiles.
/// The height of a tile.
/// X start position in the image.
/// X end position of the image.
@@ -220,7 +224,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
int cdfX,
int sourceWidth,
int sourceHeight,
- int tileWidth,
+ int tileCount,
int tileHeight,
int xStart,
int xEnd,
@@ -229,7 +233,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
int halfTileHeight = tileHeight / 2;
int cdfY = 0;
- for (int y = halfTileHeight; y < sourceHeight - halfTileHeight; y += tileHeight)
+ int y = halfTileHeight;
+ for (int tile = 0; tile < tileCount - 1; tile++)
{
int yLimit = Math.Min(y + tileHeight, sourceHeight - 1);
int tileY = 0;
@@ -247,6 +252,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
}
cdfY++;
+ y += tileHeight;
}
}
@@ -257,6 +263,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// 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.
+ /// The number of horizontal tiles.
/// The width of a tile.
/// Y start position in the image.
/// Y end position of the image.
@@ -269,6 +276,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
CdfTileData cdfData,
int cdfY,
int sourceWidth,
+ int tileCount,
int tileWidth,
int yStart,
int yEnd,
@@ -277,7 +285,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
int halfTileWidth = tileWidth / 2;
int cdfX = 0;
- for (int x = halfTileWidth; x < sourceWidth - halfTileWidth; x += tileWidth)
+ int x = halfTileWidth;
+ for (int tile = 0; tile < tileCount - 1; tile++)
{
for (int dy = yStart; dy < yEnd; dy++)
{
@@ -294,6 +303,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
}
cdfX++;
+ x += tileWidth;
}
}
diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs
index 1d9d5c986..8ddb4834d 100644
--- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs
+++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors and contributors.
+// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Normalization
@@ -25,7 +25,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
public int LuminanceLevels { get; set; } = 256;
///
- /// Gets or sets a value indicating whether to clip the histogram bins at a specific value. Defaults to false.
+ /// Gets or sets a value indicating whether to clip the histogram bins at a specific value.
+ /// It is recommended to use clipping when the AdaptiveTileInterpolation method is used, to suppress artifacts which can occur on the borders of the tiles.
+ /// Defaults to false.
///
public bool ClipHistogram { get; set; } = false;
diff --git a/tests/ImageSharp.Tests/Drawing/FillPathGradientBrushTests.cs b/tests/ImageSharp.Tests/Drawing/FillPathGradientBrushTests.cs
new file mode 100644
index 000000000..d76893108
--- /dev/null
+++ b/tests/ImageSharp.Tests/Drawing/FillPathGradientBrushTests.cs
@@ -0,0 +1,209 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
+using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
+using SixLabors.Primitives;
+using SixLabors.Shapes;
+
+using Xunit;
+
+namespace SixLabors.ImageSharp.Tests.Drawing
+{
+ [GroupOutput("Drawing/GradientBrushes")]
+ public class FillPathGradientBrushTests
+ {
+ public static ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.01f);
+
+ [Theory]
+ [WithBlankImages(10, 10, PixelTypes.Rgba32)]
+ public void FillRectangleWithDifferentColors(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ provider.VerifyOperation(
+ TolerantComparer,
+ image =>
+ {
+ ILineSegment[] path =
+ {
+ new LinearLineSegment(new PointF(0, 0), new PointF(10, 0)),
+ new LinearLineSegment(new PointF(10, 0), new PointF(10, 10)),
+ new LinearLineSegment(new PointF(10, 10), new PointF(0, 10)),
+ new LinearLineSegment(new PointF(0, 10), new PointF(0, 0))
+ };
+
+ Color[] colors = { Color.Black, Color.Red, Color.Yellow, Color.Green };
+
+ var brush = new PathGradientBrush(path, colors);
+
+ image.Mutate(x => x.Fill(brush));
+ image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false);
+ });
+ }
+
+ [Theory]
+ [WithBlankImages(10, 10, PixelTypes.Rgba32)]
+ public void FillTriangleWithDifferentColors(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ provider.VerifyOperation(
+ TolerantComparer,
+ image =>
+ {
+ ILineSegment[] path =
+ {
+ new LinearLineSegment(new PointF(5, 0), new PointF(10, 10)),
+ new LinearLineSegment(new PointF(10, 10), new PointF(0, 10)),
+ new LinearLineSegment(new PointF(0, 10), new PointF(5, 0))
+ };
+
+ Color[] colors = { Color.Red, Color.Green, Color.Blue };
+
+ var brush = new PathGradientBrush(path, colors);
+
+ image.Mutate(x => x.Fill(brush));
+ image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false);
+ });
+ }
+
+ [Theory]
+ [WithBlankImages(10, 10, PixelTypes.Rgba32)]
+ public void FillRectangleWithSingleColor(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage())
+ {
+ ILineSegment[] path =
+ {
+ new LinearLineSegment(new PointF(0, 0), new PointF(10, 0)),
+ new LinearLineSegment(new PointF(10, 0), new PointF(10, 10)),
+ new LinearLineSegment(new PointF(10, 10), new PointF(0, 10)),
+ new LinearLineSegment(new PointF(0, 10), new PointF(0, 0))
+ };
+
+ Color[] colors = { Color.Red };
+
+ var brush = new PathGradientBrush(path, colors);
+
+ image.Mutate(x => x.Fill(brush));
+
+ image.ComparePixelBufferTo(Color.Red);
+ }
+ }
+
+ [Theory]
+ [WithBlankImages(10, 10, PixelTypes.Rgba32)]
+ public void ShouldRotateTheColorsWhenThereAreMorePoints(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ provider.VerifyOperation(
+ TolerantComparer,
+ image =>
+ {
+ ILineSegment[] path =
+ {
+ new LinearLineSegment(new PointF(0, 0), new PointF(10, 0)),
+ new LinearLineSegment(new PointF(10, 0), new PointF(10, 10)),
+ new LinearLineSegment(new PointF(10, 10), new PointF(0, 10)),
+ new LinearLineSegment(new PointF(0, 10), new PointF(0, 0))
+ };
+
+ Color[] colors = { Color.Red, Color.Yellow };
+
+ var brush = new PathGradientBrush(path, colors);
+
+ image.Mutate(x => x.Fill(brush));
+ image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false);
+ });
+ }
+
+ [Theory]
+ [WithBlankImages(10, 10, PixelTypes.Rgba32)]
+ public void FillWithCustomCenterColor(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ provider.VerifyOperation(
+ TolerantComparer,
+ image =>
+ {
+ ILineSegment[] path =
+ {
+ new LinearLineSegment(new PointF(0, 0), new PointF(10, 0)),
+ new LinearLineSegment(new PointF(10, 0), new PointF(10, 10)),
+ new LinearLineSegment(new PointF(10, 10), new PointF(0, 10)),
+ new LinearLineSegment(new PointF(0, 10), new PointF(0, 0))
+ };
+
+ Color[] colors = { Color.Black, Color.Red, Color.Yellow, Color.Green };
+
+ var brush = new PathGradientBrush(path, colors, Color.White);
+
+ image.Mutate(x => x.Fill(brush));
+ image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false);
+ });
+ }
+
+ [Fact]
+ public void ShouldThrowArgumentNullExceptionWhenLinesAreNull()
+ {
+ Color[] colors = { Color.Black, Color.Red, Color.Yellow, Color.Green };
+
+ PathGradientBrush Create() => new PathGradientBrush(null, colors, Color.White);
+
+ Assert.Throws(Create);
+ }
+
+ [Fact]
+ public void ShouldThrowArgumentOutOfRangeExceptionWhenLessThan3LinesAreGiven()
+ {
+ ILineSegment[] path =
+ {
+ new LinearLineSegment(new PointF(0, 0), new PointF(10, 0)),
+ new LinearLineSegment(new PointF(10, 0), new PointF(10, 10))
+ };
+
+ Color[] colors = { Color.Black, Color.Red, Color.Yellow, Color.Green };
+
+ PathGradientBrush Create() => new PathGradientBrush(path, colors, Color.White);
+
+ Assert.Throws(Create);
+ }
+
+ [Fact]
+ public void ShouldThrowArgumentNullExceptionWhenColorsAreNull()
+ {
+ ILineSegment[] path =
+ {
+ new LinearLineSegment(new PointF(0, 0), new PointF(10, 0)),
+ new LinearLineSegment(new PointF(10, 0), new PointF(10, 10)),
+ new LinearLineSegment(new PointF(10, 10), new PointF(0, 10)),
+ new LinearLineSegment(new PointF(0, 10), new PointF(0, 0))
+ };
+
+ PathGradientBrush Create() => new PathGradientBrush(path, null, Color.White);
+
+ Assert.Throws(Create);
+ }
+
+ [Fact]
+ public void ShouldThrowArgumentOutOfRangeExceptionWhenEmptyColorArrayIsGiven()
+ {
+ ILineSegment[] path =
+ {
+ new LinearLineSegment(new PointF(0, 0), new PointF(10, 0)),
+ new LinearLineSegment(new PointF(10, 0), new PointF(10, 10)),
+ new LinearLineSegment(new PointF(10, 10), new PointF(0, 10)),
+ new LinearLineSegment(new PointF(0, 10), new PointF(0, 0))
+ };
+
+ var colors = new Color[0];
+
+ PathGradientBrush Create() => new PathGradientBrush(path, colors, Color.White);
+
+ Assert.Throws(Create);
+ }
+ }
+}
diff --git a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
index 5d8a155f0..c71232524 100644
--- a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
+++ b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
@@ -9,6 +9,7 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Normalization
{
+ // ReSharper disable InconsistentNaming
public class HistogramEqualizationTests
{
private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0456F);
@@ -113,5 +114,30 @@ namespace SixLabors.ImageSharp.Tests.Processing.Normalization
image.CompareToReferenceOutput(ValidatorComparer, provider);
}
}
+
+ ///
+ /// This is regression test for a bug with the calculation of the y-start positions,
+ /// where it could happen that one too much start position was calculated in some cases.
+ /// See: https://github.com/SixLabors/ImageSharp/pull/984
+ ///
+ [Theory]
+ [WithTestPatternImages(110, 110, PixelTypes.Rgba32)]
+ [WithTestPatternImages(170, 170, PixelTypes.Rgba32)]
+ public void Issue984(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage())
+ {
+ var options = new HistogramEqualizationOptions()
+ {
+ Method = HistogramEqualizationMethod.AdaptiveTileInterpolation,
+ LuminanceLevels = 256,
+ ClipHistogram = true,
+ NumberOfTiles = 10
+ };
+ image.Mutate(x => x.HistogramEqualization(options));
+ image.DebugSave(provider);
+ }
+ }
}
}
diff --git a/tests/Images/External b/tests/Images/External
index 36f39bc62..99a2bc523 160000
--- a/tests/Images/External
+++ b/tests/Images/External
@@ -1 +1 @@
-Subproject commit 36f39bc624f8a49caf512077bf70cab30c2e5fb4
+Subproject commit 99a2bc523cd4eb00e37af20d1b2088fa11564c57