Browse Source

Merge remote-tracking branch 'refs/remotes/origin/Core-IDisposable' into Core

Former-commit-id: dcb89b26e6586df8fad2e265558d7c59e0779794
Former-commit-id: ee25eca0cb34d6983ec9b3acce13456af6556ed6
Former-commit-id: eb700f049d4ca2f4e9ef01c2595513549eebf6a9
pull/1/head
James South 10 years ago
parent
commit
af5a1dcc59
  1. 36
      README.md
  2. 6
      src/ImageProcessorCore/Filters/Blend.cs
  3. 10
      src/ImageProcessorCore/Filters/ImageFilterExtensions.cs
  4. 2
      src/ImageProcessorCore/Formats/Png/PngEncoder.cs
  5. 2
      src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs
  6. 2
      src/ImageProcessorCore/IImageBase.cs
  7. 26
      src/ImageProcessorCore/Image.cs
  8. 159
      src/ImageProcessorCore/ImageBase.cs
  9. 6
      src/ImageProcessorCore/ImageExtensions.cs
  10. 40
      src/ImageProcessorCore/ParallelImageProcessor.cs
  11. 25
      src/ImageProcessorCore/Samplers/EntropyCrop.cs
  12. 63
      src/ImageProcessorCore/Samplers/Resize.cs
  13. 23
      tests/ImageProcessorCore.Tests/Formats/BitmapTests.cs
  14. 120
      tests/ImageProcessorCore.Tests/Formats/EncoderDecoderTests.cs
  15. 11
      tests/ImageProcessorCore.Tests/Formats/PngTests.cs
  16. 15
      tests/ImageProcessorCore.Tests/Processors/Filters/FilterTests.cs
  17. 88
      tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs

36
README.md

@ -50,7 +50,7 @@ git clone https://github.com/JimBobSquarePants/ImageProcessor
###What works so far/ What is planned? ###What works so far/ What is planned?
- Encoding/decoding of image formats (plugable), progressive required - Encoding/decoding of image formats (plugable).
- [x] Jpeg (Includes Subsampling. Progressive writing required) - [x] Jpeg (Includes Subsampling. Progressive writing required)
- [x] Bmp (Read: 32bit, 24bit, 16 bit. Write: 32bit, 24bit just now) - [x] Bmp (Read: 32bit, 24bit, 16 bit. Write: 32bit, 24bit just now)
- [x] Png (Read: TrueColor, Grayscale, Indexed. Write: True color, Indexed just now) - [x] Png (Read: TrueColor, Grayscale, Indexed. Write: True color, Indexed just now)
@ -153,7 +153,7 @@ git clone https://github.com/JimBobSquarePants/ImageProcessor
###API Changes ###API Changes
With this version the API will change dramatically. Without the constraints of `System.Drawing` I have been able to develop something much more flexible, easier to code against, and much, much less prone to memory leaks. Gone are using image classes which implements `IDisposable`, Gone are system wide proces locks with Images and processors thread safe usable in parallel processing utilizing all the availables cores. With this version the API will change dramatically. Without the constraints of `System.Drawing` I have been able to develop something much more flexible, easier to code against, and much, much less prone to memory leaks. Gone are system wide proces locks with Images and processors thread safe usable in parallel processing utilizing all the availables cores.
Image methods are also fluent which allow chaining much like the `ImageFactory` class in the Framework version. Image methods are also fluent which allow chaining much like the `ImageFactory` class in the Framework version.
@ -161,14 +161,12 @@ Here's an example of the code required to resize an image using the default Bicu
```csharp ```csharp
using (FileStream stream = File.OpenRead("foo.jpg")) using (FileStream stream = File.OpenRead("foo.jpg"))
using (Image image = new Image(stream))
using (FileStream output = File.OpenWrite("bar.jpg"))
{ {
Image image = new Image(stream); image.Resize(image.Width / 2, image.Height / 2)
using (FileStream output = File.OpenWrite("bar.jpg")) .Greyscale()
{ .Save(output);
image.Resize(image.Width / 2, image.Height / 2)
.Greyscale()
.Save(output);
}
} }
``` ```
@ -176,19 +174,17 @@ It will also be possible to pass collections of processors as params to manipula
```csharp ```csharp
using (FileStream stream = File.OpenRead("foo.jpg")) using (FileStream stream = File.OpenRead("foo.jpg"))
using (Image image = new Image(stream))
using (FileStream output = File.OpenWrite("bar.jpg"))
{ {
Image image = new Image(stream); List<IImageProcessor> processors = new List<IImageProcessor>()
using (FileStream output = File.OpenWrite("bar.jpg"))
{ {
List<IImageProcessor> processors = new List<IImageProcessor>() new GuassianBlur(5),
{ new Sobel { Greyscale = true }
new GuassianBlur(5), };
new Sobel { Greyscale = true }
}; image.Process(processors.ToArray())
.Save(output);
image.Process(processors.ToArray())
.Save(output);
}
} }
``` ```
Individual processors can be initialised and apply processing against images. This allows nesting which will allow the powerful combination of processing methods: Individual processors can be initialised and apply processing against images. This allows nesting which will allow the powerful combination of processing methods:

6
src/ImageProcessorCore/Filters/Blend.cs

@ -20,7 +20,10 @@ namespace ImageProcessorCore.Filters
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Blend"/> class. /// Initializes a new instance of the <see cref="Blend"/> class.
/// </summary> /// </summary>
/// <param name="image">The image to blend.</param> /// <param name="image">
/// The image to blend with the currently processing image.
/// Disposal of this image is the responsibility of the developer.
/// </param>
/// <param name="alpha">The opacity of the image to blend. Between 0 and 100.</param> /// <param name="alpha">The opacity of the image to blend. Between 0 and 100.</param>
public Blend(ImageBase image, int alpha = 100) public Blend(ImageBase image, int alpha = 100)
{ {
@ -69,6 +72,7 @@ namespace ImageProcessorCore.Filters
target[x, y] = color; target[x, y] = color;
} }
this.OnRowProcessed(); this.OnRowProcessed();
} }
}); });

10
src/ImageProcessorCore/Filters/ImageFilterExtensions.cs

@ -73,7 +73,10 @@ namespace ImageProcessorCore.Filters
/// Combines the given image together with the current one by blending their pixels. /// Combines the given image together with the current one by blending their pixels.
/// </summary> /// </summary>
/// <param name="source">The image this method extends.</param> /// <param name="source">The image this method extends.</param>
/// <param name="image">The image to blend with the currently processing image.</param> /// <param name="image">
/// The image to blend with the currently processing image.
/// Disposal of this image is the responsibility of the developer.
/// </param>
/// <param name="percent">The opacity of the image image to blend. Must be between 0 and 100.</param> /// <param name="percent">The opacity of the image image to blend. Must be between 0 and 100.</param>
/// <param name="progressHandler">A delegate which is called as progress is made processing the image.</param> /// <param name="progressHandler">A delegate which is called as progress is made processing the image.</param>
/// <returns>The <see cref="Image"/>.</returns> /// <returns>The <see cref="Image"/>.</returns>
@ -86,7 +89,10 @@ namespace ImageProcessorCore.Filters
/// Combines the given image together with the current one by blending their pixels. /// Combines the given image together with the current one by blending their pixels.
/// </summary> /// </summary>
/// <param name="source">The image this method extends.</param> /// <param name="source">The image this method extends.</param>
/// <param name="image">The image to blend with the currently processing image.</param> /// <param name="image">
/// The image to blend with the currently processing image.
/// Disposal of this image is the responsibility of the developer.
/// </param>
/// <param name="percent">The opacity of the image image to blend. Must be between 0 and 100.</param> /// <param name="percent">The opacity of the image image to blend. Must be between 0 and 100.</param>
/// <param name="rectangle"> /// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter. /// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.

2
src/ImageProcessorCore/Formats/Png/PngEncoder.cs

@ -18,7 +18,7 @@ namespace ImageProcessorCore.Formats
/// <summary> /// <summary>
/// Gets or sets the quality of output for images. /// Gets or sets the quality of output for images.
/// </summary> /// </summary>
public int Quality { get; set; } = int.MaxValue; public int Quality { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public string MimeType => "image/png"; public string MimeType => "image/png";

2
src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs

@ -35,7 +35,7 @@ namespace ImageProcessorCore.Formats
/// <summary> /// <summary>
/// Gets or sets the quality of output for images. /// Gets or sets the quality of output for images.
/// </summary> /// </summary>
public int Quality { get; set; } = int.MaxValue; public int Quality { get; set; }
/// <summary> /// <summary>
/// The compression level 1-9. /// The compression level 1-9.

2
src/ImageProcessorCore/IImageBase.cs

@ -17,7 +17,7 @@ namespace ImageProcessorCore
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// The returned array has a length of Width * Height * 4 bytes /// The returned array has a length of Width * Height * 4 bytes
/// and stores the blue, the green, the red and the alpha value for /// and stores the red, the green, the blue, and the alpha value for
/// each pixel in this order. /// each pixel in this order.
/// </remarks> /// </remarks>
float[] Pixels { get; } float[] Pixels { get; }

26
src/ImageProcessorCore/Image.cs

@ -45,7 +45,7 @@ namespace ImageProcessorCore
new BmpFormat(), new BmpFormat(),
new JpegFormat(), new JpegFormat(),
new PngFormat(), new PngFormat(),
new GifFormat(), new GifFormat()
}); });
/// <summary> /// <summary>
@ -223,6 +223,30 @@ namespace ImageProcessorCore
encoder.Encode(this, stream); encoder.Encode(this, stream);
} }
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (this.IsDisposed)
{
return;
}
if (disposing)
{
// Dispose of any managed resources here.
if (this.Frames.Any())
{
foreach (ImageFrame frame in this.Frames)
{
frame.Dispose();
}
this.Frames.Clear();
}
}
base.Dispose(disposing);
}
/// <summary> /// <summary>
/// Loads the image from the given stream. /// Loads the image from the given stream.
/// </summary> /// </summary>

159
src/ImageProcessorCore/ImageBase.cs

@ -6,13 +6,42 @@
namespace ImageProcessorCore namespace ImageProcessorCore
{ {
using System; using System;
using System.Runtime.InteropServices;
/// <summary> /// <summary>
/// The base class of all images. Encapsulates the basic properties and methods /// The base class of all images. Encapsulates the basic properties and methods
/// required to manipulate images. /// required to manipulate images.
/// </summary> /// </summary>
public abstract class ImageBase : IImageBase public abstract unsafe class ImageBase : IImageBase, IDisposable
{ {
/// <summary>
/// The position of the first pixel in the bitmap.
/// </summary>
private float* pixelsBase;
/// <summary>
/// The array of pixels.
/// </summary>
private float[] pixelsArray;
/// <summary>
/// Provides a way to access the pixels from unmanaged memory.
/// </summary>
private GCHandle pixelsHandle;
/// <summary>
/// A value indicating whether this instance of the given entity has been disposed.
/// </summary>
/// <value><see langword="true"/> if this instance has been disposed; otherwise, <see langword="false"/>.</value>
/// <remarks>
/// If the entity is disposed, it must not be disposed a second
/// time. The isDisposed field is set the first time the entity
/// is disposed. If the isDisposed field is true, then the Dispose()
/// method will not dispose again. This help not to prolong the entity's
/// life in the Garbage Collector.
/// </remarks>
protected bool IsDisposed;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ImageBase"/> class. /// Initializes a new instance of the <see cref="ImageBase"/> class.
/// </summary> /// </summary>
@ -23,12 +52,8 @@ namespace ImageProcessorCore
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ImageBase"/> class. /// Initializes a new instance of the <see cref="ImageBase"/> class.
/// </summary> /// </summary>
/// <param name="width"> /// <param name="width">The width of the image in pixels.</param>
/// The width of the image in pixels. /// <param name="height">The height of the image in pixels.</param>
/// </param>
/// <param name="height">
/// The height of the image in pixels.
/// </param>
/// <exception cref="ArgumentOutOfRangeException"> /// <exception cref="ArgumentOutOfRangeException">
/// Thrown if either <paramref name="width"/> or <paramref name="height"/> are less than or equal to 0. /// Thrown if either <paramref name="width"/> or <paramref name="height"/> are less than or equal to 0.
/// </exception> /// </exception>
@ -40,7 +65,10 @@ namespace ImageProcessorCore
this.Width = width; this.Width = width;
this.Height = height; this.Height = height;
this.Pixels = new float[width * height * 4]; // Assign the pointer and pixels.
this.pixelsArray = new float[width * height * 4];
this.pixelsHandle = GCHandle.Alloc(this.pixelsArray, GCHandleType.Pinned);
this.pixelsBase = (float*)this.pixelsHandle.AddrOfPinnedObject().ToPointer();
} }
/// <summary> /// <summary>
@ -56,14 +84,22 @@ namespace ImageProcessorCore
{ {
Guard.NotNull(other, nameof(other), "Other image cannot be null."); Guard.NotNull(other, nameof(other), "Other image cannot be null.");
float[] pixels = other.Pixels;
this.Width = other.Width; this.Width = other.Width;
this.Height = other.Height; this.Height = other.Height;
this.Quality = other.Quality; this.Quality = other.Quality;
this.FrameDelay = other.FrameDelay; this.FrameDelay = other.FrameDelay;
this.Pixels = new float[pixels.Length];
Array.Copy(pixels, this.Pixels, pixels.Length); // Assign the pointer and copy the pixels.
this.pixelsArray = new float[this.Width * this.Height * 4];
this.pixelsHandle = GCHandle.Alloc(this.pixelsArray, GCHandleType.Pinned);
this.pixelsBase = (float*)this.pixelsHandle.AddrOfPinnedObject().ToPointer();
Array.Copy(other.pixelsArray, this.pixelsArray, other.pixelsArray.Length);
}
/// <inheritdoc/>
~ImageBase()
{
this.Dispose(false);
} }
/// <summary> /// <summary>
@ -76,45 +112,25 @@ namespace ImageProcessorCore
/// </summary> /// </summary>
public static int MaxHeight { get; set; } = int.MaxValue; public static int MaxHeight { get; set; } = int.MaxValue;
/// <summary> /// <inheritdoc/>
/// Gets the image pixels as byte array. public float[] Pixels => this.pixelsArray;
/// </summary>
/// <remarks>
/// The returned array has a length of Width * Height * 4 bytes
/// and stores the blue, the green, the red and the alpha value for
/// each pixel in this order.
/// </remarks>
public float[] Pixels { get; private set; }
/// <summary> /// <inheritdoc/>
/// Gets the width in pixels.
/// </summary>
public int Width { get; private set; } public int Width { get; private set; }
/// <summary> /// <inheritdoc/>
/// Gets the height in pixels.
/// </summary>
public int Height { get; private set; } public int Height { get; private set; }
/// <summary> /// <inheritdoc/>
/// Gets the pixel ratio made up of the width and height.
/// </summary>
public double PixelRatio => (double)this.Width / this.Height; public double PixelRatio => (double)this.Width / this.Height;
/// <summary> /// <inheritdoc/>
/// Gets the <see cref="Rectangle"/> representing the bounds of the image.
/// </summary>
public Rectangle Bounds => new Rectangle(0, 0, this.Width, this.Height); public Rectangle Bounds => new Rectangle(0, 0, this.Width, this.Height);
/// <inheritdoc/> /// <inheritdoc/>
public int Quality { get; set; } public int Quality { get; set; }
/// <summary> /// <inheritdoc/>
/// Gets or sets the frame delay for animated images.
/// If not 0, this field specifies the number of hundredths (1/100) of a second to
/// wait before continuing with the processing of the Data Stream.
/// The clock starts ticking immediately after the graphic is rendered.
/// </summary>
public int FrameDelay { get; set; } public int FrameDelay { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
@ -133,9 +149,7 @@ namespace ImageProcessorCore
throw new ArgumentOutOfRangeException(nameof(y), "Value cannot be less than zero or greater than the bitmap height."); throw new ArgumentOutOfRangeException(nameof(y), "Value cannot be less than zero or greater than the bitmap height.");
} }
#endif #endif
return *((Color*)(this.pixelsBase + ((y * this.Width) + x) * 4));
int start = ((y * this.Width) + x) * 4;
return new Color(this.Pixels[start], this.Pixels[start + 1], this.Pixels[start + 2], this.Pixels[start + 3]);
} }
set set
@ -151,12 +165,7 @@ namespace ImageProcessorCore
throw new ArgumentOutOfRangeException(nameof(y), "Value cannot be less than zero or greater than the bitmap height."); throw new ArgumentOutOfRangeException(nameof(y), "Value cannot be less than zero or greater than the bitmap height.");
} }
#endif #endif
int start = ((y * this.Width) + x) * 4; *(Color*)(this.pixelsBase + (((y * this.Width) + x) * 4)) = value;
this.Pixels[start + 0] = value.R;
this.Pixels[start + 1] = value.G;
this.Pixels[start + 2] = value.B;
this.Pixels[start + 3] = value.A;
} }
} }
@ -181,7 +190,10 @@ namespace ImageProcessorCore
#endif #endif
this.Width = width; this.Width = width;
this.Height = height; this.Height = height;
this.Pixels = pixels;
this.pixelsArray = pixels;
this.pixelsHandle = GCHandle.Alloc(this.pixelsArray, GCHandleType.Pinned);
this.pixelsBase = (float*)this.pixelsHandle.AddrOfPinnedObject().ToPointer();
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -205,9 +217,52 @@ namespace ImageProcessorCore
#endif #endif
this.Width = width; this.Width = width;
this.Height = height; this.Height = height;
float[] clonedPixels = new float[pixels.Length];
Array.Copy(pixels, clonedPixels, pixels.Length); // Assign the pointer and copy the pixels.
this.Pixels = clonedPixels; this.pixelsArray = new float[pixels.Length];
this.pixelsHandle = GCHandle.Alloc(this.pixelsArray, GCHandleType.Pinned);
this.pixelsBase = (float*)this.pixelsHandle.AddrOfPinnedObject().ToPointer();
Array.Copy(pixels, this.pixelsArray, pixels.Length);
}
/// <inheritdoc/>
public void Dispose()
{
this.Dispose(true);
// This object will be cleaned up by the Dispose method.
// Therefore, you should call GC.SuppressFinalize to
// take this object off the finalization queue
// and prevent finalization code for this object
// from executing a second time.
GC.SuppressFinalize(this);
}
/// <summary>
/// Disposes the object and frees resources for the Garbage Collector.
/// </summary>
/// <param name="disposing">If true, the object gets disposed.</param>
protected virtual void Dispose(bool disposing)
{
if (this.IsDisposed)
{
return;
}
if (disposing)
{
// Dispose of any managed resources here.
this.pixelsArray = null;
}
if (this.pixelsHandle.IsAllocated)
{
this.pixelsHandle.Free();
this.pixelsBase = null;
}
// Note disposing is done.
this.IsDisposed = true;
} }
} }
} }

