Browse Source

Begin add resize

Not working correctly just now. Y calc is wrong.


Former-commit-id: 21b81ada54ca20a5ba1f08cb0fce9c57dd7237b2
Former-commit-id: b9800debdc5306d6935be95808bef72b34c2b9bc
Former-commit-id: b06159adfd6ec098e13f9509a14b0f5bb3d8369e
pull/17/head
James Jackson-South 11 years ago
parent
commit
4688eaa5f6
  1. 33
      src/ImageProcessor/Common/Extensions/ComparableExtensions.cs
  2. 19
      src/ImageProcessor/Common/Helpers/Guard.cs
  3. 141
      src/ImageProcessor/Common/Helpers/PixelOperations.cs
  4. 5
      src/ImageProcessor/Filters/Contrast.cs
  5. 4
      src/ImageProcessor/Formats/Bmp/BmpDecoderCore.cs
  6. 2
      src/ImageProcessor/IImageProcessor.cs
  7. 23
      src/ImageProcessor/ImageExtensions.cs
  8. 6
      src/ImageProcessor/ImageProcessor.csproj
  9. 47
      src/ImageProcessor/ParallelImageProcessor.cs
  10. 42
      src/ImageProcessor/Samplers/BicubicResampler.cs
  11. 22
      src/ImageProcessor/Samplers/IResampler.cs
  12. 28
      src/ImageProcessor/Samplers/ImageSampleExtensions.cs
  13. 149
      src/ImageProcessor/Samplers/Resize.cs
  14. 30
      tests/ImageProcessor.Tests/Filters/FilterTests.cs

33
src/ImageProcessor/Common/Extensions/ComparableExtensions.cs

@ -42,5 +42,38 @@ namespace ImageProcessor
return value;
}
/// <summary>
/// Converts an <see cref="int"/> to a <see cref="byte"/> first restricting the value between the
/// minimum and maximum allowable ranges.
/// </summary>
/// <param name="value">The <see cref="int"/> this method extends.</param>
/// <returns>The <see cref="byte"/></returns>
public static byte ToByte(this int value)
{
return (byte)value.Clamp(0, 255);
}
/// <summary>
/// Converts an <see cref="float"/> to a <see cref="byte"/> first restricting the value between the
/// minimum and maximum allowable ranges.
/// </summary>
/// <param name="value">The <see cref="float"/> this method extends.</param>
/// <returns>The <see cref="byte"/></returns>
public static byte ToByte(this float value)
{
return (byte)value.Clamp(0, 255);
}
/// <summary>
/// Converts an <see cref="double"/> to a <see cref="byte"/> first restricting the value between the
/// minimum and maximum allowable ranges.
/// </summary>
/// <param name="value">The <see cref="double"/> this method extends.</param>
/// <returns>The <see cref="byte"/></returns>
public static byte ToByte(this double value)
{
return (byte)value.Clamp(0, 255);
}
}
}

19
src/ImageProcessor/Common/Helpers/Guard.cs

@ -20,7 +20,7 @@ namespace ImageProcessor
internal static class Guard
{
/// <summary>
/// Verifies, that the method parameter with specified object value is not null
/// Verifies, that the method parameter with specified object value is not null
/// and throws an exception if it is found to be so.
/// </summary>
/// <param name="target">
@ -50,7 +50,7 @@ namespace ImageProcessor
/// <summary>
/// Verifies, that the string method parameter with specified object value and message
/// is not null, not empty and does not contain only blanks and throws an exception
/// is not null, not empty and does not contain only blanks and throws an exception
/// if the object is null.
/// </summary>
/// <param name="target">The target string, which should be checked against being null or empty.</param>
@ -86,7 +86,8 @@ namespace ImageProcessor
/// <exception cref="ArgumentException">
/// <paramref name="value"/> is greater than the maximum value.
/// </exception>
public static void MustBeLessThan<TValue>(TValue value, TValue max, string parameterName) where TValue : IComparable<TValue>
public static void MustBeLessThan<TValue>(TValue value, TValue max, string parameterName)
where TValue : IComparable<TValue>
{
if (value.CompareTo(max) >= 0)
{
@ -107,7 +108,8 @@ namespace ImageProcessor
/// <exception cref="ArgumentException">
/// <paramref name="value"/> is greater than the maximum value.
/// </exception>
public static void MustBeLessThanOrEqualTo<TValue>(TValue value, TValue max, string parameterName) where TValue : IComparable<TValue>
public static void MustBeLessThanOrEqualTo<TValue>(TValue value, TValue max, string parameterName)
where TValue : IComparable<TValue>
{
if (value.CompareTo(max) > 0)
{
@ -128,7 +130,8 @@ namespace ImageProcessor
/// <exception cref="ArgumentException">
/// <paramref name="value"/> is less than the minimum value.
/// </exception>
public static void MustBeGreaterThan<TValue>(TValue value, TValue min, string parameterName) where TValue : IComparable<TValue>
public static void MustBeGreaterThan<TValue>(TValue value, TValue min, string parameterName)
where TValue : IComparable<TValue>
{
if (value.CompareTo(min) <= 0)
{
@ -149,7 +152,8 @@ namespace ImageProcessor
/// <exception cref="ArgumentException">
/// <paramref name="value"/> is less than the minimum value.
/// </exception>
public static void MustBeGreaterThanOrEqualTo<TValue>(TValue value, TValue min, string parameterName) where TValue : IComparable<TValue>
public static void MustBeGreaterThanOrEqualTo<TValue>(TValue value, TValue min, string parameterName)
where TValue : IComparable<TValue>
{
if (value.CompareTo(min) < 0)
{
@ -171,7 +175,8 @@ namespace ImageProcessor
/// <exception cref="ArgumentException">
/// <paramref name="value"/> is less than the minimum value of greater than the maximum value.
/// </exception>
public static void MustBeBetweenOrEqualTo<TValue>(TValue value, TValue min, TValue max, string parameterName) where TValue : IComparable<TValue>
public static void MustBeBetweenOrEqualTo<TValue>(TValue value, TValue min, TValue max, string parameterName)
where TValue : IComparable<TValue>
{
if (value.CompareTo(min) < 0 || value.CompareTo(max) > 0)
{

141
src/ImageProcessor/Common/Helpers/PixelOperations.cs

@ -0,0 +1,141 @@
// <copyright file="PixelOperations.cs" company="James South">
// Copyright © James South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessor
{
using System;
/// <summary>
/// Performs per-pixel operations.
/// </summary>
public static class PixelOperations
{
/// <summary>
/// The array of bytes representing each possible value of color component
/// converted from sRGB to the linear color space.
/// </summary>
private static readonly Lazy<byte[]> LinearBytes = new Lazy<byte[]>(GetLinearBytes);
/// <summary>
/// The array of bytes representing each possible value of color component
/// converted from linear to the sRGB color space.
/// </summary>
private static readonly Lazy<byte[]> SrgbBytes = new Lazy<byte[]>(GetSrgbBytes);
/// <summary>
/// Converts an pixel from an sRGB color-space to the equivalent linear color-space.
/// </summary>
/// <param name="composite">
/// The <see cref="Bgra"/> to convert.
/// </param>
/// <returns>
/// The <see cref="Bgra"/>.
/// </returns>
public static Bgra ToLinear(Bgra composite)
{
// Create only once and lazily.
byte[] ramp = LinearBytes.Value;
return new Bgra(composite.B, ramp[composite.G], ramp[composite.R], ramp[composite.A]);
}
/// <summary>
/// Converts a pixel from a linear color-space to the equivalent sRGB color-space.
/// </summary>
/// <param name="linear">
/// The <see cref="Bgra"/> to convert.
/// </param>
/// <returns>
/// The <see cref="Bgra"/>.
/// </returns>
public static Bgra ToSrgb(Bgra linear)
{
// Create only once and lazily.
byte[] ramp = SrgbBytes.Value;
return new Bgra(linear.B, ramp[linear.G], ramp[linear.R], ramp[linear.A]);
}
/// <summary>
/// Gets an array of bytes representing each possible value of color component
/// converted from sRGB to the linear color space.
/// </summary>
/// <returns>
/// The <see cref="T:byte[]"/>.
/// </returns>
private static byte[] GetLinearBytes()
{
byte[] ramp = new byte[256];
for (int x = 0; x < 256; ++x)
{
byte val = (byte)(255f * SrgbToLinear(x / 255f)).Clamp(0, 255);
ramp[x] = val;
}
return ramp;
}
/// <summary>
/// Gets an array of bytes representing each possible value of color component
/// converted from linear to the sRGB color space.
/// </summary>
/// <returns>
/// The <see cref="T:byte[]"/>.
/// </returns>
private static byte[] GetSrgbBytes()
{
byte[] ramp = new byte[256];
for (int x = 0; x < 256; ++x)
{
byte val = (byte)(255f * LinearToSrgb(x / 255f)).Clamp(0, 255);
ramp[x] = val;
}
return ramp;
}
/// <summary>
/// Gets the correct linear value from an sRGB signal.
/// <see href="http://www.4p8.com/eric.brasseur/gamma.html#formulas"/>
/// <see href="http://entropymine.com/imageworsener/srgbformula/"/>
/// </summary>
/// <param name="signal">The signal value to convert.</param>
/// <returns>
/// The <see cref="float"/>.
/// </returns>
private static float SrgbToLinear(float signal)
{
float a = 0.055f;
if (signal <= 0.04045)
{
return signal / 12.92f;
}
return (float)Math.Pow((signal + a) / (1 + a), 2.4);
}
/// <summary>
/// Gets the correct sRGB value from an linear signal.
/// <see href="http://www.4p8.com/eric.brasseur/gamma.html#formulas"/>
/// <see href="http://entropymine.com/imageworsener/srgbformula/"/>
/// </summary>
/// <param name="signal">The signal value to convert.</param>
/// <returns>
/// The <see cref="float"/>.
/// </returns>
private static float LinearToSrgb(float signal)
{
float a = 0.055f;
if (signal <= 0.0031308)
{
return signal * 12.92f;
}
return ((float)((1 + a) * Math.Pow(signal, 1 / 2.4f))) - a;
}
}
}

5
src/ImageProcessor/Filters/Contrast.cs

@ -30,6 +30,11 @@ namespace ImageProcessor.Filters
/// </summary>
public int Value { get; }
protected override void Apply(ImageBase source, ImageBase target, Rectangle sourceRectangle, Rectangle targetRectangle, int startY, int endY)
{
throw new NotImplementedException();
}
/// <inheritdoc/>
protected override void Apply(ImageBase target, ImageBase source, Rectangle rectangle, int startY, int endY)
{

4
src/ImageProcessor/Formats/Bmp/BmpDecoderCore.cs

@ -133,7 +133,9 @@ namespace ImageProcessor.Formats
}
else if (this.infoHeader.BitsPerPixel <= 8)
{
this.ReadRgbPalette(imageData, palette,
this.ReadRgbPalette(
imageData,
palette,
this.infoHeader.Width,
this.infoHeader.Height,
this.infoHeader.BitsPerPixel);

2
src/ImageProcessor/IImageProcessor.cs

@ -29,5 +29,7 @@ namespace ImageProcessor
/// <paramref name="rectangle"/> doesnt fit the dimension of the image.
/// </exception>
void Apply(ImageBase target, ImageBase source, Rectangle rectangle);
void Apply(ImageBase target, ImageBase source, int width, int height, Rectangle targetRectangle, Rectangle sourceRectangle);
}
}

23
src/ImageProcessor/ImageExtensions.cs

@ -76,6 +76,29 @@ namespace ImageProcessor
return source;
}
/// <summary>
/// Applies the collection of processors 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="width"></param>
/// <param name="height"></param>
/// <param name="sourceRectangle"></param>
/// <param name="targetRectangle"></param>
/// <param name="processors">Any processors to apply to the image.</param>
/// <returns>The <see cref="Image"/>.</returns>
public static Image Process(this Image source, int width, int height, Rectangle sourceRectangle, Rectangle targetRectangle, params IImageProcessor[] processors)
{
// ReSharper disable once LoopCanBeConvertedToQuery
foreach (IImageProcessor filter in processors)
{
source = PerformAction(source, false, (sourceImage, targetImage) => filter.Apply(targetImage, sourceImage, width, height, targetRectangle, sourceRectangle));
}
return source;
}
/// <summary>
/// Performs the given action on the source image.
/// </summary>

6
src/ImageProcessor/ImageProcessor.csproj

@ -38,9 +38,9 @@
</PropertyGroup>
<ItemGroup>
<!-- A reference to the entire .NET Framework is automatically included -->
<Folder Include="Samplers\" />
</ItemGroup>
<ItemGroup>
<Compile Include="Common\Helpers\PixelOperations.cs" />
<Compile Include="Filters\Contrast.cs" />
<Compile Include="Filters\ImageFilterExtensions.cs" />
<Compile Include="ImageExtensions.cs" />
@ -179,6 +179,10 @@
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Numerics\Rectangle.cs" />
<Compile Include="Numerics\Size.cs" />
<Compile Include="Samplers\BicubicResampler.cs" />
<Compile Include="Samplers\ImageSampleExtensions.cs" />
<Compile Include="Samplers\IResampler.cs" />
<Compile Include="Samplers\Resize.cs" />
</ItemGroup>
<ItemGroup>
<Reference Include="ICSharpCode.SharpZipLib.Portable, Version=0.86.0.51802, Culture=neutral, processorArchitecture=MSIL">

47
src/ImageProcessor/ParallelImageProcessor.cs

@ -50,6 +50,51 @@ namespace ImageProcessor
}
}
/// <inheritdoc/>
public void Apply(ImageBase target, ImageBase source, int width, int height, Rectangle targetRectangle = default(Rectangle), Rectangle sourceRectangle = default(Rectangle))
{
this.OnApply();
byte[] pixels = new byte[width * height * 4];
target.SetPixels(width, height, pixels);
if (targetRectangle == Rectangle.Empty)
{
targetRectangle = target.Bounds;
}
if (sourceRectangle == Rectangle.Empty)
{
sourceRectangle = source.Bounds;
}
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 = targetRectangle.Bottom / partitionCount;
int yStart = current * batchSize;
int yEnd = current == partitionCount - 1 ? targetRectangle.Bottom : yStart + batchSize;
this.Apply(target, source, targetRectangle, sourceRectangle, yStart, yEnd);
});
}
Task.WaitAll(tasks);
}
else
{
this.Apply(target, source, targetRectangle, sourceRectangle, targetRectangle.Y, targetRectangle.Bottom);
}
}
/// <summary>
/// This method is called before the process is applied to prepare the processor.
/// </summary>
@ -57,6 +102,8 @@ namespace ImageProcessor
{
}
protected abstract void Apply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY);
/// <summary>
/// Apply a process to an image to alter the pixels at the area of the specified rectangle.
/// </summary>

42
src/ImageProcessor/Samplers/BicubicResampler.cs

@ -0,0 +1,42 @@
// <copyright file="BicubicResampler.cs" company="James South">
// Copyright © James South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessor.Samplers
{
/// <summary>
/// The function implements the bicubic kernel algorithm W(x) as described on
/// <see href="https://en.wikipedia.org/wiki/Bicubic_interpolation#Bicubic_convolution_algorithm">Wikipedia</see>
/// </summary>
public class BicubicResampler : IResampler
{
/// <inheritdoc/>
public double Radius => 4;
/// <inheritdoc/>
public double GetValue(double x)
{
// The coefficient.
double a = -0.5;
if (x < 0)
{
x = -x;
}
double result = 0;
if (x <= 1)
{
result = (((1.5 * x) - 2.5) * x * x) + 1;
}
else if (x < 2)
{
result = (((((a * x) + 2.5) * x) - 4) * x) + 2;
}
return result;
}
}
}

22
src/ImageProcessor/Samplers/IResampler.cs

@ -0,0 +1,22 @@
namespace ImageProcessor.Samplers
{
/// <summary>
/// Encasulates an interpolation algorithm for resampling images.
/// </summary>
public interface IResampler
{
/// <summary>
/// Gets the radius in which to sample pixels.
/// </summary>
double Radius { get; }
/// <summary>
/// Gets the result of the interpolation algorithm.
/// </summary>
/// <param name="x">The value to process.</param>
/// <returns>
/// The <see cref="double"/>
/// </returns>
double GetValue(double x);
}
}

28
src/ImageProcessor/Samplers/ImageSampleExtensions.cs

@ -0,0 +1,28 @@
// <copyright file="ImageFilterExtensions.cs" company="James South">
// Copyright © James South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessor.Samplers
{
/// <summary>
/// Exstensions methods for <see cref="Image"/> to apply samplers to the image.
/// </summary>
public static class ImageSampleExtensions
{
public static Image Resize(this Image source, int width, int height)
{
return source.Process(width, height, default(Rectangle), default(Rectangle), new Resize(new BicubicResampler(), width, height));
}
public static Image Resize(this Image source, int width, int height, IResampler sampler)
{
return source.Process(width, height, default(Rectangle), default(Rectangle), new Resize(sampler, width, height));
}
public static Image Resize(this Image source, int width, int height, IResampler sampler, Rectangle sourceRectangle, Rectangle targetRectangle)
{
return source.Process(width, height, sourceRectangle, targetRectangle, new Resize(sampler, width, height));
}
}
}

149
src/ImageProcessor/Samplers/Resize.cs

@ -0,0 +1,149 @@
using System;
namespace ImageProcessor.Samplers
{
public class Resize : ParallelImageProcessor
{
/// <summary>
/// Initializes a new instance of the <see cref="Resize"/> class.
/// </summary>
/// <param name="sampler">
/// The sampler to perform the resize operation.
/// </param>
public Resize(IResampler sampler, int width, int height)
{
Guard.NotNull(sampler, nameof(sampler));
Guard.MustBeGreaterThan(width, 0, nameof(width));
Guard.MustBeGreaterThan(height, 0, nameof(height));
this.Sampler = sampler;
this.Width = width;
this.Height = height;
}
/// <summary>
/// Gets the sampler to perform the resize operation.
/// </summary>
public IResampler Sampler { get; }
/// <summary>
/// Gets the width.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the height.
/// </summary>
public int Height { get; }
/// <inheritdoc/>
protected override void Apply(
ImageBase target,
ImageBase source,
Rectangle targetRectangle,
Rectangle sourceRectangle,
int startY,
int endY)
{
int sourceWidth = source.Width;
int sourceHeight = source.Height;
int width = target.Width;
int height = target.Height;
int startX = targetRectangle.X;
int endX = targetRectangle.Right;
int right = (int)(this.Sampler.Radius + .5);
int left = -right;
// Scaling factors
double widthFactor = sourceWidth / (double)targetRectangle.Width;
double heightFactor = sourceHeight / (double)targetRectangle.Height;
// Width and height decreased by 1
int maxHeight = sourceHeight - 1;
int maxWidth = sourceWidth - 1;
for (int y = startY; y < endY; y++)
{
if (y >= 0 && y < height)
{
// Y coordinates of source points.
double originY = ((startY - targetRectangle.Y) * heightFactor) - 0.5;
int originY1 = (int)originY;
double dy = originY - originY1;
// For each row.
for (int x = startX; x < endX; x++)
{
if (x >= 0 && x < width)
{
// X coordinates of source points.
double originX = ((x - startX) * widthFactor) - 0.5f;
int originX1 = (int)originX;
double dx = originX - originX1;
// Destination color components
double r = 0;
double g = 0;
double b = 0;
double a = 0;
for (int yy = left; yy < right; yy++)
{
// Get Y cooefficient
double kernel1 = this.Sampler.GetValue(dy - yy);
int originY2 = originY1 + yy;
if (originY2 < 0)
{
originY2 = 0;
}
if (originY2 > maxHeight)
{
originY2 = maxHeight;
}
for (int xx = left; xx < right; xx++)
{
// Get X cooefficient
double kernel2 = kernel1 * this.Sampler.GetValue(xx - dx);
int originX2 = originX1 + xx;
if (originX2 < 0)
{
originX2 = 0;
}
if (originX2 > maxWidth)
{
originX2 = maxWidth;
}
Bgra sourceColor = source[originX2, originY2];
sourceColor = PixelOperations.ToLinear(sourceColor);
r += kernel2 * sourceColor.R;
g += kernel2 * sourceColor.G;
b += kernel2 * sourceColor.B;
a += kernel2 * sourceColor.A;
}
}
Bgra destinationColor = new Bgra(b.ToByte(), g.ToByte(), r.ToByte(), a.ToByte());
destinationColor = PixelOperations.ToSrgb(destinationColor);
target[x, y] = destinationColor;
}
}
}
}
}
/// <inheritdoc/>
protected override void Apply(ImageBase target, ImageBase source, Rectangle rectangle, int startY, int endY)
{
throw new NotImplementedException();
}
}
}

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

@ -6,6 +6,7 @@ namespace ImageProcessor.Tests.Filters
using System.IO;
using ImageProcessor.Filters;
using ImageProcessor.Samplers;
using Xunit;
@ -14,8 +15,8 @@ namespace ImageProcessor.Tests.Filters
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/Bmp/Car.bmp" },
//{ "../../TestImages/Formats/Png/cmyk.png" },
//{ "../../TestImages/Formats/Gif/a.gif" },
//{ "../../TestImages/Formats/Gif/leaf.gif" },
//{ "../../TestImages/Formats/Gif/ani.gif" },
@ -54,5 +55,30 @@ namespace ImageProcessor.Tests.Filters
}
}
}
[Fact]
public void ResizeImage()
{
if (!Directory.Exists("Resized"))
{
Directory.CreateDirectory("Resized");
}
foreach (string file in Files)
{
using (FileStream stream = File.OpenRead(file))
{
Stopwatch watch = Stopwatch.StartNew();
Image image = new Image(stream);
string filename = Path.GetFileName(file);
using (FileStream output = File.OpenWrite($"Resized/{ Path.GetFileName(filename) }"))
{
image.Resize(400, 400).Save(output);
}
Trace.WriteLine($"{ filename }: { watch.ElapsedMilliseconds}ms");
}
}
}
}
}

Loading…
Cancel
Save