Browse Source

Add a recolor brush

pull/56/head
Scott Williams 9 years ago
parent
commit
5bcc92cbde
  1. 7
      src/ImageSharp/Drawing/Brushes/IBrush.cs
  2. 2
      src/ImageSharp/Drawing/Brushes/ImageBrush{TColor}.cs
  3. 2
      src/ImageSharp/Drawing/Brushes/PatternBrush{TColor}.cs
  4. 24
      src/ImageSharp/Drawing/Brushes/RecolorBrush.cs
  5. 129
      src/ImageSharp/Drawing/Brushes/RecolorBrush{TColor}.cs
  6. 2
      src/ImageSharp/Drawing/Brushes/SolidBrush{TColor}.cs
  7. 7
      src/ImageSharp/Drawing/Pens/IPen.cs
  8. 15
      src/ImageSharp/Drawing/Pens/Pen{TColor}.cs
  9. 58
      src/ImageSharp/Drawing/Processors/DrawPathProcessor.cs
  10. 2
      src/ImageSharp/Drawing/Processors/FillProcessor.cs
  11. 6
      src/ImageSharp/Drawing/Processors/FillShapeProcessor.cs
  12. 50
      src/ImageSharp/Image/IReadonlyPixelAccessor{TColor}.cs
  13. 2
      src/ImageSharp/Image/PixelAccessor{TColor}.cs
  14. 55
      tests/ImageSharp.Tests/Drawing/RecolorImageTest.cs

7
src/ImageSharp/Drawing/Brushes/IBrush.cs

@ -23,12 +23,15 @@ namespace ImageSharp.Drawing
/// <summary>
/// Creates the applicator for this brush.
/// </summary>
/// <param name="pixelSource">The pixel source.</param>
/// <param name="region">The region the brush will be applied to.</param>
/// <returns>The brush applicator for this brush</returns>
/// <returns>
/// The brush applicator for this brush
/// </returns>
/// <remarks>
/// The <paramref name="region" /> when being applied to things like shapes would usually be the
/// bounding box of the shape not necessarily the bounds of the whole image
/// </remarks>
IBrushApplicator<TColor> CreateApplicator(RectangleF region);
IBrushApplicator<TColor> CreateApplicator(IReadonlyPixelAccessor<TColor> pixelSource, RectangleF region);
}
}

2
src/ImageSharp/Drawing/Brushes/ImageBrush{TColor}.cs

@ -32,7 +32,7 @@ namespace ImageSharp.Drawing.Brushes
}
/// <inheritdoc />
public IBrushApplicator<TColor> CreateApplicator(RectangleF region)
public IBrushApplicator<TColor> CreateApplicator(IReadonlyPixelAccessor<TColor> sourcePixels, RectangleF region)
{
return new ImageBrushApplicator(this.image, region);
}

2
src/ImageSharp/Drawing/Brushes/PatternBrush{TColor}.cs

@ -95,7 +95,7 @@ namespace ImageSharp.Drawing.Brushes
}
/// <inheritdoc />
public IBrushApplicator<TColor> CreateApplicator(RectangleF region)
public IBrushApplicator<TColor> CreateApplicator(IReadonlyPixelAccessor<TColor> sourcePixels, RectangleF region)
{
return new PatternBrushApplicator(this.pattern, this.stride);
}

24
src/ImageSharp/Drawing/Brushes/RecolorBrush.cs

@ -0,0 +1,24 @@
// <copyright file="RecolorBrush.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Brushes
{
/// <summary>
/// Provides an implementation of a recolor brush for painting color changes.
/// </summary>
public class RecolorBrush : RecolorBrush<Color>
{
/// <summary>
/// Initializes a new instance of the <see cref="RecolorBrush" /> class.
/// </summary>
/// <param name="sourceColor">Color of the source.</param>
/// <param name="targetColor">Color of the target.</param>
/// <param name="threashold">The threashold.</param>
public RecolorBrush(Color sourceColor, Color targetColor, float threashold)
: base(sourceColor, targetColor, threashold)
{
}
}
}

