mirror of https://github.com/SixLabors/ImageSharp
Browse Source
Former-commit-id: c9c4f16067cbca67fd055702fa15e71995e8ff35 Former-commit-id: 6ffebcceb66d94c8a7920cb02edfcd572db29ac3 Former-commit-id: 09721ad14a254868241f53d43095212ab20fefc8af/merge-core
5 changed files with 301 additions and 2 deletions
@ -0,0 +1,70 @@ |
|||
// <copyright file="Pixelate.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; |
|||
using System; |
|||
|
|||
/// <summary>
|
|||
/// Extension methods for the <see cref="Image{TColor, TPacked}"/> type.
|
|||
/// </summary>
|
|||
public static partial class ImageExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Alters the colors of the image recreating an oil painting effect.
|
|||
/// </summary>
|
|||
/// <typeparam name="TColor">The pixel format.</typeparam>
|
|||
/// <typeparam name="TPacked">The packed format. <example>uint, long, float.</example></typeparam>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <param name="levels">The number of intensity levels. Higher values result in a broader range of colour intensities forming part of the result image.</param>
|
|||
/// <param name="brushSize">The number of neighbouring pixels used in calculating each individual pixel value.</param>
|
|||
/// <param name="progressHandler">A delegate which is called as progress is made processing the image.</param>
|
|||
/// <returns>The <see cref="Image{TColor, TPacked}"/>.</returns>
|
|||
public static Image<TColor, TPacked> OilPaint<TColor, TPacked>(this Image<TColor, TPacked> source, int levels = 10, int brushSize = 15, ProgressEventHandler progressHandler = null) |
|||
where TColor : IPackedVector<TPacked> |
|||
where TPacked : struct |
|||
{ |
|||
return OilPaint(source, levels, brushSize, source.Bounds, progressHandler); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Alters the colors of the image recreating an oil painting effect.
|
|||
/// </summary>
|
|||
/// <typeparam name="TColor">The pixel format.</typeparam>
|
|||
/// <typeparam name="TPacked">The packed format. <example>uint, long, float.</example></typeparam>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <param name="levels">The number of intensity levels. Higher values result in a broader range of colour intensities forming part of the result image.</param>
|
|||
/// <param name="brushSize">The number of neighbouring pixels used in calculating each individual pixel value.</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{TColor, TPacked}"/>.</returns>
|
|||
public static Image<TColor, TPacked> OilPaint<TColor, TPacked>(this Image<TColor, TPacked> source, int levels, int brushSize, Rectangle rectangle, ProgressEventHandler progressHandler = null) |
|||
where TColor : IPackedVector<TPacked> |
|||
where TPacked : struct |
|||
{ |
|||
Guard.MustBeGreaterThan(levels, 0, nameof(levels)); |
|||
|
|||
if (brushSize <= 0 || brushSize > source.Height || brushSize > source.Width) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(brushSize)); |
|||
} |
|||
|
|||
OilPaintingProcessor<TColor, TPacked> processor = new OilPaintingProcessor<TColor, TPacked>(levels, brushSize); |
|||
processor.OnProgress += progressHandler; |
|||
|
|||
try |
|||
{ |
|||
return source.Process(rectangle, processor); |
|||
} |
|||
finally |
|||
{ |
|||
processor.OnProgress -= progressHandler; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,154 @@ |
|||
// <copyright file="OilPaintingProcessor.cs" company="James Jackson-South">
|
|||
// Copyright (c) James Jackson-South and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
// </copyright>
|
|||
|
|||
namespace ImageProcessorCore.Processors |
|||
{ |
|||
using System; |
|||
using System.Numerics; |
|||
using System.Threading.Tasks; |
|||
|
|||
/// <summary>
|
|||
/// An <see cref="IImageSampler{TColor,TPacked}"/> to apply an oil painting effect to an <see cref="Image{TColor, TPacked}"/>.
|
|||
/// </summary>
|
|||
/// <remarks>Adapted from <see href="https://softwarebydefault.com/2013/06/29/oil-painting-cartoon-filter/"/> by Dewald Esterhuizen.</remarks>
|
|||
/// <typeparam name="TColor">The pixel format.</typeparam>
|
|||
/// <typeparam name="TPacked">The packed format. <example>uint, long, float.</example></typeparam>
|
|||
public class OilPaintingProcessor<TColor, TPacked> : ImageSampler<TColor, TPacked> |
|||
where TColor : IPackedVector<TPacked> |
|||
where TPacked : struct |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="OilPaintingProcessor{T,TP}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="levels">The number of intensity levels. Higher values result in a broader range of colour intensities forming part of the result image.</param>
|
|||
/// <param name="brushSize">The number of neighbouring pixels used in calculating each individual pixel value.</param>
|
|||
public OilPaintingProcessor(int levels, int brushSize) |
|||
{ |
|||
Guard.MustBeGreaterThan(levels, 0, nameof(levels)); |
|||
Guard.MustBeGreaterThan(brushSize, 0, nameof(brushSize)); |
|||
|
|||
this.Levels = levels; |
|||
this.BrushSize = brushSize; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the intensity levels
|
|||
/// </summary>
|
|||
public int Levels { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the brush size
|
|||
/// </summary>
|
|||
public int BrushSize { get; } |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void Apply(ImageBase<TColor, TPacked> target, ImageBase<TColor, TPacked> source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY) |
|||
{ |
|||
int startX = sourceRectangle.X; |
|||
int endX = sourceRectangle.Right; |
|||
int radius = this.BrushSize >> 1; |
|||
int levels = this.Levels; |
|||
|
|||
// 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; |
|||
} |
|||
|
|||
using (PixelAccessor<TColor, TPacked> sourcePixels = source.Lock()) |
|||
using (PixelAccessor<TColor, TPacked> targetPixels = target.Lock()) |
|||
{ |
|||
Parallel.For( |
|||
minY, |
|||
maxY, |
|||
this.ParallelOptions, |
|||
y => |
|||
{ |
|||
for (int x = startX; x < endX; x++) |
|||
{ |
|||
int maxIntensity = 0; |
|||
int maxIndex = 0; |
|||
|
|||
int[] intensityBin = new int[levels]; |
|||
float[] redBin = new float[levels]; |
|||
float[] blueBin = new float[levels]; |
|||
float[] greenBin = new float[levels]; |
|||
|
|||
for (int fy = 0; fy <= radius; fy++) |
|||
{ |
|||
int fyr = fy - radius; |
|||
int offsetY = y + fyr; |
|||
|
|||
// Skip the current row
|
|||
if (offsetY < minY) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
// Outwith the current bounds so break.
|
|||
if (offsetY >= maxY) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
for (int fx = 0; fx <= radius; fx++) |
|||
{ |
|||
int fxr = fx - radius; |
|||
int offsetX = x + fxr; |
|||
|
|||
// Skip the column
|
|||
if (offsetX < 0) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
if (offsetX < maxX) |
|||
{ |
|||
// ReSharper disable once AccessToDisposedClosure
|
|||
Vector4 color = sourcePixels[offsetX, offsetY].ToVector4(); |
|||
|
|||
float sourceRed = color.X; |
|||
float sourceBlue = color.Z; |
|||
float sourceGreen = color.Y; |
|||
|
|||
int currentIntensity = (int)Math.Round(((sourceBlue + sourceGreen + sourceRed) / 3.0 * (levels - 1))); |
|||
|
|||
intensityBin[currentIntensity] += 1; |
|||
blueBin[currentIntensity] += sourceBlue; |
|||
greenBin[currentIntensity] += sourceGreen; |
|||
redBin[currentIntensity] += sourceRed; |
|||
|
|||
if (intensityBin[currentIntensity] > maxIntensity) |
|||
{ |
|||
maxIntensity = intensityBin[currentIntensity]; |
|||
maxIndex = currentIntensity; |
|||
} |
|||
} |
|||
} |
|||
|
|||
float red = Math.Abs(redBin[maxIndex] / maxIntensity); |
|||
float green = Math.Abs(greenBin[maxIndex] / maxIntensity); |
|||
float blue = Math.Abs(blueBin[maxIndex] / maxIntensity); |
|||
|
|||
Vector4 targetColor = targetPixels[x, y].ToVector4(); |
|||
TColor packed = default(TColor); |
|||
packed.PackFromVector4(new Vector4(red, green, blue, targetColor.Z)); |
|||
targetPixels[x, y] = packed; |
|||
} |
|||
|
|||
} |
|||
|
|||
this.OnRowProcessed(); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
// <copyright file="OilPaintTest.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; |
|||
using System.IO; |
|||
|
|||
using Xunit; |
|||
|
|||
public class OilPaintTest : FileTestBase |
|||
{ |
|||
public static readonly TheoryData<Tuple<int, int>> OilPaintValues |
|||
= new TheoryData<Tuple<int, int>> |
|||
{ |
|||
new Tuple<int, int>(15,10), |
|||
new Tuple<int, int>(6,5) |
|||
}; |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(OilPaintValues))] |
|||
public void ImageShouldApplyOilPaintFilter(Tuple<int, int> value) |
|||
{ |
|||
const string path = "TestOutput/OilPaint"; |
|||
if (!Directory.Exists(path)) |
|||
{ |
|||
Directory.CreateDirectory(path); |
|||
} |
|||
|
|||
foreach (string file in Files) |
|||
{ |
|||
using (FileStream stream = File.OpenRead(file)) |
|||
{ |
|||
string filename = Path.GetFileNameWithoutExtension(file) + "-" + value + Path.GetExtension(file); |
|||
|
|||
Image image = new Image(stream); |
|||
using (FileStream output = File.OpenWrite($"{path}/{filename}")) |
|||
{ |
|||
image.OilPaint(value.Item1, value.Item2) |
|||
.Save(output); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(OilPaintValues))] |
|||
public void ImageShouldApplyOilPaintFilterInBox(Tuple<int, int> value) |
|||
{ |
|||
const string path = "TestOutput/OilPaint"; |
|||
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.OilPaint(value.Item1, value.Item2, new Rectangle(10, 10, image.Width / 2, image.Height / 2)) |
|||
.Save(output); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue