Browse Source

Sanitation, performance, vignette, and glow

TODO convolution and colormatrix sanitation


Former-commit-id: 2dc40b04a3d2c7bcf539488d4c0559fd42a48125
Former-commit-id: e79bce183a3fce5bd19c05271ac55dc756f74098
Former-commit-id: 43fa71e167864c565463fd3d2250a9603125661a
af/merge-core
James Jackson-South 10 years ago
parent
commit
6574466aac
  1. 118
      src/ImageProcessorCore/Filters/Glow.cs
  2. 50
      src/ImageProcessorCore/Filters/Processors/AlphaProcessor.cs
  3. 59
      src/ImageProcessorCore/Filters/Processors/BackgroundColorProcessor.cs
  4. 3
      src/ImageProcessorCore/Filters/Processors/Binarization/BinaryThresholdProcessor.cs
  5. 61
      src/ImageProcessorCore/Filters/Processors/BlendProcessor.cs
  6. 53
      src/ImageProcessorCore/Filters/Processors/BrightnessProcessor.cs
  7. 55
      src/ImageProcessorCore/Filters/Processors/ContrastProcessor.cs
  8. 51
      src/ImageProcessorCore/Filters/Processors/GlowProcessor.cs
  9. 49
      src/ImageProcessorCore/Filters/Processors/InvertProcessor.cs
  10. 75
      src/ImageProcessorCore/Filters/Processors/PixelateProcessor.cs
  11. 52
      src/ImageProcessorCore/Filters/Processors/VignetteProcessor.cs
  12. 118
      src/ImageProcessorCore/Filters/Vignette.cs
  13. 2
      tests/ImageProcessorCore.Tests/FileTestBase.cs
  14. 28
      tests/ImageProcessorCore.Tests/Processors/Filters/AlphaTest.cs
  15. 110
      tests/ImageProcessorCore.Tests/Processors/Filters/GlowTest.cs
  16. 24
      tests/ImageProcessorCore.Tests/Processors/Filters/InvertTest.cs
  17. 28
      tests/ImageProcessorCore.Tests/Processors/Filters/PixelateTest.cs
  18. 110
      tests/ImageProcessorCore.Tests/Processors/Filters/VignetteTest.cs

118
src/ImageProcessorCore/Filters/Glow.cs

@ -0,0 +1,118 @@
// <copyright file="Glow.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore
{
using Processors;
/// <summary>
/// Extension methods for the <see cref="Image{T,TP}"/> type.
/// </summary>
public static partial class ImageExtensions
{
/// <summary>
/// Applies a radial glow effect to an image.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="source">The image this method extends.</param>
/// <param name="progressHandler">A delegate which is called as progress is made processing the image.</param>
/// <returns>The <see cref="Image{T,TP}"/>.</returns>
public static Image<T, TP> Glow<T, TP>(this Image<T, TP> source, ProgressEventHandler progressHandler = null)
where T : IPackedVector<TP>
where TP : struct
{
return Glow(source, default(T), source.Bounds.Width * .5F, source.Bounds.Height * .5F, source.Bounds, progressHandler);
}
/// <summary>
/// Applies a radial glow effect to an image.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="source">The image this method extends.</param>
/// <param name="color">The color to set as the glow.</param>
/// <param name="progressHandler">A delegate which is called as progress is made processing the image.</param>
/// <returns>The <see cref="Image{T,TP}"/>.</returns>
public static Image<T, TP> Glow<T, TP>(this Image<T, TP> source, T color, ProgressEventHandler progressHandler = null)
where T : IPackedVector<TP>
where TP : struct
{
return Glow(source, color, source.Bounds.Width * .5F, source.Bounds.Height * .5F, source.Bounds, progressHandler);
}
/// <summary>
/// Applies a radial glow effect to an image.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="source">The image this method extends.</param>
/// <param name="radiusX">The the x-radius.</param>
/// <param name="radiusY">The the y-radius.</param>
/// <param name="progressHandler">A delegate which is called as progress is made processing the image.</param>
/// <returns>The <see cref="Image{T,TP}"/>.</returns>
public static Image<T, TP> Glow<T, TP>(this Image<T, TP> source, float radiusX, float radiusY, ProgressEventHandler progressHandler = null)
where T : IPackedVector<TP>
where TP : struct
{
return Glow(source, default(T), radiusX, radiusY, source.Bounds, progressHandler);
}
/// <summary>
/// Applies a radial glow effect to an image.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="source">The image this method extends.</param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <param name="progressHandler">A delegate which is called as progress is made processing the image.</param>
/// <returns>The <see cref="Image{T,TP}"/>.</returns>
public static Image<T, TP> Glow<T, TP>(this Image<T, TP> source, Rectangle rectangle, ProgressEventHandler progressHandler = null)
where T : IPackedVector<TP>
where TP : struct
{
return Glow(source, default(T), 0, 0, rectangle, progressHandler);
}
/// <summary>
/// Applies a radial glow effect to an image.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="source">The image this method extends.</param>
/// <param name="color">The color to set as the glow.</param>
/// <param name="radiusX">The the x-radius.</param>
/// <param name="radiusY">The the y-radius.</param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <param name="progressHandler">A delegate which is called as progress is made processing the image.</param>
/// <returns>The <see cref="Image{T,TP}"/>.</returns>
public static Image<T, TP> Glow<T, TP>(this Image<T, TP> source, T color, float radiusX, float radiusY, Rectangle rectangle, ProgressEventHandler progressHandler = null)
where T : IPackedVector<TP>
where TP : struct
{
GlowProcessor<T, TP> processor = new GlowProcessor<T, TP> { RadiusX = radiusX, RadiusY = radiusY };
if (!color.Equals(default(T)))
{
processor.GlowColor = color;
}
processor.OnProgress += progressHandler;
try
{
return source.Process(rectangle, processor);
}
finally
{
processor.OnProgress -= progressHandler;
}
}
}
}

50
src/ImageProcessorCore/Filters/Processors/AlphaProcessor.cs

@ -5,11 +5,12 @@
namespace ImageProcessorCore.Processors
{
using System;
using System.Numerics;
using System.Threading.Tasks;
/// <summary>
/// An <see cref="IImageProcessor{T,TP}"/> to change the Alpha of an <see cref="Image{T,TP}"/>.
/// An <see cref="IImageProcessor{T,TP}"/> to change the alpha component of an <see cref="Image{T,TP}"/>.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
@ -38,38 +39,49 @@ namespace ImageProcessorCore.Processors
/// <inheritdoc/>
protected override void Apply(ImageBase<T, TP> target, ImageBase<T, TP> source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY)
{
float alpha = this.Value / 100f;
int sourceY = sourceRectangle.Y;
int sourceBottom = sourceRectangle.Bottom;
float alpha = this.Value / 100F;
int startX = sourceRectangle.X;
int endX = sourceRectangle.Right;
// Align start/end positions.
int minX = Math.Max(0, startX);
int maxX = Math.Min(source.Width, endX);
int minY = Math.Max(0, startY);
int maxY = Math.Min(source.Height, endY);
// Reset offset if necessary.
if (minX > 0)
{
startX = 0;
}
if (minY > 0)
{
startY = 0;
}
Vector4 alphaVector = new Vector4(1, 1, 1, alpha);
using (IPixelAccessor<T, TP> sourcePixels = source.Lock())
using (IPixelAccessor<T, TP> targetPixels = target.Lock())
{
Parallel.For(
startY,
endY,
minY,
maxY,
this.ParallelOptions,
y =>
{
if (y >= sourceY && y < sourceBottom)
int offsetY = y - startY;
for (int x = minX; x < maxX; x++)
{
for (int x = startX; x < endX; x++)
{
Vector4 color = sourcePixels[x, y].ToVector4();
color *= alphaVector;
T packed = default(T);
packed.PackFromVector4(color);
targetPixels[x, y] = packed;
}
this.OnRowProcessed();
int offsetX = x - startX;
T packed = default(T);
packed.PackFromVector4(sourcePixels[offsetX, offsetY].ToVector4() * alphaVector);
targetPixels[offsetX, offsetY] = packed;
}
});
this.OnRowProcessed();
});
}
}
}

59
src/ImageProcessorCore/Filters/Processors/BackgroundColorProcessor.cs