6
src/ImageProcessorCore/ImageExtensions.cs

@ -53,6 +53,7 @@ namespace ImageProcessorCore
/// <summary> /// <summary>
/// Applies the collection of processors to the image. /// Applies the collection of processors to the image.
/// <remarks>This method does not resize the target image.</remarks>
/// </summary> /// </summary>
/// <param name="source">The image this method extends.</param> /// <param name="source">The image this method extends.</param>
/// <param name="processors">Any processors to apply to the image.</param> /// <param name="processors">Any processors to apply to the image.</param>
@ -85,6 +86,9 @@ namespace ImageProcessorCore
/// <summary> /// <summary>
/// Applies the collection of processors to the image. /// Applies the collection of processors to the image.
/// <remarks>
/// This method is not chainable.
/// </remarks>
/// </summary> /// </summary>
/// <param name="source">The source image. Cannot be null.</param> /// <param name="source">The source image. Cannot be null.</param>
/// <param name="width">The target image width.</param> /// <param name="width">The target image width.</param>
@ -164,6 +168,8 @@ namespace ImageProcessorCore
} }
} }
// Clean up.
source.Dispose();
return transformedImage; return transformedImage;
} }
} }

40
src/ImageProcessorCore/ParallelImageProcessor.cs

@ -37,10 +37,7 @@ namespace ImageProcessorCore
{ {
try try
{ {
// We don't want to affect the original source pixels so we make clone here. this.OnApply(source, target, target.Bounds, sourceRectangle);
ImageFrame frame = source as ImageFrame;
Image temp = frame != null ? new Image(frame) : new Image((Image)source);
this.OnApply(temp, target, target.Bounds, sourceRectangle);
this.numRowsProcessed = 0; this.numRowsProcessed = 0;
this.totalRows = sourceRectangle.Height; this.totalRows = sourceRectangle.Height;
@ -54,24 +51,27 @@ namespace ImageProcessorCore
for (int p = 0; p < partitionCount; p++) for (int p = 0; p < partitionCount; p++)
{ {
int current = p; int current = p;
tasks[p] = Task.Run(() => tasks[p] = Task.Run(
{ () =>
int batchSize = sourceRectangle.Height / partitionCount; {
int yStart = sourceRectangle.Y + (current * batchSize); int batchSize = sourceRectangle.Height / partitionCount;
int yEnd = current == partitionCount - 1 ? sourceRectangle.Bottom : yStart + batchSize; int yStart = sourceRectangle.Y + (current * batchSize);
int yEnd = current == partitionCount - 1
this.Apply(target, temp, target.Bounds, sourceRectangle, yStart, yEnd); ? sourceRectangle.Bottom
}); : yStart + batchSize;
this.Apply(target, source, target.Bounds, sourceRectangle, yStart, yEnd);
});
} }
Task.WaitAll(tasks); Task.WaitAll(tasks);
} }
else else
{ {
this.Apply(target, temp, target.Bounds, sourceRectangle, sourceRectangle.Y, sourceRectangle.Bottom); this.Apply(target, source, target.Bounds, sourceRectangle, sourceRectangle.Y, sourceRectangle.Bottom);
} }
this.AfterApply(temp, target, target.Bounds, sourceRectangle); this.AfterApply(source, target, target.Bounds, sourceRectangle);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -93,10 +93,7 @@ namespace ImageProcessorCore
sourceRectangle = source.Bounds; sourceRectangle = source.Bounds;
} }
// We don't want to affect the original source pixels so we make clone here. this.OnApply(source, target, target.Bounds, sourceRectangle);
ImageFrame frame = source as ImageFrame;
Image temp = frame != null ? new Image(frame) : new Image((Image)source);
this.OnApply(temp, target, target.Bounds, sourceRectangle);
targetRectangle = target.Bounds; targetRectangle = target.Bounds;
this.numRowsProcessed = 0; this.numRowsProcessed = 0;
@ -117,7 +114,7 @@ namespace ImageProcessorCore
int yStart = current * batchSize; int yStart = current * batchSize;
int yEnd = current == partitionCount - 1 ? targetRectangle.Bottom : yStart + batchSize; int yEnd = current == partitionCount - 1 ? targetRectangle.Bottom : yStart + batchSize;
this.Apply(target, temp, targetRectangle, sourceRectangle, yStart, yEnd); this.Apply(target, source, targetRectangle, sourceRectangle, yStart, yEnd);
}); });
} }
@ -125,14 +122,13 @@ namespace ImageProcessorCore
} }
else else
{ {
this.Apply(target, temp, targetRectangle, sourceRectangle, targetRectangle.Y, targetRectangle.Bottom); this.Apply(target, source, targetRectangle, sourceRectangle, targetRectangle.Y, targetRectangle.Bottom);
} }
this.AfterApply(temp, target, target.Bounds, sourceRectangle); this.AfterApply(source, target, target.Bounds, sourceRectangle);
} }
catch (Exception ex) catch (Exception ex)
{ {
throw new ImageProcessingException($"An error occured when processing the image using {this.GetType().Name}. See the inner exception for more detail.", ex); throw new ImageProcessingException($"An error occured when processing the image using {this.GetType().Name}. See the inner exception for more detail.", ex);
} }
} }

