diff --git a/README.md b/README.md
index 594c0c1fb..171be566e 100644
--- a/README.md
+++ b/README.md
@@ -144,6 +144,7 @@ git clone https://github.com/JimBobSquarePants/ImageProcessor
- [x] Pixelate
- [x] Blend
- [ ] Mask
+ - [x] Oil Painting
- [x] Vignette
- [x] Glow
- [x] Threshold
diff --git a/src/ImageProcessorCore/Samplers/OilPainting.cs b/src/ImageProcessorCore/Samplers/OilPainting.cs
new file mode 100644
index 000000000..9b5975734
--- /dev/null
+++ b/src/ImageProcessorCore/Samplers/OilPainting.cs
@@ -0,0 +1,70 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageProcessorCore
+{
+ using Processors;
+ using System;
+
+ ///
+ /// Extension methods for the type.
+ ///
+ public static partial class ImageExtensions
+ {
+ ///
+ /// Alters the colors of the image recreating an oil painting effect.
+ ///
+ /// The pixel format.
+ /// The packed format. uint, long, float.
+ /// The image this method extends.
+ /// The number of intensity levels. Higher values result in a broader range of colour intensities forming part of the result image.
+ /// The number of neighbouring pixels used in calculating each individual pixel value.
+ /// A delegate which is called as progress is made processing the image.
+ /// The .
+ public static Image OilPaint(this Image source, int levels = 10, int brushSize = 15, ProgressEventHandler progressHandler = null)
+ where TColor : IPackedVector
+ where TPacked : struct
+ {
+ return OilPaint(source, levels, brushSize, source.Bounds, progressHandler);
+ }
+
+ ///
+ /// Alters the colors of the image recreating an oil painting effect.
+ ///
+ /// The pixel format.
+ /// The packed format. uint, long, float.
+ /// The image this method extends.
+ /// The number of intensity levels. Higher values result in a broader range of colour intensities forming part of the result image.
+ /// The number of neighbouring pixels used in calculating each individual pixel value.
+ ///
+ /// The structure that specifies the portion of the image object to alter.
+ ///
+ /// A delegate which is called as progress is made processing the image.
+ /// The .
+ public static Image OilPaint(this Image source, int levels, int brushSize, Rectangle rectangle, ProgressEventHandler progressHandler = null)
+ where TColor : IPackedVector
+ where TPacked : struct
+ {
+ Guard.MustBeGreaterThan(levels, 0, nameof(levels));
+
+ if (brushSize <= 0 || brushSize > source.Height || brushSize > source.Width)
+ {
+ throw new ArgumentOutOfRangeException(nameof(brushSize));
+ }
+
+ OilPaintingProcessor processor = new OilPaintingProcessor(levels, brushSize);
+ processor.OnProgress += progressHandler;
+
+ try
+ {
+ return source.Process(rectangle, processor);
+ }
+ finally
+ {
+ processor.OnProgress -= progressHandler;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ImageProcessorCore/Samplers/Pixelate.cs b/src/ImageProcessorCore/Samplers/Pixelate.cs
index a9b7f63af..4dd17ad13 100644
--- a/src/ImageProcessorCore/Samplers/Pixelate.cs
+++ b/src/ImageProcessorCore/Samplers/Pixelate.cs
@@ -14,7 +14,7 @@ namespace ImageProcessorCore
public static partial class ImageExtensions
{
///
- /// Pixelates and image with the given pixel size.
+ /// Pixelates an image with the given pixel size.
///
/// The pixel format.
/// The packed format. uint, long, float.
@@ -30,7 +30,7 @@ namespace ImageProcessorCore
}
///
- /// Pixelates and image with the given pixel size.
+ /// Pixelates an image with the given pixel size.
///
/// The pixel format.
/// The packed format. uint, long, float.
diff --git a/src/ImageProcessorCore/Samplers/Processors/OilPaintingProcessor.cs b/src/ImageProcessorCore/Samplers/Processors/OilPaintingProcessor.cs
new file mode 100644
index 000000000..8388bad30
--- /dev/null
+++ b/src/ImageProcessorCore/Samplers/Processors/OilPaintingProcessor.cs
@@ -0,0 +1,154 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageProcessorCore.Processors
+{
+ using System;
+ using System.Numerics;
+ using System.Threading.Tasks;
+
+ ///
+ /// An to apply an oil painting effect to an .
+ ///
+ /// Adapted from by Dewald Esterhuizen.
+ /// The pixel format.
+ /// The packed format. uint, long, float.
+ public class OilPaintingProcessor : ImageSampler
+ where TColor : IPackedVector
+ where TPacked : struct
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The number of intensity levels. Higher values result in a broader range of colour intensities forming part of the result image.
+ /// The number of neighbouring pixels used in calculating each individual pixel value.
+ public OilPaintingProcessor(int levels, int brushSize)
+ {
+ Guard.MustBeGreaterThan(levels, 0, nameof(levels));
+ Guard.MustBeGreaterThan(brushSize, 0, nameof(brushSize));
+
+ this.Levels = levels;
+ this.BrushSize = brushSize;
+ }
+
+ ///
+ /// Gets the intensity levels
+ ///
+ public int Levels { get; }
+
+ ///
+ /// Gets the brush size
+ ///
+ public int BrushSize { get; }
+
+ ///
+ protected override void Apply(ImageBase target, ImageBase 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 sourcePixels = source.Lock())
+ using (PixelAccessor 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();
+ });
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/ImageProcessorCore.Tests/Processors/Samplers/OilPaintTest.cs b/tests/ImageProcessorCore.Tests/Processors/Samplers/OilPaintTest.cs
new file mode 100644
index 000000000..de43faf4d
--- /dev/null
+++ b/tests/ImageProcessorCore.Tests/Processors/Samplers/OilPaintTest.cs
@@ -0,0 +1,74 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageProcessorCore.Tests
+{
+ using System;
+ using System.IO;
+
+ using Xunit;
+
+ public class OilPaintTest : FileTestBase
+ {
+ public static readonly TheoryData> OilPaintValues
+ = new TheoryData>
+ {
+ new Tuple(15,10),
+ new Tuple(6,5)
+ };
+
+ [Theory]
+ [MemberData(nameof(OilPaintValues))]
+ public void ImageShouldApplyOilPaintFilter(Tuple 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 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);
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file