Browse Source

Merge branch 'feature/fixAdaptiveHistIssue' into js/update-submodule

af/merge-core
Anton Firszov 7 years ago
parent
commit
d116a80937
  1. 286
      src/ImageSharp.Drawing/Processing/PathGradientBrush.cs
  2. 32
      src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs
  3. 6
      src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs
  4. 209
      tests/ImageSharp.Tests/Drawing/FillPathGradientBrushTests.cs
  5. 26
      tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
  6. 2
      tests/Images/External

286
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
{
/// <summary>
/// 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.
/// </summary>
public sealed class PathGradientBrush : IBrush
{
private readonly Polygon path;
private readonly IList<Edge> edges;
private readonly Color centerColor;
/// <summary>
/// Initializes a new instance of the <see cref="PathGradientBrush"/> class.
/// </summary>
/// <param name="lines">Line segments of a polygon that represents the gradient area.</param>
/// <param name="colors">Array of colors that correspond to each point in the polygon.</param>
/// <param name="centerColor">Color at the center of the gradient area to which the other colors converge.</param>
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();
}
/// <summary>
/// Initializes a new instance of the <see cref="PathGradientBrush"/> class.
/// </summary>
/// <param name="lines">Line segments of a polygon that represents the gradient area.</param>
/// <param name="colors">Array of colors that correspond to each point in the polygon.</param>
public PathGradientBrush(ILineSegment[] lines, Color[] colors)
: this(lines, colors, CalculateCenterColor(colors))
{
}
/// <inheritdoc />
public BrushApplicator<TPixel> CreateApplicator<TPixel>(
ImageFrame<TPixel> source,
RectangleF region,
GraphicsOptions options)
where TPixel : struct, IPixel<TPixel>
{
return new PathGradientBrushApplicator<TPixel>(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; }
}
/// <summary>
/// An edge of the polygon that represents the gradient area.
/// </summary>
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));
}
/// <summary>
/// The path gradient brush applicator.
/// </summary>
private class PathGradientBrushApplicator<TPixel> : BrushApplicator<TPixel>
where TPixel : struct, IPixel<TPixel>
{
private readonly Path path;
private readonly PointF center;
private readonly Vector4 centerColor;
private readonly float maxDistance;
private readonly IList<Edge> edges;
/// <summary>
/// Initializes a new instance of the <see cref="PathGradientBrushApplicator{TPixel}"/> class.
/// </summary>
/// <param name="source">The source image.</param>
/// <param name="path">A polygon that represents the gradient area.</param>
/// <param name="edges">Edges of the polygon.</param>
/// <param name="centerColor">Color at the center of the gradient area to which the other colors converge.</param>
/// <param name="options">The options.</param>
public PathGradientBrushApplicator(
ImageFrame<TPixel> source,
Path path,
IList<Edge> 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();
}
/// <inheritdoc />
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<TPixel>();
}
if (!this.path.Contains(point))
{
return Color.Transparent.ToPixel<TPixel>();
}
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<TPixel>();
}
}
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;
}
/// <inheritdoc />
public override void Dispose()
{
}
}
}
}

