Browse Source

Finish resize modes [skip ci]

Former-commit-id: 7f2b4f6be701e280da5697663ecfb33daead5c8f
Former-commit-id: 3a71ce1ae1387d1647a2fefc3f53122a5b717c3b
Former-commit-id: 6b203558a57cd9623a67982213e004f5d068763f
af/merge-core
James Jackson-South 10 years ago
parent
commit
1f32cace7e
  1. 3
      src/ImageProcessorCore/IImageBase.cs
  2. 3
      src/ImageProcessorCore/ImageBase.cs
  3. 2
      src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs
  4. 13
      src/ImageProcessorCore/Samplers/Resize.cs
  5. 174
      src/ImageProcessorCore/Samplers/ResizeHelper.cs
  6. 103
      tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs

3
src/ImageProcessorCore/IImageBase.cs

@ -6,6 +6,7 @@
namespace ImageProcessorCore
{
using System;
using System.Runtime.CompilerServices;
/// <summary>
/// Encapsulates the basic properties and methods required to manipulate images.
@ -67,7 +68,7 @@ namespace ImageProcessorCore
/// than zero and smaller than the width of the pixel.
/// </param>
/// <returns>The <see cref="Color"/> at the specified position.</returns>
Color this[int x, int y] { get; set; }
Color this[int x, int y, [CallerLineNumber] int line = 0] { get; set; }
/// <summary>
/// Sets the pixel array of the image to the given value.

3
src/ImageProcessorCore/ImageBase.cs

@ -6,6 +6,7 @@
namespace ImageProcessorCore
{
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
/// <summary>
@ -134,7 +135,7 @@ namespace ImageProcessorCore
public int FrameDelay { get; set; }
/// <inheritdoc/>
public Color this[int x, int y]
public Color this[int x, int y, [CallerLineNumber] int line = 0]
{
get
{

2
src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs

@ -74,7 +74,7 @@ namespace ImageProcessorCore.Samplers
try
{
return source.Process(source.Width, source.Height, source.Bounds, source.Bounds, processor);
return source.Process(source.Width, source.Height, source.Bounds, Rectangle.Empty, processor);
}
finally
{

13
src/ImageProcessorCore/Samplers/Resize.cs

@ -5,7 +5,6 @@
namespace ImageProcessorCore.Samplers
{
using System;
using System.Threading.Tasks;
/// <summary>
@ -56,6 +55,7 @@ namespace ImageProcessorCore.Samplers
int width = target.Width;
int height = target.Height;
int sourceHeight = sourceRectangle.Height;
int targetY = targetRectangle.Y;
int targetBottom = targetRectangle.Bottom;
int startX = targetRectangle.X;
@ -103,9 +103,11 @@ namespace ImageProcessorCore.Samplers
endY,
y =>
{
// Ensure offsets are normalised for cropping and padding.
int offsetY = y - startY;
for (int x = startX; x < endX; x++)
{
// Ensure offsets are normalised for cropping and padding.
int offsetX = x - startX;
float sum = this.HorizontalWeights[offsetX].Sum;
@ -118,7 +120,7 @@ namespace ImageProcessorCore.Samplers
{
Weight xw = horizontalValues[i];
int originX = xw.Index;
Color sourceColor = compand ? Color.Expand(source[originX, y]) : source[originX, y];
Color sourceColor = compand ? Color.Expand(source[originX, offsetY]) : source[originX, offsetY];
destination += sourceColor * xw.Value;
}
@ -127,9 +129,9 @@ namespace ImageProcessorCore.Samplers
destination = Color.Compress(destination);
}
if (x >= 0 && x < width)
if (x >= 0 && x < width && offsetY >= 0 && offsetY < sourceHeight)
{
this.firstPass[x, y] = destination;
this.firstPass[x, offsetY] = destination;
}
}
});
@ -142,7 +144,6 @@ namespace ImageProcessorCore.Samplers
{
// Ensure offsets are normalised for cropping and padding.
int offsetY = y - startY;
float sum = this.VerticalWeights[offsetY].Sum;
Weight[] verticalValues = this.VerticalWeights[offsetY].Values;

174
src/ImageProcessorCore/Samplers/ResizeHelper.cs

@ -28,8 +28,13 @@ namespace ImageProcessorCore.Samplers
{
case ResizeMode.Pad:
return CalculatePadRectangle(source, options);
case ResizeMode.BoxPad:
return CalculateBoxPadRectangle(source, options);
case ResizeMode.Max:
return CalculateMaxRectangle(source, options);
case ResizeMode.Min:
return CalculateMinRectangle(source, options);
// TODO: Additional modes
// Default case ResizeMode.Crop
default:
return CalculateCropRectangle(source, options);
@ -231,5 +236,172 @@ namespace ImageProcessorCore.Samplers
return new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight);
}
/// <summary>
/// Calculates the target rectangle for box 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 CalculateBoxPadRectangle(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);
}
int sourceWidth = source.Width;
int sourceHeight = source.Height;
// Fractional variants for preserving aspect ratio.
double percentHeight = Math.Abs(height / (double)sourceHeight);
double percentWidth = Math.Abs(width / (double)sourceWidth);
int boxPadHeight = height > 0 ? height : Convert.ToInt32(sourceHeight * percentWidth);
int boxPadWidth = width > 0 ? width : Convert.ToInt32(sourceWidth * percentHeight);
// Only calculate if upscaling.
if (sourceWidth < boxPadWidth && sourceHeight < boxPadHeight)
{
int destinationX;
int destinationY;
int destinationWidth = sourceWidth;
int destinationHeight = sourceHeight;
width = boxPadWidth;
height = boxPadHeight;
switch (options.Position)
{
case AnchorPosition.Left:
destinationY = (height - sourceHeight) / 2;
destinationX = 0;
break;
case AnchorPosition.Right:
destinationY = (height - sourceHeight) / 2;
destinationX = width - sourceWidth;
break;
case AnchorPosition.TopRight:
destinationY = 0;
destinationX = width - sourceWidth;
break;
case AnchorPosition.Top:
destinationY = 0;
destinationX = (width - sourceWidth) / 2;
break;
case AnchorPosition.TopLeft:
destinationY = 0;
destinationX = 0;
break;
case AnchorPosition.BottomRight:
destinationY = height - sourceHeight;
destinationX = width - sourceWidth;
break;
case AnchorPosition.Bottom:
destinationY = height - sourceHeight;
destinationX = (width - sourceWidth) / 2;
break;
case AnchorPosition.BottomLeft:
destinationY = height - sourceHeight;
destinationX = 0;
break;
default:
destinationY = (height - sourceHeight) / 2;
destinationX = (width - sourceWidth) / 2;
break;
}
return new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight);
}
// Switch to pad mode to downscale and calculate from there.
return CalculatePadRectangle(source, options);
}
/// <summary>
/// Calculates the target rectangle for max 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 CalculateMaxRectangle(ImageBase source, ResizeOptions options)
{
int width = options.Size.Width;
int height = options.Size.Height;
int destinationWidth = width;
int destinationHeight = height;
// Fractional variants for preserving aspect ratio.
double percentHeight = Math.Abs(height / (double)source.Height);
double percentWidth = Math.Abs(width / (double)source.Width);
// Integers must be cast to doubles to get needed precision
double ratio = (double)options.Size.Height / options.Size.Width;
double sourceRatio = (double)source.Height / source.Width;
if (sourceRatio < ratio)
{
destinationHeight = Convert.ToInt32(source.Height * percentWidth);
}
else
{
destinationWidth = Convert.ToInt32(source.Width * percentHeight);
}
return new Rectangle(0, 0, destinationWidth, destinationHeight);
}
/// <summary>
/// Calculates the target rectangle for min 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 CalculateMinRectangle(ImageBase source, ResizeOptions options)
{
int width = options.Size.Width;
int height = options.Size.Height;
int destinationWidth;
int destinationHeight;
// Fractional variants for preserving aspect ratio.
double percentHeight = Math.Abs(height / (double)source.Height);
double percentWidth = Math.Abs(width / (double)source.Width);
height = height > 0 ? height : Convert.ToInt32(source.Height * percentWidth);
width = width > 0 ? width : Convert.ToInt32(source.Width * percentHeight);
double sourceRatio = (double)source.Height / source.Width;
// Find the shortest distance to go.
int widthDiff = source.Width - width;
int heightDiff = source.Height - height;
if (widthDiff < heightDiff)
{
destinationHeight = Convert.ToInt32(width * sourceRatio);
destinationWidth = width;
}
else if (widthDiff > heightDiff)
{
destinationHeight = height;
destinationWidth = Convert.ToInt32(height / sourceRatio);
}
else
{
destinationWidth = width;
destinationHeight = height;
}
return new Rectangle(0, 0, destinationWidth, destinationHeight);
}
}
}

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