@ -38,45 +38,60 @@ namespace ImageProcessorCore.Processors
/// <inheritdoc/>
protected override void Apply(ImageBase<T, TP> target, ImageBase<T, TP> source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY)
{
int sourceY = sourceRectangle.Y;
int sourceBottom = sourceRectangle.Bottom;
int startX = sourceRectangle.X;
int endX = sourceRectangle.Right;
// Align start/end positions.
int minX = Math.Max(0, startX);
int maxX = Math.Min(source.Width, endX);
int minY = Math.Max(0, startY);
int maxY = Math.Min(source.Height, endY);
// Reset offset if necessary.
if (minX > 0)
{
startX = 0;
}
if (minY > 0)
{
startY = 0;
}
Vector4 backgroundColor = this.Value.ToVector4();
using (IPixelAccessor<T, TP> sourcePixels = source.Lock())
using (IPixelAccessor<T, TP> targetPixels = target.Lock())
{
Parallel.For(
startY,
endY,
minY,
maxY,
this.ParallelOptions,
y =>
{
if (y >= sourceY && y < sourceBottom)
int offsetY = y - startY;
for (int x = minX; x < maxX; x++)
{
for (int x = startX; x < endX; x++)
{
Vector4 color = sourcePixels[x, y].ToVector4();
float a = color.W;
int offsetX = x - startX;
Vector4 color = sourcePixels[offsetX, offsetY].ToVector4();
float a = color.W;
if (a < 1 && a > 0)
{
color = Vector4.Lerp(color, backgroundColor, .5f);
}
if (Math.Abs(a) < Epsilon)
{
color = backgroundColor;
}
if (a < 1 && a > 0)
{
color = Vector4.Lerp(color, backgroundColor, .5F);
}
T packed = default(T);
packed.PackFromVector4(color);
targetPixels[x, y] = packed;
if (Math.Abs(a) < Epsilon)
{
color = backgroundColor;
}
this.OnRowProcessed();
T packed = default(T);
packed.PackFromVector4(color);
targetPixels[offsetX, offsetY] = packed;
}
this.OnRowProcessed();
});
}
}

3
src/ImageProcessorCore/Filters/Processors/Binarization/BinaryThresholdProcessor.cs

@ -9,8 +9,7 @@ namespace ImageProcessorCore.Processors
/// <summary>
/// An <see cref="IImageProcessor{T,TP}"/> to perform binary threshold filtering against an
/// <see cref="Image"/>. The image will be converted to Grayscale before thresholding
/// occurs.
/// <see cref="Image"/>. The image will be converted to Grayscale before thresholding occurs.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>

61
src/ImageProcessorCore/Filters/Processors/BlendProcessor.cs

@ -5,6 +5,7 @@
namespace ImageProcessorCore.Processors
{
using System;
using System.Numerics;
using System.Threading.Tasks;
@ -23,7 +24,7 @@ namespace ImageProcessorCore.Processors
private readonly ImageBase<T, TP> blend;
/// <summary>
/// Initializes a new instance of the <see cref="BlendProcessor"/> class.
/// Initializes a new instance of the <see cref="BlendProcessor{T,TP}"/> class.
/// </summary>
/// <param name="image">
/// The image to blend with the currently processing image.
@ -45,48 +46,62 @@ namespace ImageProcessorCore.Processors
/// <inheritdoc/>
protected override void Apply(ImageBase<T, TP> target, ImageBase<T, TP> source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY)
{
int sourceY = sourceRectangle.Y;
int sourceBottom = sourceRectangle.Bottom;
int startX = sourceRectangle.X;
int endX = sourceRectangle.Right;
Rectangle bounds = this.blend.Bounds;
float alpha = this.Value / 100f;
// Align start/end positions.
int minX = Math.Max(0, startX);
int maxX = Math.Min(source.Width, endX);
int minY = Math.Max(0, startY);
int maxY = Math.Min(source.Height, endY);
// Reset offset if necessary.
if (minX > 0)
{
startX = 0;
}
if (minY > 0)
{
startY = 0;
}
float alpha = this.Value / 100F;
using (IPixelAccessor<T, TP> toBlendPixels = this.blend.Lock())
using (IPixelAccessor<T, TP> sourcePixels = source.Lock())
using (IPixelAccessor<T, TP> targetPixels = target.Lock())
{
Parallel.For(
startY,
endY,
minY,
maxY,
this.ParallelOptions,
y =>
{
if (y >= sourceY && y < sourceBottom)
int offsetY = y - startY;
for (int x = minX; x < maxX; x++)
{
for (int x = startX; x < endX; x++)
int offsetX = x - startX;
Vector4 color = sourcePixels[offsetX, offsetY].ToVector4();
if (bounds.Contains(offsetX, offsetY))
{
Vector4 color = sourcePixels[x, y].ToVector4();
Vector4 blendedColor = toBlendPixels[offsetX, offsetY].ToVector4();
if (bounds.Contains(x, y))
if (blendedColor.W > 0)
{
Vector4 blendedColor = toBlendPixels[x, y].ToVector4();
if (blendedColor.W > 0)
{
// Lerping colors is dependent on the alpha of the blended color
float alphaFactor = alpha > 0 ? alpha : blendedColor.W;
color = Vector4.Lerp(color, blendedColor, alphaFactor);
}
// Lerping colors is dependent on the alpha of the blended color
color = Vector4.Lerp(color, blendedColor, alpha > 0 ? alpha : blendedColor.W);
}
T packed = default(T);
packed.PackFromVector4(color);
targetPixels[x, y] = packed;
}
this.OnRowProcessed();
T packed = default(T);
packed.PackFromVector4(color);
targetPixels[offsetX, offsetY] = packed;
}
this.OnRowProcessed();
});
}
}

53
src/ImageProcessorCore/Filters/Processors/BrightnessProcessor.cs

@ -5,6 +5,7 @@
namespace ImageProcessorCore.Processors
{
using System;
using System.Numerics;
using System.Threading.Tasks;
@ -18,7 +19,7 @@ namespace ImageProcessorCore.Processors
where TP : struct
{
/// <summary>
/// Initializes a new instance of the <see cref="BrightnessProcessor"/> class.
/// Initializes a new instance of the <see cref="BrightnessProcessor{T,TP}"/> class.
/// </summary>
/// <param name="brightness">The new brightness of the image. Must be between -100 and 100.</param>
/// <exception cref="ArgumentException">
@ -38,39 +39,53 @@ namespace ImageProcessorCore.Processors
/// <inheritdoc/>
protected override void Apply(ImageBase<T, TP> target, ImageBase<T, TP> source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY)
{
float brightness = this.Value / 100f;
int sourceY = sourceRectangle.Y;
int sourceBottom = sourceRectangle.Bottom;
float brightness = this.Value / 100F;
int startX = sourceRectangle.X;
int endX = sourceRectangle.Right;
// Align start/end positions.
int minX = Math.Max(0, startX);
int maxX = Math.Min(source.Width, endX);
int minY = Math.Max(0, startY);
int maxY = Math.Min(source.Height, endY);
// Reset offset if necessary.
if (minX > 0)
{
startX = 0;
}
if (minY > 0)
{
startY = 0;
}
using (IPixelAccessor<T, TP> sourcePixels = source.Lock())
using (IPixelAccessor<T, TP> targetPixels = target.Lock())
{
Parallel.For(
startY,
endY,
minY,
maxY,
this.ParallelOptions,
y =>
{
if (y >= sourceY && y < sourceBottom)
int offsetY = y - startY;
for (int x = minX; x < maxX; x++)
{
for (int x = startX; x < endX; x++)
{
// TODO: Check this with other formats.
Vector4 vector = sourcePixels[x, y].ToVector4().Expand();
Vector3 transformed = new Vector3(vector.X, vector.Y, vector.Z);
transformed += new Vector3(brightness);
vector = new Vector4(transformed, vector.W);
int offsetX = x - startX;
T packed = default(T);
packed.PackFromVector4(vector.Compress());
// TODO: Check this with other formats.
Vector4 vector = sourcePixels[offsetX, offsetY].ToVector4().Expand();
Vector3 transformed = new Vector3(vector.X, vector.Y, vector.Z) + new Vector3(brightness);
vector = new Vector4(transformed, vector.W);
targetPixels[x, y] = packed;
}
T packed = default(T);
packed.PackFromVector4(vector.Compress());
this.OnRowProcessed();
targetPixels[offsetX, offsetY] = packed;
}
this.OnRowProcessed();
});
}
}

55
src/ImageProcessorCore/Filters/Processors/ContrastProcessor.cs

@ -5,11 +5,12 @@
namespace ImageProcessorCore.Processors
{
using System;
using System.Numerics;
using System.Threading.Tasks;
/// <summary>
/// An <see cref="IImageProcessor{T,TP}"/> to change the contrast of an <see cref="Image"/>.
/// An <see cref="IImageProcessor{T,TP}"/> to change the contrast of an <see cref="Image{T,TP}"/>.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
@ -38,37 +39,53 @@ namespace ImageProcessorCore.Processors
/// <inheritdoc/>
protected override void Apply(ImageBase<T, TP> target, ImageBase<T, TP> source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY)
{
float contrast = (100f + this.Value) / 100f;
int sourceY = sourceRectangle.Y;
int sourceBottom = sourceRectangle.Bottom;
float contrast = (100F + this.Value) / 100F;
int startX = sourceRectangle.X;
int endX = sourceRectangle.Right;
Vector4 contrastVector = new Vector4(contrast, contrast, contrast, 1);
Vector4 shiftVector = new Vector4(.5f, .5f, .5f, 1);
Vector4 shiftVector = new Vector4(.5F, .5F, .5F, 1);
// Align start/end positions.
int minX = Math.Max(0, startX);
int maxX = Math.Min(source.Width, endX);
int minY = Math.Max(0, startY);
int maxY = Math.Min(source.Height, endY);
// Reset offset if necessary.
if (minX > 0)
{
startX = 0;
}
if (minY > 0)
{
startY = 0;
}
using (IPixelAccessor<T, TP> sourcePixels = source.Lock())
using (IPixelAccessor<T, TP> targetPixels = target.Lock())
{
Parallel.For(
startY,
endY,
minY,
maxY,
this.ParallelOptions,
y =>
{
if (y >= sourceY && y < sourceBottom)
int offsetY = y - startY;
for (int x = minX; x < maxX; x++)
{
for (int x = startX; x < endX; x++)
{
Vector4 vector = (sourcePixels[x, y]).ToVector4().Expand();
vector -= shiftVector;
vector *= contrastVector;
vector += shiftVector;
T packed = default(T);
packed.PackFromVector4(vector.Compress());
targetPixels[x, y] = packed;
}
this.OnRowProcessed();
int offsetX = x - startX;
Vector4 vector = sourcePixels[offsetX, offsetY].ToVector4().Expand();
vector -= shiftVector;
vector *= contrastVector;
vector += shiftVector;
T packed = default(T);
packed.PackFromVector4(vector.Compress());
targetPixels[offsetX, offsetY] = packed;
}
this.OnRowProcessed();
});
}
}

51
src/ImageProcessorCore/Filters/Processors/GlowProcessor.cs

@ -10,7 +10,7 @@ namespace ImageProcessorCore.Processors
using System.Threading.Tasks;
/// <summary>
/// Creates a glow effect on the image
/// An <see cref="IImageProcessor{T,TP}"/> that applies a radial glow effect an <see cref="Image{T,TP}"/>.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
@ -19,11 +19,13 @@ namespace ImageProcessorCore.Processors
where TP : struct
{
/// <summary>
/// Initializes a new instance of the <see cref="GlowProcessor"/> class.
/// Initializes a new instance of the <see cref="GlowProcessor{T,TP}"/> class.
/// </summary>
public GlowProcessor()
{
this.GlowColor.PackFromVector4(Color.White.ToVector4());
T color = default(T);
color.PackFromVector4(Color.White.ToVector4());
this.GlowColor = color;
}
/// <summary>
@ -47,29 +49,48 @@ namespace ImageProcessorCore.Processors
int startX = sourceRectangle.X;
int endX = sourceRectangle.Right;
T glowColor = this.GlowColor;
Vector2 centre = Rectangle.Center(targetRectangle).ToVector2();
float rX = this.RadiusX > 0 ? this.RadiusX : targetRectangle.Width / 2f;
float rY = this.RadiusY > 0 ? this.RadiusY : targetRectangle.Height / 2f;
float maxDistance = (float)Math.Sqrt(rX * rX + rY * rY);
Vector2 centre = Rectangle.Center(sourceRectangle).ToVector2();
float rX = this.RadiusX > 0 ? Math.Min(this.RadiusX, sourceRectangle.Width * .5F) : sourceRectangle.Width * .5F;
float rY = this.RadiusY > 0 ? Math.Min(this.RadiusY, sourceRectangle.Height * .5F) : sourceRectangle.Height * .5F;
float maxDistance = (float)Math.Sqrt((rX * rX) + (rY * rY));
// Align start/end positions.
int minX = Math.Max(0, startX);
int maxX = Math.Min(source.Width, endX);
int minY = Math.Max(0, startY);
int maxY = Math.Min(source.Height, endY);
// Reset offset if necessary.
if (minX > 0)
{
startX = 0;
}
if (minY > 0)
{
startY = 0;
}
using (IPixelAccessor<T, TP> sourcePixels = source.Lock())
using (IPixelAccessor<T, TP> targetPixels = target.Lock())
{
Parallel.For(
startY,
endY,
minY,
maxY,
this.ParallelOptions,
y =>
{
for (int x = startX; x < endX; x++)
int offsetY = y - startY;
for (int x = minX; x < maxX; x++)
{
int offsetX = x - startX;
// TODO: Premultiply?
float distance = Vector2.Distance(centre, new Vector2(x, y));
Vector4 sourceColor = sourcePixels[x, y].ToVector4();
Vector4 result = Vector4.Lerp(glowColor.ToVector4(), sourceColor, .5f * (distance / maxDistance));
float distance = Vector2.Distance(centre, new Vector2(offsetX, offsetY));
Vector4 sourceColor = sourcePixels[offsetX, offsetY].ToVector4();
T packed = default(T);
packed.PackFromVector4(result);
targetPixels[x, y] = packed;
packed.PackFromVector4(Vector4.Lerp(glowColor.ToVector4(), sourceColor, .5F * (distance / maxDistance)));
targetPixels[offsetX, offsetY] = packed;
}
this.OnRowProcessed();

49
src/ImageProcessorCore/Filters/Processors/InvertProcessor.cs

@ -5,12 +5,15 @@
namespace ImageProcessorCore.Processors
{
using System;
using System.Numerics;
using System.Threading.Tasks;
/// <summary>
/// An <see cref="IImageProcessor{T,TP}"/> to invert the colors of an <see cref="Image"/>.
/// An <see cref="IImageProcessor{T,TP}"/> to invert the colors of an <see cref="Image{T,TP}"/>.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
public class InvertProcessor<T, TP> : ImageProcessor<T, TP>
where T : IPackedVector<TP>
where TP : struct
@ -18,35 +21,49 @@ namespace ImageProcessorCore.Processors
/// <inheritdoc/>
protected override void Apply(ImageBase<T, TP> target, ImageBase<T, TP> source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY)
{
int sourceY = sourceRectangle.Y;
int sourceBottom = sourceRectangle.Bottom;
int startX = sourceRectangle.X;
int endX = sourceRectangle.Right;
Vector3 inverseVector = Vector3.One;
// Align start/end positions.
int minX = Math.Max(0, startX);
int maxX = Math.Min(source.Width, endX);
int minY = Math.Max(0, startY);
int maxY = Math.Min(source.Height, endY);
// Reset offset if necessary.
if (minX > 0)
{
startX = 0;
}
if (minY > 0)
{
startY = 0;
}
using (IPixelAccessor<T, TP> sourcePixels = source.Lock())
using (IPixelAccessor<T, TP> targetPixels = target.Lock())
{
Parallel.For(
startY,
endY,
minY,
maxY,
this.ParallelOptions,
y =>
{
if (y >= sourceY && y < sourceBottom)
int offsetY = y - startY;
for (int x = minX; x < maxX; x++)
{
for (int x = startX; x < endX; x++)
{
Vector4 color = sourcePixels[x, y].ToVector4();
Vector3 vector = inverseVector - new Vector3(color.X, color.Y, color.Z);
int offsetX = x - startX;
Vector4 color = sourcePixels[offsetX, offsetY].ToVector4();
Vector3 vector = inverseVector - new Vector3(color.X, color.Y, color.Z);
T packed = default(T);
packed.PackFromVector4(new Vector4(vector, color.W));
targetPixels[x, y] = packed;
}
this.OnRowProcessed();
T packed = default(T);
packed.PackFromVector4(new Vector4(vector, color.W));
targetPixels[offsetX, offsetY] = packed;
}
this.OnRowProcessed();
});
}
}

75
src/ImageProcessorCore/Filters/Processors/PixelateProcessor.cs

@ -5,11 +5,12 @@
namespace ImageProcessorCore.Processors
{
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
/// <summary>
/// An <see cref="IImageProcessor{T,TP}"/> to invert the colors of an <see cref="Image{T,TP}"/>.
/// An <see cref="IImageProcessor{T,TP}"/> to pixelate the colors of an <see cref="Image{T,TP}"/>.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
@ -38,15 +39,30 @@ namespace ImageProcessorCore.Processors
/// <inheritdoc/>
protected override void Apply(ImageBase<T, TP> target, ImageBase<T, TP> source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY)
{
int sourceY = sourceRectangle.Y;
int sourceBottom = sourceRectangle.Bottom;
int startX = sourceRectangle.X;
int endX = sourceRectangle.Right;
int size = this.Value;
int offset = this.Value / 2;
// Align start/end positions.
int minX = Math.Max(0, startX);
int maxX = Math.Min(source.Width, endX);
int minY = Math.Max(0, startY);
int maxY = Math.Min(source.Height, endY);
// Reset offset if necessary.
if (minX > 0)
{
startX = 0;
}
if (minY > 0)
{
startY = 0;
}
// Get the range on the y-plane to choose from.
IEnumerable<int> range = EnumerableExtensions.SteppedRange(startY, i => i < endY, size);
IEnumerable<int> range = EnumerableExtensions.SteppedRange(minY, i => i < maxY, size);
using (IPixelAccessor<T, TP> sourcePixels = source.Lock())
using (IPixelAccessor<T, TP> targetPixels = target.Lock())
@ -56,41 +72,40 @@ namespace ImageProcessorCore.Processors
this.ParallelOptions,
y =>
{
if (y >= sourceY && y < sourceBottom)
int offsetY = y - startY;
int offsetPy = offset;
for (int x = minX; x < maxX; x += size)
{
for (int x = startX; x < endX; x += size)
{
int offsetX = offset;
int offsetY = offset;
int offsetX = x - startX;
int offsetPx = offset;
// Make sure that the offset is within the boundary of the
// image.
while (y + offsetY >= sourceBottom)
{
offsetY--;
}
// Make sure that the offset is within the boundary of the image.
while (offsetY + offsetPy >= maxY)
{
offsetPy--;
}
while (x + offsetX >= endX)
{
offsetX--;
}
while (x + offsetPx >= maxX)
{
offsetPx--;
}
// Get the pixel color in the centre of the soon to be pixelated area.
// ReSharper disable AccessToDisposedClosure
T pixel = sourcePixels[x + offsetX, y + offsetY];
// Get the pixel color in the centre of the soon to be pixelated area.
// ReSharper disable AccessToDisposedClosure
T pixel = sourcePixels[offsetX + offsetPx, offsetY + offsetPy];
// For each pixel in the pixelate size, set it to the centre color.
for (int l = y; l < y + size && l < sourceBottom; l++)
// For each pixel in the pixelate size, set it to the centre color.
for (int l = offsetY; l < offsetY + size && l < maxY; l++)
{
for (int k = offsetX; k < offsetX + size && k < maxX; k++)
{
for (int k = x; k < x + size && k < endX; k++)
{
targetPixels[k, l] = pixel;
}
targetPixels[k, l] = pixel;
}
}
this.OnRowProcessed();
}
this.OnRowProcessed();
});
}
}

52
src/ImageProcessorCore/Filters/Processors/VignetteProcessor.cs

@ -10,7 +10,7 @@ namespace ImageProcessorCore.Processors
using System.Threading.Tasks;
/// <summary>
/// Creates a vignette effect on the image
/// An <see cref="IImageProcessor{T,TP}"/> that applies a radial vignette effect to an <see cref="Image{T,TP}"/>.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
@ -19,11 +19,13 @@ namespace ImageProcessorCore.Processors
where TP : struct
{
/// <summary>
/// Initializes a new instance of the <see cref="VignetteProcessor"/> class.
/// Initializes a new instance of the <see cref="VignetteProcessor{T,TP}"/> class.
/// </summary>
public VignetteProcessor()
{
this.VignetteColor.PackFromVector4(Color.Black.ToVector4());
T color = default(T);
color.PackFromVector4(Color.Black.ToVector4());
this.VignetteColor = color;
}
/// <summary>
@ -47,30 +49,48 @@ namespace ImageProcessorCore.Processors
int startX = sourceRectangle.X;
int endX = sourceRectangle.Right;
T vignetteColor = this.VignetteColor;
Vector2 centre = Rectangle.Center(targetRectangle).ToVector2();
float rX = this.RadiusX > 0 ? this.RadiusX : targetRectangle.Width / 2f;
float rY = this.RadiusY > 0 ? this.RadiusY : targetRectangle.Height / 2f;
float maxDistance = (float)Math.Sqrt(rX * rX + rY * rY);
Vector2 centre = Rectangle.Center(sourceRectangle).ToVector2();
float rX = this.RadiusX > 0 ? Math.Min(this.RadiusX, sourceRectangle.Width * .5F) : sourceRectangle.Width * .5F;
float rY = this.RadiusY > 0 ? Math.Min(this.RadiusY, sourceRectangle.Height * .5F) : sourceRectangle.Height * .5F;
float maxDistance = (float)Math.Sqrt((rX * rX) + (rY * rY));
// Align start/end positions.
int minX = Math.Max(0, startX);
int maxX = Math.Min(source.Width, endX);
int minY = Math.Max(0, startY);
int maxY = Math.Min(source.Height, endY);
// Reset offset if necessary.
if (minX > 0)
{
startX = 0;
}
if (minY > 0)
{
startY = 0;
}
using (IPixelAccessor<T, TP> sourcePixels = source.Lock())
using (IPixelAccessor<T, TP> targetPixels = target.Lock())
{
Parallel.For(
startY,
endY,
minY,
maxY,
this.ParallelOptions,
y =>
{
for (int x = startX; x < endX; x++)
int offsetY = y - startY;
for (int x = minX; x < maxX; x++)
{
float distance = Vector2.Distance(centre, new Vector2(x, y));
Vector4 sourceColor = sourcePixels[x, y].ToVector4();
Vector4 result = Vector4.Lerp(vignetteColor.ToVector4(), sourceColor, 1 - .9f * (distance / maxDistance));
int offsetX = x - startX;
float distance = Vector2.Distance(centre, new Vector2(offsetX, offsetY));
Vector4 sourceColor = sourcePixels[offsetX, offsetY].ToVector4();
T packed = default(T);
packed.PackFromVector4(result);
targetPixels[x, y] = packed;
packed.PackFromVector4(Vector4.Lerp(vignetteColor.ToVector4(), sourceColor, 1 - (.9F * (distance / maxDistance))));
targetPixels[offsetX, offsetY] = packed;
}
this.OnRowProcessed();
});
}

118
src/ImageProcessorCore/Filters/Vignette.cs

@ -0,0 +1,118 @@
// <copyright file="Vignette.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore
{
using Processors;
/// <summary>
/// Extension methods for the <see cref="Image{T,TP}"/> type.
/// </summary>
public static partial class ImageExtensions
{
/// <summary>
/// Applies a radial vignette effect to an image.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="source">The image this method extends.</param>
/// <param name="progressHandler">A delegate which is called as progress is made processing the image.</param>
/// <returns>The <see cref="Image{T,TP}"/>.</returns>
public static Image<T, TP> Vignette<T, TP>(this Image<T, TP> source, ProgressEventHandler progressHandler = null)
where T : IPackedVector<TP>
where TP : struct
{
return Vignette(source, default(T), source.Bounds.Width * .5F, source.Bounds.Height * .5F, source.Bounds, progressHandler);
}
/// <summary>
/// Applies a radial vignette effect to an image.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="source">The image this method extends.</param>
/// <param name="color">The color to set as the vignette.</param>
/// <param name="progressHandler">A delegate which is called as progress is made processing the image.</param>
/// <returns>The <see cref="Image{T,TP}"/>.</returns>
public static Image<T, TP> Vignette<T, TP>(this Image<T, TP> source, T color, ProgressEventHandler progressHandler = null)
where T : IPackedVector<TP>
where TP : struct
{
return Vignette(source, color, source.Bounds.Width * .5F, source.Bounds.Height * .5F, source.Bounds, progressHandler);
}
/// <summary>
/// Applies a radial vignette effect to an image.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="source">The image this method extends.</param>
/// <param name="radiusX">The the x-radius.</param>
/// <param name="radiusY">The the y-radius.</param>
/// <param name="progressHandler">A delegate which is called as progress is made processing the image.</param>
/// <returns>The <see cref="Image{T,TP}"/>.</returns>
public static Image<T, TP> Vignette<T, TP>(this Image<T, TP> source, float radiusX, float radiusY, ProgressEventHandler progressHandler = null)
where T : IPackedVector<TP>
where TP : struct
{
return Vignette(source, default(T), radiusX, radiusY, source.Bounds, progressHandler);
}
/// <summary>
/// Applies a radial vignette effect to an image.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="source">The image this method extends.</param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <param name="progressHandler">A delegate which is called as progress is made processing the image.</param>
/// <returns>The <see cref="Image{T,TP}"/>.</returns>
public static Image<T, TP> Vignette<T, TP>(this Image<T, TP> source, Rectangle rectangle, ProgressEventHandler progressHandler = null)
where T : IPackedVector<TP>
where TP : struct
{
return Vignette(source, default(T), 0, 0, rectangle, progressHandler);
}
/// <summary>
/// Applies a radial vignette effect to an image.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="source">The image this method extends.</param>
/// <param name="color">The color to set as the vignette.</param>
/// <param name="radiusX">The the x-radius.</param>
/// <param name="radiusY">The the y-radius.</param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <param name="progressHandler">A delegate which is called as progress is made processing the image.</param>
/// <returns>The <see cref="Image{T,TP}"/>.</returns>
public static Image<T, TP> Vignette<T, TP>(this Image<T, TP> source, T color, float radiusX, float radiusY, Rectangle rectangle, ProgressEventHandler progressHandler = null)
where T : IPackedVector<TP>
where TP : struct
{
VignetteProcessor<T, TP> processor = new VignetteProcessor<T, TP> { RadiusX = radiusX, RadiusY = radiusY };
if (!color.Equals(default(T)))
{
processor.VignetteColor = color;
}
processor.OnProgress += progressHandler;
try
{
return source.Process(rectangle, processor);
}
finally
{
processor.OnProgress -= progressHandler;
}
}
}
}

2
tests/ImageProcessorCore.Tests/FileTestBase.cs

@ -31,7 +31,7 @@ namespace ImageProcessorCore.Tests
// "TestImages/Formats/Bmp/neg_height.bmp", // Perf: Enable for local testing only
//"TestImages/Formats/Png/blur.png", // Perf: Enable for local testing only
//"TestImages/Formats/Png/indexed.png", // Perf: Enable for local testing only
//TestImages/Formats/Png/splash.png",
"TestImages/Formats/Png/splash.png",
"TestImages/Formats/Gif/rings.gif",
//"TestImages/Formats/Gif/giphy.gif" // Perf: Enable for local testing only
};

28
tests/ImageProcessorCore.Tests/Processors/Filters/AlphaTest.cs

@ -15,7 +15,7 @@ namespace ImageProcessorCore.Tests
= new TheoryData<int>
{
20 ,
80 ,
80
};
[Theory]
@ -43,5 +43,31 @@ namespace ImageProcessorCore.Tests
}
}
}
[Theory]
[MemberData("AlphaValues")]
public void ImageShouldApplyAlphaFilterInBox(int value)
{
const string path = "TestOutput/Alpha";
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
foreach (string file in Files)
{
using (FileStream stream = File.OpenRead(file))
{
string filename = Path.GetFileNameWithoutExtension(file) + "-" + value + "-InBox" + Path.GetExtension(file);
Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.Alpha(value, new Rectangle(10, 10, image.Width / 2, image.Height / 2))
.Save(output);
}
}
}
}
}
}

110
tests/ImageProcessorCore.Tests/Processors/Filters/GlowTest.cs

@ -0,0 +1,110 @@
// <copyright file="GlowTest.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Tests
{
using System.IO;
using Xunit;
public class GlowTest : FileTestBase
{
[Fact]
public void ImageShouldApplyGlowFilter()
{
const string path = "TestOutput/Glow";
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
foreach (string file in Files)
{
using (FileStream stream = File.OpenRead(file))
{
string filename = Path.GetFileName(file);
Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.Glow()
.Save(output);
}
}
}
}
[Fact]
public void ImageShouldApplyGlowFilterColor()
{
const string path = "TestOutput/Glow";
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
foreach (string file in Files)
{
using (FileStream stream = File.OpenRead(file))
{
string filename = Path.GetFileNameWithoutExtension(file) + "-Color" + Path.GetExtension(file);
Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.Glow(Color.HotPink)
.Save(output);
}
}
}
}
[Fact]
public void ImageShouldApplyGlowFilterRadius()
{
const string path = "TestOutput/Glow";
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
foreach (string file in Files)
{
using (FileStream stream = File.OpenRead(file))
{
string filename = Path.GetFileNameWithoutExtension(file) + "-Radius" + Path.GetExtension(file);
Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.Glow(image.Width / 4, image.Height / 4)
.Save(output);
}
}
}
}
[Fact]
public void ImageShouldApplyGlowFilterInBox()
{
const string path = "TestOutput/Glow";
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
foreach (string file in Files)
{
using (FileStream stream = File.OpenRead(file))
{
string filename = Path.GetFileNameWithoutExtension(file) + "-InBox" + Path.GetExtension(file);
Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.Glow(new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2))
.Save(output);
}
}
}
}
}
}