129
src/ImageSharp/Drawing/Brushes/RecolorBrush{TColor}.cs

@ -0,0 +1,129 @@
// <copyright file="RecolorBrush{TColor}.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Brushes
{
using System;
using System.Numerics;
using Processors;
/// <summary>
/// Provides an implementation of a brush that can recolor an image
/// </summary>
/// <typeparam name="TColor">The pixel format.</typeparam>
public class RecolorBrush<TColor> : IBrush<TColor>
where TColor : struct, IPackedPixel, IEquatable<TColor>
{
/// <summary>
/// Initializes a new instance of the <see cref="RecolorBrush{TColor}" /> class.
/// </summary>
/// <param name="sourceColor">Color of the source.</param>
/// <param name="targetColor">Color of the target.</param>
/// <param name="threashold">The threashold as a value between 0 and 1.</param>
public RecolorBrush(TColor sourceColor, TColor targetColor, float threashold)
{
this.SourceColor = sourceColor;
this.Threashold = threashold;
this.TargetColor = targetColor;
}
/// <summary>
/// Gets the threashold.
/// </summary>
/// <value>
/// The threashold.
/// </value>
public float Threashold { get; }
/// <summary>
/// Gets the source color.
/// </summary>
/// <value>
/// The color of the source.
/// </value>
public TColor SourceColor { get; }
/// <summary>
/// Gets the target color.
/// </summary>
/// <value>
/// The color of the target.
/// </value>
public TColor TargetColor { get; }
/// <inheritdoc />
public IBrushApplicator<TColor> CreateApplicator(IReadonlyPixelAccessor<TColor> sourcePixels, RectangleF region)
{
return new RecolorBrushApplicator(sourcePixels, this.SourceColor, this.TargetColor, this.Threashold);
}
/// <summary>
/// The recolor brush applicator.
/// </summary>
private class RecolorBrushApplicator : IBrushApplicator<TColor>
{
/// <summary>
/// The source pixel accessor.
/// </summary>
private readonly IReadonlyPixelAccessor<TColor> source;
private readonly Vector4 sourceColor;
private readonly Vector4 targetColor;
private readonly float threashold;
private readonly float totalDistance;
/// <summary>
/// Initializes a new instance of the <see cref="RecolorBrushApplicator" /> class.
/// </summary>
/// <param name="sourcePixels">The source pixels.</param>
/// <param name="sourceColor">Color of the source.</param>
/// <param name="targetColor">Color of the target.</param>
/// <param name="threashold">The threashold .</param>
public RecolorBrushApplicator(IReadonlyPixelAccessor<TColor> sourcePixels, TColor sourceColor, TColor targetColor, float threashold)
{
this.source = sourcePixels;
this.sourceColor = sourceColor.ToVector4();
this.targetColor = targetColor.ToVector4();
// lets hack a min max extreams for a color space by letteing the IPackedPixle clamp our values to something in the correct spaces :)
TColor maxColor = default(TColor);
maxColor.PackFromVector4(new Vector4(float.MaxValue));
TColor minColor = default(TColor);
minColor.PackFromVector4(new Vector4(float.MinValue));
this.totalDistance = Vector4.DistanceSquared(maxColor.ToVector4(), minColor.ToVector4());
this.threashold = this.totalDistance * threashold;
}
/// <summary>
/// Gets the color for a single pixel.
/// </summary>
/// <param name="point">The point.</param>
/// <returns>
/// The color
/// </returns>
public TColor GetColor(Vector2 point)
{
// Offset the requested pixel by the value in the rectangle (the shapes position)
TColor result = this.source[(int)point.X, (int)point.Y];
Vector4 background = result.ToVector4();
float distance = Vector4.DistanceSquared(background, this.sourceColor);
if (distance <= this.threashold)
{
var lerpAmount = (this.threashold - distance) / this.threashold;
Vector4 blended = Vector4BlendTransforms.PremultipliedLerp(background, this.targetColor, lerpAmount);
result.PackFromVector4(blended);
}
return result;
}
/// <inheritdoc />
public void Dispose()
{
// we didn't make the lock on the PixelAccessor we shouldn't release it.
}
}
}
}