25
src/ImageProcessorCore/Samplers/EntropyCrop.cs

@ -42,20 +42,21 @@ namespace ImageProcessorCore.Samplers
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnApply(ImageBase source, ImageBase target, Rectangle targetRectangle, Rectangle sourceRectangle) protected override void OnApply(ImageBase source, ImageBase target, Rectangle targetRectangle, Rectangle sourceRectangle)
{ {
ImageBase temp = new Image(source.Width, source.Height); using (ImageBase temp = new Image(source.Width, source.Height))
{
// Detect the edges. // Detect the edges.
new Sobel().Apply(temp, source, sourceRectangle); new Sobel().Apply(temp, source, sourceRectangle);
// Apply threshold binarization filter. // Apply threshold binarization filter.
new Threshold(.5f).Apply(temp, temp, sourceRectangle); new Threshold(.5f).Apply(temp, temp, sourceRectangle);
// Search for the first white pixels // Search for the first white pixels
Rectangle rectangle = ImageMaths.GetFilteredBoundingRectangle(temp, 0); Rectangle rectangle = ImageMaths.GetFilteredBoundingRectangle(temp, 0);
// Reset the target pixel to the correct size. // Reset the target pixel to the correct size.
target.SetPixels(rectangle.Width, rectangle.Height, new float[rectangle.Width * rectangle.Height * 4]); target.SetPixels(rectangle.Width, rectangle.Height, new float[rectangle.Width * rectangle.Height * 4]);
this.cropRectangle = rectangle; this.cropRectangle = rectangle;
}
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -73,7 +74,7 @@ namespace ImageProcessorCore.Samplers
int endX = this.cropRectangle.Width; int endX = this.cropRectangle.Width;
int maxX = this.cropRectangle.Right - 1; int maxX = this.cropRectangle.Right - 1;
int maxY = this.cropRectangle.Bottom - 1; int maxY = this.cropRectangle.Bottom - 1;
Parallel.For( Parallel.For(
startY, startY,
endY, endY,

63
src/ImageProcessorCore/Samplers/Resize.cs

@ -54,7 +54,6 @@ namespace ImageProcessorCore.Samplers
int sourceBottom = source.Bounds.Bottom; int sourceBottom = source.Bounds.Bottom;
int targetY = targetRectangle.Y; int targetY = targetRectangle.Y;
int targetBottom = targetRectangle.Bottom;
int startX = targetRectangle.X; int startX = targetRectangle.X;
int endX = targetRectangle.Right; int endX = targetRectangle.Right;
bool compand = this.Compand; bool compand = this.Compand;
@ -70,20 +69,18 @@ namespace ImageProcessorCore.Samplers
endY, endY,
y => y =>
{ {
if (y >= targetY && y < targetBottom) // Y coordinates of source points
{ int originY = (int)((y - targetY) * heightFactor);
// Y coordinates of source points
int originY = (int)((y - targetY) * heightFactor);
for (int x = startX; x < endX; x++) for (int x = startX; x < endX; x++)
{ {
// X coordinates of source points // X coordinates of source points
int originX = (int)((x - startX) * widthFactor); int originX = (int)((x - startX) * widthFactor);
target[x, y] = source[originX, originY]; target[x, y] = source[originX, originY];
}
this.OnRowProcessed();
} }
this.OnRowProcessed();
}); });
// Break out now. // Break out now.
@ -127,32 +124,30 @@ namespace ImageProcessorCore.Samplers
endY, endY,
y => y =>
{ {
if (y >= targetY && y < targetBottom) Weight[] verticalValues = this.VerticalWeights[y].Values;
for (int x = startX; x < endX; x++)
{ {
Weight[] verticalValues = this.VerticalWeights[y].Values; // Destination color components
Color destination = new Color();
for (int x = startX; x < endX; x++) foreach (Weight yw in verticalValues)
{ {
// Destination color components int originY = yw.Index;
Color destination = new Color(); int originX = x;
Color sourceColor = compand ? Color.Expand(this.firstPass[originX, originY]) : this.firstPass[originX, originY];
foreach (Weight yw in verticalValues) destination += sourceColor * yw.Value;
{
int originY = yw.Index;
int originX = x;
Color sourceColor = compand ? Color.Expand(this.firstPass[originX, originY]) : this.firstPass[originX, originY];
destination += sourceColor * yw.Value;
}
if (compand)
{
destination = Color.Compress(destination);
}
target[x, y] = destination;
} }
this.OnRowProcessed();
if (compand)
{
destination = Color.Compress(destination);
}
target[x, y] = destination;
} }
this.OnRowProcessed();
}); });
} }
@ -164,6 +159,8 @@ namespace ImageProcessorCore.Samplers
{ {
target.ClonePixels(target.Width, target.Height, source.Pixels); target.ClonePixels(target.Width, target.Height, source.Pixels);
} }
this.firstPass?.Dispose();
} }
} }
} }

