Browse Source

Begin add ResizeMode [skip ci]

Former-commit-id: 7d28a87716e4b7c2dea73bc0fb804873d30372f6
Former-commit-id: 433c2d3c310ed0e361c42601ae4432d97a074fcf
Former-commit-id: abf2e280afbcc9bc0d02cdfe74921f2763ce0f8d
af/merge-core
James Jackson-South 10 years ago
parent
commit
b1156feb3e
  1. 4
      src/ImageProcessorCore/Common/Extensions/ByteExtensions.cs
  2. 8
      src/ImageProcessorCore/ParallelImageProcessor.cs
  3. 58
      src/ImageProcessorCore/Samplers/AnchorPosition.cs
  4. 35
      src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs
  5. 72
      src/ImageProcessorCore/Samplers/Resize.cs
  6. 235
      src/ImageProcessorCore/Samplers/ResizeHelper.cs
  7. 47
      src/ImageProcessorCore/Samplers/ResizeMode.cs
  8. 40
      src/ImageProcessorCore/Samplers/ResizeOptions.cs
  9. 71
      tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs

4
src/ImageProcessorCore/Common/Extensions/ByteExtensions.cs

@ -33,7 +33,7 @@ namespace ImageProcessorCore
result = new byte[bytes.Length * 8 / bits]; result = new byte[bytes.Length * 8 / bits];
// BUGFIX I dont think it should be there, but I am not sure if it breaks something else // BUGFIX I dont think it should be there, but I am not sure if it breaks something else
//int factor = (int)Math.Pow(2, bits) - 1; // int factor = (int)Math.Pow(2, bits) - 1;
int mask = 0xFF >> (8 - bits); int mask = 0xFF >> (8 - bits);
int resultOffset = 0; int resultOffset = 0;
@ -41,7 +41,7 @@ namespace ImageProcessorCore
{ {
for (int shift = 0; shift < 8; shift += bits) for (int shift = 0; shift < 8; shift += bits)
{ {
int colorIndex = ((b >> (8 - bits - shift)) & mask); // * (255 / factor); int colorIndex = (b >> (8 - bits - shift)) & mask; // * (255 / factor);
result[resultOffset] = (byte)colorIndex; result[resultOffset] = (byte)colorIndex;

8
src/ImageProcessorCore/ParallelImageProcessor.cs

@ -93,9 +93,13 @@ namespace ImageProcessorCore
sourceRectangle = source.Bounds; sourceRectangle = source.Bounds;
} }
this.OnApply(source, target, target.Bounds, sourceRectangle); if (targetRectangle == Rectangle.Empty)
{
targetRectangle = target.Bounds;
}
this.OnApply(source, target, targetRectangle, sourceRectangle);
targetRectangle = target.Bounds;
this.numRowsProcessed = 0; this.numRowsProcessed = 0;
this.totalRows = targetRectangle.Bottom; this.totalRows = targetRectangle.Bottom;

58
src/ImageProcessorCore/Samplers/AnchorPosition.cs

@ -0,0 +1,58 @@
// <copyright file="AnchorPosition.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Samplers
{
/// <summary>
/// Enumerated anchor positions to apply to resized images.
/// </summary>
public enum AnchorPosition
{
/// <summary>
/// Anchors the position of the image to the center of it's bounding container.
/// </summary>
Center,
/// <summary>
/// Anchors the position of the image to the top of it's bounding container.
/// </summary>
Top,
/// <summary>
/// Anchors the position of the image to the bottom of it's bounding container.
/// </summary>
Bottom,
/// <summary>
/// Anchors the position of the image to the left of it's bounding container.
/// </summary>
Left,
/// <summary>
/// Anchors the position of the image to the right of it's bounding container.
/// </summary>
Right,
/// <summary>
/// Anchors the position of the image to the top left side of it's bounding container.
/// </summary>
TopLeft,
/// <summary>
/// Anchors the position of the image to the top right side of it's bounding container.
/// </summary>
TopRight,
/// <summary>
/// Anchors the position of the image to the bottom right side of it's bounding container.
/// </summary>
BottomRight,
/// <summary>
/// Anchors the position of the image to the bottom left side of it's bounding container.
/// </summary>
BottomLeft
}
}

35
src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs

@ -82,6 +82,32 @@ namespace ImageProcessorCore.Samplers
} }
} }
/// <summary>
/// Resizes an image in accordance with the given <see cref="ResizeOptions"/>.
/// </summary>
/// <param name="source">The image to resize.</param>
/// <param name="options">The resize options.</param>
/// <param name="progressHandler">A delegate which is called as progress is made processing the image.</param>
/// <returns>The <see cref="Image"/></returns>
/// <remarks>Passing zero for one of height or width within the resize options will automatically preserve the aspect ratio of the original image</remarks>
public static Image Resize(this Image source, ResizeOptions options, ProgressEventHandler progressHandler = null)
{
// Ensure size is populated acros both dimensions.
if (options.Size.Width == 0 && options.Size.Height > 0)
{
options.Size = new Size(source.Width * options.Size.Height / source.Height, options.Size.Height);
}
if (options.Size.Height == 0 && options.Size.Width > 0)
{
options.Size = new Size(options.Size.Width, source.Height * options.Size.Width / source.Width);
}
Rectangle targetRectangle = ResizeHelper.CalculateTargetLocationAndBounds(source, options);
return Resize(source, options.Size.Width, options.Size.Height, options.Sampler, source.Bounds, targetRectangle, options.Compand, progressHandler);
}
/// <summary> /// <summary>
/// Resizes an image to the given width and height. /// Resizes an image to the given width and height.
/// </summary> /// </summary>
@ -124,7 +150,7 @@ namespace ImageProcessorCore.Samplers
/// <remarks>Passing zero for one of height or width will automatically preserve the aspect ratio of the original image</remarks> /// <remarks>Passing zero for one of height or width will automatically preserve the aspect ratio of the original image</remarks>
public static Image Resize(this Image source, int width, int height, IResampler sampler, bool compand, ProgressEventHandler progressHandler = null) public static Image Resize(this Image source, int width, int height, IResampler sampler, bool compand, ProgressEventHandler progressHandler = null)
{ {
return Resize(source, width, height, sampler, source.Bounds, compand, progressHandler); return Resize(source, width, height, sampler, source.Bounds, new Rectangle(0, 0, width, height), compand, progressHandler);
} }
/// <summary> /// <summary>
@ -138,11 +164,14 @@ namespace ImageProcessorCore.Samplers
/// <param name="sourceRectangle"> /// <param name="sourceRectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to draw. /// The <see cref="Rectangle"/> structure that specifies the portion of the image object to draw.
/// </param> /// </param>
/// <param name="targetRectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the target image object to draw to.
/// </param>
/// <param name="compand">Whether to compress and expand the image color-space to gamma correct the image during processing.</param> /// <param name="compand">Whether to compress and expand the image color-space to gamma correct the image during processing.</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>
/// <remarks>Passing zero for one of height or width will automatically preserve the aspect ratio of the original image</remarks> /// <remarks>Passing zero for one of height or width will automatically preserve the aspect ratio of the original image</remarks>
public static Image Resize(this Image source, int width, int height, IResampler sampler, Rectangle sourceRectangle, bool compand = false, ProgressEventHandler progressHandler = null) public static Image Resize(this Image source, int width, int height, IResampler sampler, Rectangle sourceRectangle, Rectangle targetRectangle, bool compand = false, ProgressEventHandler progressHandler = null)
{ {
if (width == 0 && height > 0) if (width == 0 && height > 0)
{ {
@ -159,7 +188,7 @@ namespace ImageProcessorCore.Samplers
try try
{ {
return source.Process(width, height, sourceRectangle, new Rectangle(0, 0, width, height), processor); return source.Process(width, height, sourceRectangle, targetRectangle, processor);
} }
finally finally
{ {

72
src/ImageProcessorCore/Samplers/Resize.cs

@ -5,6 +5,7 @@
namespace ImageProcessorCore.Samplers namespace ImageProcessorCore.Samplers
{ {
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
/// <summary> /// <summary>
@ -47,12 +48,14 @@ namespace ImageProcessorCore.Samplers
protected override void Apply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY) protected override void Apply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY)
{ {
// Jump out, we'll deal with that later. // Jump out, we'll deal with that later.
if (source.Bounds == target.Bounds) // TODO: Add rectangle comparison.
if (source.Bounds == target.Bounds && sourceRectangle == targetRectangle)
{ {
return; return;
} }
int sourceBottom = source.Bounds.Bottom; int width = target.Width;
int height = target.Height;
int targetY = targetRectangle.Y; int targetY = targetRectangle.Y;
int targetBottom = targetRectangle.Bottom; int targetBottom = targetRectangle.Bottom;
int startX = targetRectangle.X; int startX = targetRectangle.X;
@ -82,6 +85,7 @@ namespace ImageProcessorCore.Samplers
target[x, y] = source[originX, originY]; target[x, y] = source[originX, originY];
} }
this.OnRowProcessed(); this.OnRowProcessed();
} }
}); });
@ -92,16 +96,20 @@ namespace ImageProcessorCore.Samplers
// Interpolate the image using the calculated weights. // Interpolate the image using the calculated weights.
// A 2-pass 1D algorithm appears to be faster than splitting a 1-pass 2D algorithm // A 2-pass 1D algorithm appears to be faster than splitting a 1-pass 2D algorithm
// First process the columns. // First process the columns. Since we are not using multiple threads startY and endY
// are the upper and lower bounds of the source rectangle.
Parallel.For( Parallel.For(
0, startY,
sourceBottom, endY,
y => y =>
{ {
for (int x = startX; x < endX; x++) for (int x = startX; x < endX; x++)
{ {
float sum = this.HorizontalWeights[x].Sum; // Ensure offsets are normalised for cropping and padding.
Weight[] horizontalValues = this.HorizontalWeights[x].Values; int offsetX = x - startX;
float sum = this.HorizontalWeights[offsetX].Sum;
Weight[] horizontalValues = this.HorizontalWeights[offsetX].Values;
// Destination color components // Destination color components
Color destination = new Color(); Color destination = new Color();
@ -119,7 +127,10 @@ namespace ImageProcessorCore.Samplers
destination = Color.Compress(destination); destination = Color.Compress(destination);
} }
this.firstPass[x, y] = destination; if (x >= 0 && x < width)
{
this.firstPass[x, y] = destination;
}
} }
}); });
@ -129,34 +140,37 @@ namespace ImageProcessorCore.Samplers
endY, endY,
y => y =>
{ {
if (y >= targetY && y < targetBottom) // Ensure offsets are normalised for cropping and padding.
int offsetY = y - startY;
float sum = this.VerticalWeights[offsetY].Sum;
Weight[] verticalValues = this.VerticalWeights[offsetY].Values;
for (int x = 0; x < width; x++)
{ {
float sum = this.VerticalWeights[y].Sum; // Destination color components
Weight[] verticalValues = this.VerticalWeights[y].Values; Color destination = new Color();
for (int x = startX; x < endX; x++) for (int i = 0; i < sum; i++)
{ {
// Destination color components Weight yw = verticalValues[i];
Color destination = new Color(); int originY = yw.Index;
Color sourceColor = compand ? Color.Expand(this.firstPass[x, originY]) : this.firstPass[x, originY];
for (int i = 0; i < sum; i++) destination += sourceColor * yw.Value;
{ }
Weight yw = verticalValues[i];
int originY = yw.Index;
Color sourceColor = compand ? Color.Expand(this.firstPass[x, originY]) : this.firstPass[x, originY];
destination += sourceColor * yw.Value;
}
if (compand) if (compand)
{ {
destination = Color.Compress(destination); destination = Color.Compress(destination);
} }
if (y >= 0 && y < height)
{
target[x, y] = destination; target[x, y] = destination;
} }
this.OnRowProcessed();
} }
this.OnRowProcessed();
}); });
} }
@ -164,7 +178,7 @@ namespace ImageProcessorCore.Samplers
protected override void AfterApply(ImageBase source, ImageBase target, Rectangle targetRectangle, Rectangle sourceRectangle) protected override void AfterApply(ImageBase source, ImageBase target, Rectangle targetRectangle, Rectangle sourceRectangle)
{ {
// Copy the pixels over. // Copy the pixels over.
if (source.Bounds == target.Bounds) if (source.Bounds == target.Bounds && sourceRectangle == targetRectangle)
{ {
target.ClonePixels(target.Width, target.Height, source.Pixels); target.ClonePixels(target.Width, target.Height, source.Pixels);
} }

235
src/ImageProcessorCore/Samplers/ResizeHelper.cs

@ -0,0 +1,235 @@
// <copyright file="ResizeHelper.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Samplers
{
using System;
using System.Linq;
/// <summary>
/// Provides methods to help calculate the target rectangle when resizing using the
/// <see cref="ResizeMode"/> enumeration.
/// </summary>
internal static class ResizeHelper
{
/// <summary>
/// Calculates the target location and bounds to perform the resize operation against.
/// </summary>
/// <param name="source">The source image.</param>
/// <param name="options">The resize options.</param>
/// <returns>
/// The <see cref="Rectangle"/>.
/// </returns>
public static Rectangle CalculateTargetLocationAndBounds(ImageBase source, ResizeOptions options)
{
switch (options.Mode)
{
case ResizeMode.Pad:
return CalculatePadRectangle(source, options);
// TODO: Additional modes
// Default case ResizeMode.Crop
default:
return CalculateCropRectangle(source, options);
}
}
/// <summary>
/// Calculates the target rectangle for crop mode.
/// </summary>
/// <param name="source">The source image.</param>
/// <param name="options">The resize options.</param>
/// <returns>
/// The <see cref="Rectangle"/>.
/// </returns>
private static Rectangle CalculateCropRectangle(ImageBase source, ResizeOptions options)
{
int width = options.Size.Width;
int height = options.Size.Height;
if (width <= 0 || height <= 0)
{
return new Rectangle(0, 0, source.Width, source.Height);
}
double ratio;
int sourceWidth = source.Width;
int sourceHeight = source.Height;
int destinationX = 0;
int destinationY = 0;
int destinationWidth = width;
int destinationHeight = height;
// Fractional variants for preserving aspect ratio.
double percentHeight = Math.Abs(height / (double)sourceHeight);
double percentWidth = Math.Abs(width / (double)sourceWidth);
if (percentHeight < percentWidth)
{
ratio = percentWidth;
if (options.CenterCoordinates.Any())
{
double center = -(ratio * sourceHeight) * options.CenterCoordinates.First();
destinationY = (int)center + (height / 2);
if (destinationY > 0)
{
destinationY = 0;
}
if (destinationY < (int)(height - (sourceHeight * ratio)))
{
destinationY = (int)(height - (sourceHeight * ratio));
}
}
else
{
switch (options.Position)
{
case AnchorPosition.Top:
case AnchorPosition.TopLeft:
case AnchorPosition.TopRight:
destinationY = 0;
break;
case AnchorPosition.Bottom:
case AnchorPosition.BottomLeft:
case AnchorPosition.BottomRight:
destinationY = (int)(height - (sourceHeight * ratio));
break;
default:
destinationY = (int)((height - (sourceHeight * ratio)) / 2);
break;
}
}
destinationHeight = (int)Math.Ceiling(sourceHeight * percentWidth);
}
else
{
ratio = percentHeight;
if (options.CenterCoordinates.Any())
{
double center = -(ratio * sourceWidth) * options.CenterCoordinates.ToArray()[1];
destinationX = (int)center + (width / 2);
if (destinationX > 0)
{
destinationX = 0;
}
if (destinationX < (int)(width - (sourceWidth * ratio)))
{
destinationX = (int)(width - (sourceWidth * ratio));
}
}
else
{
switch (options.Position)
{
case AnchorPosition.Left:
case AnchorPosition.TopLeft:
case AnchorPosition.BottomLeft:
destinationX = 0;
break;
case AnchorPosition.Right:
case AnchorPosition.TopRight:
case AnchorPosition.BottomRight:
destinationX = (int)(width - (sourceWidth * ratio));
break;
default:
destinationX = (int)((width - (sourceWidth * ratio)) / 2);
break;
}
}
destinationWidth = (int)Math.Ceiling(sourceWidth * percentHeight);
}
return new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight);
}
/// <summary>
/// Calculates the target rectangle for pad mode.
/// </summary>
/// <param name="source">The source image.</param>
/// <param name="options">The resize options.</param>
/// <returns>
/// The <see cref="Rectangle"/>.
/// </returns>
private static Rectangle CalculatePadRectangle(ImageBase source, ResizeOptions options)
{
int width = options.Size.Width;
int height = options.Size.Height;
if (width <= 0 || height <= 0)
{
return new Rectangle(0, 0, source.Width, source.Height);
}
double ratio;
int sourceWidth = source.Width;
int sourceHeight = source.Height;
int destinationX = 0;
int destinationY = 0;
int destinationWidth = width;
int destinationHeight = height;
// Fractional variants for preserving aspect ratio.
double percentHeight = Math.Abs(height / (double)sourceHeight);
double percentWidth = Math.Abs(width / (double)sourceWidth);
if (percentHeight < percentWidth)
{
ratio = percentHeight;
destinationWidth = Convert.ToInt32(sourceWidth * percentHeight);
switch (options.Position)
{
case AnchorPosition.Left:
case AnchorPosition.TopLeft:
case AnchorPosition.BottomLeft:
destinationX = 0;
break;
case AnchorPosition.Right:
case AnchorPosition.TopRight:
case AnchorPosition.BottomRight:
destinationX = (int)(width - (sourceWidth * ratio));
break;
default:
destinationX = Convert.ToInt32((width - (sourceWidth * ratio)) / 2);
break;
}
}
else
{
ratio = percentWidth;
destinationHeight = Convert.ToInt32(sourceHeight * percentWidth);
switch (options.Position)
{
case AnchorPosition.Top:
case AnchorPosition.TopLeft:
case AnchorPosition.TopRight:
destinationY = 0;
break;
case AnchorPosition.Bottom:
case AnchorPosition.BottomLeft:
case AnchorPosition.BottomRight:
destinationY = (int)(height - (sourceHeight * ratio));
break;
default:
destinationY = (int)((height - (sourceHeight * ratio)) / 2);
break;
}
}
return new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight);
}
}
}

