Browse Source

First rough filter API attempt

Former-commit-id: 6941eb37bf679465857517bfd16ebd910801f430
Former-commit-id: b1fd2d8e4d90eb282201ed81a088e0f7b9c72278
Former-commit-id: 5d1db23e65e6c613a151b8afa3c6b30453206b0a
af/merge-core
James Jackson-South 11 years ago
parent
commit
8946611b97
  1. 70
      src/ImageProcessor/Filters/Contrast.cs
  2. 34
      src/ImageProcessor/Filters/IImageFilter.cs
  3. 81
      src/ImageProcessor/Filters/ImageFilterExtensions.cs
  4. 57
      src/ImageProcessor/Filters/ParallelImageFilter.cs
  5. 74
      src/ImageProcessor/Formats/Jpg/JpegDecoder.cs
  6. 3
      src/ImageProcessor/Image.cs
  7. 6
      src/ImageProcessor/ImageProcessor.csproj
  8. 23
      src/ImageProcessor/Samplers/IImageSampler.cs
  9. 58
      tests/ImageProcessor.Tests/Filters/FilterTests.cs
  10. 3
      tests/ImageProcessor.Tests/Formats/EncoderDecoderTests.cs
  11. 1
      tests/ImageProcessor.Tests/ImageProcessor.Tests.csproj

70
src/ImageProcessor/Filters/Contrast.cs

@ -0,0 +1,70 @@
// <copyright file="Contrast.cs" company="James South">
// Copyright © James South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessor.Filters
{
using System;
/// <summary>
/// An <see cref="IImageFilter"/> to change the contrast of an <see cref="Image"/>.
/// </summary>
public class Contrast : ParallelImageFilter
{
/// <summary>
/// Initializes a new instance of the <see cref="Contrast"/> class.
/// </summary>
/// <param name="contrast">The new contrast of the image. Must be between -100 and 100.</param>
/// <exception cref="ArgumentException">
/// <paramref name="contrast"/> is less than -100 is greater than 100.
/// </exception>
public Contrast(int contrast)
{
Guard.MustBeBetweenOrEqualTo(contrast, -100, 100, nameof(contrast));
this.Value = contrast;
}
/// <summary>
/// Gets the contrast value.
/// </summary>
public int Value { get; }
/// <inheritdoc/>
protected override void Apply(ImageBase target, ImageBase source, Rectangle rectangle, int startY, int endY)
{
double contrast = (100.0 + this.Value) / 100.0;
for (int y = startY; y < endY; y++)
{
for (int x = rectangle.X; x < rectangle.Right; x++)
{
Bgra color = source[x, y];
double r = color.R / 255.0;
r -= 0.5;
r *= contrast;
r += 0.5;
r *= 255;
r = r.Clamp(0, 255);
double g = color.G / 255.0;
g -= 0.5;
g *= contrast;
g += 0.5;
g *= 255;
g = g.Clamp(0, 255);
double b = color.B / 255.0;
b -= 0.5;
b *= contrast;
b += 0.5;
b *= 255;
b = b.Clamp(0, 255);
target[x, y] = new Bgra((byte)b, (byte)g, (byte)r, color.A);
}
}
}
}
}

34
src/ImageProcessor/Filters/IImageFilter.cs

@ -0,0 +1,34 @@
namespace ImageProcessor.Filters
{
/// <summary>
/// Image processing filter interface.
/// </summary>
/// <remarks>
/// The interface defines the set of methods, which should be
/// provided by all image processing filters. Methods of this interface
/// manipulate the original image.
/// </remarks>
public interface IImageFilter
{
/// <summary>
/// Apply filter to an image at the area of the specified rectangle.
/// </summary>
/// <param name="target">Target image to apply filter to.</param>
/// <param name="source">The source image. Cannot be null.</param>
/// <param name="rectangle">The rectangle, which defines the area of the
/// image where the filter should be applied to.</param>
/// <remarks>The method keeps the source image unchanged and returns the
/// the result of image processing filter as new image.</remarks>
/// <exception cref="System.ArgumentNullException">
/// <paramref name="target"/>
/// is null.
/// - or -
/// <paramref name="source"/>
/// is null.
/// </exception>
/// <exception cref="System.ArgumentException">
/// <paramref name="rectangle"/> doesnt fit the dimension of the image.
/// </exception>
void Apply(ImageBase target, ImageBase source, Rectangle rectangle);
}
}