23
tests/ImageProcessorCore.Tests/Formats/BitmapTests.cs

@ -27,20 +27,21 @@ namespace ImageProcessorCore.Tests
using (FileStream stream = File.OpenRead(file)) using (FileStream stream = File.OpenRead(file))
{ {
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
Image image = new Image(stream); using (Image image = new Image(stream))
string encodeFilename = "TestOutput/Encode/Bitmap/" + "24-" + Path.GetFileNameWithoutExtension(file) + ".bmp";
using (FileStream output = File.OpenWrite(encodeFilename))
{ {
image.Save(output, new BmpEncoder { BitsPerPixel = BmpBitsPerPixel.Pixel24 }); string encodeFilename = "TestOutput/Encode/Bitmap/" + "24-" + Path.GetFileNameWithoutExtension(file) + ".bmp";
}
encodeFilename = "TestOutput/Encode/Bitmap/" + "32-" + Path.GetFileNameWithoutExtension(file) + ".bmp"; using (FileStream output = File.OpenWrite(encodeFilename))
{
image.Save(output, new BmpEncoder { BitsPerPixel = BmpBitsPerPixel.Pixel24 });
}
using (FileStream output = File.OpenWrite(encodeFilename)) encodeFilename = "TestOutput/Encode/Bitmap/" + "32-" + Path.GetFileNameWithoutExtension(file) + ".bmp";
{
image.Save(output, new BmpEncoder { BitsPerPixel = BmpBitsPerPixel.Pixel32 }); using (FileStream output = File.OpenWrite(encodeFilename))
{
image.Save(output, new BmpEncoder { BitsPerPixel = BmpBitsPerPixel.Pixel32 });
}
} }
Trace.WriteLine($"{file} : {watch.ElapsedMilliseconds}ms"); Trace.WriteLine($"{file} : {watch.ElapsedMilliseconds}ms");

120
tests/ImageProcessorCore.Tests/Formats/EncoderDecoderTests.cs

@ -30,13 +30,14 @@ namespace ImageProcessorCore.Tests
using (FileStream stream = File.OpenRead(file)) using (FileStream stream = File.OpenRead(file))
{ {
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
Image image = new Image(stream); using (Image image = new Image(stream))
string encodeFilename = "TestOutput/Encode/" + Path.GetFileName(file);
using (FileStream output = File.OpenWrite(encodeFilename))
{ {
image.Save(output); string encodeFilename = "TestOutput/Encode/" + Path.GetFileName(file);
using (FileStream output = File.OpenWrite(encodeFilename))
{
image.Save(output);
}
} }
Trace.WriteLine($"{file} : {watch.ElapsedMilliseconds}ms"); Trace.WriteLine($"{file} : {watch.ElapsedMilliseconds}ms");
@ -56,30 +57,37 @@ namespace ImageProcessorCore.Tests
{ {
using (FileStream stream = File.OpenRead(file)) using (FileStream stream = File.OpenRead(file))
{ {
Image image = new Image(stream); using (Image image = new Image(stream))
IQuantizer quantizer = new OctreeQuantizer();
QuantizedImage quantizedImage = quantizer.Quantize(image, 256);
using (FileStream output = File.OpenWrite($"TestOutput/Quantize/Octree-{Path.GetFileName(file)}"))
{ {
quantizedImage.ToImage().Save(output, image.CurrentImageFormat); IQuantizer quantizer = new OctreeQuantizer();
} QuantizedImage quantizedImage = quantizer.Quantize(image, 256);
quantizer = new WuQuantizer(); using (FileStream output = File.OpenWrite($"TestOutput/Quantize/Octree-{Path.GetFileName(file)}"))
quantizedImage = quantizer.Quantize(image, 256); {
using (Image qi = quantizedImage.ToImage())
{
qi.Save(output, image.CurrentImageFormat);
}
}
using (FileStream output = File.OpenWrite($"TestOutput/Quantize/Wu-{Path.GetFileName(file)}")) quantizer = new WuQuantizer();
{ quantizedImage = quantizer.Quantize(image, 256);
quantizedImage.ToImage().Save(output, image.CurrentImageFormat);
}
quantizer = new PaletteQuantizer(); using (FileStream output = File.OpenWrite($"TestOutput/Quantize/Wu-{Path.GetFileName(file)}"))
quantizedImage = quantizer.Quantize(image, 256); {
quantizedImage.ToImage().Save(output, image.CurrentImageFormat);
}
using (FileStream output = File.OpenWrite($"TestOutput/Quantize/Palette-{Path.GetFileName(file)}")) quantizer = new PaletteQuantizer();
{ quantizedImage = quantizer.Quantize(image, 256);
quantizedImage.ToImage().Save(output, image.CurrentImageFormat);
using (FileStream output = File.OpenWrite($"TestOutput/Quantize/Palette-{Path.GetFileName(file)}"))
{
using (Image qi = quantizedImage.ToImage())
{
qi.Save(output, image.CurrentImageFormat);
}
}
} }
} }
} }
@ -97,26 +105,27 @@ namespace ImageProcessorCore.Tests
{ {
using (FileStream stream = File.OpenRead(file)) using (FileStream stream = File.OpenRead(file))
{ {
Image image = new Image(stream); using (Image image = new Image(stream))
using (FileStream output = File.OpenWrite($"TestOutput/Format/{Path.GetFileNameWithoutExtension(file)}.gif"))
{ {
image.SaveAsGif(output); using (FileStream output = File.OpenWrite($"TestOutput/Format/{Path.GetFileNameWithoutExtension(file)}.gif"))
} {
image.SaveAsGif(output);
}
using (FileStream output = File.OpenWrite($"TestOutput/Format/{Path.GetFileNameWithoutExtension(file)}.bmp")) using (FileStream output = File.OpenWrite($"TestOutput/Format/{Path.GetFileNameWithoutExtension(file)}.bmp"))
{ {
image.SaveAsBmp(output); image.SaveAsBmp(output);
} }
using (FileStream output = File.OpenWrite($"TestOutput/Format/{Path.GetFileNameWithoutExtension(file)}.jpg")) using (FileStream output = File.OpenWrite($"TestOutput/Format/{Path.GetFileNameWithoutExtension(file)}.jpg"))
{ {
image.SaveAsJpeg(output); image.SaveAsJpeg(output);
} }
using (FileStream output = File.OpenWrite($"TestOutput/Format/{Path.GetFileNameWithoutExtension(file)}.png")) using (FileStream output = File.OpenWrite($"TestOutput/Format/{Path.GetFileNameWithoutExtension(file)}.png"))
{ {
image.SaveAsPng(output); image.SaveAsPng(output);
}
} }
} }
} }
@ -134,22 +143,25 @@ namespace ImageProcessorCore.Tests
{ {
using (FileStream stream = File.OpenRead(file)) using (FileStream stream = File.OpenRead(file))
{ {
Image image = new Image(stream); using (Image image = new Image(stream))
byte[] serialized;
using (MemoryStream memoryStream = new MemoryStream())
{ {
image.Save(memoryStream); byte[] serialized;
memoryStream.Flush(); using (MemoryStream memoryStream = new MemoryStream())
serialized = memoryStream.ToArray(); {
} image.Save(memoryStream);
memoryStream.Flush();
using (MemoryStream memoryStream = new MemoryStream(serialized)) serialized = memoryStream.ToArray();
{ }
Image image2 = new Image(memoryStream);
using (FileStream output = File.OpenWrite($"TestOutput/Serialized/{Path.GetFileName(file)}")) using (MemoryStream memoryStream = new MemoryStream(serialized))
{ {
image2.Save(output); using (Image image2 = new Image(memoryStream))
{
using (FileStream output = File.OpenWrite($"TestOutput/Serialized/{Path.GetFileName(file)}"))
{
image2.Save(output);
}
}
} }
} }
} }

11
tests/ImageProcessorCore.Tests/Formats/PngTests.cs

@ -25,12 +25,13 @@ namespace ImageProcessorCore.Tests
{ {
using (FileStream stream = File.OpenRead(file)) using (FileStream stream = File.OpenRead(file))
{ {
Image image = new Image(stream); using (Image image = new Image(stream))
using (FileStream output = File.OpenWrite($"TestOutput/Encode/Png/{Path.GetFileNameWithoutExtension(file)}.png"))
{ {
image.Quality = 256; using (FileStream output = File.OpenWrite($"TestOutput/Encode/Png/{Path.GetFileNameWithoutExtension(file)}.png"))
image.Save(output, new PngFormat()); {
image.Quality = 256;
image.Save(output, new PngFormat());
}
} }
} }
} }

15
tests/ImageProcessorCore.Tests/Processors/Filters/FilterTests.cs

@ -70,15 +70,16 @@ namespace ImageProcessorCore.Tests
using (FileStream stream = File.OpenRead(file)) using (FileStream stream = File.OpenRead(file))
{ {
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
Image image = new Image(stream); using (Image image = new Image(stream))
string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file);
using (FileStream output = File.OpenWrite($"TestOutput/Filter/{ Path.GetFileName(filename) }"))
{ {
processor.OnProgress += this.ProgressUpdate; string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file);
image.Process(processor).Save(output); using (FileStream output = File.OpenWrite($"TestOutput/Filter/{Path.GetFileName(filename)}"))
processor.OnProgress -= this.ProgressUpdate; {
processor.OnProgress += this.ProgressUpdate;
image.Process(processor).Save(output);
processor.OnProgress -= this.ProgressUpdate;
}
} }
Trace.WriteLine($"{ name }: { watch.ElapsedMilliseconds}ms"); Trace.WriteLine($"{ name }: { watch.ElapsedMilliseconds}ms");
} }
} }

88
tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs

@ -60,14 +60,17 @@
{ {
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
Image image = new Image(stream); Image image = new Image(stream);
string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file); string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file);
using (FileStream output = File.OpenWrite($"TestOutput/Sample/{ Path.GetFileName(filename) }")) using (FileStream output = File.OpenWrite($"TestOutput/Sample/{Path.GetFileName(filename)}"))
{ {
processor.OnProgress += this.ProgressUpdate; processor.OnProgress += this.ProgressUpdate;
// Not Chainable.
image = image.Process(image.Width / 2, image.Height / 2, processor); image = image.Process(image.Width / 2, image.Height / 2, processor);
image.Save(output); image.Save(output);
processor.OnProgress -= this.ProgressUpdate; processor.OnProgress -= this.ProgressUpdate;
} }
image.Dispose();
Trace.WriteLine($"{ name }: { watch.ElapsedMilliseconds}ms"); Trace.WriteLine($"{ name }: { watch.ElapsedMilliseconds}ms");
} }
@ -88,14 +91,15 @@
using (FileStream stream = File.OpenRead(file)) using (FileStream stream = File.OpenRead(file))
{ {
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
Image image = new Image(stream); using (Image image = new Image(stream))
string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file);
using (FileStream output = File.OpenWrite($"TestOutput/Resize/{filename}"))
{ {
image.Resize(image.Width / 2, image.Height / 2, sampler, false, this.ProgressUpdate) string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file);
.Save(output); using (FileStream output = File.OpenWrite($"TestOutput/Resize/{filename}"))
{
image.Resize(image.Width / 2, image.Height / 2, sampler, false, this.ProgressUpdate)
.Save(output);
}
} }
Trace.WriteLine($"{name}: {watch.ElapsedMilliseconds}ms"); Trace.WriteLine($"{name}: {watch.ElapsedMilliseconds}ms");
} }
} }
@ -116,12 +120,14 @@
using (FileStream stream = File.OpenRead(file)) using (FileStream stream = File.OpenRead(file))
{ {
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
Image image = new Image(stream); using (Image image = new Image(stream))
string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file);
using (FileStream output = File.OpenWrite($"TestOutput/Resize/{filename}"))
{ {
image.Resize(image.Width / 3, 0, new TriangleResampler(), false, this.ProgressUpdate) string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file);
.Save(output); using (FileStream output = File.OpenWrite($"TestOutput/Resize/{filename}"))
{
image.Resize(image.Width / 3, 0, new TriangleResampler(), false, this.ProgressUpdate)
.Save(output);
}
} }
Trace.WriteLine($"{name}: {watch.ElapsedMilliseconds}ms"); Trace.WriteLine($"{name}: {watch.ElapsedMilliseconds}ms");
@ -144,12 +150,14 @@
using (FileStream stream = File.OpenRead(file)) using (FileStream stream = File.OpenRead(file))
{ {
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
Image image = new Image(stream); using (Image image = new Image(stream))
string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file);
using (FileStream output = File.OpenWrite($"TestOutput/Resize/{filename}"))
{ {
image.Resize(0, image.Height / 3, new TriangleResampler(), false, this.ProgressUpdate) string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file);
.Save(output); using (FileStream output = File.OpenWrite($"TestOutput/Resize/{filename}"))
{
image.Resize(0, image.Height / 3, new TriangleResampler(), false, this.ProgressUpdate)
.Save(output);
}
} }
Trace.WriteLine($"{name}: {watch.ElapsedMilliseconds}ms"); Trace.WriteLine($"{name}: {watch.ElapsedMilliseconds}ms");
@ -171,12 +179,14 @@
using (FileStream stream = File.OpenRead(file)) using (FileStream stream = File.OpenRead(file))
{ {
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
Image image = new Image(stream); using (Image image = new Image(stream))
string filename = Path.GetFileNameWithoutExtension(file) + "-" + rotateType + flipType + Path.GetExtension(file);
using (FileStream output = File.OpenWrite($"TestOutput/RotateFlip/{filename}"))
{ {
image.RotateFlip(rotateType, flipType, this.ProgressUpdate) string filename = Path.GetFileNameWithoutExtension(file) + "-" + rotateType + flipType
.Save(output); + Path.GetExtension(file);
using (FileStream output = File.OpenWrite($"TestOutput/RotateFlip/{filename}"))
{
image.RotateFlip(rotateType, flipType, this.ProgressUpdate).Save(output);
}
} }
Trace.WriteLine($"{rotateType + "-" + flipType}: {watch.ElapsedMilliseconds}ms"); Trace.WriteLine($"{rotateType + "-" + flipType}: {watch.ElapsedMilliseconds}ms");
@ -198,13 +208,15 @@
using (FileStream stream = File.OpenRead(file)) using (FileStream stream = File.OpenRead(file))
{ {
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
Image image = new Image(stream); using (Image image = new Image(stream))
string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file);
using (FileStream output = File.OpenWrite($"TestOutput/Rotate/{filename}"))
{ {
image.Rotate(45, sampler, false, this.ProgressUpdate) string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file);
//.BackgroundColor(Color.Aqua) using (FileStream output = File.OpenWrite($"TestOutput/Rotate/{filename}"))
.Save(output); {
image.Rotate(45, sampler, false, this.ProgressUpdate)
//.BackgroundColor(Color.Aqua)
.Save(output);
}
} }
Trace.WriteLine($"{name}: {watch.ElapsedMilliseconds}ms"); Trace.WriteLine($"{name}: {watch.ElapsedMilliseconds}ms");
@ -224,11 +236,13 @@
{ {
using (FileStream stream = File.OpenRead(file)) using (FileStream stream = File.OpenRead(file))
{ {
Image image = new Image(stream); using (Image image = new Image(stream))
string filename = Path.GetFileNameWithoutExtension(file) + "-EntropyCrop" + Path.GetExtension(file);
using (FileStream output = File.OpenWrite($"TestOutput/EntropyCrop/{filename}"))
{ {
image.EntropyCrop(.5f, this.ProgressUpdate).Save(output); string filename = Path.GetFileNameWithoutExtension(file) + "-EntropyCrop" + Path.GetExtension(file);
using (FileStream output = File.OpenWrite($"TestOutput/EntropyCrop/{filename}"))
{
image.EntropyCrop(.5f, this.ProgressUpdate).Save(output);
}
} }
} }
} }
@ -246,11 +260,13 @@
{ {
using (FileStream stream = File.OpenRead(file)) using (FileStream stream = File.OpenRead(file))
{ {
Image image = new Image(stream); using (Image image = new Image(stream))
string filename = Path.GetFileNameWithoutExtension(file) + "-Crop" + Path.GetExtension(file);
using (FileStream output = File.OpenWrite($"TestOutput/Crop/{filename}"))
{ {
image.Crop(image.Width / 2, image.Height / 2, this.ProgressUpdate).Save(output); string filename = Path.GetFileNameWithoutExtension(file) + "-Crop" + Path.GetExtension(file);
using (FileStream output = File.OpenWrite($"TestOutput/Crop/{filename}"))
{
image.Crop(image.Width / 2, image.Height / 2, this.ProgressUpdate).Save(output);
}
} }
} }
} }

Loading…
Cancel
Save