diff --git a/src/ImageProcessorCore/Samplers/Resampler.cs b/src/ImageProcessorCore/Samplers/Resampler.cs index 4b7239af4..0554b18e0 100644 --- a/src/ImageProcessorCore/Samplers/Resampler.cs +++ b/src/ImageProcessorCore/Samplers/Resampler.cs @@ -11,6 +11,8 @@ namespace ImageProcessorCore.Samplers /// /// Provides methods that allow the resampling of images using various algorithms. + /// + /// /// public abstract class Resampler : ImageSampler { @@ -52,74 +54,171 @@ namespace ImageProcessorCore.Samplers /// protected Weights[] PrecomputeWeights(int destinationSize, int sourceSize) { + float xscale = destinationSize / (float)sourceSize; + float width; IResampler sampler = this.Sampler; - float ratio = sourceSize / (float)destinationSize; - float scale = ratio; + float fwidth = sampler.Radius; + float fscale; + double left; + double right; + double weight = 0; + int n = 0; + int k; - // When shrinking, broaden the effective kernel support so that we still + Weights[] result = new Weights[destinationSize]; + + // When expanding, broaden the effective kernel support so that we still // visit every source pixel. - if (scale < 1) + if (xscale < 0) { - scale = 1; - } + width = sampler.Radius / xscale; + fscale = 1 / xscale; - float scaledRadius = (float)Math.Ceiling(scale * sampler.Radius); - Weights[] result = new Weights[destinationSize]; - - // Make the weights slices, one source for each column or row. - Parallel.For( - 0, - destinationSize, - i => + // Make the weights slices, one source for each column or row. + for (int i = 0; i < destinationSize; i++) { - float center = ((i + .5f) * ratio) - 0.5f; - int start = (int)Math.Ceiling(center - scaledRadius); - - if (start < 0) - { - start = 0; - } - - int end = (int)Math.Floor(center + scaledRadius); - - if (end > sourceSize) + float centre = i / xscale; + left = Math.Ceiling(centre - width); + right = Math.Floor(centre + width); + float sum = 0; + result[i] = new Weights(); + List builder = new List(); + for (double j = left; j <= right; j++) { - end = sourceSize; - - if (end < start) + weight = centre - j; + weight = sampler.GetValue((float)weight / fscale) / fscale; + if (j < 0) + { + n = (int)-j; + } + else if (j >= sourceSize) + { + n = (int)((sourceSize - j) + sourceSize - 1); + } + else { - end = start; + n = (int)j; } + + sum++; + builder.Add(new Weight(n, (float)weight)); } + result[i].Values = builder.ToArray(); + result[i].Sum = sum; + } + } + else + { + // Make the weights slices, one source for each column or row. + for (int i = 0; i < destinationSize; i++) + { + float centre = i / xscale; + left = Math.Ceiling(centre - fwidth); + right = Math.Floor(centre + fwidth); float sum = 0; result[i] = new Weights(); List builder = new List(); - for (int a = start; a < end; a++) + for (double j = left; j <= right; j++) { - float w = sampler.GetValue((a - center) / scale); - - if (w < 0 || w > 0) + weight = centre - j; + weight = sampler.GetValue((float)weight); + if (j < 0) { - sum += w; - builder.Add(new Weight(a, w)); + n = (int)-j; + } + else if (j >= sourceSize) + { + n = (int)((sourceSize - j) + sourceSize - 1); + } + else + { + n = (int)j; } - } - // Normalise the values - if (sum > 0 || sum < 0) - { - builder.ForEach(w => w.Value /= sum); + sum++; + builder.Add(new Weight(n, (float)weight)); } result[i].Values = builder.ToArray(); result[i].Sum = sum; - }); + } + } return result; } + //protected Weights[] PrecomputeWeights(int destinationSize, int sourceSize) + //{ + // IResampler sampler = this.Sampler; + // float ratio = sourceSize / (float)destinationSize; + // float scale = ratio; + + // // When shrinking, broaden the effective kernel support so that we still + // // visit every source pixel. + // if (scale < 1) + // { + // scale = 1; + // } + + // float scaledRadius = (float)Math.Ceiling(scale * sampler.Radius); + // Weights[] result = new Weights[destinationSize]; + + // // Make the weights slices, one source for each column or row. + // Parallel.For( + // 0, + // destinationSize, + // i => + // { + // float center = ((i + .5f) * ratio) - 0.5f; + // int start = (int)Math.Ceiling(center - scaledRadius); + + // if (start < 0) + // { + // start = 0; + // } + + // int end = (int)Math.Floor(center + scaledRadius); + + // if (end > sourceSize) + // { + // end = sourceSize; + + // if (end < start) + // { + // end = start; + // } + // } + + // float sum = 0; + // result[i] = new Weights(); + + // List builder = new List(); + // for (int a = start; a < end; a++) + // { + // float w = sampler.GetValue((a - center) / scale); + + // if (w < 0 || w > 0) + // { + // sum += w; + // builder.Add(new Weight(a, w)); + // } + // } + + // // Normalise the values + // if (sum > 0 || sum < 0) + // { + // 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. /// diff --git a/src/ImageProcessorCore/Samplers/Resize.cs b/src/ImageProcessorCore/Samplers/Resize.cs index 03aad0ec8..e7de40c44 100644 --- a/src/ImageProcessorCore/Samplers/Resize.cs +++ b/src/ImageProcessorCore/Samplers/Resize.cs @@ -54,6 +54,7 @@ namespace ImageProcessorCore.Samplers int sourceBottom = source.Bounds.Bottom; int targetY = targetRectangle.Y; + int targetBottom = targetRectangle.Bottom; int startX = targetRectangle.X; int endX = targetRectangle.Right; bool compand = this.Compand; @@ -69,18 +70,20 @@ namespace ImageProcessorCore.Samplers endY, y => { - // Y coordinates of source points - int originY = (int)((y - targetY) * heightFactor); - - for (int x = startX; x < endX; x++) + if (y >= targetY && y < targetBottom) { - // X coordinates of source points - int originX = (int)((x - startX) * widthFactor); + // Y coordinates of source points + int originY = (int)((y - targetY) * heightFactor); - target[x, y] = source[originX, originY]; - } + for (int x = startX; x < endX; x++) + { + // X coordinates of source points + int originX = (int)((x - startX) * widthFactor); - this.OnRowProcessed(); + target[x, y] = source[originX, originY]; + } + this.OnRowProcessed(); + } }); // Break out now. @@ -97,13 +100,15 @@ namespace ImageProcessorCore.Samplers { for (int x = startX; x < endX; x++) { + float sum = this.HorizontalWeights[x].Sum; Weight[] horizontalValues = this.HorizontalWeights[x].Values; // Destination color components Color destination = new Color(); - foreach (Weight xw in horizontalValues) + for (int i = 0; i < sum; i++) { + Weight xw = horizontalValues[i]; int originX = xw.Index; Color sourceColor = compand ? Color.Expand(source[originX, y]) : source[originX, y]; destination += sourceColor * xw.Value; @@ -124,30 +129,34 @@ namespace ImageProcessorCore.Samplers endY, y => { - Weight[] verticalValues = this.VerticalWeights[y].Values; - - for (int x = startX; x < endX; x++) + if (y >= targetY && y < targetBottom) { - // Destination color components - Color destination = new Color(); + float sum = this.VerticalWeights[y].Sum; + Weight[] verticalValues = this.VerticalWeights[y].Values; - foreach (Weight yw in verticalValues) + for (int x = startX; x < endX; x++) { - int originY = yw.Index; - int originX = x; - Color sourceColor = compand ? Color.Expand(this.firstPass[originX, originY]) : this.firstPass[originX, originY]; - destination += sourceColor * yw.Value; + // Destination color components + Color destination = new Color(); + + for (int i = 0; i < sum; i++) + { + Weight yw = verticalValues[i]; + int originY = yw.Index; + Color sourceColor = compand ? Color.Expand(this.firstPass[x, originY]) : this.firstPass[x, originY]; + destination += sourceColor * yw.Value; + } + + if (compand) + { + destination = Color.Compress(destination); + } + + target[x, y] = destination; } - if (compand) - { - destination = Color.Compress(destination); - } - - target[x, y] = destination; + this.OnRowProcessed(); } - - this.OnRowProcessed(); }); } @@ -159,8 +168,6 @@ namespace ImageProcessorCore.Samplers { target.ClonePixels(target.Width, target.Height, source.Pixels); } - - this.firstPass?.Dispose(); } } } \ No newline at end of file diff --git a/tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs b/tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs index 2e9eef9cf..df1914c3e 100644 --- a/tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs +++ b/tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs @@ -15,13 +15,13 @@ { { "Bicubic", new BicubicResampler() }, { "Triangle", new TriangleResampler() }, + { "NearestNeighbor", new NearestNeighborResampler() }, // Perf: Enable for local testing only //{ "Box", new BoxResampler() }, //{ "Lanczos3", new Lanczos3Resampler() }, //{ "Lanczos5", new Lanczos5Resampler() }, //{ "Lanczos8", new Lanczos8Resampler() }, //{ "MitchellNetravali", new MitchellNetravaliResampler() }, - { "NearestNeighbor", new NearestNeighborResampler() }, //{ "Hermite", new HermiteResampler() }, //{ "Spline", new SplineResampler() }, //{ "Robidoux", new RobidouxResampler() }, @@ -60,17 +60,14 @@ { Stopwatch watch = Stopwatch.StartNew(); Image image = new Image(stream); - string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file); - using (FileStream output = File.OpenWrite($"TestOutput/Sample/{Path.GetFileName(filename)}")) + using (FileStream output = File.OpenWrite($"TestOutput/Sample/{ Path.GetFileName(filename) }")) { processor.OnProgress += this.ProgressUpdate; - // Not Chainable. image = image.Process(image.Width / 2, image.Height / 2, processor); image.Save(output); processor.OnProgress -= this.ProgressUpdate; } - image.Dispose(); Trace.WriteLine($"{ name }: { watch.ElapsedMilliseconds}ms"); } @@ -91,15 +88,14 @@ using (FileStream stream = File.OpenRead(file)) { Stopwatch watch = Stopwatch.StartNew(); - using (Image image = new Image(stream)) + Image image = new Image(stream); + string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file); + using (FileStream output = File.OpenWrite($"TestOutput/Resize/{filename}")) { - string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file); - using (FileStream output = File.OpenWrite($"TestOutput/Resize/{filename}")) - { - image.Resize(image.Width / 2, image.Height / 2, sampler, false, this.ProgressUpdate) - .Save(output); - } + image.Resize(image.Width / 2, image.Height / 2, sampler, false, this.ProgressUpdate) + .Save(output); } + Trace.WriteLine($"{name}: {watch.ElapsedMilliseconds}ms"); } } @@ -120,14 +116,12 @@ using (FileStream stream = File.OpenRead(file)) { Stopwatch watch = Stopwatch.StartNew(); - using (Image image = new Image(stream)) + Image image = new Image(stream); + string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file); + using (FileStream output = File.OpenWrite($"TestOutput/Resize/{filename}")) { - string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file); - using (FileStream output = File.OpenWrite($"TestOutput/Resize/{filename}")) - { - image.Resize(image.Width / 3, 0, new TriangleResampler(), false, this.ProgressUpdate) - .Save(output); - } + image.Resize(image.Width / 3, 0, new TriangleResampler(), false, this.ProgressUpdate) + .Save(output); } Trace.WriteLine($"{name}: {watch.ElapsedMilliseconds}ms"); @@ -150,14 +144,12 @@ using (FileStream stream = File.OpenRead(file)) { Stopwatch watch = Stopwatch.StartNew(); - using (Image image = new Image(stream)) + Image image = new Image(stream); + string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file); + using (FileStream output = File.OpenWrite($"TestOutput/Resize/{filename}")) { - string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file); - using (FileStream output = File.OpenWrite($"TestOutput/Resize/{filename}")) - { - image.Resize(0, image.Height / 3, new TriangleResampler(), false, this.ProgressUpdate) - .Save(output); - } + image.Resize(0, image.Height / 3, new TriangleResampler(), false, this.ProgressUpdate) + .Save(output); } Trace.WriteLine($"{name}: {watch.ElapsedMilliseconds}ms"); @@ -179,14 +171,12 @@ using (FileStream stream = File.OpenRead(file)) { Stopwatch watch = Stopwatch.StartNew(); - using (Image image = new Image(stream)) + Image image = new Image(stream); + string filename = Path.GetFileNameWithoutExtension(file) + "-" + rotateType + flipType + Path.GetExtension(file); + using (FileStream output = File.OpenWrite($"TestOutput/RotateFlip/{filename}")) { - string filename = Path.GetFileNameWithoutExtension(file) + "-" + rotateType + flipType - + Path.GetExtension(file); - using (FileStream output = File.OpenWrite($"TestOutput/RotateFlip/{filename}")) - { - image.RotateFlip(rotateType, flipType, this.ProgressUpdate).Save(output); - } + image.RotateFlip(rotateType, flipType, this.ProgressUpdate) + .Save(output); } Trace.WriteLine($"{rotateType + "-" + flipType}: {watch.ElapsedMilliseconds}ms"); @@ -194,9 +184,8 @@ } } - [Theory] - [MemberData("ReSamplers")] - public void ImageShouldRotate(string name, IResampler sampler) + [Fact] + public void ImageShouldRotate() { if (!Directory.Exists("TestOutput/Rotate")) { @@ -208,18 +197,16 @@ using (FileStream stream = File.OpenRead(file)) { Stopwatch watch = Stopwatch.StartNew(); - using (Image image = new Image(stream)) + Image image = new Image(stream); + string filename = Path.GetFileName(file); + using (FileStream output = File.OpenWrite($"TestOutput/Rotate/{filename}")) { - string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file); - using (FileStream output = File.OpenWrite($"TestOutput/Rotate/{filename}")) - { - image.Rotate(45, sampler, false, this.ProgressUpdate) - //.BackgroundColor(Color.Aqua) - .Save(output); - } + image.Rotate(45, this.ProgressUpdate) + .BackgroundColor(Color.Pink) + .Save(output); } - Trace.WriteLine($"{name}: {watch.ElapsedMilliseconds}ms"); + Trace.WriteLine($"{watch.ElapsedMilliseconds}ms"); } } } @@ -236,13 +223,11 @@ { using (FileStream stream = File.OpenRead(file)) { - using (Image image = new Image(stream)) + Image image = new Image(stream); + string filename = Path.GetFileNameWithoutExtension(file) + "-EntropyCrop" + Path.GetExtension(file); + using (FileStream output = File.OpenWrite($"TestOutput/EntropyCrop/{filename}")) { - string filename = Path.GetFileNameWithoutExtension(file) + "-EntropyCrop" + Path.GetExtension(file); - using (FileStream output = File.OpenWrite($"TestOutput/EntropyCrop/{filename}")) - { - image.EntropyCrop(.5f, this.ProgressUpdate).Save(output); - } + image.EntropyCrop(.5f, this.ProgressUpdate).Save(output); } } } @@ -260,13 +245,11 @@ { using (FileStream stream = File.OpenRead(file)) { - using (Image image = new Image(stream)) + Image image = new Image(stream); + string filename = Path.GetFileNameWithoutExtension(file) + "-Crop" + Path.GetExtension(file); + using (FileStream output = File.OpenWrite($"TestOutput/Crop/{filename}")) { - string filename = Path.GetFileNameWithoutExtension(file) + "-Crop" + Path.GetExtension(file); - using (FileStream output = File.OpenWrite($"TestOutput/Crop/{filename}")) - { - image.Crop(image.Width / 2, image.Height / 2, this.ProgressUpdate).Save(output); - } + image.Crop(image.Width / 2, image.Height / 2, this.ProgressUpdate).Save(output); } } }