// // 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; } } } }