81
src/ImageProcessor/Filters/ImageFilterExtensions.cs

@ -0,0 +1,81 @@
// <copyright file="ImageFilterExtensions.cs" company="James South">
// Copyright © James South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessor.Filters
{
using System;
/// <summary>
/// Exstension methods for performing filtering methods again an image.
/// </summary>
public static class ImageFilterExtensions
{
/// <summary>
/// Applies the collection of filters to the image.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="filters">Any filters to apply to the image.</param>
/// <returns>The <see cref="Image"/>.</returns>
public static Image Filter(this Image source, params IImageFilter[] filters) => Filter(source, source.Bounds, filters);
/// <summary>
/// Applies the collection of filters to the image.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="rectangle">
/// The rectangle defining the bounds of the pixels the image filter with adjust.</param>
/// <param name="filters">Any filters to apply to the image.</param>
/// <returns>The <see cref="Image"/>.</returns>
public static Image Filter(this Image source, Rectangle rectangle, params IImageFilter[] filters)
{
// ReSharper disable once LoopCanBeConvertedToQuery
foreach (IImageFilter filter in filters)
{
source = PerformAction(source, true, (sourceImage, targetImage) => filter.Apply(targetImage, sourceImage, rectangle));
}
return source;
}
/// <summary>
/// Alters the contrast component of the image.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="amount">The new contrast of the image. Must be between -100 and 100.</param>
/// <returns>The <see cref="Image"/>.</returns>
public static Image Contrast(this Image source, int amount) => source.Filter(new Contrast(amount));
/// <summary>
/// Performs the given action on the source image.
/// </summary>
/// <param name="source">The image to perform the action against.</param>
/// <param name="clone">Whether to clone the image.</param>
/// <param name="action">The <see cref="Action"/> to perform against the image.</param>
/// <returns>The <see cref="Image"/>.</returns>
private static Image PerformAction(Image source, bool clone, Action<ImageBase, ImageBase> action)
{
Image transformedImage = clone ? new Image(source) : new Image(source.Width, source.Height);
action(source, transformedImage);
for (int i = 0; i < source.Frames.Count; i++)
{
ImageFrame frame = source.Frames[i];
ImageFrame tranformedFrame = new ImageFrame(frame);
action(frame, tranformedFrame);
if (!clone)
{
transformedImage.Frames.Add(tranformedFrame);
}
else
{
transformedImage.Frames[i] = tranformedFrame;
}
}
return transformedImage;
}
}
}

57
src/ImageProcessor/Filters/ParallelImageFilter.cs

@ -0,0 +1,57 @@
namespace ImageProcessor.Filters
{
using System;
using System.Threading.Tasks;
/// <summary>
/// Allows the application of filters using prallel processing.
/// </summary>
public abstract class ParallelImageFilter : IImageFilter
{
/// <summary>
/// Gets or sets the count of workers to run the filter in parallel.
/// </summary>
public int Parallelism { get; set; } = Environment.ProcessorCount;
/// <inheritdoc/>
public void Apply(ImageBase target, ImageBase source, Rectangle rectangle)
{
this.OnApply();
if (this.Parallelism > 1)
{
int partitionCount = this.Parallelism;
Task[] tasks = new Task[partitionCount];
for (int p = 0; p < partitionCount; p++)
{
int current = p;
tasks[p] = Task.Run(() =>
{
int batchSize = rectangle.Height / partitionCount;
int yStart = rectangle.Y + (current * batchSize);
int yEnd = current == partitionCount - 1 ? rectangle.Bottom : yStart + batchSize;
this.Apply(target, source, rectangle, yStart, yEnd);
});
}
Task.WaitAll(tasks);
}
else
{
this.Apply(target, source, rectangle, rectangle.Y, rectangle.Bottom);
}
}
/// <summary>
/// This method is called before the filter is applied to prepare the filter.
/// </summary>
protected virtual void OnApply()
{
}
protected abstract void Apply(ImageBase target, ImageBase source, Rectangle rectangle, int startY, int endY);
}
}

