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 namespace ImageProcessorCore
{ {
using System; using System;
using System.Runtime.CompilerServices;
/// <summary> /// <summary>
/// Encapsulates the basic properties and methods required to manipulate images. /// 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. /// than zero and smaller than the width of the pixel.
/// </param> /// </param>
/// <returns>The <see cref="Color"/> at the specified position.</returns> /// <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> /// <summary>
/// Sets the pixel array of the image to the given value. /// Sets the pixel array of the image to the given value.

3
src/ImageProcessorCore/ImageBase.cs

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

2
src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs

@ -74,7 +74,7 @@ namespace ImageProcessorCore.Samplers
try 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 finally
{ {

13
src/ImageProcessorCore/Samplers/Resize.cs

@ -5,7 +5,6 @@
namespace ImageProcessorCore.Samplers namespace ImageProcessorCore.Samplers
{ {
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
/// <summary> /// <summary>
@ -56,6 +55,7 @@ namespace ImageProcessorCore.Samplers
int width = target.Width; int width = target.Width;
int height = target.Height; int height = target.Height;
int sourceHeight = sourceRectangle.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;
@ -103,9 +103,11 @@ namespace ImageProcessorCore.Samplers
endY, endY,
y => y =>
{ {
// Ensure offsets are normalised for cropping and padding.
int offsetY = y - startY;
for (int x = startX; x < endX; x++) for (int x = startX; x < endX; x++)
{ {
// Ensure offsets are normalised for cropping and padding.
int offsetX = x - startX; int offsetX = x - startX;
float sum = this.HorizontalWeights[offsetX].Sum; float sum = this.HorizontalWeights[offsetX].Sum;
@ -118,7 +120,7 @@ namespace ImageProcessorCore.Samplers
{ {
Weight xw = horizontalValues[i]; Weight xw = horizontalValues[i];
int originX = xw.Index; 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; destination += sourceColor * xw.Value;
} }
@ -127,9 +129,9 @@ namespace ImageProcessorCore.Samplers
destination = Color.Compress(destination); 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. // Ensure offsets are normalised for cropping and padding.
int offsetY = y - startY; int offsetY = y - startY;
float sum = this.VerticalWeights[offsetY].Sum; float sum = this.VerticalWeights[offsetY].Sum;
Weight[] verticalValues = this.VerticalWeights[offsetY].Values; Weight[] verticalValues = this.VerticalWeights[offsetY].Values;

174
src/ImageProcessorCore/Samplers/ResizeHelper.cs

@ -28,8 +28,13 @@ namespace ImageProcessorCore.Samplers
{ {
case ResizeMode.Pad: case ResizeMode.Pad:
return CalculatePadRectangle(source, options); 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 case ResizeMode.Crop
default: default:
return CalculateCropRectangle(source, options); return CalculateCropRectangle(source, options);
@ -231,5 +236,172 @@ namespace ImageProcessorCore.Samplers
return new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight); 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() 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) image.Resize(options, this.ProgressUpdate)
@ -192,7 +192,6 @@
Trace.WriteLine($"{filename}: {watch.ElapsedMilliseconds}ms"); Trace.WriteLine($"{filename}: {watch.ElapsedMilliseconds}ms");
} }
} }
} }
[Fact] [Fact]
@ -215,7 +214,7 @@
{ {
ResizeOptions options = new ResizeOptions() ResizeOptions options = new ResizeOptions()
{ {
Size = new Size(image.Width + 200, image.Height), Size = new Size(image.Width , image.Height + 200),
Mode = ResizeMode.Pad Mode = ResizeMode.Pad
}; };
@ -226,7 +225,105 @@
Trace.WriteLine($"{filename}: {watch.ElapsedMilliseconds}ms"); 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] [Theory]

Loading…
Cancel
Save