/// <inheritdoc/> protected override void OnFrameApply(ImageFrame <TPixel> source) { MemoryAllocator memoryAllocator = this.Configuration.MemoryAllocator; int numberOfPixels = source.Width * source.Height; var workingRect = new Rectangle(0, 0, source.Width, source.Height); using (IMemoryOwner <int> histogramBuffer = memoryAllocator.Allocate <int>(this.LuminanceLevels, AllocationOptions.Clean)) using (IMemoryOwner <int> cdfBuffer = memoryAllocator.Allocate <int>(this.LuminanceLevels, AllocationOptions.Clean)) { // Build the histogram of the grayscale levels. ParallelHelper.IterateRows( workingRect, this.Configuration, rows => { ref int histogramBase = ref MemoryMarshal.GetReference(histogramBuffer.GetSpan()); for (int y = rows.Min; y < rows.Max; y++) { ref TPixel pixelBase = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y)); for (int x = 0; x < workingRect.Width; x++) { int luminance = GetLuminance(Unsafe.Add(ref pixelBase, x), this.LuminanceLevels); Unsafe.Add(ref histogramBase, luminance)++; } } });
/// <inheritdoc/> protected override void OnFrameApply(ImageFrame <TPixelDst> source, Rectangle sourceRectangle, Configuration configuration) { Image <TPixelSrc> targetImage = this.Image; PixelBlender <TPixelDst> blender = this.Blender; int locationY = this.Location.Y; // Align start/end positions. Rectangle bounds = targetImage.Bounds(); int minX = Math.Max(this.Location.X, sourceRectangle.X); int maxX = Math.Min(this.Location.X + bounds.Width, sourceRectangle.Width); int targetX = minX - this.Location.X; int minY = Math.Max(this.Location.Y, sourceRectangle.Y); int maxY = Math.Min(this.Location.Y + bounds.Height, sourceRectangle.Bottom); int width = maxX - minX; var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); ParallelHelper.IterateRows( workingRect, configuration, rows => { for (int y = rows.Min; y < rows.Max; y++) { Span <TPixelDst> background = source.GetPixelRowSpan(y).Slice(minX, width); Span <TPixelSrc> foreground = targetImage.GetPixelRowSpan(y - locationY).Slice(targetX, width); blender.Blend <TPixelSrc>(configuration, background, background, foreground, this.Opacity); } }); }
public void IterateRows_OverMinimumPixelsLimit_IntervalsAreCorrect( int maxDegreeOfParallelism, int minY, int maxY, int expectedStepLength, int expectedLastStepLength) { var parallelSettings = new ParallelExecutionSettings( maxDegreeOfParallelism, 1, Configuration.Default.MemoryAllocator); var rectangle = new Rectangle(0, minY, 10, maxY - minY); int actualNumberOfSteps = 0; ParallelHelper.IterateRows( rectangle, parallelSettings, rows => { Assert.True(rows.Min >= minY); Assert.True(rows.Max <= maxY); int step = rows.Max - rows.Min; int expected = rows.Max < maxY ? expectedStepLength : expectedLastStepLength; Interlocked.Increment(ref actualNumberOfSteps); Assert.Equal(expected, step); }); Assert.Equal(maxDegreeOfParallelism, actualNumberOfSteps); }
public void IterateRows_WithEffectiveMinimumPixelsLimit( int maxDegreeOfParallelism, int minimumPixelsProcessedPerTask, int width, int height, int expectedNumberOfSteps, int expectedStepLength, int expectedLastStepLength) { var parallelSettings = new ParallelExecutionSettings( maxDegreeOfParallelism, minimumPixelsProcessedPerTask, Configuration.Default.MemoryAllocator); var rectangle = new Rectangle(0, 0, width, height); int actualNumberOfSteps = 0; ParallelHelper.IterateRows( rectangle, parallelSettings, rows => { Assert.True(rows.Min >= 0); Assert.True(rows.Max <= height); int step = rows.Max - rows.Min; int expected = rows.Max < height ? expectedStepLength : expectedLastStepLength; Interlocked.Increment(ref actualNumberOfSteps); Assert.Equal(expected, step); }); Assert.Equal(expectedNumberOfSteps, actualNumberOfSteps); }
/// <summary> /// Rotates the image 90 degrees clockwise at the centre point. /// </summary> /// <param name="source">The source image.</param> /// <param name="destination">The destination image.</param> /// <param name="configuration">The configuration.</param> private void Rotate90(ImageFrame <TPixel> source, ImageFrame <TPixel> destination, Configuration configuration) { int width = source.Width; int height = source.Height; Rectangle destinationBounds = destination.Bounds(); ParallelHelper.IterateRows( source.Bounds(), configuration, rows => { for (int y = rows.Min; y < rows.Max; y++) { Span <TPixel> sourceRow = source.GetPixelRowSpan(y); int newX = height - y - 1; for (int x = 0; x < width; x++) { // TODO: Optimize this: if (destinationBounds.Contains(newX, x)) { destination[newX, x] = sourceRow[x]; } } } }); }
public void IterateRows_OverMinimumPixelsLimit_ShouldVisitAllRows( int maxDegreeOfParallelism, int minY, int maxY, int expectedStepLength, int expectedLastStepLength) { var parallelSettings = new ParallelExecutionSettings( maxDegreeOfParallelism, 1, Configuration.Default.MemoryAllocator); var rectangle = new Rectangle(0, minY, 10, maxY - minY); int[] expectedData = Enumerable.Repeat(0, minY).Concat(Enumerable.Range(minY, maxY - minY)).ToArray(); var actualData = new int[maxY]; ParallelHelper.IterateRows( rectangle, parallelSettings, rows => { for (int y = rows.Min; y < rows.Max; y++) { actualData[y] = y; } }); Assert.Equal(expectedData, actualData); }
/// <inheritdoc/> protected override void OnFrameApply(ImageFrame <TPixel> source, ImageFrame <TPixel> destination, Rectangle sourceRectangle, Configuration configuration) { // Handle resize dimensions identical to the original if (source.Width == destination.Width && source.Height == destination.Height && sourceRectangle == this.CropRectangle) { // the cloned will be blank here copy all the pixel data over source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); return; } var rect = Rectangle.Intersect(this.CropRectangle, sourceRectangle); // Copying is cheap, we should process more pixels per task: ParallelExecutionSettings parallelSettings = configuration.GetParallelSettings().MultiplyMinimumPixelsPerTask(4); ParallelHelper.IterateRows( rect, parallelSettings, rows => { for (int y = rows.Min; y < rows.Max; y++) { Span <TPixel> sourceRow = source.GetPixelRowSpan(y).Slice(rect.Left); Span <TPixel> targetRow = destination.GetPixelRowSpan(y - rect.Top); sourceRow.Slice(0, rect.Width).CopyTo(targetRow); } }); }
/// <inheritdoc/> protected override void OnFrameApply(ImageFrame <TPixel> source) { int brushSize = this.definition.BrushSize; if (brushSize <= 0 || brushSize > source.Height || brushSize > source.Width) { throw new ArgumentOutOfRangeException(nameof(brushSize)); } int startY = this.SourceRectangle.Y; int endY = this.SourceRectangle.Bottom; int startX = this.SourceRectangle.X; int endX = this.SourceRectangle.Right; int maxY = endY - 1; int maxX = endX - 1; int radius = brushSize >> 1; int levels = this.definition.Levels; int rowWidth = source.Width; int rectangleWidth = this.SourceRectangle.Width; Configuration configuration = this.Configuration; using Buffer2D <TPixel> targetPixels = this.Configuration.MemoryAllocator.Allocate2D <TPixel>(source.Size()); source.CopyTo(targetPixels); var workingRect = Rectangle.FromLTRB(startX, startY, endX, endY); ParallelHelper.IterateRows( workingRect, this.Configuration, (rows) => { /* Allocate the two temporary Vector4 buffers, one for the source row and one for the target row. * The ParallelHelper.IterateRowsWithTempBuffers overload is not used in this case because * the two allocated buffers have a length equal to the width of the source image, * and not just equal to the width of the target rectangle to process. * Furthermore, there are two buffers being allocated in this case, so using that overload would * have still required the explicit allocation of the secondary buffer. * Similarly, one temporary float buffer is also allocated from the pool, and that is used * to create the target bins for all the color channels being processed. * This buffer is only rented once outside of the main processing loop, and its contents * are cleared for each loop iteration, to avoid the repeated allocation for each processed pixel. */ using (IMemoryOwner <Vector4> sourceRowBuffer = configuration.MemoryAllocator.Allocate <Vector4>(rowWidth)) using (IMemoryOwner <Vector4> targetRowBuffer = configuration.MemoryAllocator.Allocate <Vector4>(rowWidth)) using (IMemoryOwner <float> bins = configuration.MemoryAllocator.Allocate <float>(levels * 4)) { Span <Vector4> sourceRowVector4Span = sourceRowBuffer.Memory.Span; Span <Vector4> sourceRowAreaVector4Span = sourceRowVector4Span.Slice(startX, rectangleWidth); Span <Vector4> targetRowVector4Span = targetRowBuffer.Memory.Span; Span <Vector4> targetRowAreaVector4Span = targetRowVector4Span.Slice(startX, rectangleWidth); ref float binsRef = ref bins.GetReference(); ref int intensityBinRef = ref Unsafe.As <float, int>(ref binsRef); ref float redBinRef = ref Unsafe.Add(ref binsRef, levels);
public void IterateRowsRequiresValidRectangle(int width, int height) { var parallelSettings = default(ParallelExecutionSettings); var rect = new Rectangle(0, 0, width, height); ArgumentOutOfRangeException ex = Assert.Throws <ArgumentOutOfRangeException>( () => ParallelHelper.IterateRows(rect, parallelSettings, rows => { })); Assert.Contains(width <= 0 ? "Width" : "Height", ex.Message); }
/// <summary> /// Swaps the image at the Y-axis, which goes vertically through the middle at half of the width of the image. /// </summary> /// <param name="source">The source image to apply the process to.</param> /// <param name="configuration">The configuration.</param> private void FlipY(ImageFrame <TPixel> source, Configuration configuration) { ParallelHelper.IterateRows( source.Bounds(), configuration, rows => { for (int y = rows.Min; y < rows.Max; y++) { source.GetPixelRowSpan(y).Reverse(); } }); }
public void IterateRectangularBuffer( int maxDegreeOfParallelism, int bufferWidth, int bufferHeight, int rectX, int rectY, int rectWidth, int rectHeight) { MemoryAllocator memoryAllocator = Configuration.Default.MemoryAllocator; using (Buffer2D <Point> expected = memoryAllocator.Allocate2D <Point>(bufferWidth, bufferHeight, AllocationOptions.Clean)) using (Buffer2D <Point> actual = memoryAllocator.Allocate2D <Point>(bufferWidth, bufferHeight, AllocationOptions.Clean)) { var rect = new Rectangle(rectX, rectY, rectWidth, rectHeight); void FillRow(int y, Buffer2D <Point> buffer) { for (int x = rect.Left; x < rect.Right; x++) { buffer[x, y] = new Point(x, y); } } // Fill Expected data: for (int y = rectY; y < rect.Bottom; y++) { FillRow(y, expected); } // Fill actual data using IterateRows: var settings = new ParallelExecutionSettings(maxDegreeOfParallelism, memoryAllocator); ParallelHelper.IterateRows( rect, settings, rows => { this.output.WriteLine(rows.ToString()); for (int y = rows.Min; y < rows.Max; y++) { FillRow(y, actual); } }); // Assert: TestImageExtensions.CompareBuffers(expected.GetSpan(), actual.GetSpan()); } }
/// <inheritdoc/> protected override void OnFrameApply( ImageFrame <TPixelBg> source, Rectangle sourceRectangle, Configuration configuration) { Image <TPixelFg> targetImage = this.Image; PixelBlender <TPixelBg> blender = this.Blender; int locationY = this.Location.Y; // Align start/end positions. Rectangle bounds = targetImage.Bounds(); int minX = Math.Max(this.Location.X, sourceRectangle.X); int maxX = Math.Min(this.Location.X + bounds.Width, sourceRectangle.Right); int targetX = minX - this.Location.X; int minY = Math.Max(this.Location.Y, sourceRectangle.Y); int maxY = Math.Min(this.Location.Y + bounds.Height, sourceRectangle.Bottom); int width = maxX - minX; var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); // not a valid operation because rectangle does not overlap with this image. if (workingRect.Width <= 0 || workingRect.Height <= 0) { throw new ImageProcessingException( "Cannot draw image because the source image does not overlap the target image."); } ParallelHelper.IterateRows( workingRect, configuration, rows => { for (int y = rows.Min; y < rows.Max; y++) { Span <TPixelBg> background = source.GetPixelRowSpan(y).Slice(minX, width); Span <TPixelFg> foreground = targetImage.GetPixelRowSpan(y - locationY).Slice(targetX, width); blender.Blend <TPixelFg>(configuration, background, background, foreground, this.Opacity); } }); }
/// <inheritdoc/> protected override void OnFrameApply( ImageFrame <TPixel> source, Rectangle sourceRectangle, Configuration configuration) { float threshold = this.Threshold * 255F; TPixel upper = this.UpperColor; TPixel lower = this.LowerColor; var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); int startY = interest.Y; int endY = interest.Bottom; int startX = interest.X; int endX = interest.Right; bool isAlphaOnly = typeof(TPixel) == typeof(Alpha8); var workingRect = Rectangle.FromLTRB(startX, startY, endX, endY); ParallelHelper.IterateRows( workingRect, configuration, rows => { for (int y = rows.Min; y < rows.Max; y++) { Span <TPixel> row = source.GetPixelRowSpan(y); Rgba32 rgba = default; for (int x = startX; x < endX; x++) { ref TPixel color = ref row[x]; color.ToRgba32(ref rgba); // Convert to grayscale using ITU-R Recommendation BT.709 if required float luminance = isAlphaOnly ? rgba.A : (.2126F * rgba.R) + (.7152F * rgba.G) + (.0722F * rgba.B); color = luminance >= threshold ? upper : lower; } } }); }
/// <inheritdoc/> protected override void OnFrameApply(ImageFrame <TPixel> source) { byte threshold = (byte)MathF.Round(this.definition.Threshold * 255F); TPixel upper = this.definition.UpperColor.ToPixel <TPixel>(); TPixel lower = this.definition.LowerColor.ToPixel <TPixel>(); Rectangle sourceRectangle = this.SourceRectangle; Configuration configuration = this.Configuration; var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); int startY = interest.Y; int endY = interest.Bottom; int startX = interest.X; int endX = interest.Right; bool isAlphaOnly = typeof(TPixel) == typeof(Alpha8); var workingRect = Rectangle.FromLTRB(startX, startY, endX, endY); ParallelHelper.IterateRows( workingRect, configuration, rows => { Rgba32 rgba = default; for (int y = rows.Min; y < rows.Max; y++) { Span <TPixel> row = source.GetPixelRowSpan(y); for (int x = startX; x < endX; x++) { ref TPixel color = ref row[x]; color.ToRgba32(ref rgba); // Convert to grayscale using ITU-R Recommendation BT.709 if required byte luminance = isAlphaOnly ? rgba.A : ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B); color = luminance >= threshold ? upper : lower; } } }); }
/// <summary> /// Rotates the image 180 degrees clockwise at the centre point. /// </summary> /// <param name="source">The source image.</param> /// <param name="destination">The destination image.</param> /// <param name="configuration">The configuration.</param> private void Rotate180(ImageFrame <TPixel> source, ImageFrame <TPixel> destination, Configuration configuration) { int width = source.Width; int height = source.Height; ParallelHelper.IterateRows( source.Bounds(), configuration, rows => { for (int y = rows.Min; y < rows.Max; y++) { Span <TPixel> sourceRow = source.GetPixelRowSpan(y); Span <TPixel> targetRow = destination.GetPixelRowSpan(height - y - 1); for (int x = 0; x < width; x++) { targetRow[width - x - 1] = sourceRow[x]; } } }); }
/// <inheritdoc/> protected override void OnFrameApply(ImageFrame <TPixel> source, Rectangle sourceRectangle, Configuration configuration) { var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); Matrix4x4 matrix = this.Matrix; ParallelHelper.IterateRows( interest, configuration, rows => { for (int y = rows.Min; y < rows.Max; y++) { Span <TPixel> row = source.GetPixelRowSpan(y); for (int x = interest.X; x < interest.Right; x++) { ref TPixel pixel = ref row[x]; var vector = Vector4.Transform(pixel.ToVector4(), matrix); pixel.PackFromVector4(vector); } } }); }
/// <inheritdoc/> protected override void OnFrameApply(ImageFrame <TPixel> source, ImageFrame <TPixel> destination, Rectangle sourceRectangle, Configuration configuration) { // Handle resize dimensions identical to the original if (source.Width == destination.Width && source.Height == destination.Height && sourceRectangle == this.ResizeRectangle) { // The cloned will be blank here copy all the pixel data over source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); return; } int width = this.Width; int height = this.Height; int sourceX = sourceRectangle.X; int sourceY = sourceRectangle.Y; int startY = this.ResizeRectangle.Y; int endY = this.ResizeRectangle.Bottom; int startX = this.ResizeRectangle.X; int endX = this.ResizeRectangle.Right; int minX = Math.Max(0, startX); int maxX = Math.Min(width, endX); int minY = Math.Max(0, startY); int maxY = Math.Min(height, endY); if (this.Sampler is NearestNeighborResampler) { var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); // Scaling factors float widthFactor = sourceRectangle.Width / (float)this.ResizeRectangle.Width; float heightFactor = sourceRectangle.Height / (float)this.ResizeRectangle.Height; ParallelHelper.IterateRows( workingRect, configuration, rows => { for (int y = rows.Min; y < rows.Max; y++) { // Y coordinates of source points Span <TPixel> sourceRow = source.GetPixelRowSpan((int)(((y - startY) * heightFactor) + sourceY)); Span <TPixel> targetRow = destination.GetPixelRowSpan(y); for (int x = minX; x < maxX; x++) { // X coordinates of source points targetRow[x] = sourceRow[(int)(((x - startX) * widthFactor) + sourceX)]; } } }); return; } int sourceHeight = source.Height; PixelConversionModifiers conversionModifiers = PixelConversionModifiers.Premultiply; if (this.Compand) { conversionModifiers |= PixelConversionModifiers.Scale | PixelConversionModifiers.SRgbCompand; } // 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. Since we are not using multiple threads startY and endY // are the upper and lower bounds of the source rectangle. using (Buffer2D <Vector4> firstPassPixelsTransposed = source.MemoryAllocator.Allocate2D <Vector4>(sourceHeight, width)) { firstPassPixelsTransposed.MemorySource.Clear(); var processColsRect = new Rectangle(0, 0, source.Width, sourceRectangle.Bottom); ParallelHelper.IterateRowsWithTempBuffer <Vector4>( processColsRect, configuration, (rows, tempRowBuffer) => { for (int y = rows.Min; y < rows.Max; y++) { Span <TPixel> sourceRow = source.GetPixelRowSpan(y).Slice(sourceX); Span <Vector4> tempRowSpan = tempRowBuffer.Span.Slice(sourceX); PixelOperations <TPixel> .Instance.ToVector4(configuration, sourceRow, tempRowSpan, conversionModifiers); ref Vector4 firstPassBaseRef = ref firstPassPixelsTransposed.Span[y]; for (int x = minX; x < maxX; x++) { ResizeKernel kernel = this.horizontalKernelMap.GetKernel(x - startX); Unsafe.Add(ref firstPassBaseRef, x * sourceHeight) = kernel.Convolve(tempRowSpan); } } }); var processRowsRect = Rectangle.FromLTRB(0, minY, width, maxY); // Now process the rows. ParallelHelper.IterateRowsWithTempBuffer <Vector4>( processRowsRect, configuration, (rows, tempRowBuffer) => { Span <Vector4> tempRowSpan = tempRowBuffer.Span; for (int y = rows.Min; y < rows.Max; y++) { // Ensure offsets are normalized for cropping and padding. ResizeKernel kernel = this.verticalKernelMap.GetKernel(y - startY); ref Vector4 tempRowBase = ref MemoryMarshal.GetReference(tempRowSpan); for (int x = 0; x < width; x++) { Span <Vector4> firstPassColumn = firstPassPixelsTransposed.GetRowSpan(x).Slice(sourceY); // Destination color components Unsafe.Add(ref tempRowBase, x) = kernel.Convolve(firstPassColumn); } Span <TPixel> targetRowSpan = destination.GetPixelRowSpan(y); PixelOperations <TPixel> .Instance.FromVector4Destructive(configuration, tempRowSpan, targetRowSpan, conversionModifiers); } }); } }
/// <inheritdoc/> protected override void OnFrameApply( ImageFrame <TPixel> source, ImageFrame <TPixel> destination, Rectangle sourceRectangle, Configuration configuration) { int height = this.TargetDimensions.Height; int width = this.TargetDimensions.Width; Rectangle sourceBounds = source.Bounds(); var targetBounds = new Rectangle(0, 0, width, height); // Since could potentially be resizing the canvas we might need to re-calculate the matrix Matrix3x2 matrix = this.GetProcessingMatrix(sourceBounds, targetBounds); // Convert from screen to world space. Matrix3x2.Invert(matrix, out matrix); if (this.Sampler is NearestNeighborResampler) { ParallelHelper.IterateRows( targetBounds, configuration, rows => { for (int y = rows.Min; y < rows.Max; y++) { Span <TPixel> destRow = destination.GetPixelRowSpan(y); for (int x = 0; x < width; x++) { var point = Point.Transform(new Point(x, y), matrix); if (sourceBounds.Contains(point.X, point.Y)) { destRow[x] = source[point.X, point.Y]; } } } }); return; } int maxSourceX = source.Width - 1; int maxSourceY = source.Height - 1; (float radius, float scale, float ratio)xRadiusScale = this.GetSamplingRadius(source.Width, destination.Width); (float radius, float scale, float ratio)yRadiusScale = this.GetSamplingRadius(source.Height, destination.Height); float xScale = xRadiusScale.scale; float yScale = yRadiusScale.scale; var radius = new Vector2(xRadiusScale.radius, yRadiusScale.radius); IResampler sampler = this.Sampler; var maxSource = new Vector4(maxSourceX, maxSourceY, maxSourceX, maxSourceY); int xLength = (int)MathF.Ceiling((radius.X * 2) + 2); int yLength = (int)MathF.Ceiling((radius.Y * 2) + 2); MemoryAllocator memoryAllocator = configuration.MemoryAllocator; using (Buffer2D <float> yBuffer = memoryAllocator.Allocate2D <float>(yLength, height)) using (Buffer2D <float> xBuffer = memoryAllocator.Allocate2D <float>(xLength, height)) { ParallelHelper.IterateRows( targetBounds, configuration, rows => { for (int y = rows.Min; y < rows.Max; y++) { ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y)); ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y)); ref float xSpanRef = ref MemoryMarshal.GetReference(xBuffer.GetRowSpan(y)); for (int x = 0; x < width; x++) { // Use the single precision position to calculate correct bounding pixels // otherwise we get rogue pixels outside of the bounds. var point = Vector2.Transform(new Vector2(x, y), matrix); // Clamp sampling pixel radial extents to the source image edges Vector2 maxXY = point + radius; Vector2 minXY = point - radius; // max, maxY, minX, minY var extents = new Vector4( MathF.Floor(maxXY.X + .5F), MathF.Floor(maxXY.Y + .5F), MathF.Ceiling(minXY.X - .5F), MathF.Ceiling(minXY.Y - .5F)); int right = (int)extents.X; int bottom = (int)extents.Y; int left = (int)extents.Z; int top = (int)extents.W; extents = Vector4.Clamp(extents, Vector4.Zero, maxSource); int maxX = (int)extents.X; int maxY = (int)extents.Y; int minX = (int)extents.Z; int minY = (int)extents.W; if (minX == maxX || minY == maxY) { continue; } // It appears these have to be calculated on-the-fly. // Precalculating transformed weights would require prior knowledge of every transformed pixel location // since they can be at sub-pixel positions on both axis. // I've optimized where I can but am always open to suggestions. if (yScale > 1 && xScale > 1) { CalculateWeightsDown( top, bottom, minY, maxY, point.Y, sampler, yScale, ref ySpanRef, yLength); CalculateWeightsDown( left, right, minX, maxX, point.X, sampler, xScale, ref xSpanRef, xLength); } else { CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef); CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef); } // Now multiply the results against the offsets Vector4 sum = Vector4.Zero; for (int yy = 0, j = minY; j <= maxY; j++, yy++) { float yWeight = Unsafe.Add(ref ySpanRef, yy); for (int xx = 0, i = minX; i <= maxX; i++, xx++) { float xWeight = Unsafe.Add(ref xSpanRef, xx); // Values are first premultiplied to prevent darkening of edge pixels var current = source[i, j].ToVector4(); Vector4Utils.Premultiply(ref current); sum += current * xWeight * yWeight; } } ref TPixel dest = ref Unsafe.Add(ref destRowRef, x); // Reverse the premultiplication Vector4Utils.UnPremultiply(ref sum); dest.FromVector4(sum); }
/// <inheritdoc/> protected override void OnFrameApply(ImageFrame <TPixel> source) { TPixel color = this.definition.Color.ToPixel <TPixel>(); GraphicsOptions graphicsOptions = this.definition.GraphicsOptions; int startY = this.SourceRectangle.Y; int endY = this.SourceRectangle.Bottom; int startX = this.SourceRectangle.X; int endX = this.SourceRectangle.Right; // Align start/end positions. int minX = Math.Max(0, startX); int maxX = Math.Min(source.Width, endX); int minY = Math.Max(0, startY); int maxY = Math.Min(source.Height, endY); // Reset offset if necessary. if (minX > 0) { startX = 0; } if (minY > 0) { startY = 0; } int width = maxX - minX; var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); using (IMemoryOwner <TPixel> colors = source.MemoryAllocator.Allocate <TPixel>(width)) using (IMemoryOwner <float> amount = source.MemoryAllocator.Allocate <float>(width)) { // Be careful! Do not capture colorSpan & amountSpan in the lambda below! Span <TPixel> colorSpan = colors.GetSpan(); Span <float> amountSpan = amount.GetSpan(); colorSpan.Fill(color); amountSpan.Fill(graphicsOptions.BlendPercentage); PixelBlender <TPixel> blender = PixelOperations <TPixel> .Instance.GetPixelBlender(graphicsOptions); ParallelHelper.IterateRows( workingRect, this.Configuration, rows => { for (int y = rows.Min; y < rows.Max; y++) { Span <TPixel> destination = source.GetPixelRowSpan(y - startY).Slice(minX - startX, width); // This switched color & destination in the 2nd and 3rd places because we are applying the target color under the current one blender.Blend( source.Configuration, destination, colors.GetSpan(), destination, amount.GetSpan()); } }); } }
/// <inheritdoc/> protected override void OnFrameApply( ImageFrame <TPixel> source, Rectangle sourceRectangle, Configuration configuration) { int kernelYHeight = this.KernelY.Rows; int kernelYWidth = this.KernelY.Columns; int kernelXHeight = this.KernelX.Rows; int kernelXWidth = this.KernelX.Columns; int radiusY = kernelYHeight >> 1; int radiusX = kernelXWidth >> 1; int startY = sourceRectangle.Y; int endY = sourceRectangle.Bottom; int startX = sourceRectangle.X; int endX = sourceRectangle.Right; int maxY = endY - 1; int maxX = endX - 1; using (Buffer2D <TPixel> targetPixels = configuration.MemoryAllocator.Allocate2D <TPixel>(source.Width, source.Height)) { source.CopyTo(targetPixels); var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); ParallelHelper.IterateRows( workingRectangle, configuration, rows => { for (int y = rows.Min; y < rows.Max; y++) { Span <TPixel> sourceRow = source.GetPixelRowSpan(y); Span <TPixel> targetRow = targetPixels.GetRowSpan(y); for (int x = startX; x < endX; x++) { float rX = 0; float gX = 0; float bX = 0; float rY = 0; float gY = 0; float bY = 0; // Apply each matrix multiplier to the color components for each pixel. for (int fy = 0; fy < kernelYHeight; fy++) { int fyr = fy - radiusY; int offsetY = y + fyr; offsetY = offsetY.Clamp(0, maxY); Span <TPixel> sourceOffsetRow = source.GetPixelRowSpan(offsetY); for (int fx = 0; fx < kernelXWidth; fx++) { int fxr = fx - radiusX; int offsetX = x + fxr; offsetX = offsetX.Clamp(0, maxX); Vector4 currentColor = sourceOffsetRow[offsetX].ToVector4().Premultiply(); if (fy < kernelXHeight) { Vector4 kx = this.KernelX[fy, fx] * currentColor; rX += kx.X; gX += kx.Y; bX += kx.Z; } if (fx < kernelYWidth) { Vector4 ky = this.KernelY[fy, fx] * currentColor; rY += ky.X; gY += ky.Y; bY += ky.Z; } } } float red = MathF.Sqrt((rX * rX) + (rY * rY)); float green = MathF.Sqrt((gX * gX) + (gY * gY)); float blue = MathF.Sqrt((bX * bX) + (bY * bY)); ref TPixel pixel = ref targetRow[x]; pixel.PackFromVector4( new Vector4(red, green, blue, sourceRow[x].ToVector4().W).UnPremultiply()); } } }); Buffer2D <TPixel> .SwapOrCopyContent(source.PixelBuffer, targetPixels); } }
/// <inheritdoc/> protected override void OnFrameApply(ImageFrame <TPixel> source) { Rectangle sourceRectangle = this.SourceRectangle; Configuration configuration = this.Configuration; int startX = sourceRectangle.X; int endX = sourceRectangle.Right; int startY = sourceRectangle.Y; int endY = sourceRectangle.Bottom; // Align start/end positions. int minX = Math.Max(0, startX); int maxX = Math.Min(source.Width, endX); int minY = Math.Max(0, startY); int maxY = Math.Min(source.Height, endY); int width = maxX - minX; var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); IBrush brush = this.definition.Brush; GraphicsOptions options = this.definition.Options; // If there's no reason for blending, then avoid it. if (this.IsSolidBrushWithoutBlending(out SolidBrush solidBrush)) { ParallelExecutionSettings parallelSettings = configuration.GetParallelSettings().MultiplyMinimumPixelsPerTask(4); TPixel colorPixel = solidBrush.Color.ToPixel <TPixel>(); ParallelHelper.IterateRows( workingRect, parallelSettings, rows => { for (int y = rows.Min; y < rows.Max; y++) { source.GetPixelRowSpan(y).Slice(minX, width).Fill(colorPixel); } }); } else { // Reset offset if necessary. if (minX > 0) { startX = 0; } if (minY > 0) { startY = 0; } using (IMemoryOwner <float> amount = source.MemoryAllocator.Allocate <float>(width)) using (BrushApplicator <TPixel> applicator = brush.CreateApplicator( source, sourceRectangle, options)) { amount.GetSpan().Fill(1f); ParallelHelper.IterateRows( workingRect, configuration, rows => { for (int y = rows.Min; y < rows.Max; y++) { int offsetY = y - startY; int offsetX = minX - startX; applicator.Apply(amount.GetSpan(), offsetX, offsetY); } }); } } }
/// <inheritdoc/> protected override void OnFrameApply(ImageFrame <TPixel> source, Rectangle sourceRectangle, Configuration configuration) { int kernelLength = this.KernelXY.Rows; int radius = kernelLength >> 1; int startY = sourceRectangle.Y; int endY = sourceRectangle.Bottom; int startX = sourceRectangle.X; int endX = sourceRectangle.Right; int maxY = endY - 1; int maxX = endX - 1; using (Buffer2D <TPixel> targetPixels = configuration.MemoryAllocator.Allocate2D <TPixel>(source.Size())) { source.CopyTo(targetPixels); var workingRect = Rectangle.FromLTRB(startX, startY, endX, endY); ParallelHelper.IterateRows( workingRect, configuration, rows => { for (int y = rows.Min; y < rows.Max; y++) { Span <TPixel> sourceRow = source.GetPixelRowSpan(y); Span <TPixel> targetRow = targetPixels.GetRowSpan(y); for (int x = startX; x < endX; x++) { float red = 0; float green = 0; float blue = 0; // Apply each matrix multiplier to the color components for each pixel. for (int fy = 0; fy < kernelLength; fy++) { int fyr = fy - radius; int offsetY = y + fyr; offsetY = offsetY.Clamp(0, maxY); Span <TPixel> sourceOffsetRow = source.GetPixelRowSpan(offsetY); for (int fx = 0; fx < kernelLength; fx++) { int fxr = fx - radius; int offsetX = x + fxr; offsetX = offsetX.Clamp(0, maxX); Vector4 currentColor = sourceOffsetRow[offsetX].ToVector4().Premultiply(); currentColor *= this.KernelXY[fy, fx]; red += currentColor.X; green += currentColor.Y; blue += currentColor.Z; } } ref TPixel pixel = ref targetRow[x]; pixel.PackFromVector4( new Vector4(red, green, blue, sourceRow[x].ToVector4().W).UnPremultiply()); } } }); Buffer2D <TPixel> .SwapOrCopyContent(source.PixelBuffer, targetPixels); } }
/// <inheritdoc /> protected override void OnFrameApply(ImageFrame <TPixel> source) { DenseMatrix <float>[] kernels = this.Kernels.Flatten(); int startY = this.SourceRectangle.Y; int endY = this.SourceRectangle.Bottom; int startX = this.SourceRectangle.X; int endX = this.SourceRectangle.Right; // Align start/end positions. int minX = Math.Max(0, startX); int maxX = Math.Min(source.Width, endX); int minY = Math.Max(0, startY); int maxY = Math.Min(source.Height, endY); // we need a clean copy for each pass to start from using (ImageFrame <TPixel> cleanCopy = source.Clone()) { using (var processor = new ConvolutionProcessor <TPixel>(this.Configuration, kernels[0], true, this.Source, this.SourceRectangle)) { processor.Apply(source); } if (kernels.Length == 1) { return; } int shiftY = startY; int shiftX = startX; // Reset offset if necessary. if (minX > 0) { shiftX = 0; } if (minY > 0) { shiftY = 0; } var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); // Additional runs. // ReSharper disable once ForCanBeConvertedToForeach for (int i = 1; i < kernels.Length; i++) { using (ImageFrame <TPixel> pass = cleanCopy.Clone()) { using (var processor = new ConvolutionProcessor <TPixel>(this.Configuration, kernels[i], true, this.Source, this.SourceRectangle)) { processor.Apply(pass); } Buffer2D <TPixel> passPixels = pass.PixelBuffer; Buffer2D <TPixel> targetPixels = source.PixelBuffer; ParallelHelper.IterateRows( workingRect, this.Configuration, rows => { for (int y = rows.Min; y < rows.Max; y++) { int offsetY = y - shiftY; ref TPixel passPixelsBase = ref MemoryMarshal.GetReference(passPixels.GetRowSpan(offsetY)); ref TPixel targetPixelsBase = ref MemoryMarshal.GetReference(targetPixels.GetRowSpan(offsetY)); for (int x = minX; x < maxX; x++) { int offsetX = x - shiftX; // Grab the max components of the two pixels ref TPixel currentPassPixel = ref Unsafe.Add(ref passPixelsBase, offsetX); ref TPixel currentTargetPixel = ref Unsafe.Add(ref targetPixelsBase, offsetX); var pixelValue = Vector4.Max( currentPassPixel.ToVector4(), currentTargetPixel.ToVector4()); currentTargetPixel.FromVector4(pixelValue); } } });
/// <inheritdoc/> protected override void OnFrameApply( ImageFrame <TPixel> source, Rectangle sourceRectangle, Configuration configuration) { int startY = sourceRectangle.Y; int endY = sourceRectangle.Bottom; int startX = sourceRectangle.X; int endX = sourceRectangle.Right; TPixel glowColor = this.GlowColor; Vector2 centre = Rectangle.Center(sourceRectangle); float maxDistance = this.Radius > 0 ? Math.Min(this.Radius, sourceRectangle.Width * .5F) : sourceRectangle.Width * .5F; // Align start/end positions. int minX = Math.Max(0, startX); int maxX = Math.Min(source.Width, endX); int minY = Math.Max(0, startY); int maxY = Math.Min(source.Height, endY); // Reset offset if necessary. if (minX > 0) { startX = 0; } if (minY > 0) { startY = 0; } int width = maxX - minX; using (IMemoryOwner <TPixel> rowColors = Configuration.Default.MemoryAllocator.Allocate <TPixel>(width)) { Buffer2D <TPixel> sourcePixels = source.PixelBuffer; rowColors.GetSpan().Fill(glowColor); var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); ParallelHelper.IterateRows( workingRect, configuration, rows => { for (int y = rows.Min; y < rows.Max; y++) { int offsetY = y - startY; for (int x = minX; x < maxX; x++) { int offsetX = x - startX; float distance = Vector2.Distance(centre, new Vector2(offsetX, offsetY)); Vector4 sourceColor = sourcePixels[offsetX, offsetY].ToVector4(); TPixel packed = default; packed.FromVector4( PremultipliedLerp( sourceColor, glowColor.ToVector4(), 1 - (.95F * (distance / maxDistance)))); sourcePixels[offsetX, offsetY] = packed; } } }); } }
/// <inheritdoc/> protected override void OnFrameApply(ImageFrame <TPixel> source, ImageFrame <TPixel> destination) { Rectangle sourceRectangle = this.SourceRectangle; Configuration configuration = this.Configuration; // Handle resize dimensions identical to the original if (source.Width == destination.Width && source.Height == destination.Height && sourceRectangle == this.targetRectangle) { // The cloned will be blank here copy all the pixel data over source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); return; } int width = this.targetWidth; int height = this.targetHeight; int sourceX = sourceRectangle.X; int sourceY = sourceRectangle.Y; int startY = this.targetRectangle.Y; int startX = this.targetRectangle.X; var targetWorkingRect = Rectangle.Intersect( this.targetRectangle, new Rectangle(0, 0, width, height)); if (this.resampler is NearestNeighborResampler) { // Scaling factors float widthFactor = sourceRectangle.Width / (float)this.targetRectangle.Width; float heightFactor = sourceRectangle.Height / (float)this.targetRectangle.Height; ParallelHelper.IterateRows( targetWorkingRect, configuration, rows => { for (int y = rows.Min; y < rows.Max; y++) { // Y coordinates of source points Span <TPixel> sourceRow = source.GetPixelRowSpan((int)(((y - startY) * heightFactor) + sourceY)); Span <TPixel> targetRow = destination.GetPixelRowSpan(y); for (int x = targetWorkingRect.Left; x < targetWorkingRect.Right; x++) { // X coordinates of source points targetRow[x] = sourceRow[(int)(((x - startX) * widthFactor) + sourceX)]; } } }); return; } PixelConversionModifiers conversionModifiers = PixelConversionModifiers.Premultiply.ApplyCompanding(this.compand); BufferArea <TPixel> sourceArea = source.PixelBuffer.GetArea(sourceRectangle); // To reintroduce parallel processing, we to launch multiple workers // for different row intervals of the image. using (var worker = new ResizeWorker <TPixel>( configuration, sourceArea, conversionModifiers, this.horizontalKernelMap, this.verticalKernelMap, width, targetWorkingRect, this.targetRectangle.Location)) { worker.Initialize(); var workingInterval = new RowInterval(targetWorkingRect.Top, targetWorkingRect.Bottom); worker.FillDestinationPixels(workingInterval, destination.PixelBuffer); } }
/// <inheritdoc/> protected override void OnFrameApply( ImageFrame <TPixel> source, Rectangle sourceRectangle, Configuration configuration) { int brushSize = this.definition.BrushSize; if (brushSize <= 0 || brushSize > source.Height || brushSize > source.Width) { throw new ArgumentOutOfRangeException(nameof(brushSize)); } int startY = sourceRectangle.Y; int endY = sourceRectangle.Bottom; int startX = sourceRectangle.X; int endX = sourceRectangle.Right; int maxY = endY - 1; int maxX = endX - 1; int radius = brushSize >> 1; int levels = this.definition.Levels; using (Buffer2D <TPixel> targetPixels = configuration.MemoryAllocator.Allocate2D <TPixel>(source.Size())) { source.CopyTo(targetPixels); var workingRect = Rectangle.FromLTRB(startX, startY, endX, endY); ParallelHelper.IterateRows( workingRect, configuration, rows => { for (int y = rows.Min; y < rows.Max; y++) { Span <TPixel> sourceRow = source.GetPixelRowSpan(y); Span <TPixel> targetRow = targetPixels.GetRowSpan(y); for (int x = startX; x < endX; x++) { int maxIntensity = 0; int maxIndex = 0; int[] intensityBin = new int[levels]; float[] redBin = new float[levels]; float[] blueBin = new float[levels]; float[] greenBin = new float[levels]; for (int fy = 0; fy <= radius; fy++) { int fyr = fy - radius; int offsetY = y + fyr; offsetY = offsetY.Clamp(0, maxY); Span <TPixel> sourceOffsetRow = source.GetPixelRowSpan(offsetY); for (int fx = 0; fx <= radius; fx++) { int fxr = fx - radius; int offsetX = x + fxr; offsetX = offsetX.Clamp(0, maxX); var vector = sourceOffsetRow[offsetX].ToVector4(); float sourceRed = vector.X; float sourceBlue = vector.Z; float sourceGreen = vector.Y; int currentIntensity = (int)MathF.Round( (sourceBlue + sourceGreen + sourceRed) / 3F * (levels - 1)); intensityBin[currentIntensity]++; blueBin[currentIntensity] += sourceBlue; greenBin[currentIntensity] += sourceGreen; redBin[currentIntensity] += sourceRed; if (intensityBin[currentIntensity] > maxIntensity) { maxIntensity = intensityBin[currentIntensity]; maxIndex = currentIntensity; } } float red = MathF.Abs(redBin[maxIndex] / maxIntensity); float green = MathF.Abs(greenBin[maxIndex] / maxIntensity); float blue = MathF.Abs(blueBin[maxIndex] / maxIntensity); ref TPixel pixel = ref targetRow[x]; pixel.FromVector4( new Vector4(red, green, blue, sourceRow[x].ToVector4().W)); } } } }); Buffer2D <TPixel> .SwapOrCopyContent(source.PixelBuffer, targetPixels); } }
/// <summary> /// Applies the process to the specified portion of the specified <see cref="ImageFrame{TPixel}"/> at the specified location /// and with the specified size. /// </summary> /// <param name="targetPixels">The target pixels to apply the process to.</param> /// <param name="sourcePixels">The source pixels. Cannot be null.</param> /// <param name="sourceRectangle"> /// The <see cref="Rectangle"/> structure that specifies the portion of the image object to draw. /// </param> /// <param name="kernel">The kernel operator.</param> /// <param name="configuration">The <see cref="Configuration"/></param> private void ApplyConvolution( Buffer2D <TPixel> targetPixels, Buffer2D <TPixel> sourcePixels, Rectangle sourceRectangle, DenseMatrix <float> kernel, // TODO: Can't use 'in' as pass by ref to lambda expression. Configuration configuration) { int kernelHeight = kernel.Rows; int kernelWidth = kernel.Columns; int radiusY = kernelHeight >> 1; int radiusX = kernelWidth >> 1; int startY = sourceRectangle.Y; int endY = sourceRectangle.Bottom; int startX = sourceRectangle.X; int endX = sourceRectangle.Right; int maxY = endY - 1; int maxX = endX - 1; var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); ParallelHelper.IterateRows( workingRectangle, configuration, rows => { for (int y = rows.Min; y < rows.Max; y++) { Span <TPixel> targetRow = targetPixels.GetRowSpan(y); for (int x = startX; x < endX; x++) { Vector4 destination = default; // Apply each matrix multiplier to the color components for each pixel. for (int fy = 0; fy < kernelHeight; fy++) { int fyr = fy - radiusY; int offsetY = y + fyr; offsetY = offsetY.Clamp(0, maxY); Span <TPixel> row = sourcePixels.GetRowSpan(offsetY); for (int fx = 0; fx < kernelWidth; fx++) { int fxr = fx - radiusX; int offsetX = x + fxr; offsetX = offsetX.Clamp(0, maxX); Vector4 currentColor = row[offsetX].ToVector4().Premultiply(); destination += kernel[fy, fx] * currentColor; } } ref TPixel pixel = ref targetRow[x]; pixel.PackFromVector4(destination.UnPremultiply()); } } }); }