47
src/ImageProcessorCore/Samplers/ResizeMode.cs

@ -0,0 +1,47 @@
// <copyright file="ResizeMode.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Samplers
{
/// <summary>
/// Enumerated resize modes to apply to resized images.
/// </summary>
public enum ResizeMode
{
/// <summary>
/// Crops the resized image to fit the bounds of its container.
/// </summary>
Crop,
/// <summary>
/// Pads the resized image to fit the bounds of its container.
/// If only one dimension is passed, will maintain the original aspect ratio.
/// </summary>
Pad,
/// <summary>
/// Stretches the resized image to fit the bounds of its container.
/// </summary>
Stretch,
/// <summary>
/// Constrains the resized image to fit the bounds of its container maintaining
/// the original aspect ratio.
/// </summary>
Max,
/// <summary>
/// Resizes the image until the shortest side reaches the set given dimension.
/// </summary>
Min,
/// <summary>
/// Pads the image to fit the bound of the container without resizing the
/// original source.
/// When downscaling, performs the same functionality as <see cref="ResizeMode.Pad"/>
/// </summary>
BoxPad
}
}

40
src/ImageProcessorCore/Samplers/ResizeOptions.cs

@ -0,0 +1,40 @@
// <copyright file="ResizeOptions.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Samplers
{
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// The resize options for resizing images against certain modes.
/// </summary>
public class ResizeOptions
{
/// <summary>
/// Gets or sets the resize mode.
/// </summary>
public ResizeMode Mode { get; set; } = ResizeMode.Crop;
/// <summary>
/// Gets or sets the anchor position.
/// </summary>
public AnchorPosition Position { get; set; } = AnchorPosition.Center;
/// <summary>
/// Gets or sets the center coordinates.
/// </summary>
public IEnumerable<float> CenterCoordinates { get; set; } = Enumerable.Empty<float>();
/// <summary>
/// Gets or sets the target size.
/// </summary>
public Size Size { get; set; }
public IResampler Sampler { get; set; } = new BicubicResampler();
public bool Compand { get; set; }
}
}

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