2
src/ImageSharp/Drawing/Brushes/SolidBrush{TColor}.cs

@ -40,7 +40,7 @@ namespace ImageSharp.Drawing.Brushes
public TColor Color => this.color;
/// <inheritdoc />
public IBrushApplicator<TColor> CreateApplicator(RectangleF region)
public IBrushApplicator<TColor> CreateApplicator(IReadonlyPixelAccessor<TColor> sourcePixels, RectangleF region)
{
return new SolidBrushApplicator(this.color);
}

7
src/ImageSharp/Drawing/Pens/IPen.cs

@ -18,11 +18,14 @@ namespace ImageSharp.Drawing.Pens
/// <summary>
/// Creates the applicator for applying this pen to an Image
/// </summary>
/// <param name="pixelSource">The pixel source.</param>
/// <param name="region">The region the pen will be applied to.</param>
/// <returns>Returns a the applicator for the pen.</returns>
/// <returns>
/// Returns a the applicator for the pen.
/// </returns>
/// <remarks>
/// The <paramref name="region" /> when being applied to things like shapes would usually be the bounding box of the shape not necessarily the shape of the whole image.
/// </remarks>
IPenApplicator<TColor> CreateApplicator(RectangleF region);
IPenApplicator<TColor> CreateApplicator(IReadonlyPixelAccessor<TColor> pixelSource, RectangleF region);
}
}

15
src/ImageSharp/Drawing/Pens/Pen{TColor}.cs