@ -182,7 +182,7 @@
{
ResizeOptions options = new ResizeOptions()
{
Size = new Size(image.Width / 2, image.Height)
Size = new Size(image.Width , image.Height / 2)
};
image.Resize(options, this.ProgressUpdate)
@ -192,7 +192,6 @@
Trace.WriteLine($"{filename}: {watch.ElapsedMilliseconds}ms");
}
}
}
[Fact]
@ -215,7 +214,7 @@
{
ResizeOptions options = new ResizeOptions()
{
Size = new Size(image.Width + 200, image.Height),
Size = new Size(image.Width , image.Height + 200),
Mode = ResizeMode.Pad
};
@ -226,7 +225,105 @@
Trace.WriteLine($"{filename}: {watch.ElapsedMilliseconds}ms");
}
}
}
[Fact]
public void ImageShouldResizeWithBoxPadMode()
{
if (!Directory.Exists("TestOutput/ResizeBoxPad"))
{
Directory.CreateDirectory("TestOutput/ResizeBoxPad");
}
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/ResizeBoxPad/{filename}"))
{
ResizeOptions options = new ResizeOptions()
{
Size = new Size(image.Width + 200, image.Height + 200),
Mode = ResizeMode.BoxPad
};
image.Resize(options, this.ProgressUpdate)
.Save(output);
}
Trace.WriteLine($"{filename}: {watch.ElapsedMilliseconds}ms");
}
}
}
[Fact]
public void ImageShouldResizeWithMaxMode()
{
if (!Directory.Exists("TestOutput/ResizeMax"))
{
Directory.CreateDirectory("TestOutput/ResizeMax");
}
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/ResizeMax/{filename}"))
{
ResizeOptions options = new ResizeOptions()
{
Size = new Size(image.Width + 200, image.Height),
Mode = ResizeMode.Max
};
image.Resize(options, this.ProgressUpdate)
.Save(output);
}
Trace.WriteLine($"{filename}: {watch.ElapsedMilliseconds}ms");
}
}
}
[Fact]
public void ImageShouldResizeWithMinMode()
{
if (!Directory.Exists("TestOutput/ResizeMin"))
{
Directory.CreateDirectory("TestOutput/ResizeMin");
}
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/ResizeMin/{filename}"))
{
ResizeOptions options = new ResizeOptions()
{
Size = new Size(image.Width + 200, image.Height),
Mode = ResizeMode.Min
};
image.Resize(options, this.ProgressUpdate)
.Save(output);
}
Trace.WriteLine($"{filename}: {watch.ElapsedMilliseconds}ms");
}
}
}
[Theory]

Loading…
Cancel
Save