@ -141,7 +141,7 @@
Directory.CreateDirectory("TestOutput/Resize"); Directory.CreateDirectory("TestOutput/Resize");
} }
var name = "FixedHeight"; string name = "FixedHeight";
foreach (string file in Files) foreach (string file in Files)
{ {
@ -162,6 +162,73 @@
} }
} }
[Fact]
public void ImageShouldResizeWithCropMode()
{
if (!Directory.Exists("TestOutput/ResizeCrop"))
{
Directory.CreateDirectory("TestOutput/ResizeCrop");
}
foreach (string file in Files)
{
using (FileStream stream = File.OpenRead(file))
{
Stopwatch watch = Stopwatch.StartNew();
string filename = Path.GetFileName(file);
using (Image image = new Image(stream))
using (FileStream output = File.OpenWrite($"TestOutput/ResizeCrop/{filename}"))
{
ResizeOptions options = new ResizeOptions()
{
Size = new Size(image.Width / 2, image.Height)
};
image.Resize(options, this.ProgressUpdate)
.Save(output);
}
Trace.WriteLine($"{filename}: {watch.ElapsedMilliseconds}ms");
}
}
}
[Fact]
public void ImageShouldResizeWithPadMode()
{
if (!Directory.Exists("TestOutput/ResizePad"))
{
Directory.CreateDirectory("TestOutput/ResizePad");
}
foreach (string file in Files)
{
using (FileStream stream = File.OpenRead(file))
{
Stopwatch watch = Stopwatch.StartNew();
string filename = Path.GetFileName(file);
using (Image image = new Image(stream))
using (FileStream output = File.OpenWrite($"TestOutput/ResizePad/{filename}"))
{
ResizeOptions options = new ResizeOptions()
{
Size = new Size(image.Width + 200, image.Height),
Mode = ResizeMode.Pad
};
image.Resize(options, this.ProgressUpdate)
.Save(output);
}
Trace.WriteLine($"{filename}: {watch.ElapsedMilliseconds}ms");
}
}
}
[Theory] [Theory]
[MemberData("RotateFlips")] [MemberData("RotateFlips")]
public void ImageShouldRotateFlip(RotateType rotateType, FlipType flipType) public void ImageShouldRotateFlip(RotateType rotateType, FlipType flipType)
@ -281,7 +348,7 @@
{ {
using (FileStream stream = File.OpenRead(file)) using (FileStream stream = File.OpenRead(file))
{ {
string filename = Path.GetFileNameWithoutExtension(file) + "-Crop" + Path.GetExtension(file); string filename = Path.GetFileNameWithoutExtension(file) + "-Crop" + Path.GetExtension(file);
using (Image image = new Image(stream)) using (Image image = new Image(stream))

Loading…
Cancel
Save