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];
// 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 resultOffset = 0;
@ -41,7 +41,7 @@ namespace ImageProcessorCore
{
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;

8
src/ImageProcessorCore/ParallelImageProcessor.cs

@ -93,9 +93,13 @@ namespace ImageProcessorCore
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.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>
/// Resizes an image to the given width and height.
/// </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>
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>
@ -138,11 +164,14 @@ namespace ImageProcessorCore.Samplers
/// <param name="sourceRectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to draw.
/// </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="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 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)
{
@ -159,7 +188,7 @@ namespace ImageProcessorCore.Samplers
try
{
return source.Process(width, height, sourceRectangle, new Rectangle(0, 0, width, height), processor);
return source.Process(width, height, sourceRectangle, targetRectangle, processor);
}
finally
{

72
src/ImageProcessorCore/Samplers/Resize.cs

@ -5,6 +5,7 @@
namespace ImageProcessorCore.Samplers
{
using System;
using System.Threading.Tasks;
/// <summary>
@ -47,12 +48,14 @@ namespace ImageProcessorCore.Samplers
protected override void Apply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY)
{
// 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;
}
int sourceBottom = source.Bounds.Bottom;
int width = target.Width;
int height = target.Height;
int targetY = targetRectangle.Y;
int targetBottom = targetRectangle.Bottom;
int startX = targetRectangle.X;
@ -82,6 +85,7 @@ namespace ImageProcessorCore.Samplers
target[x, y] = source[originX, originY];
}
this.OnRowProcessed();
}
});
@ -92,16 +96,20 @@ namespace ImageProcessorCore.Samplers
// Interpolate the image using the calculated weights.
// 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(
0,
sourceBottom,
startY,
endY,
y =>
{
for (int x = startX; x < endX; x++)
{
float sum = this.HorizontalWeights[x].Sum;
Weight[] horizontalValues = this.HorizontalWeights[x].Values;
// Ensure offsets are normalised for cropping and padding.
int offsetX = x - startX;
float sum = this.HorizontalWeights[offsetX].Sum;
Weight[] horizontalValues = this.HorizontalWeights[offsetX].Values;
// Destination color components
Color destination = new Color();
@ -119,7 +127,10 @@ namespace ImageProcessorCore.Samplers
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,
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;
Weight[] verticalValues = this.VerticalWeights[y].Values;
// Destination color components
Color destination = new Color();
for (int x = startX; x < endX; x++)
for (int i = 0; i < sum; i++)
{
// Destination color components
Color destination = new Color();
for (int i = 0; i < sum; i++)
{
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;
}
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)
{
destination = Color.Compress(destination);
}
if (compand)
{
destination = Color.Compress(destination);
}
if (y >= 0 && y < height)
{
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)
{
// Copy the pixels over.
if (source.Bounds == target.Bounds)
if (source.Bounds == target.Bounds && sourceRectangle == targetRectangle)
{
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");
}
var name = "FixedHeight";
string name = "FixedHeight";
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]
[MemberData("RotateFlips")]
public void ImageShouldRotateFlip(RotateType rotateType, FlipType flipType)
@ -281,7 +348,7 @@
{
using (FileStream stream = File.OpenRead(file))
{
string filename = Path.GetFileNameWithoutExtension(file) + "-Crop" + Path.GetExtension(file);
using (Image image = new Image(stream))

Loading…
Cancel
Save