74
src/ImageProcessor/Formats/Jpg/JpegDecoder.cs

@ -1,12 +1,7 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="JpegEncoder.cs" company="James South">
// Copyright © James South and contributors.
// Licensed under the Apache License, Version 2.0.
// <copyright file="JpegDecoder.cs" company="James South">
// Copyright © James South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
// <summary>
// Image decoder for generating an image out of a jpg stream.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace ImageProcessor.Formats
{
@ -42,7 +37,11 @@ namespace ImageProcessor.Formats
{
Guard.NotNullOrEmpty(extension, "extension");
if (extension.StartsWith(".")) extension = extension.Substring(1);
if (extension.StartsWith("."))
{
extension = extension.Substring(1);
}
return extension.Equals("JPG", StringComparison.OrdinalIgnoreCase) ||
extension.Equals("JPEG", StringComparison.OrdinalIgnoreCase) ||
extension.Equals("JFIF", StringComparison.OrdinalIgnoreCase);
@ -68,7 +67,7 @@ namespace ImageProcessor.Formats
if (header.Length >= 11)
{
bool isJpeg = IsJpeg(header);
bool isExif = IsExif(header);
bool isExif = this.IsExif(header);
isSupported = isJpeg || isExif;
}
@ -76,30 +75,6 @@ namespace ImageProcessor.Formats
return isSupported;
}
private bool IsExif(byte[] header)
{
bool isExif =
header[6] == 0x45 && // E
header[7] == 0x78 && // x
header[8] == 0x69 && // i
header[9] == 0x66 && // f
header[10] == 0x00;
return isExif;
}
private static bool IsJpeg(byte[] header)
{
bool isJpg =
header[6] == 0x4A && // J
header[7] == 0x46 && // F
header[8] == 0x49 && // I
header[9] == 0x46 && // F
header[10] == 0x00;
return isJpg;
}
/// <summary>
/// Decodes the image from the specified stream and sets
/// the data to image.
@ -137,7 +112,7 @@ namespace ImageProcessor.Formats
{
Sample sample = row.GetAt(x);
int offset = (y * pixelWidth + x) * 4;
int offset = ((y * pixelWidth) + x) * 4;
pixels[offset + 0] = (byte)sample[2];
pixels[offset + 1] = (byte)sample[1];
@ -148,5 +123,34 @@ namespace ImageProcessor.Formats
image.SetPixels(pixelWidth, pixelHeight, pixels);
}
/// <summary>
///
/// </summary>
/// <param name="header"></param>
/// <returns></returns>
private bool IsExif(byte[] header)
{
bool isExif =
header[6] == 0x45 && // E
header[7] == 0x78 && // x
header[8] == 0x69 && // i
header[9] == 0x66 && // f
header[10] == 0x00;
return isExif;
}
private static bool IsJpeg(byte[] header)
{
bool isJpg =
header[6] == 0x4A && // J
header[7] == 0x46 && // F
header[8] == 0x49 && // I
header[9] == 0x46 && // F
header[10] == 0x00;
return isJpg;
}
}
}

3
src/ImageProcessor/Image.cs

@ -61,6 +61,7 @@ namespace ImageProcessor
{
this.HorizontalResolution = DefaultHorizontalResolution;
this.VerticalResolution = DefaultVerticalResolution;
this.CurrentImageFormat = DefaultFormats.Value.First(f => f.GetType() == typeof(PngFormat));
}
/// <summary>
@ -74,6 +75,7 @@ namespace ImageProcessor
{
this.HorizontalResolution = DefaultHorizontalResolution;
this.VerticalResolution = DefaultVerticalResolution;
this.CurrentImageFormat = DefaultFormats.Value.First(f => f.GetType() == typeof(PngFormat));
}
/// <summary>
@ -97,6 +99,7 @@ namespace ImageProcessor
this.HorizontalResolution = DefaultHorizontalResolution;
this.VerticalResolution = DefaultVerticalResolution;
this.CurrentImageFormat = other.CurrentImageFormat;
}
/// <summary>

6
src/ImageProcessor/ImageProcessor.csproj

