//
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
//
namespace ImageProcessor.Samplers
{
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Threading.Tasks;
///
/// Provides methods that allow the resampling of images using various algorithms.
///
public class Resampler : ParallelImageProcessor
{
///
/// The angle of rotation.
///
private float angle;
///
/// The horizontal weights.
///
private Weights[] horizontalWeights;
///
/// The vertical weights.
///
private Weights[] verticalWeights;
///
/// Initializes a new instance of the class.
///
///
/// The sampler to perform the resize operation.
///
public Resampler(IResampler sampler)
{
Guard.NotNull(sampler, nameof(sampler));
this.Sampler = sampler;
}
///
/// Gets the sampler to perform the resize operation.
///
public IResampler Sampler { get; }
///
/// Gets or sets the angle of rotation.
///
public float Angle
{
get
{
return this.angle;
}
set
{
if (value > 360)
{
value -= 360;
}
if (value < 0)
{
value += 360;
}
this.angle = value;
}
}
///
protected override void OnApply(ImageBase source, ImageBase target, Rectangle targetRectangle, Rectangle sourceRectangle)
{
if (!(this.Sampler is NearestNeighborResampler))
{
this.horizontalWeights = this.PrecomputeWeights(targetRectangle.Width, sourceRectangle.Width);
this.verticalWeights = this.PrecomputeWeights(targetRectangle.Height, sourceRectangle.Height);
}
}
///
protected override void Apply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY)
{
bool rotate = this.angle > 0 && this.angle < 360;
// Split the two methods up so we can keep standard resize as performant as possible.
if (rotate)
{
this.ApplyResizeAndRotate(target, source, targetRectangle, sourceRectangle, startY, endY);
}
else
{
this.ApplyResizeOnly(target, source, targetRectangle, startY, endY);
}
}
///
/// Resamples the specified at the specified location
/// and with the specified size.
///
/// Target image to apply the process to.
/// The source image. Cannot be null.
///
/// The structure that specifies the location and size of the drawn image.
/// The image is scaled to fit the rectangle.
///
/// The index of the row within the source image to start processing.
/// The index of the row within the source image to end processing.
///
/// The method keeps the source image unchanged and returns the
/// the result of image process as new image.
///
private void ApplyResizeOnly(ImageBase target, ImageBase source, Rectangle targetRectangle, int startY, int endY)
{
if (source.Bounds == target.Bounds)
{
target.ClonePixels(target.Width, target.Height, source.Pixels);
return;
}
int targetY = targetRectangle.Y;
int targetBottom = targetRectangle.Bottom;
int startX = targetRectangle.X;
int endX = targetRectangle.Right;
if (this.Sampler is NearestNeighborResampler)
{
// Scaling factors
float widthFactor = source.Width / (float)target.Width;
float heightFactor = source.Height / (float)target.Height;
Parallel.For(
startY,
endY,
y =>
{
if (y >= targetY && y < targetBottom)
{
// Y coordinates of source points
int originY = (int)((y - targetY) * heightFactor);
for (int x = startX; x < endX; x++)
{
// X coordinates of source points
int originX = (int)((x - startX) * widthFactor);
target[x, y] = source[originX, originY];
}
}
});
// Break out now.
return;
}
// Interpolate the image using the calculated weights.
Parallel.For(
startY,
endY,
y =>
{
if (y >= targetY && y < targetBottom)
{
Weight[] verticalValues = this.verticalWeights[y].Values;
for (int x = startX; x < endX; x++)
{
Weight[] horizontalValues = this.horizontalWeights[x].Values;
// Destination color components
Color destination = new Color(0, 0, 0, 0);
foreach (Weight yw in verticalValues)
{
int originY = yw.Index;
foreach (Weight xw in horizontalValues)
{
int originX = xw.Index;
Color sourceColor = Color.InverseCompand(source[originX, originY]);
float weight = yw.Value * xw.Value;
destination.R += sourceColor.R * weight;
destination.G += sourceColor.G * weight;
destination.B += sourceColor.B * weight;
destination.A += sourceColor.A * weight;
}
}
destination = Color.Compand(destination);
// Round alpha values in an attempt to prevent bleed.
destination.A = (float)Math.Round(destination.A, 2);
target[x, y] = destination;
}
}
});
}
///
/// Resamples and rotates the specified at the specified location
/// and with the specified size.
///
/// Target image to apply the process to.
/// The source image. Cannot be null.
///
/// The structure that specifies the location and size of the drawn image.
/// The image is scaled to fit the rectangle.
///
///
/// The structure that specifies the portion of the image object to draw.
///
/// The index of the row within the source image to start processing.
/// The index of the row within the source image to end processing.
///
/// The method keeps the source image unchanged and returns the
/// the result of image process as new image.
///
private void ApplyResizeAndRotate(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY)
{
int targetY = targetRectangle.Y;
int targetBottom = targetRectangle.Bottom;
int startX = targetRectangle.X;
int endX = targetRectangle.Right;
float negativeAngle = -this.angle;
Vector2 centre = Rectangle.Center(sourceRectangle);
if (this.Sampler is NearestNeighborResampler)
{
// Scaling factors
float widthFactor = source.Width / (float)target.Width;
float heightFactor = source.Height / (float)target.Height;
Parallel.For(
startY,
endY,
y =>
{
if (y >= targetY && y < targetBottom)
{
// Y coordinates of source points
int originY = (int)((y - targetY) * heightFactor);
for (int x = startX; x < endX; x++)
{
// X coordinates of source points
int originX = (int)((x - startX) * widthFactor);
// Rotate at the centre point
Vector2 rotated = ImageMaths.RotatePoint(new Vector2(originX, originY), centre, negativeAngle);
int rotatedX = (int)rotated.X;
int rotatedY = (int)rotated.Y;
if (sourceRectangle.Contains(rotatedX, rotatedY))
{
target[x, y] = source[rotatedX, rotatedY];
}
}
}
});
// Break out now.
return;
}
// Interpolate the image using the calculated weights.
Parallel.For(
startY,
endY,
y =>
{
if (y >= targetY && y < targetBottom)
{
Weight[] verticalValues = this.verticalWeights[y].Values;
for (int x = startX; x < endX; x++)
{
Weight[] horizontalValues = this.horizontalWeights[x].Values;
// Destination color components
Color destination = new Color(0, 0, 0, 0);
foreach (Weight yw in verticalValues)
{
int originY = yw.Index;
foreach (Weight xw in horizontalValues)
{
int originX = xw.Index;
// Rotate at the centre point
Vector2 rotated = ImageMaths.RotatePoint(new Vector2(originX, originY), centre, negativeAngle);
int rotatedX = (int)rotated.X;
int rotatedY = (int)rotated.Y;
if (sourceRectangle.Contains(rotatedX, rotatedY))
{
Color sourceColor = Color.InverseCompand(source[rotatedX, rotatedY]);
float weight = yw.Value * xw.Value;
destination.R += sourceColor.R * weight;
destination.G += sourceColor.G * weight;
destination.B += sourceColor.B * weight;
destination.A += sourceColor.A * weight;
}
}
}
destination = Color.Compand(destination);
// Round alpha values in an attempt to prevent bleed.
destination.A = (float)Math.Round(destination.A, 2);
target[x, y] = destination;
}
}
});
}
///
/// Computes the weights to apply at each pixel when resizing.
///
/// The destination section size.
/// The source section size.
///
/// The .
///
private Weights[] PrecomputeWeights(int destinationSize, int sourceSize)
{
IResampler sampler = this.Sampler;
float du = sourceSize / (float)destinationSize;
float scale = du;
if (scale < 1)
{
scale = 1;
}
float ru = (float)Math.Ceiling(scale * sampler.Radius);
Weights[] result = new Weights[destinationSize];
Parallel.For(
0,
destinationSize,
i =>
{
float fu = ((i + .5f) * du) - 0.5f;
int startU = (int)Math.Ceiling(fu - ru);
if (startU < 0)
{
startU = 0;
}
int endU = (int)Math.Floor(fu + ru);
if (endU > sourceSize - 1)
{
endU = sourceSize - 1;
}
float sum = 0;
result[i] = new Weights();
List builder = new List();
for (int a = startU; a <= endU; a++)
{
float w = sampler.GetValue((a - fu) / scale);
if (w < 0 || w > 0)
{
sum += w;
builder.Add(new Weight(a, w));
}
}
// Normalise the values
if (Math.Abs(sum) > 0.00001f)
{
builder.ForEach(w => w.Value /= sum);
}
result[i].Values = builder.ToArray();
result[i].Sum = sum;
});
return result;
}
///
/// Represents the weight to be added to a scaled pixel.
///
protected class Weight
{
///
/// The pixel index.
///
public readonly int Index;
///
/// Initializes a new instance of the class.
///
/// The index.
/// The value.
public Weight(int index, float value)
{
this.Index = index;
this.Value = value;
}
///
/// Gets or sets the result of the interpolation algorithm.
///
public float Value { get; set; }
}
///
/// Represents a collection of weights and their sum.
///
protected class Weights
{
///
/// Gets or sets the values.
///
public Weight[] Values { get; set; }
///
/// Gets or sets the sum.
///
public float Sum { get; set; }
}
}
}