32
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)>(); var tileYStartPositions = new List<(int y, int cdfY)>();
int cdfY = 0; 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++; cdfY++;
yStart += tileHeight;
} }
Parallel.For( Parallel.For(
@ -92,7 +94,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
ref TPixel sourceBase = ref source.GetPixelReference(0, 0); ref TPixel sourceBase = ref source.GetPixelReference(0, 0);
int cdfX = 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 tileY = 0;
int yEnd = Math.Min(y + tileHeight, sourceHeight); int yEnd = Math.Min(y + tileHeight, sourceHeight);
@ -125,24 +128,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
} }
cdfX++; cdfX++;
x += tileWidth;
} }
}); });
ref TPixel pixelsBase = ref source.GetPixelReference(0, 0); ref TPixel pixelsBase = ref source.GetPixelReference(0, 0);
// Fix left column // 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 // Fix right column
int rightBorderStartX = ((this.Tiles - 1) * tileWidth) + halfTileWidth; 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 // 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 // Fix bottom row
int bottomBorderStartY = ((this.Tiles - 1) * tileHeight) + halfTileHeight; 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 // Left top corner
ProcessCornerTile(ref pixelsBase, cdfData, sourceWidth, 0, 0, xStart: 0, xEnd: halfTileWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels); 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
/// <param name="cdfX">The X index of the lookup table to use.</param> /// <param name="cdfX">The X index of the lookup table to use.</param>
/// <param name="sourceWidth">The source image width.</param> /// <param name="sourceWidth">The source image width.</param>
/// <param name="sourceHeight">The source image height.</param> /// <param name="sourceHeight">The source image height.</param>
/// <param name="tileWidth">The width of a tile.</param> /// <param name="tileCount">The number of vertical tiles.</param>
/// <param name="tileHeight">The height of a tile.</param> /// <param name="tileHeight">The height of a tile.</param>
/// <param name="xStart">X start position in the image.</param> /// <param name="xStart">X start position in the image.</param>
/// <param name="xEnd">X end position of the image.</param> /// <param name="xEnd">X end position of the image.</param>
@ -220,7 +224,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
int cdfX, int cdfX,
int sourceWidth, int sourceWidth,
int sourceHeight, int sourceHeight,
int tileWidth, int tileCount,
int tileHeight, int tileHeight,
int xStart, int xStart,
int xEnd, int xEnd,
@ -229,7 +233,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
int halfTileHeight = tileHeight / 2; int halfTileHeight = tileHeight / 2;
int cdfY = 0; 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 yLimit = Math.Min(y + tileHeight, sourceHeight - 1);
int tileY = 0; int tileY = 0;
@ -247,6 +252,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
} }
cdfY++; cdfY++;
y += tileHeight;
} }
} }
@ -257,6 +263,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// <param name="cdfData">The pre-computed lookup tables to remap the grey values for each tiles.</param> /// <param name="cdfData">The pre-computed lookup tables to remap the grey values for each tiles.</param>
/// <param name="cdfY">The Y index of the lookup table to use.</param> /// <param name="cdfY">The Y index of the lookup table to use.</param>
/// <param name="sourceWidth">The source image width.</param> /// <param name="sourceWidth">The source image width.</param>
/// <param name="tileCount">The number of horizontal tiles.</param>
/// <param name="tileWidth">The width of a tile.</param> /// <param name="tileWidth">The width of a tile.</param>
/// <param name="yStart">Y start position in the image.</param> /// <param name="yStart">Y start position in the image.</param>
/// <param name="yEnd">Y end position of the image.</param> /// <param name="yEnd">Y end position of the image.</param>
@ -269,6 +276,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
CdfTileData cdfData, CdfTileData cdfData,
int cdfY, int cdfY,
int sourceWidth, int sourceWidth,
int tileCount,
int tileWidth, int tileWidth,
int yStart, int yStart,
int yEnd, int yEnd,
@ -277,7 +285,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
int halfTileWidth = tileWidth / 2; int halfTileWidth = tileWidth / 2;
int cdfX = 0; 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++) for (int dy = yStart; dy < yEnd; dy++)
{ {
@ -294,6 +303,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
} }
cdfX++; cdfX++;
x += tileWidth;
} }
} }

6
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. // Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Normalization namespace SixLabors.ImageSharp.Processing.Processors.Normalization
@ -25,7 +25,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
public int LuminanceLevels { get; set; } = 256; public int LuminanceLevels { get; set; } = 256;
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public bool ClipHistogram { get; set; } = false; public bool ClipHistogram { get; set; } = false;

209
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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
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<ArgumentNullException>(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<ArgumentOutOfRangeException>(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<ArgumentNullException>(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<ArgumentOutOfRangeException>(Create);
}
}
}

26
tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs

@ -9,6 +9,7 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Normalization namespace SixLabors.ImageSharp.Tests.Processing.Normalization
{ {
// ReSharper disable InconsistentNaming
public class HistogramEqualizationTests public class HistogramEqualizationTests
{ {
private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0456F); private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0456F);
@ -113,5 +114,30 @@ namespace SixLabors.ImageSharp.Tests.Processing.Normalization
image.CompareToReferenceOutput(ValidatorComparer, provider); image.CompareToReferenceOutput(ValidatorComparer, provider);
} }
} }
/// <summary>
/// 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
/// </summary>
[Theory]
[WithTestPatternImages(110, 110, PixelTypes.Rgba32)]
[WithTestPatternImages(170, 170, PixelTypes.Rgba32)]
public void Issue984<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> 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);
}
}
} }
} }

2
tests/Images/External

@ -1 +1 @@
Subproject commit 36f39bc624f8a49caf512077bf70cab30c2e5fb4 Subproject commit 99a2bc523cd4eb00e37af20d1b2088fa11564c57
Loading…
Cancel
Save