24
tests/ImageProcessorCore.Tests/Processors/Filters/InvertTest.cs

@ -34,5 +34,29 @@ namespace ImageProcessorCore.Tests
}
}
}
[Fact]
public void ImageShouldApplyInvertFilterInBox()
{
const string path = "TestOutput/Invert";
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
foreach (string file in Files)
{
using (FileStream stream = File.OpenRead(file))
{
string filename = Path.GetFileNameWithoutExtension(file);
Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"{path}/{filename}-InBox{Path.GetExtension(file)}"))
{
image.Invert(new Rectangle(10, 10, image.Width / 2, image.Height / 2))
.Save(output);
}
}
}
}
}
}

28
tests/ImageProcessorCore.Tests/Processors/Filters/PixelateTest.cs

@ -15,7 +15,7 @@ namespace ImageProcessorCore.Tests
= new TheoryData<int>
{
4 ,
8 ,
8
};
[Theory]
@ -43,5 +43,31 @@ namespace ImageProcessorCore.Tests
}
}
}
[Theory]
[MemberData("PixelateValues")]
public void ImageShouldApplyPixelateFilterInBox(int value)
{
const string path = "TestOutput/Pixelate";
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
foreach (string file in Files)
{
using (FileStream stream = File.OpenRead(file))
{
string filename = Path.GetFileNameWithoutExtension(file) + "-" + value + "-InBox" + Path.GetExtension(file);
Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.Pixelate(value, new Rectangle(10, 10, image.Width / 2, image.Height / 2))
.Save(output);
}
}
}
}
}
}