@ -103,6 +103,7 @@ namespace ImageSharp.Drawing.Pens
/// <summary>
/// Creates the applicator for applying this pen to an Image
/// </summary>
/// <param name="sourcePixels">The source pixels.</param>
/// <param name="region">The region the pen will be applied to.</param>
/// <returns>
/// Returns a the applicator for the pen.
@ -111,16 +112,16 @@ namespace ImageSharp.Drawing.Pens
/// The <paramref name="region" /> when being applied to things like shapes would ussually be the
/// bounding box of the shape not necorserrally the shape of the whole image
/// </remarks>
public IPenApplicator<TColor> CreateApplicator(RectangleF region)
public IPenApplicator<TColor> CreateApplicator(IReadonlyPixelAccessor<TColor> sourcePixels, RectangleF region)
{
if (this.pattern == null || this.pattern.Length < 2)
{
// if there is only one item in the pattern then 100% of it will
// be solid so use the quicker applicator
return new SolidPenApplicator(this.Brush, region, this.Width);
return new SolidPenApplicator(sourcePixels, this.Brush, region, this.Width);
}
return new PatternPenApplicator(this.Brush, region, this.Width, this.pattern);
return new PatternPenApplicator(sourcePixels, this.Brush, region, this.Width, this.pattern);
}
private class SolidPenApplicator : IPenApplicator<TColor>
@ -128,9 +129,9 @@ namespace ImageSharp.Drawing.Pens
private readonly IBrushApplicator<TColor> brush;
private readonly float halfWidth;
public SolidPenApplicator(IBrush<TColor> brush, RectangleF region, float width)
public SolidPenApplicator(IReadonlyPixelAccessor<TColor> sourcePixels, IBrush<TColor> brush, RectangleF region, float width)
{
this.brush = brush.CreateApplicator(region);
this.brush = brush.CreateApplicator(sourcePixels, region);
this.halfWidth = width / 2;
this.RequiredRegion = RectangleF.Outset(region, width);
}
@ -171,9 +172,9 @@ namespace ImageSharp.Drawing.Pens
private readonly float[] pattern;
private readonly float totalLength;
public PatternPenApplicator(IBrush<TColor> brush, RectangleF region, float width, float[] pattern)
public PatternPenApplicator(IReadonlyPixelAccessor<TColor> sourcePixels, IBrush<TColor> brush, RectangleF region, float width, float[] pattern)
{
this.brush = brush.CreateApplicator(region);
this.brush = brush.CreateApplicator(sourcePixels, region);
this.halfWidth = width / 2;
this.totalLength = 0;

58
src/ImageSharp/Drawing/Processors/DrawPathProcessor.cs

@ -86,7 +86,8 @@ namespace ImageSharp.Drawing.Processors
/// <inheritdoc/>
protected override void OnApply(ImageBase<TColor> source, Rectangle sourceRectangle)
{
using (IPenApplicator<TColor> applicator = this.pen.CreateApplicator(this.region))
using (PixelAccessor<TColor> sourcePixels = source.Lock())
using (IPenApplicator<TColor> applicator = this.pen.CreateApplicator(sourcePixels, this.region))
{
var rect = RectangleF.Ceiling(applicator.RequiredRegion);
@ -117,45 +118,42 @@ namespace ImageSharp.Drawing.Processors
polyStartY = 0;
}
using (PixelAccessor<TColor> sourcePixels = source.Lock())
Parallel.For(
minY,
maxY,
this.ParallelOptions,
y =>
{
Parallel.For(
minY,
maxY,
this.ParallelOptions,
y =>
int offsetY = y - polyStartY;
var currentPoint = default(Vector2);
for (int x = minX; x < maxX; x++)
{
int offsetY = y - polyStartY;
var currentPoint = default(Vector2);
for (int x = minX; x < maxX; x++)
{
int offsetX = x - startX;
currentPoint.X = offsetX;
currentPoint.Y = offsetY;
int offsetX = x - startX;
currentPoint.X = offsetX;
currentPoint.Y = offsetY;
var dist = this.Closest(currentPoint);
var dist = this.Closest(currentPoint);
var color = applicator.GetColor(dist);
var color = applicator.GetColor(dist);
var opacity = this.Opacity(color.DistanceFromElement);
var opacity = this.Opacity(color.DistanceFromElement);
if (opacity > Epsilon)
{
int offsetColorX = x - minX;
if (opacity > Epsilon)
{
int offsetColorX = x - minX;
Vector4 backgroundVector = sourcePixels[offsetX, offsetY].ToVector4();
Vector4 sourceVector = color.Color.ToVector4();
Vector4 backgroundVector = sourcePixels[offsetX, offsetY].ToVector4();
Vector4 sourceVector = color.Color.ToVector4();
var finalColor = Vector4BlendTransforms.PremultipliedLerp(backgroundVector, sourceVector, opacity);
finalColor.W = backgroundVector.W;
var finalColor = Vector4BlendTransforms.PremultipliedLerp(backgroundVector, sourceVector, opacity);
finalColor.W = backgroundVector.W;
TColor packed = default(TColor);
packed.PackFromVector4(finalColor);
sourcePixels[offsetX, offsetY] = packed;
}
TColor packed = default(TColor);
packed.PackFromVector4(finalColor);
sourcePixels[offsetX, offsetY] = packed;
}
});
}
}
});
}
}

2
src/ImageSharp/Drawing/Processors/FillProcessor.cs

