From e0208e114eebe79043d67fe8f0f605d20ddb2ca0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 10 Dec 2015 18:10:16 +1100 Subject: [PATCH] Begin Vignette. Former-commit-id: c5e1903acb354e047d73223867a04ae29f301354 Former-commit-id: a86e30bf71e3eab130a4cc8d9879aa2d55db440b Former-commit-id: 272754ac9ccd2c97cfeb320d3e25ff431ac43de4 --- appveyor.yml | 20 +- src/ImageProcessor/Filters/Blend.cs | 9 +- src/ImageProcessor/Filters/Vignette.cs | 77 +++++++ src/ImageProcessor/Numerics/Ellipse.cs | 188 ++++++++++++++++++ src/ImageProcessor/Numerics/Rectangle.cs | 2 +- src/ImageProcessor/ParallelImageProcessor.cs | 20 ++ .../Processors/Filters/FilterTests.cs | 65 +++--- 7 files changed, 335 insertions(+), 46 deletions(-) create mode 100644 src/ImageProcessor/Filters/Vignette.cs create mode 100644 src/ImageProcessor/Numerics/Ellipse.cs diff --git a/appveyor.yml b/appveyor.yml index d8223ad37..fd3b29259 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -40,13 +40,13 @@ test_script: artifacts: - path: artifacts\bin\ImageProcessor\**\*.nupkg -deploy: - # MyGet Deployment for builds & releases - - provider: NuGet - server: https://www.myget.org/F/imageprocessor/api/v2/package - symbol_server: https://nuget.symbolsource.org/MyGet/imageprocessor - api_key: - secure: fz0rUrt3B1HczUC1ZehwVsrFSWX9WZGDQoueDztLte9/+yQG+BBU7UrO+coE8lUf - artifact: /.*\.nupkg/ - on: - branch: V3 \ No newline at end of file +#deploy: +# # MyGet Deployment for builds & releases +# - provider: NuGet +# server: https://www.myget.org/F/imageprocessor/api/v2/package +# symbol_server: https://nuget.symbolsource.org/MyGet/imageprocessor +# api_key: +# secure: fz0rUrt3B1HczUC1ZehwVsrFSWX9WZGDQoueDztLte9/+yQG+BBU7UrO+coE8lUf +# artifact: /.*\.nupkg/ +# on: +# branch: V3 \ No newline at end of file diff --git a/src/ImageProcessor/Filters/Blend.cs b/src/ImageProcessor/Filters/Blend.cs index 23697b092..9447c93b3 100644 --- a/src/ImageProcessor/Filters/Blend.cs +++ b/src/ImageProcessor/Filters/Blend.cs @@ -59,9 +59,12 @@ namespace ImageProcessor.Filters { Color blendedColor = this.toBlend[x, y]; - // Lerping colors is dependent on the alpha of the blended color - float alphaFactor = alpha > 0 ? alpha : blendedColor.A; - color = Color.Lerp(color, blendedColor, alphaFactor); + if (blendedColor.A > 0) + { + // Lerping colors is dependent on the alpha of the blended color + float alphaFactor = alpha > 0 ? alpha : blendedColor.A; + color = Color.Lerp(color, blendedColor, alphaFactor); + } } target[x, y] = color; diff --git a/src/ImageProcessor/Filters/Vignette.cs b/src/ImageProcessor/Filters/Vignette.cs new file mode 100644 index 000000000..547308cc9 --- /dev/null +++ b/src/ImageProcessor/Filters/Vignette.cs @@ -0,0 +1,77 @@ +namespace ImageProcessor.Filters +{ + using System.Numerics; + using System.Threading.Tasks; + + /// + /// Creates a vignette + /// + public class Vignette : ParallelImageProcessor + { + /// + /// Used to hold a copy of the target image. + /// + private readonly Image targetCopy = new Image(); + + /// + /// Gets or sets the vignette color to apply. + /// + public Color Color { get; set; } = Color.Black; + + /// + /// Gets or sets the the x-radius. + /// + public float RadiusX { get; set; } + + /// + /// Gets or sets the the y-radius. + /// + public float RadiusY { get; set; } + + /// + protected override void OnApply(ImageBase source, ImageBase target, Rectangle targetRectangle, Rectangle sourceRectangle) + { + this.targetCopy.SetPixels(target.Width, target.Height, target.Pixels); + target.SetPixels(target.Width, target.Height, new float[target.Pixels.Length]); + } + + /// + protected override void Apply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY) + { + int startX = sourceRectangle.X; + int endX = sourceRectangle.Right; + + Vector2 centre = Rectangle.Center(targetRectangle); + int centerX = (int)centre.X; + int centerY = (int)centre.Y; + float rX = this.RadiusX > 0 ? this.RadiusX : targetRectangle.Width / 2f; + float rY = this.RadiusY > 0 ? this.RadiusY : targetRectangle.Height / 2f; + + Ellipse ellipse = new Ellipse(new Point(centerX, centerY), rX - 1, rY - 1); + + Parallel.For( + startY, + endY, + y => + { + for (int x = startX; x < endX; x++) + { + if (!ellipse.Contains(x, y)) + { + target[x, y] = Color.Black; + } + } + }); + } + + /// + protected override void AfterApply(ImageBase source, ImageBase target, Rectangle targetRectangle, Rectangle sourceRectangle) + { + new GuassianBlur(30).Apply(target, target, targetRectangle); + Image temp = new Image(this.targetCopy); + new Blend(target, 40).Apply(this.targetCopy, temp, targetRectangle); + target.SetPixels(temp.Width, temp.Height, this.targetCopy.Pixels); + } + } +} + diff --git a/src/ImageProcessor/Numerics/Ellipse.cs b/src/ImageProcessor/Numerics/Ellipse.cs new file mode 100644 index 000000000..9bd340920 --- /dev/null +++ b/src/ImageProcessor/Numerics/Ellipse.cs @@ -0,0 +1,188 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessor +{ + using System; + using System.ComponentModel; + using System.Numerics; + + public struct Ellipse : IEquatable + { + /// + /// The center point. + /// + private Point center; + + /// + /// The epsilon for comparing floating point numbers. + /// + private const float Epsilon = 0.0001f; + + /// + /// Represents a that has X and Y values set to zero. + /// + public static readonly Ellipse Empty = default(Ellipse); + + public Ellipse(Point center, float radiusX, float radiusY) + { + this.center = center; + this.RadiusX = radiusX; + this.RadiusY = radiusY; + } + + /// + /// Gets the x-radius of this . + /// + public float RadiusX { get; } + + /// + /// Gets the y-radius of this . + /// + public float RadiusY { get; } + + /// + /// Gets a value indicating whether this is empty. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsEmpty => this.center.IsEmpty + && Math.Abs(this.RadiusX) < Epsilon + && Math.Abs(this.RadiusY) < Epsilon; + + /// + /// Compares two objects for equality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + public static bool operator ==(Ellipse left, Ellipse right) + { + return left.Equals(right); + } + + /// + /// Compares two objects for inequality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + public static bool operator !=(Ellipse left, Ellipse right) + { + return !left.Equals(right); + } + + /// + /// Returns the center point of the given + /// + /// The ellipse + /// + public static Vector2 Center(Ellipse ellipse) + { + return new Vector2(ellipse.center.X, ellipse.center.Y); + } + + /// + /// Determines if the specfied point is contained within the rectangular region defined by + /// this . + /// + /// The x-coordinate of the given point. + /// The y-coordinate of the given point. + /// The + public bool Contains(int x, int y) + { + if (this.RadiusX <= 0 || this.RadiusY <= 0) + { + return false; + } + + //This is a more general form of the circle equation + // X^2/a^2 + Y^2/b^2 <= 1 + Point normalized = new Point(x - this.center.X, y - this.center.Y); + int nX = normalized.X; + int nY = normalized.Y; + + //return (double)(nX * nX) / (this.RadiusX * this.RadiusX) + // + (double)(nY * nY) / (this.RadiusY * this.RadiusY) + // <= 1.0; + + return ((double)(nX * nX) / (this.RadiusX * this.RadiusX)) + + ((double)(nY * nY) / (this.RadiusY * this.RadiusY)) + <= 1.0; + } + + /// + public override bool Equals(object obj) + { + if (!(obj is Ellipse)) + { + return false; + } + + Ellipse other = (Ellipse)obj; + + return other.center == this.center + && Math.Abs(other.RadiusX - this.RadiusX) < Epsilon + && Math.Abs(other.RadiusY - this.RadiusY) < Epsilon; + } + + /// + public override int GetHashCode() + { + return this.GetHashCode(this); + } + + /// + public override string ToString() + { + if (this.IsEmpty) + { + return "Ellipse [ Empty ]"; + } + + return + $"Ellipse [ RadiusX={this.RadiusX}, RadiusY={this.RadiusX}, Centre={this.center.X},{this.center.Y} ]"; + } + + /// + public bool Equals(Ellipse other) + { + return this.center.Equals(other.center) + && this.RadiusX.Equals(other.RadiusX) + && this.RadiusX.Equals(other.RadiusY); + } + + /// + /// Returns the hash code for this instance. + /// + /// + /// The instance of to return the hash code for. + /// + /// + /// A 32-bit signed integer that is the hash code for this instance. + /// + private int GetHashCode(Ellipse point) + { + unchecked + { + int hashCode = point.center.GetHashCode(); + hashCode = (hashCode * 397) ^ point.RadiusX.GetHashCode(); + hashCode = (hashCode * 397) ^ point.RadiusY.GetHashCode(); + return hashCode; + } + } + } +} diff --git a/src/ImageProcessor/Numerics/Rectangle.cs b/src/ImageProcessor/Numerics/Rectangle.cs index c7e97b956..3cd603d96 100644 --- a/src/ImageProcessor/Numerics/Rectangle.cs +++ b/src/ImageProcessor/Numerics/Rectangle.cs @@ -160,7 +160,7 @@ namespace ImageProcessor /// Returns the center point of the given /// /// The rectangle - /// + /// public static Vector2 Center(Rectangle rectangle) { return new Vector2(rectangle.Left + rectangle.Width / 2, rectangle.Top + rectangle.Height / 2); diff --git a/src/ImageProcessor/ParallelImageProcessor.cs b/src/ImageProcessor/ParallelImageProcessor.cs index 610d60c0c..cb5c1a36a 100644 --- a/src/ImageProcessor/ParallelImageProcessor.cs +++ b/src/ImageProcessor/ParallelImageProcessor.cs @@ -51,6 +51,8 @@ namespace ImageProcessor { this.Apply(target, temp, target.Bounds, sourceRectangle, sourceRectangle.Y, sourceRectangle.Bottom); } + + this.AfterApply(temp, target, target.Bounds, sourceRectangle); } /// @@ -96,6 +98,8 @@ namespace ImageProcessor { this.Apply(target, temp, targetRectangle, sourceRectangle, targetRectangle.Y, targetRectangle.Bottom); } + + this.AfterApply(temp, target, target.Bounds, sourceRectangle); } /// @@ -134,5 +138,21 @@ namespace ImageProcessor /// the result of image process as new image. /// protected abstract void Apply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY); + + /// + /// This method is called after the process is applied to prepare the processor. + /// + /// The source image. Cannot be null. + /// Target image to apply the process to. + /// + /// The structure that specifies the location and size of the drawn image. + /// The image is scaled to fit the rectangle. + /// + /// + /// The structure that specifies the portion of the image object to draw. + /// + protected virtual void AfterApply(ImageBase source, ImageBase target, Rectangle targetRectangle, Rectangle sourceRectangle) + { + } } } diff --git a/tests/ImageProcessor.Tests/Processors/Filters/FilterTests.cs b/tests/ImageProcessor.Tests/Processors/Filters/FilterTests.cs index d4e0d610e..ae854d8e8 100644 --- a/tests/ImageProcessor.Tests/Processors/Filters/FilterTests.cs +++ b/tests/ImageProcessor.Tests/Processors/Filters/FilterTests.cs @@ -13,38 +13,39 @@ namespace ImageProcessor.Tests { public static readonly TheoryData Filters = new TheoryData { - { "Brightness-50", new Brightness(50) }, - { "Brightness--50", new Brightness(-50) }, - { "Contrast-50", new Contrast(50) }, - { "Contrast--50", new Contrast(-50) }, - { "BackgroundColor", new BackgroundColor(new Color(243 / 255f, 87 / 255f, 161 / 255f))}, - { "Blend", new Blend(new Image(File.OpenRead("TestImages/Formats/Bmp/Car.bmp")),50)}, - { "Saturation-50", new Saturation(50) }, - { "Saturation--50", new Saturation(-50) }, - { "Alpha--50", new Alpha(50) }, - { "Invert", new Invert() }, - { "Sepia", new Sepia() }, - { "BlackWhite", new BlackWhite() }, - { "Lomograph", new Lomograph() }, - { "Polaroid", new Polaroid() }, - { "Kodachrome", new Kodachrome() }, - { "GreyscaleBt709", new GreyscaleBt709() }, - { "GreyscaleBt601", new GreyscaleBt601() }, - { "Kayyali", new Kayyali() }, - { "Kirsch", new Kirsch() }, - { "Laplacian3X3", new Laplacian3X3() }, - { "Laplacian5X5", new Laplacian5X5() }, - { "LaplacianOfGaussian", new LaplacianOfGaussian() }, - { "Prewitt", new Prewitt() }, - { "RobertsCross", new RobertsCross() }, - { "Scharr", new Scharr() }, - { "Sobel", new Sobel {Greyscale = true} }, - { "Pixelate", new Pixelate(8) }, - { "GuassianBlur", new GuassianBlur(10) }, - { "GuassianSharpen", new GuassianSharpen(10) }, - { "Hue-180", new Hue(180) }, - { "Hue--180", new Hue(-180) }, - { "BoxBlur", new BoxBlur(10) }, + //{ "Brightness-50", new Brightness(50) }, + //{ "Brightness--50", new Brightness(-50) }, + //{ "Contrast-50", new Contrast(50) }, + //{ "Contrast--50", new Contrast(-50) }, + //{ "BackgroundColor", new BackgroundColor(new Color(243 / 255f, 87 / 255f, 161 / 255f))}, + //{ "Blend", new Blend(new Image(File.OpenRead("TestImages/Formats/Bmp/Car.bmp")),50)}, + //{ "Saturation-50", new Saturation(50) }, + //{ "Saturation--50", new Saturation(-50) }, + //{ "Alpha--50", new Alpha(50) }, + //{ "Invert", new Invert() }, + //{ "Sepia", new Sepia() }, + //{ "BlackWhite", new BlackWhite() }, + //{ "Lomograph", new Lomograph() }, + //{ "Polaroid", new Polaroid() }, + //{ "Kodachrome", new Kodachrome() }, + //{ "GreyscaleBt709", new GreyscaleBt709() }, + //{ "GreyscaleBt601", new GreyscaleBt601() }, + //{ "Kayyali", new Kayyali() }, + //{ "Kirsch", new Kirsch() }, + //{ "Laplacian3X3", new Laplacian3X3() }, + //{ "Laplacian5X5", new Laplacian5X5() }, + //{ "LaplacianOfGaussian", new LaplacianOfGaussian() }, + //{ "Prewitt", new Prewitt() }, + //{ "RobertsCross", new RobertsCross() }, + //{ "Scharr", new Scharr() }, + //{ "Sobel", new Sobel {Greyscale = true} }, + //{ "Pixelate", new Pixelate(8) }, + //{ "GuassianBlur", new GuassianBlur(10) }, + //{ "GuassianSharpen", new GuassianSharpen(10) }, + //{ "Hue-180", new Hue(180) }, + //{ "Hue--180", new Hue(-180) }, + //{ "BoxBlur", new BoxBlur(10) }, + {"Vignette", new Vignette()} }; [Theory]