110
tests/ImageProcessorCore.Tests/Processors/Filters/VignetteTest.cs

@ -0,0 +1,110 @@
// <copyright file="VignetteTest.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Tests
{
using System.IO;
using Xunit;
public class VignetteTest : FileTestBase
{
[Fact]
public void ImageShouldApplyVignetteFilter()
{
const string path = "TestOutput/Vignette";
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
foreach (string file in Files)
{
using (FileStream stream = File.OpenRead(file))
{
string filename = Path.GetFileName(file);
Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.Vignette()
.Save(output);
}
}
}
}
[Fact]
public void ImageShouldApplyVignetteFilterColor()
{
const string path = "TestOutput/Vignette";
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
foreach (string file in Files)
{
using (FileStream stream = File.OpenRead(file))
{
string filename = Path.GetFileNameWithoutExtension(file) + "-Color" + Path.GetExtension(file);
Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.Vignette(Color.HotPink)
.Save(output);
}
}
}
}
[Fact]
public void ImageShouldApplyVignetteFilterRadius()
{
const string path = "TestOutput/Vignette";
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
foreach (string file in Files)
{
using (FileStream stream = File.OpenRead(file))
{
string filename = Path.GetFileNameWithoutExtension(file) + "-Radius" + Path.GetExtension(file);
Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.Vignette(image.Width / 4, image.Height / 4)
.Save(output);
}
}
}
}
[Fact]
public void ImageShouldApplyVignetteFilterInBox()
{
const string path = "TestOutput/Vignette";
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
foreach (string file in Files)
{
using (FileStream stream = File.OpenRead(file))
{
string filename = Path.GetFileNameWithoutExtension(file) + "-InBox" + Path.GetExtension(file);
Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.Vignette(new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2))
.Save(output);
}
}
}
}
}
}
Loading…
Cancel
Save