@ -62,7 +62,7 @@ namespace ImageSharp.Drawing.Processors
// for example If brush is SolidBrush<TColor> then we could just get the color upfront
// and skip using the IBrushApplicator<TColor>?.
using (PixelAccessor<TColor> sourcePixels = source.Lock())
using (IBrushApplicator<TColor> applicator = this.brush.CreateApplicator(sourceRectangle))
using (IBrushApplicator<TColor> applicator = this.brush.CreateApplicator(sourcePixels, sourceRectangle))
{
Parallel.For(
minY,

6
src/ImageSharp/Drawing/Processors/FillShapeProcessor.cs

@ -53,9 +53,9 @@ namespace ImageSharp.Drawing.Processors
int endX = rect.Right + DrawPadding;
int minX = Math.Max(sourceRectangle.Left, startX);
int maxX = Math.Min(sourceRectangle.Right, endX);
int maxX = Math.Min(sourceRectangle.Right - 1, endX);
int minY = Math.Max(sourceRectangle.Top, polyStartY);
int maxY = Math.Min(sourceRectangle.Bottom, polyEndY);
int maxY = Math.Min(sourceRectangle.Bottom - 1, polyEndY);
// Align start/end positions.
minX = Math.Max(0, minX);
@ -75,7 +75,7 @@ namespace ImageSharp.Drawing.Processors
}
using (PixelAccessor<TColor> sourcePixels = source.Lock())
using (IBrushApplicator<TColor> applicator = this.fillColor.CreateApplicator(rect))
using (IBrushApplicator<TColor> applicator = this.fillColor.CreateApplicator(sourcePixels, rect))
{
Parallel.For(
minY,

50
src/ImageSharp/Image/IReadonlyPixelAccessor{TColor}.cs

@ -0,0 +1,50 @@
// <copyright file="IReadonlyPixelAccessor{TColor}.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp
{
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
/// <summary>
/// Provides per-pixel readonly access to generic <see cref="Image{TColor}"/> pixels.
/// </summary>
/// <typeparam name="TColor">The pixel format.</typeparam>
public interface IReadonlyPixelAccessor<TColor>
{
/// <summary>
/// Gets the size of a single pixel in the number of bytes.
/// </summary>
int PixelSize { get; }
/// <summary>
/// Gets the width of one row in the number of bytes.
/// </summary>
int RowStride { get; }
/// <summary>
/// Gets the width of the image.
/// </summary>
int Width { get; }
/// <summary>
/// Gets the height of the image.
/// </summary>
int Height { get; }
/// <summary>
/// Gets or sets the pixel at the specified position.
/// </summary>
/// <param name="x">The x-coordinate of the pixel. Must be greater than zero and smaller than the width of the pixel.</param>
/// <param name="y">The y-coordinate of the pixel. Must be greater than zero and smaller than the width of the pixel.</param>
/// <returns>The <see typeparam="TColor"/> at the specified position.</returns>
TColor this[int x, int y]
{
get;
}
}
}

2
src/ImageSharp/Image/PixelAccessor{TColor}.cs

@ -14,7 +14,7 @@ namespace ImageSharp
/// Provides per-pixel access to generic <see cref="Image{TColor}"/> pixels.
/// </summary>
/// <typeparam name="TColor">The pixel format.</typeparam>
public unsafe class PixelAccessor<TColor> : IDisposable
public unsafe class PixelAccessor<TColor> : IReadonlyPixelAccessor<TColor>, IDisposable
where TColor : struct, IPackedPixel, IEquatable<TColor>
{
/// <summary>

55
tests/ImageSharp.Tests/Drawing/RecolorImageTest.cs

@ -0,0 +1,55 @@
// <copyright file="BlendTest.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Tests
{
using ImageSharp.Drawing.Brushes;
using System.IO;
using System.Linq;
using Xunit;
public class RecolorImageTest : FileTestBase
{
[Fact]
public void ImageShouldRecolorYellowToHotPink()
{
string path = CreateOutputDirectory("Drawing", "RecolorImage");
var brush = new RecolorBrush(Color.Yellow, Color.HotPink, 0.2f);
foreach (TestFile file in Files)
{
Image image = file.CreateImage();
using (FileStream output = File.OpenWrite($"{path}/{file.FileName}"))
{
image.Fill(brush)
.Save(output);
}
}
}
[Fact]
public void ImageShouldRecolorYellowToHotPinkInARectangle()
{
string path = CreateOutputDirectory("Drawing", "RecolorImage");
var brush = new RecolorBrush(Color.Yellow, Color.HotPink, 0.2f);
foreach (TestFile file in Files)
{
Image image = file.CreateImage();
using (FileStream output = File.OpenWrite($"{path}/Shaped_{file.FileName}"))
{
var imageHeight = image.Height;
image.Fill(brush, new Rectangle(0, imageHeight/2 - imageHeight/4, image.Width, imageHeight/2))
.Save(output);
}
}
}
}
}
Loading…
Cancel
Save