@ -38,9 +38,12 @@
</PropertyGroup>
<ItemGroup>
<!-- A reference to the entire .NET Framework is automatically included -->
<Folder Include="Filters\Transforms\" />
</ItemGroup>
<ItemGroup>
<Compile Include="Filters\Contrast.cs" />
<Compile Include="Filters\ImageFilterExtensions.cs" />
<Compile Include="Filters\ParallelImageFilter.cs" />
<Compile Include="Filters\IImageFilter.cs" />
<Compile Include="Colors\Hsv.cs" />
<Compile Include="Colors\YCbCr.cs" />
<Compile Include="Common\Extensions\ByteExtensions.cs" />
@ -174,6 +177,7 @@
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Numerics\Rectangle.cs" />
<Compile Include="Numerics\Size.cs" />
<Compile Include="Samplers\IImageSampler.cs" />
</ItemGroup>
<ItemGroup>
<Reference Include="ICSharpCode.SharpZipLib.Portable, Version=0.86.0.51802, Culture=neutral, processorArchitecture=MSIL">

23
src/ImageProcessor/Samplers/IImageSampler.cs

@ -0,0 +1,23 @@
// <copyright file="IImageSampler.cs" company="James South">
// Copyright © James South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessor.Samplers
{
/// <summary>
/// Encapsulates the methods required for all image sampling (resizing) algorithms.
/// </summary>
public interface IImageSampler
{
/// <summary>
/// Resizes the specified source image by creating a new image with
/// the specified size which is a resized version of the passed image.
/// </summary>
/// <param name="source">The source image.</param>
/// <param name="target">The target image.</param>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
void Sample(ImageBase source, ImageBase target, int width, int height);
}
}

58
tests/ImageProcessor.Tests/Filters/FilterTests.cs

@ -0,0 +1,58 @@

namespace ImageProcessor.Tests.Filters
{
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using ImageProcessor.Filters;
using Xunit;
public class FilterTests
{
public static readonly List<string> Files = new List<string>
{
{ "../../TestImages/Formats/Jpg/Backdrop.jpg"},
{ "../../TestImages/Formats/Bmp/Car.bmp" },
{ "../../TestImages/Formats/Png/cmyk.png" },
//{ "../../TestImages/Formats/Gif/a.gif" },
//{ "../../TestImages/Formats/Gif/leaf.gif" },
//{ "../../TestImages/Formats/Gif/ani.gif" },
//{ "../../TestImages/Formats/Gif/ani2.gif" },
//{ "../../TestImages/Formats/Gif/giphy.gif" },
};
public static readonly TheoryData<string, IImageFilter> Filters = new TheoryData<string, IImageFilter>
{
{ "Contrast-50", new Contrast(50) },
{ "Contrast--50", new Contrast(-50) },
};
[Theory]
[MemberData("Filters")]
public void FilterImage(string name, IImageFilter filter)
{
if (!Directory.Exists("Filtered"))
{
Directory.CreateDirectory("Filtered");
}
foreach (string file in Files)
{
using (FileStream stream = File.OpenRead(file))
{
Stopwatch watch = Stopwatch.StartNew();
Image image = new Image(stream);
string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file);
using (FileStream output = File.OpenWrite($"Filtered/{ Path.GetFileName(filename) }"))
{
image.Filter(filter).Save(output);
}
Trace.WriteLine($"{ name }: { watch.ElapsedMilliseconds}ms");
}
}
}
}
}

3
tests/ImageProcessor.Tests/Formats/EncoderDecoderTests.cs

@ -2,9 +2,8 @@
{
using System.Diagnostics;
using System.IO;
using System.Linq;
using ImageProcessor.Formats;
using Formats;
using Xunit;

1
tests/ImageProcessor.Tests/ImageProcessor.Tests.csproj

@ -53,6 +53,7 @@
<ItemGroup>
<Compile Include="Colors\ColorConversionTests.cs" />
<Compile Include="Colors\ColorTests.cs" />
<Compile Include="Filters\FilterTests.cs" />
<Compile Include="Formats\EncoderDecoderTests.cs" />
<Compile Include="Helpers\GuardTests.cs" />
<Compile Include="Numerics\RectangleTests.cs" />

Loading…
Cancel
Save