From ec5bb8feb897dff0a2c31836bcb549dc715c0fef Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 8 Sep 2016 16:22:55 +1000 Subject: [PATCH] Add oil painting effect. Former-commit-id: c9c4f16067cbca67fd055702fa15e71995e8ff35 Former-commit-id: 6ffebcceb66d94c8a7920cb02edfcd572db29ac3 Former-commit-id: 09721ad14a254868241f53d43095212ab20fefc8 --- README.md | 1 + .../Samplers/OilPainting.cs | 70 ++++++++ src/ImageProcessorCore/Samplers/Pixelate.cs | 4 +- .../Processors/OilPaintingProcessor.cs | 154 ++++++++++++++++++ .../Processors/Samplers/OilPaintTest.cs | 74 +++++++++ 5 files changed, 301 insertions(+), 2 deletions(-) create mode 100644 src/ImageProcessorCore/Samplers/OilPainting.cs create mode 100644 src/ImageProcessorCore/Samplers/Processors/OilPaintingProcessor.cs create mode 100644 tests/ImageProcessorCore.Tests/Processors/Samplers/OilPaintTest.cs 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