From 9be3a0f42dbb7bea2f027fa84d67bb603146f8ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Thu, 30 Apr 2026 18:33:27 +0300 Subject: [PATCH 01/17] feat: add border modes --- src/BMPConvolver.Core/BorderMode.cs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/BMPConvolver.Core/BorderMode.cs diff --git a/src/BMPConvolver.Core/BorderMode.cs b/src/BMPConvolver.Core/BorderMode.cs new file mode 100644 index 0000000..93a32f3 --- /dev/null +++ b/src/BMPConvolver.Core/BorderMode.cs @@ -0,0 +1,8 @@ +namespace BMPConvolver.Core; + +public enum BorderMode +{ + Zero = 0, + Clamp = 1, +} + From 40711248675384d200939f05167e6869f0f9dbcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Thu, 30 Apr 2026 18:33:47 +0300 Subject: [PATCH 02/17] feat: add kernel type --- src/BMPConvolver.Core/Kernel.cs | 143 ++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 src/BMPConvolver.Core/Kernel.cs diff --git a/src/BMPConvolver.Core/Kernel.cs b/src/BMPConvolver.Core/Kernel.cs new file mode 100644 index 0000000..ee290eb --- /dev/null +++ b/src/BMPConvolver.Core/Kernel.cs @@ -0,0 +1,143 @@ +namespace BMPConvolver.Core; + +public sealed class Kernel +{ + public int Width { get; } + public int Height { get; } + public int CenterX { get; } + public int CenterY { get; } + public float[] Weights { get; } + + public Kernel(int width, int height, int centerX, int centerY, float[] weights) + { + if( width <= 0 ) throw new ArgumentOutOfRangeException(nameof(width), width, "Size must be a positive integer."); + if( height <= 0 ) throw new ArgumentOutOfRangeException(nameof(height), height, "Size must be a positive integer."); + if( centerX < 0 ) throw new ArgumentOutOfRangeException(nameof(centerX), centerX, "Center must be within the kernel bounds."); + if( centerX >= width ) throw new ArgumentOutOfRangeException(nameof(centerX), centerX, "Center must be within the kernel bounds."); + if( centerY < 0 ) throw new ArgumentOutOfRangeException(nameof(centerY), centerY, "Center must be within the kernel bounds."); + if( centerY >= height ) throw new ArgumentOutOfRangeException(nameof(centerY), centerY, "Center must be within the kernel bounds."); + ArgumentNullException.ThrowIfNull(weights); + if (weights.Length != width * height) throw new ArgumentException(nameof(weights), "Weights length must be width*height."); + + Width = width; + Height = height; + CenterX = centerX; + CenterY = centerY; + Weights = weights; + } + + public float Get(int x, int y) => Weights[(y * Width) + x]; + + public static Kernel Identity() + => Delta(0, 0); + + public static Kernel Zero(int width, int height) + { + var centerX = width / 2; + var centerY = height / 2; + return new Kernel(width, height, centerX, centerY, new float[width * height]); + } + + /// + /// Discrete shift kernel: output(x,y) = input(x-dx, y-dy) + /// + public static Kernel Delta(int dx, int dy) + { + var width = Math.Abs(dx) * 2 + 1; + var height = Math.Abs(dy) * 2 + 1; + var centerX = width / 2; + var centerY = height / 2; + + var weights = new float[width * height]; + var kx = centerX - dx; + var ky = centerY - dy; + weights[(ky * width) + kx] = 1f; + return new Kernel(width, height, centerX, centerY, weights); + } + + /// + /// Box blur kernel of the given size. + /// + public static Kernel BoxBlur(int size = 3) + { + if (size <= 0 || size % 2 == 0) + throw new ArgumentOutOfRangeException(nameof(size), "Size must be a positive odd number."); + + var weight = 1f / (size * size); + var weights = new float[size * size]; + Array.Fill(weights, weight); + var center = size / 2; + + return new Kernel(size, size, center, center, weights); + } + + /// + /// Sharpening kernel (Laplacian-based unsharp mask). + /// + public static Kernel Sharpen() + => new( + width: 3, + height: 3, + centerX: 1, + centerY: 1, + weights: + [ + 0f, -1f, 0f, + -1f, 5f, -1f, + 0f, -1f, 0f, + ]); + + /// + /// Composition for Zero border (infinite zero-extended domain): + /// applying a then b equals applying Compose(a, b). + /// + public static Kernel Compose(Kernel a, Kernel b) + { + var width = a.Width + b.Width - 1; + var height = a.Height + b.Height - 1; + var centerX = a.CenterX + b.CenterX; + var centerY = a.CenterY + b.CenterY; + var weights = new float[width * height]; + + for (var bY = 0; bY < b.Height; bY++) + for (var bX = 0; bX < b.Width; bX++) + { + var bWeight = b.Get(bX, bY); + if (bWeight == 0f) continue; + + for (var aY = 0; aY < a.Height; aY++) + for (var aX = 0; aX < a.Width; aX++) + { + var aWeight = a.Get(aX, aY); + if (aWeight == 0f) continue; + + var rX = aX + bX; + var rY = aY + bY; + weights[(rY * width) + rX] += bWeight * aWeight; + } + } + + return new Kernel(width, height, centerX, centerY, weights); + } + + public Kernel PadZeros(int padLeft, int padTop, int padRight, int padBottom) + { + if (padLeft < 0) throw new ArgumentOutOfRangeException(nameof(padLeft)); + if (padTop < 0) throw new ArgumentOutOfRangeException(nameof(padTop)); + if (padRight < 0) throw new ArgumentOutOfRangeException(nameof(padRight)); + if (padBottom < 0) throw new ArgumentOutOfRangeException(nameof(padBottom)); + + var newW = Width + padLeft + padRight; + var newH = Height + padTop + padBottom; + var newWeights = new float[newW * newH]; + + for (var y = 0; y < Height; y++) + { + var srcRow = y * Width; + var dstRow = (y + padTop) * newW + padLeft; + Array.Copy(Weights, srcRow, newWeights, dstRow, Width); + } + + return new Kernel(newW, newH, CenterX + padLeft, CenterY + padTop, newWeights); + } +} From 0b84ba72f0cf3445c9cf32186d0d1af86c272279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Thu, 30 Apr 2026 18:34:41 +0300 Subject: [PATCH 03/17] feat: add type for grey image --- src/BMPConvolver.Core/ImageTypes/GrayImage.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/BMPConvolver.Core/ImageTypes/GrayImage.cs diff --git a/src/BMPConvolver.Core/ImageTypes/GrayImage.cs b/src/BMPConvolver.Core/ImageTypes/GrayImage.cs new file mode 100644 index 0000000..226e639 --- /dev/null +++ b/src/BMPConvolver.Core/ImageTypes/GrayImage.cs @@ -0,0 +1,30 @@ +namespace BMPConvolver.Core; + +public sealed class GrayImage +{ + public int Width { get; } + public int Height { get; } + public float[] Pixels { get; } + + public GrayImage(int width, int height, float[] pixels) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(width); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(height); + ArgumentNullException.ThrowIfNull(pixels); + if (pixels.Length != width * height) throw new ArgumentException("Pixels length must be equal to width * height."); + + Width = width; + Height = height; + Pixels = pixels; + } + + public float GetPixel(int x, int y) => Pixels[(y * Width) + x]; + public void SetPixel(int x, int y, float value) => Pixels[(y * Width) + x] = value; + + public GrayImage Clone() + { + var copy = new float[Pixels.Length]; + Array.Copy(Pixels, copy, copy.Length); + return new GrayImage(Width, Height, copy); + } +} From 41884f34c4d8a06a41aeecdd92ad12b8489729e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Thu, 30 Apr 2026 18:35:03 +0300 Subject: [PATCH 04/17] feat: add image io --- src/BMPConvolver.Core/ImageIO/GrayImageIO.cs | 49 ++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/BMPConvolver.Core/ImageIO/GrayImageIO.cs diff --git a/src/BMPConvolver.Core/ImageIO/GrayImageIO.cs b/src/BMPConvolver.Core/ImageIO/GrayImageIO.cs new file mode 100644 index 0000000..0a2a5ee --- /dev/null +++ b/src/BMPConvolver.Core/ImageIO/GrayImageIO.cs @@ -0,0 +1,49 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace BMPConvolver.Core.ImageSharp; + +public static class GrayImageIo +{ + public static GrayImage LoadAsGray(string path) + { + using var image = Image.Load(path); + var pixels = new float[image.Width * image.Height]; + var bytes = new byte[pixels.Length]; + + image.CopyPixelDataTo(bytes); + for (var i = 0; i < bytes.Length; i++) + pixels[i] = bytes[i] / 255f; + + return new GrayImage(image.Width, image.Height, pixels); + } + + public static void SaveGrayAsBmp(GrayImage image, string path) + { + using var outImage = new Image(image.Width, image.Height); + var src = image.Pixels; + var dest = new L8[src.Length]; + + // Convert float to L8 + for (int i = 0; i < src.Length; i++) + { + float v = Math.Clamp(src[i], 0f, 1f); + dest[i] = new L8((byte)MathF.Round(v * 255f)); + } + + outImage.ProcessPixelRows(accessor => + { + int offset = 0; + int width = image.Width; + + for (var y = 0; y < image.Height; y++) + { + dest.AsSpan(offset, width).CopyTo(accessor.GetRowSpan(y)); + offset += width; + } + }); + + outImage.SaveAsBmp(path); + } +} + From 538bf024fcc5957f1cf11ab7d350575a324e1fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Thu, 30 Apr 2026 18:35:23 +0300 Subject: [PATCH 05/17] feat: add main convolver --- src/BMPConvolver.Core/Convolver.cs | 55 ++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/BMPConvolver.Core/Convolver.cs diff --git a/src/BMPConvolver.Core/Convolver.cs b/src/BMPConvolver.Core/Convolver.cs new file mode 100644 index 0000000..5e03a60 --- /dev/null +++ b/src/BMPConvolver.Core/Convolver.cs @@ -0,0 +1,55 @@ +namespace BMPConvolver.Core; + +public static class Convolver +{ + public static GrayImage ConvolveSequential(GrayImage input, Kernel kernel, BorderMode borderMode) + { + ArgumentNullException.ThrowIfNull(input); + ArgumentNullException.ThrowIfNull(kernel); + + var output = new float[input.Pixels.Length]; + ConvolveInternal(input, output, kernel, borderMode); + return new GrayImage(input.Width, input.Height, output); + } + + private static void ConvolveInternal(GrayImage input, float[] output, Kernel kernel, BorderMode borderMode) + { + var src = input.Pixels; + var weights = kernel.Weights; + + var yEnd = input.Height; + var xEnd = input.Width; + + for (var y = 0; y < yEnd; y++) + for (var x = 0; x < xEnd; x++) + { + var sum = 0f; + + for (var ky = 0; ky < kernel.Height; ky++) + { + var iy = y + (ky - kernel.CenterY); + var yInRange = (uint)iy < (uint)input.Height; + if (!yInRange && borderMode == BorderMode.Zero) continue; + if (!yInRange && borderMode == BorderMode.Clamp) iy = iy < 0 ? 0 : (input.Height - 1); + + var srcRow = iy * input.Width; + var kRow = ky * kernel.Width; + + for (var kx = 0; kx < kernel.Width; kx++) + { + var ix = x + (kx - kernel.CenterX); + var xInRange = (uint)ix < (uint)input.Width; + if (!xInRange && borderMode == BorderMode.Zero) continue; + if (!xInRange && borderMode == BorderMode.Clamp) ix = ix < 0 ? 0 : (input.Width - 1); + + var pixel = src[srcRow + ix]; + var weight = weights[kRow + kx]; + sum += pixel * weight; + } + } + + output[(y * input.Width) + x] = sum; + } + } +} + From ca4f74bce2958febe766247cf23af66e3df4b506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Thu, 30 Apr 2026 18:37:50 +0300 Subject: [PATCH 06/17] feat: add cli --- Convolver.sln | 3 + src/BMPConvolver.Cli/BMPConvolver.Cli.csproj | 18 ++++ .../KernelParser/KernelTextParser.cs | 66 +++++++++++++ src/BMPConvolver.Cli/Program.cs | 97 +++++++++++++++++++ 4 files changed, 184 insertions(+) create mode 100644 src/BMPConvolver.Cli/BMPConvolver.Cli.csproj create mode 100644 src/BMPConvolver.Cli/KernelParser/KernelTextParser.cs create mode 100644 src/BMPConvolver.Cli/Program.cs diff --git a/Convolver.sln b/Convolver.sln index 706996e..7960b42 100644 --- a/Convolver.sln +++ b/Convolver.sln @@ -7,6 +7,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E7170ACE-9AA EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BMPConvolver.Core", "src\BMPConvolver.Core\BMPConvolver.Core.csproj", "{523C0934-7353-45C5-AB89-DFB54C98071F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BMPConvolver.Cli", "src\BMPConvolver.Cli\BMPConvolver.Cli.csproj", "{A99E9047-15E4-492A-8C77-B2C82CAF0B0B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -35,5 +37,6 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {523C0934-7353-45C5-AB89-DFB54C98071F} = {E7170ACE-9AAD-4689-ADC6-22E048D93B80} + {A99E9047-15E4-492A-8C77-B2C82CAF0B0B} = {E7170ACE-9AAD-4689-ADC6-22E048D93B80} EndGlobalSection EndGlobal diff --git a/src/BMPConvolver.Cli/BMPConvolver.Cli.csproj b/src/BMPConvolver.Cli/BMPConvolver.Cli.csproj new file mode 100644 index 0000000..a233af0 --- /dev/null +++ b/src/BMPConvolver.Cli/BMPConvolver.Cli.csproj @@ -0,0 +1,18 @@ + + + + + + + + + + + + Exe + net8.0 + enable + enable + + + diff --git a/src/BMPConvolver.Cli/KernelParser/KernelTextParser.cs b/src/BMPConvolver.Cli/KernelParser/KernelTextParser.cs new file mode 100644 index 0000000..06d7a94 --- /dev/null +++ b/src/BMPConvolver.Cli/KernelParser/KernelTextParser.cs @@ -0,0 +1,66 @@ +using System.Globalization; +using BMPConvolver.Core; + +namespace BMPConvolver.Cli.KernelParser; + +public static class KernelTextParser +{ + /// + /// Parses a 2D kernel from text. + /// Rows can be separated by ';' or newlines, values separated by whitespace and/or ','. + /// Center defaults to (width/2, height/2) unless specified. + /// + public static Kernel Parse(string text, int? centerX = null, int? centerY = null) + { + ArgumentNullException.ThrowIfNull(text); + + var rows = new List(); + + foreach (var line in SplitLinesAndSemicolons(text)) + { + var values = line + .Replace(',', ' ') + .Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(t => float.Parse(t, CultureInfo.InvariantCulture)) + .ToArray(); + + if (values.Length == 0) continue; + rows.Add(values); + } + + if (rows.Count == 0) throw new FormatException("Kernel text has no numeric rows."); + + var width = rows[0].Length; + if (width == 0) throw new FormatException("Kernel width must be > 0."); + + var height = rows.Count; + for (var i = 1; i < height; i++) + if (rows[i].Length != width) + throw new FormatException("All kernel rows must have the same number of values."); + + + var cx = centerX ?? (width / 2); + var cy = centerY ?? (height / 2); + + var weights = new float[width * height]; + for (var y = 0; y < height; y++) + { + var row = rows[y]; + Array.Copy(row, 0, weights, y * width, width); + } + + return new Kernel(width, height, cx, cy, weights); + } + + private static IEnumerable SplitLinesAndSemicolons(string text) + { + // Normalize CRLF/CR -> LF first, then split further by ';' + var normalized = text.Replace("\r\n", "\n").Replace('\r', '\n'); + foreach (var line in normalized.Split('\n')) + { + var parts = line.Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ); + foreach (var p in parts) + yield return p; + } + } +} \ No newline at end of file diff --git a/src/BMPConvolver.Cli/Program.cs b/src/BMPConvolver.Cli/Program.cs new file mode 100644 index 0000000..004c736 --- /dev/null +++ b/src/BMPConvolver.Cli/Program.cs @@ -0,0 +1,97 @@ +using BMPConvolver.Core; +using BMPConvolver.Core.ImageSharp; +using BMPConvolver.Cli.KernelParser; + +static int PrintUsage() +{ + Console.Error.WriteLine("Usage:"); + Console.Error.WriteLine(" BMPConvolver.Cli [--border zero|clamp]"); + Console.Error.WriteLine(" [--kernel box3|sharpen|identity] [--kernel-text \"...\"] [--kernel-file path.txt]"); + Console.Error.WriteLine(); + Console.Error.WriteLine("Kernel text format:"); + Console.Error.WriteLine(" Rows separated by ';' or newlines, values by spaces/commas."); + return 2; +} + +if (args.Length < 2) return PrintUsage(); + +var inputPath = args[0]; +var outputPath = args[1]; + +var border = "zero"; +string? kernelPreset = null; +string? kernelText = null; +string? kernelFile = null; + +for (var i = 2; i < args.Length; i++) +{ + var a = args[i]; + if (a == "--border" && i + 1 < args.Length) { border = args[++i]; continue; } + if (a == "--kernel" && i + 1 < args.Length) { kernelPreset = args[++i]; continue; } + if (a == "--kernel-text" && i + 1 < args.Length) { kernelText = args[++i]; continue; } + if (a == "--kernel-file" && i + 1 < args.Length) { kernelFile = args[++i]; continue; } + + return PrintUsage(); +} + +BorderMode borderMode; +Kernel kernel; +try +{ + borderMode = border.ToLowerInvariant() switch + { + "zero" => BorderMode.Zero, + "clamp" => BorderMode.Clamp, + _ => throw new ArgumentException($"Unknown border mode: {border}") + }; + + kernel = ResolveKernel(kernelPreset, kernelText, kernelFile); +} +catch (Exception e) +{ + Console.Error.WriteLine(e.Message); + return 2; +} + +static Kernel ResolveKernel(string? preset, string? text, string? file) +{ + var specified = 0; + if (!string.IsNullOrWhiteSpace(preset)) specified++; + if (!string.IsNullOrWhiteSpace(text)) specified++; + if (!string.IsNullOrWhiteSpace(file)) specified++; + if (specified > 1) throw new ArgumentException("You can specify only one of --kernel, --kernel-text, --kernel-file."); + + if (!string.IsNullOrWhiteSpace(text)) + return KernelTextParser.Parse(text); + + if (!string.IsNullOrWhiteSpace(file)) + return KernelTextParser.Parse(File.ReadAllText(file)); + + var p = (preset ?? "box3").Trim().ToLowerInvariant(); + return p switch + { + "identity" => Kernel.Identity(), + "box3" => Kernel.BoxBlur(), + "sharpen" => Kernel.Sharpen(), + _ => throw new ArgumentException($"Unknown kernel preset: {preset}. Use box3|sharpen|identity.") + }; +} + +var input = GrayImageIo.LoadAsGray(inputPath); + +var timer = System.Diagnostics.Stopwatch.StartNew(); +GrayImage output; +try +{ + output = Convolver.ConvolveSequential(input, kernel, borderMode); +} +catch (Exception e) +{ + Console.Error.WriteLine(e.Message); + return 2; +} +timer.Stop(); + +GrayImageIo.SaveGrayAsBmp(output, outputPath); +Console.WriteLine($"Done. {input.Width}x{input.Height}, border={border}, elapsed={timer.ElapsedMilliseconds} ms"); +return 0; From b90f74ffe54ae63a585aa17124e372088f454595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Thu, 30 Apr 2026 18:39:50 +0300 Subject: [PATCH 07/17] test: add core tests --- Convolver.sln | 5 + .../BMPConvolver.Tests.csproj | 35 ++ .../CoreTests/CoreUnitTests.cs | 320 ++++++++++++++++++ 3 files changed, 360 insertions(+) create mode 100644 test/BMPConvolver.Tests/BMPConvolver.Tests.csproj create mode 100644 test/BMPConvolver.Tests/CoreTests/CoreUnitTests.cs diff --git a/Convolver.sln b/Convolver.sln index 7960b42..024a03a 100644 --- a/Convolver.sln +++ b/Convolver.sln @@ -9,6 +9,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BMPConvolver.Core", "src\BM EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BMPConvolver.Cli", "src\BMPConvolver.Cli\BMPConvolver.Cli.csproj", "{A99E9047-15E4-492A-8C77-B2C82CAF0B0B}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5F4B7EAD-0AB8-4FBF-85F1-EA0BEE09832F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BMPConvolver.Tests", "test\BMPConvolver.Tests\BMPConvolver.Tests.csproj", "{69A51A1D-CE2E-4C75-AD2E-64ADC5F8E593}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -38,5 +42,6 @@ Global GlobalSection(NestedProjects) = preSolution {523C0934-7353-45C5-AB89-DFB54C98071F} = {E7170ACE-9AAD-4689-ADC6-22E048D93B80} {A99E9047-15E4-492A-8C77-B2C82CAF0B0B} = {E7170ACE-9AAD-4689-ADC6-22E048D93B80} + {69A51A1D-CE2E-4C75-AD2E-64ADC5F8E593} = {5F4B7EAD-0AB8-4FBF-85F1-EA0BEE09832F} EndGlobalSection EndGlobal diff --git a/test/BMPConvolver.Tests/BMPConvolver.Tests.csproj b/test/BMPConvolver.Tests/BMPConvolver.Tests.csproj new file mode 100644 index 0000000..1310f25 --- /dev/null +++ b/test/BMPConvolver.Tests/BMPConvolver.Tests.csproj @@ -0,0 +1,35 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/BMPConvolver.Tests/CoreTests/CoreUnitTests.cs b/test/BMPConvolver.Tests/CoreTests/CoreUnitTests.cs new file mode 100644 index 0000000..4ecea21 --- /dev/null +++ b/test/BMPConvolver.Tests/CoreTests/CoreUnitTests.cs @@ -0,0 +1,320 @@ +using BMPConvolver.Core; +using OpenCvSharp; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace BMPConvolver.Tests.CoreTests; + +public class CoreTests +{ + private const int Seed = 1; + private const float Epsilon = 1e-6f; + + [Theory] + [InlineData(1, 1)] + [InlineData(32, 32)] + [InlineData(120, 30)] + [InlineData(256, 128)] + [InlineData(512, 256)] + [InlineData(512, 512)] + [InlineData(768, 512)] + public void IdentityKernel_IsIdentity(int width, int height) + { + var img = RandomImage(width, height); + var outImg = Convolver.ConvolveSequential(img, Kernel.Identity(), BorderMode.Zero); + AssertImagesEqual(img, outImg); + } + + [Theory] + [InlineData(1, 1, 3, 3)] + [InlineData(17, 9, 5, 7)] + [InlineData(128, 512, 7, 5)] + [InlineData(256, 256, 9, 9)] + [InlineData(320, 240, 11, 7)] + [InlineData(512, 128, 15, 13)] + public void ZeroKernel_ProducesZeros(int width, int height, int kernelWidth, int kernelHeight) + { + var img = RandomImage(width, height); + var kernel = Kernel.Zero(kernelWidth, kernelHeight); + var outImg = Convolver.ConvolveSequential(img, kernel, BorderMode.Zero); + Assert.All(outImg.Pixels, w => Assert.Equal(0f, w)); + } + + [Theory] + [InlineData(5, 5, 1, 2, 3, 4)] + [InlineData(19, 11, 2, 2, 2, 2)] + [InlineData(31, 29, 4, 3, 5, 2)] + [InlineData(64, 64, 8, 8, 8, 8)] + [InlineData(127, 65, 6, 5, 6, 5)] + public void PaddingKernelWithZeros_DoesNotChangeResult(int width, int height, int padLeft, int padTop, int padRight, int padBottom) + { + var img = RandomImage(width, height); + var kernel = RandomKernelOdd(maxSize: 7); + var padded = kernel.PadZeros(padLeft, padTop, padRight, padBottom); + + var image_A = Convolver.ConvolveSequential(img, kernel, BorderMode.Zero); + var image_B = Convolver.ConvolveSequential(img, padded, BorderMode.Zero); + AssertImagesEqual(image_A, image_B, Epsilon); + } + + [Theory] + [InlineData(9, 7)] + [InlineData(31, 29)] + [InlineData(63, 61)] + [InlineData(80, 72)] + [InlineData(128, 96)] + public void ShiftAndInverseShift_ComposeToIdentity(int width, int height) + { + var img = RandomImage(width, height); + var dx = 2; + var dy = -1; + var shift = Kernel.Delta(dx, dy); + var inv = Kernel.Delta(-dx, -dy); + var composed = Kernel.Compose(shift, inv); + + var seq = Convolver.ConvolveSequential(Convolver.ConvolveSequential(img, shift, BorderMode.Zero), inv, BorderMode.Zero); + var one = Convolver.ConvolveSequential(img, composed, BorderMode.Zero); + // Composition is guaranteed on the "safe" interior. When applying filters sequentially on a finite array, + // the intermediate result is implicitly cropped, so border pixels can differ from a single composed kernel. + AssertImagesEqualInterior(seq, one, marginX: Math.Abs(dx) * 2, marginY: Math.Abs(dy) * 2, eps: Epsilon); + } + + [Theory] + [InlineData(17, 13, 3, 3, 5, 5)] + [InlineData(64, 31, 1, 1, 7, 3)] + [InlineData(128, 128, 5, 5, 3, 7)] + [InlineData(192, 97, 7, 7, 9, 5)] + [InlineData(256, 64, 9, 9, 3, 3)] + public void SequentialApplication_EqualsKernelComposition(int width, int height, int KernelWidth_1, int KernelHeight_1, int KernelWidth_2, int KernelHeight_2) + { + var img = RandomImage(width, height); + var a = RandomKernelOddFrom(KernelWidth_1, KernelHeight_1); + var b = RandomKernelOddFrom(KernelWidth_2, KernelHeight_2); + + var seq = Convolver.ConvolveSequential(Convolver.ConvolveSequential(img, a, BorderMode.Zero), b, BorderMode.Zero); + var composed = Kernel.Compose(a, b); + var one = Convolver.ConvolveSequential(img, composed, BorderMode.Zero); + + var marginX = (a.Width / 2) + (b.Width / 2); + var marginY = (a.Height / 2) + (b.Height / 2); + AssertImagesEqualInterior(seq, one, marginX, marginY, eps: Epsilon); + } + + [Theory] + [InlineData(64, 48)] + [InlineData(128, 128)] + [InlineData(256, 64)] + [InlineData(192, 512)] + public void BoxBlur3_MatchesImageSharpBoxBlurRadius1(int width, int height) + { + var img = RandomImage(width, height); + var kernel = Kernel.BoxBlur(); + + var ours = Convolver.ConvolveSequential(img, kernel, BorderMode.Clamp); + var oursBytes = ToL8BytesClamped(ours); + + using var sharp = ToImageL8(img); + + sharp.Mutate(c => c.BoxBlur(1)); + + var sharpBytes = FromImageL8ToBytes(sharp); + + Assert.Equal(oursBytes.Length, sharpBytes.Length); + for (var i = 0; i < oursBytes.Length; i++) + Assert.True(Math.Abs(oursBytes[i] - sharpBytes[i]) <= 1, $"Byte index {i}: ours={oursBytes[i]} sharp={sharpBytes[i]}"); + } + + [Theory] + [InlineData(BorderMode.Zero, 1, 1)] + [InlineData(BorderMode.Zero, 2, 7)] + [InlineData(BorderMode.Zero, 13, 9)] + [InlineData(BorderMode.Zero, 64, 33)] + [InlineData(BorderMode.Zero, 128, 64)] + [InlineData(BorderMode.Zero, 192, 128)] + [InlineData(BorderMode.Clamp, 1, 1)] + [InlineData(BorderMode.Clamp, 2, 7)] + [InlineData(BorderMode.Clamp, 13, 9)] + [InlineData(BorderMode.Clamp, 64, 33)] + [InlineData(BorderMode.Clamp, 128, 64)] + [InlineData(BorderMode.Clamp, 192, 128)] + public void Convolution_MatchesOpenCv_Filter2D_ForRandomKernels(BorderMode borderMode, int width, int height) + { + var kernel = RandomKernelOdd(maxSize: 9); + + var img = RandomImage(width, height); + + var ours = Convolver.ConvolveSequential(img, kernel, borderMode); + var cv = OpenCvFilter2D(img, kernel, borderMode); + + AssertImagesEqual(ours, cv, eps: Epsilon); + } + + private static GrayImage RandomImage(int width, int height) + { + var rnd = new Random(Seed); + var pixels = new float[width * height]; + for (var i = 0; i < pixels.Length; i++) + pixels[i] = (float)rnd.NextDouble(); + return new GrayImage(width, height, pixels); + } + + private static Kernel RandomKernelOdd(int maxSize) + { + var rnd = new Random(Seed); + var width= 1 + 2 * rnd.Next(0, Math.Max(1, (maxSize + 1) / 2)); + var height = 1 + 2 * rnd.Next(0, Math.Max(1, (maxSize + 1) / 2)); + return RandomKernelOddFrom(width, height); + } + + private static Kernel RandomKernelOddFrom(int wRequested, int hRequested) + { + var width= wRequested <= 1 ? 1 : (wRequested % 2 == 1 ? wRequested : wRequested + 1); + var height = hRequested <= 1 ? 1 : (hRequested % 2 == 1 ? hRequested : hRequested + 1); + + var rnd = new Random(Seed); + var weights = new float[width * height]; + for (var i = 0; i < weights.Length; i++) + { + weights[i] = ((float)rnd.NextDouble() - 0.5f) * 0.5f; + } + return new Kernel(width, height, width/ 2, height / 2, weights); + } + + private static void AssertImagesEqual(GrayImage a, GrayImage b, float eps = 0f) + { + Assert.Equal(a.Width, b.Width); + Assert.Equal(a.Height, b.Height); + Assert.Equal(a.Pixels.Length, b.Pixels.Length); + + for (var i = 0; i < a.Pixels.Length; i++) + { + var da = a.Pixels[i]; + var db = b.Pixels[i]; + if (eps == 0f) + Assert.Equal(da, db); + else + Assert.True(MathF.Abs(da - db) <= eps, $"Index {i}: {da} != {db} (eps={eps})"); + } + } + + private static void AssertImagesEqualInterior(GrayImage a, GrayImage b, int marginX, int marginY, float eps = 0f) + { + Assert.Equal(a.Width, b.Width); + Assert.Equal(a.Height, b.Height); + + var width= a.Width; + var height = a.Height; + var x0 = Math.Clamp(marginX, 0, width); + var y0 = Math.Clamp(marginY, 0, height); + var x1 = Math.Clamp(width - marginX, 0, width); + var y1 = Math.Clamp(height - marginY, 0, height); + + for (var y = y0; y < y1; y++) + for (var x = x0; x < x1; x++) + { + var i = (y * width) + x; + var da = a.Pixels[i]; + var db = b.Pixels[i]; + Assert.True(MathF.Abs(da - db) <= eps, $"({x},{y}): {da} != {db} (eps={eps})"); + } + } + + private static Image ToImageL8(GrayImage img) + { + var image = new Image(img.Width, img.Height); + var width= img.Width; + var height = img.Height; + var src = img.Pixels; + + image.ProcessPixelRows(accessor => + { + for (var y = 0; y < height; y++) + { + var row = accessor.GetRowSpan(y); + var srcRow = y * width; + for (var x = 0; x < width; x++) + { + var weight = src[srcRow + x]; + if (weight < 0f) weight = 0f; + if (weight > 1f) weight = 1f; + row[x] = new L8((byte)MathF.Round(weight * 255f)); + } + } + }); + + return image; + } + + private static byte[] FromImageL8ToBytes(Image img) + { + var width= img.Width; + var height = img.Height; + var bytes = new byte[width * height]; + img.ProcessPixelRows(accessor => + { + for (var y = 0; y < height; y++) + { + var row = accessor.GetRowSpan(y); + var dstRow = y * width; + for (var x = 0; x < width; x++) + bytes[dstRow + x] = row[x].PackedValue; + } + }); + return bytes; + } + + private static byte[] ToL8BytesClamped(GrayImage img) + { + var bytes = new byte[img.Pixels.Length]; + for (var i = 0; i < img.Pixels.Length; i++) + { + var weight = img.Pixels[i]; + if (weight < 0f) weight = 0f; + if (weight > 1f) weight = 1f; + bytes[i] = (byte)MathF.Round(weight * 255f); + } + return bytes; + } + + private static GrayImage OpenCvFilter2D(GrayImage img, Kernel kernel, BorderMode borderMode) + { + using var src = new Mat(img.Height, img.Width, MatType.CV_32FC1); + var i = 0; + for (var y = 0; y < img.Height; y++) + for (var x = 0; x < img.Width; x++) + src.Set(y, x, img.Pixels[i++]); + + using var kernelMat = new Mat(kernel.Height, kernel.Width, MatType.CV_32FC1); + i = 0; + for (var y = 0; y < kernel.Height; y++) + for (var x = 0; x < kernel.Width; x++) + kernelMat.Set(y, x, kernel.Weights[i++]); + + using var dst = new Mat(img.Height, img.Width, MatType.CV_32FC1); + + var border = borderMode switch + { + BorderMode.Zero => BorderTypes.Constant, + BorderMode.Clamp => BorderTypes.Replicate, + _ => throw new ArgumentOutOfRangeException(nameof(borderMode)) + }; + + Cv2.Filter2D( + src: src, + dst: dst, + ddepth: MatType.CV_32FC1, + kernel: kernelMat, + anchor: new OpenCvSharp.Point(kernel.CenterX, kernel.CenterY), + delta: 0, + borderType: border); + + var outPixels = new float[img.Width * img.Height]; + i = 0; + for (var y = 0; y < img.Height; y++) + for (var x = 0; x < img.Width; x++) + outPixels[i++] = dst.Get(y, x); + + return new GrayImage(img.Width, img.Height, outPixels); + } +} \ No newline at end of file From 494d039925ba27638a5098ea879c89de5974a56a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Thu, 30 Apr 2026 18:40:11 +0300 Subject: [PATCH 08/17] test: add cli tests --- .../CliTests/CliUnitTests.cs | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 test/BMPConvolver.Tests/CliTests/CliUnitTests.cs diff --git a/test/BMPConvolver.Tests/CliTests/CliUnitTests.cs b/test/BMPConvolver.Tests/CliTests/CliUnitTests.cs new file mode 100644 index 0000000..673e93e --- /dev/null +++ b/test/BMPConvolver.Tests/CliTests/CliUnitTests.cs @@ -0,0 +1,112 @@ +using BMPConvolver.Cli.KernelParser; + +namespace BMPConvolver.Tests.CLITests; + +public class CliTests +{ + [Fact] + public void KernelTextParser_ParsesSemicolonSeparatedMatrix() + { + var k = KernelTextParser.Parse("0 -1 0; -1 5 -1; 0 -1 0"); + Assert.Equal(3, k.Width); + Assert.Equal(3, k.Height); + Assert.Equal(1, k.CenterX); + Assert.Equal(1, k.CenterY); + Assert.Equal(5f, k.Get(1, 1)); + Assert.Equal(-1f, k.Get(1, 0)); + } + + [Fact] + public void KernelTextParser_ParsesNewlinesAndComments() + { + var text = """ + 0 0 0 + 0 1 0 + 0 0 0 + """; + var k = KernelTextParser.Parse(text); + Assert.Equal(3, k.Width); + Assert.Equal(3, k.Height); + Assert.Equal(1f, k.Get(1, 1)); + } + + [Fact] + public void KernelTextParser_ParsesCommaSeparatedValues() + { + var k = KernelTextParser.Parse("1,2,3;4,5,6;7,8,9"); + Assert.Equal(3, k.Width); + Assert.Equal(3, k.Height); + Assert.Equal(1f, k.Get(0, 0)); + Assert.Equal(5f, k.Get(1, 1)); + Assert.Equal(9f, k.Get(2, 2)); + } + + [Fact] + public void KernelTextParser_ParsesSingleRow() + { + var k = KernelTextParser.Parse("1 2 3"); + Assert.Equal(3, k.Width); + Assert.Equal(1, k.Height); + Assert.Equal(1, k.CenterX); + Assert.Equal(0, k.CenterY); + Assert.Equal(1f, k.Get(0, 0)); + Assert.Equal(2f, k.Get(1, 0)); + Assert.Equal(3f, k.Get(2, 0)); + } + + [Fact] + public void KernelTextParser_ParsesWithCustomCenter() + { + var k = KernelTextParser.Parse("1 2 3;4 5 6", centerX: 0, centerY: 1); + Assert.Equal(3, k.Width); + Assert.Equal(2, k.Height); + Assert.Equal(0, k.CenterX); + Assert.Equal(1, k.CenterY); + } + + [Fact] + public void KernelTextParser_ThrowsOnEmptyText() + { + Assert.Throws(() => KernelTextParser.Parse("")); + } + + [Fact] + public void KernelTextParser_ThrowsOnNoNumericRows() + { + Assert.Throws(() => KernelTextParser.Parse(" ; ; ")); + } + + [Fact] + public void KernelTextParser_ThrowsOnInconsistentRowLengths() + { + Assert.Throws(() => KernelTextParser.Parse("1 2;3 4 5")); + } + + [Fact] + public void KernelTextParser_IgnoresEmptyLines() + { + var text = """ + + 1 2 + + 3 4 + + """; + var k = KernelTextParser.Parse(text); + Assert.Equal(2, k.Width); + Assert.Equal(2, k.Height); + Assert.Equal(1f, k.Get(0, 0)); + Assert.Equal(4f, k.Get(1, 1)); + } + + [Fact] + public void KernelTextParser_ParsesFloatingPointValues() + { + var k = KernelTextParser.Parse("0.5 -0.25 1.0"); + Assert.Equal(3, k.Width); + Assert.Equal(1, k.Height); + Assert.Equal(0.5f, k.Get(0, 0)); + Assert.Equal(-0.25f, k.Get(1, 0)); + Assert.Equal(1.0f, k.Get(2, 0)); + } +} \ No newline at end of file From 8122e35e7532611a452334f6bc3a8a9626097233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Thu, 30 Apr 2026 18:41:40 +0300 Subject: [PATCH 09/17] ci: add ci tests --- .github/workflows/dotnet.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/dotnet.yml diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..e471b25 --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,30 @@ +# This workflow will build a .NET project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net + +name: .NET + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - name: Install OpenCV + run: sudo apt-get update && sudo apt-get install -y libjpeg-dev libpng-dev libtiff-dev libavcodec-dev libavformat-dev libswscale-dev libv4l-dev libxvidcore-dev libx264-dev libopencv-dev + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Test + run: dotnet test --no-build --verbosity normal From 24171528b65f87fe49c4c5cfde9804ade19d5ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Wed, 13 May 2026 20:42:32 +0300 Subject: [PATCH 10/17] add readme --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 8b13789..84f56c6 100644 --- a/README.md +++ b/README.md @@ -1 +1,23 @@ +Для запуска приложения выполните следующую команду: + +```bash +dotnet run --project src/BMPConvolver.Cli/BMPConvolver.Cli.csproj [опции] +``` + +Опции включают: +- `--mode seq|par`: режим выполнения (последовательный или параллельный) +- `--partition pixels|rows|cols|grid`: способ разделения работы +- `--grid x`: размер сетки для grid partition +- `--border zero|clamp`: режим обработки границ +- `--kernel box3|sharpen|identity`: предустановленный kernel +- `--kernel-text "..."`: kernel в текстовом формате +- `--kernel-file path.txt`: kernel из файла + +## Тесты + +Для запуска тестов: + +```bash +dotnet test +``` From 115753c6f47c0b82e61c5b5bee432c1bb0008dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Thu, 30 Apr 2026 18:51:33 +0300 Subject: [PATCH 11/17] feat: add work partirioning modes --- .../WorkPartitioning/Partitioner.cs | 70 +++++++++++++++++++ .../WorkPartitioning/PartitioningMode.cs | 10 +++ .../WorkPartitioning/WorkRect.cs | 8 +++ 3 files changed, 88 insertions(+) create mode 100644 src/BMPConvolver.Core/WorkPartitioning/Partitioner.cs create mode 100644 src/BMPConvolver.Core/WorkPartitioning/PartitioningMode.cs create mode 100644 src/BMPConvolver.Core/WorkPartitioning/WorkRect.cs diff --git a/src/BMPConvolver.Core/WorkPartitioning/Partitioner.cs b/src/BMPConvolver.Core/WorkPartitioning/Partitioner.cs new file mode 100644 index 0000000..adfae9d --- /dev/null +++ b/src/BMPConvolver.Core/WorkPartitioning/Partitioner.cs @@ -0,0 +1,70 @@ +namespace BMPConvolver.Core.WorkPartitioning; + +public static class Partitioner +{ + public static IEnumerable Create(int width, int height, PartitioningMode mode, int gridX = 1, int gridY = 1) + { + ArgumentNullException.ThrowIfNull(width); + ArgumentNullException.ThrowIfNull(height); + + return mode switch + { + PartitioningMode.Pixels => Pixels(width, height), + PartitioningMode.Rows => Rows(width, height), + PartitioningMode.Columns => Columns(width, height), + PartitioningMode.Grid => Grid(width, height, gridX, gridY), + _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unknown partitioning mode."), + }; + } + + private static IEnumerable Pixels(int width, int height) + { + // One pixel per task is intentionally "bad" for overhead and cache locality, + // but it's useful to study the effects the user asked about. + for (var y = 0; y < height; y++) + for (var x = 0; x < width; x++) + yield return new WorkRect(x, y, 1, 1); + } + + private static IEnumerable Rows(int width, int height) + { + for (var y = 0; y < height; y++) + yield return new WorkRect(0, y, width, 1); + } + + private static IEnumerable Columns(int width, int height) + { + for (var x = 0; x < width; x++) + yield return new WorkRect(x, 0, 1, height); + } + + private static IEnumerable Grid(int width, int height, int gridX, int gridY) + { + ArgumentNullException.ThrowIfNull(gridX); + ArgumentNullException.ThrowIfNull(gridY); + + gridX = Math.Min(gridX, width); + gridY = Math.Min(gridY, height); + + var baseWidth = width / gridX; + var remWidth = width % gridX; + var baseHeight = height / gridY; + var remHeight = height % gridY; + + var curY = 0; + for (var gy = 0; gy < gridY; gy++) + { + var squareHeight = baseHeight + (gy < remHeight ? 1 : 0); // первые remHeight блоков выше на 1 пиксель + var curX = 0; + for (var gx = 0; gx < gridX; gx++) + { + var squareWidth = baseWidth + (gx < remWidth ? 1 : 0); // первые remWidth блоков шире на 1 пиксель + yield return new WorkRect(curX, curY, squareWidth, squareHeight); + + curX += squareWidth; + } + curY += squareHeight; + } + } +} + diff --git a/src/BMPConvolver.Core/WorkPartitioning/PartitioningMode.cs b/src/BMPConvolver.Core/WorkPartitioning/PartitioningMode.cs new file mode 100644 index 0000000..2663a99 --- /dev/null +++ b/src/BMPConvolver.Core/WorkPartitioning/PartitioningMode.cs @@ -0,0 +1,10 @@ +namespace BMPConvolver.Core.WorkPartitioning; + +public enum PartitioningMode +{ + Pixels = 0, + Rows = 1, + Columns = 2, + Grid = 3, +} + diff --git a/src/BMPConvolver.Core/WorkPartitioning/WorkRect.cs b/src/BMPConvolver.Core/WorkPartitioning/WorkRect.cs new file mode 100644 index 0000000..3f9ce36 --- /dev/null +++ b/src/BMPConvolver.Core/WorkPartitioning/WorkRect.cs @@ -0,0 +1,8 @@ +namespace BMPConvolver.Core.WorkPartitioning; + +public readonly record struct WorkRect(int X, int Y, int Width, int Height) +{ + public int X2Exclusive => X + Width; + public int Y2Exclusive => Y + Height; +} + From 0dbf03a396d600cbf05990f0e52c31907f9de8af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Thu, 30 Apr 2026 18:51:56 +0300 Subject: [PATCH 12/17] feat: add parallel convolution --- src/BMPConvolver.Core/Convolver.cs | 42 +++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/src/BMPConvolver.Core/Convolver.cs b/src/BMPConvolver.Core/Convolver.cs index 5e03a60..75a9fe2 100644 --- a/src/BMPConvolver.Core/Convolver.cs +++ b/src/BMPConvolver.Core/Convolver.cs @@ -1,3 +1,5 @@ +using BMPConvolver.Core.WorkPartitioning; + namespace BMPConvolver.Core; public static class Convolver @@ -8,20 +10,48 @@ public static GrayImage ConvolveSequential(GrayImage input, Kernel kernel, Borde ArgumentNullException.ThrowIfNull(kernel); var output = new float[input.Pixels.Length]; - ConvolveInternal(input, output, kernel, borderMode); + ConvolveInternal(input, output, kernel, borderMode, new WorkRect(0, 0, input.Width, input.Height)); + return new GrayImage(input.Width, input.Height, output); + } + + public static GrayImage ConvolveParallel( + GrayImage input, + Kernel kernel, + BorderMode borderMode, + PartitioningMode partitioningMode, + int gridX = 1, + int gridY = 1, + int? maxDegreeOfParallelism = null) + { + ArgumentNullException.ThrowIfNull(input); + ArgumentNullException.ThrowIfNull(kernel); + + var output = new float[input.Pixels.Length]; + + var rects = Partitioner.Create(input.Width, input.Height, partitioningMode, gridX, gridY); + var opts = new ParallelOptions + { + MaxDegreeOfParallelism = maxDegreeOfParallelism ?? -1 + }; + + Parallel.ForEach(rects, opts, rect => + { + ConvolveInternal(input, output, kernel, borderMode, rect); + }); + return new GrayImage(input.Width, input.Height, output); } - private static void ConvolveInternal(GrayImage input, float[] output, Kernel kernel, BorderMode borderMode) + private static void ConvolveInternal(GrayImage input, float[] output, Kernel kernel, BorderMode borderMode, WorkRect rect) { var src = input.Pixels; var weights = kernel.Weights; - var yEnd = input.Height; - var xEnd = input.Width; + var yEnd = rect.Y2Exclusive; + var xEnd = rect.X2Exclusive; - for (var y = 0; y < yEnd; y++) - for (var x = 0; x < xEnd; x++) + for (var y = rect.Y; y < yEnd; y++) + for (var x = rect.X; x < xEnd; x++) { var sum = 0f; From ac7a183664c3e233f49dcf22027eede415f09b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Thu, 30 Apr 2026 18:54:02 +0300 Subject: [PATCH 13/17] feat: add benchmarks --- .gitignore | 1 + Convolver.sln | 5 ++ .../BMPConvolver.Benchmarks.csproj | 18 ++++ .../BMPConvolver.Benchmarks/ConvolverBench.cs | 87 +++++++++++++++++++ bench/BMPConvolver.Benchmarks/Program.cs | 12 +++ 5 files changed, 123 insertions(+) create mode 100644 bench/BMPConvolver.Benchmarks/BMPConvolver.Benchmarks.csproj create mode 100644 bench/BMPConvolver.Benchmarks/ConvolverBench.cs create mode 100644 bench/BMPConvolver.Benchmarks/Program.cs diff --git a/.gitignore b/.gitignore index d69c327..9d901db 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ bld/ # .NET Core project.lock.json project.fragment.lock.json +BenchmarkDotNet.Artifacts # ASP.NET Scaffolding ScaffoldingReadMe.txt diff --git a/Convolver.sln b/Convolver.sln index 024a03a..6396c93 100644 --- a/Convolver.sln +++ b/Convolver.sln @@ -13,6 +13,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5F4B7EAD-0 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BMPConvolver.Tests", "test\BMPConvolver.Tests\BMPConvolver.Tests.csproj", "{69A51A1D-CE2E-4C75-AD2E-64ADC5F8E593}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "bench", "bench", "{2507B97A-9F2C-4A9C-A7A3-58541DDFA976}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BMPConvolver.Benchmarks", "bench\BMPConvolver.Benchmarks\BMPConvolver.Benchmarks.csproj", "{4E6BFCD2-D954-4FB9-AC30-1024FA7A9A3C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,5 +47,6 @@ Global {523C0934-7353-45C5-AB89-DFB54C98071F} = {E7170ACE-9AAD-4689-ADC6-22E048D93B80} {A99E9047-15E4-492A-8C77-B2C82CAF0B0B} = {E7170ACE-9AAD-4689-ADC6-22E048D93B80} {69A51A1D-CE2E-4C75-AD2E-64ADC5F8E593} = {5F4B7EAD-0AB8-4FBF-85F1-EA0BEE09832F} + {4E6BFCD2-D954-4FB9-AC30-1024FA7A9A3C} = {2507B97A-9F2C-4A9C-A7A3-58541DDFA976} EndGlobalSection EndGlobal diff --git a/bench/BMPConvolver.Benchmarks/BMPConvolver.Benchmarks.csproj b/bench/BMPConvolver.Benchmarks/BMPConvolver.Benchmarks.csproj new file mode 100644 index 0000000..599fe74 --- /dev/null +++ b/bench/BMPConvolver.Benchmarks/BMPConvolver.Benchmarks.csproj @@ -0,0 +1,18 @@ + + + + + + + + + + + + Exe + net8.0 + enable + enable + + + diff --git a/bench/BMPConvolver.Benchmarks/ConvolverBench.cs b/bench/BMPConvolver.Benchmarks/ConvolverBench.cs new file mode 100644 index 0000000..4e3653c --- /dev/null +++ b/bench/BMPConvolver.Benchmarks/ConvolverBench.cs @@ -0,0 +1,87 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using BMPConvolver.Core; +using BMPConvolver.Core.WorkPartitioning; + +namespace BMPConvolver.Benchmarks; + +[Config(typeof(Config))] +[HideColumns(Column.RatioSD, Column.AllocRatio, Column.Gen0, Column.Gen1, Column.Gen2)] +public class ConvolutionBench +{ + private sealed class Config : ManualConfig + { + public Config() + { + AddJob(Job.Default.WithWarmupCount(3).WithIterationCount(15)); + } + } + private const int Seed = 1; + + [Params(1024, 2048, 4096)] + public int Width { get; set; } + + [Params(1024, 2048, 4096)] + public int Height { get; set; } + + [Params(BorderMode.Zero, BorderMode.Clamp)] + public BorderMode Border { get; set; } + + public int KernelSize { get; set; } = 3; + + public int Grid { get; set; } = 8; + + private GrayImage _input = null!; + private Kernel _kernel = null!; + + [GlobalSetup] + public void Setup() + { + _input = RandomImage(Width, Height); + _kernel = RandomKernelOddFrom(KernelSize, KernelSize); + } + + [Benchmark(Baseline = true, Description = "Sequential")] + public GrayImage Sequential() + => Convolver.ConvolveSequential(_input, _kernel, Border); + + [Benchmark(Description = "Parallel.Pixels")] + public GrayImage ParallelByPixels() + => Convolver.ConvolveParallel(_input, _kernel, Border, PartitioningMode.Pixels, gridX: Grid, gridY: Grid); + + [Benchmark(Description = "Parallel.Rows")] + public GrayImage ParallelByRows() + => Convolver.ConvolveParallel(_input, _kernel, Border, PartitioningMode.Rows, gridX: Grid, gridY: Grid); + + [Benchmark(Description = "Parallel.Columns")] + public GrayImage ParallelByColumns() + => Convolver.ConvolveParallel(_input, _kernel, Border, PartitioningMode.Columns, gridX: Grid, gridY: Grid); + + [Benchmark(Description = "Parallel.Grid")] + public GrayImage ParallelByGrid() + => Convolver.ConvolveParallel(_input, _kernel, Border, PartitioningMode.Grid, gridX: Grid, gridY: Grid); + + private static GrayImage RandomImage(int width, int height) + { + var rnd = new Random(Seed); + var pixels = new float[width * height]; + for (var i = 0; i < pixels.Length; i++) + pixels[i] = (float)rnd.NextDouble(); + return new GrayImage(width, height, pixels); + } + + private static Kernel RandomKernelOddFrom(int widthRequested, int heightRequested) + { + var width = widthRequested <= 1 ? 1 : (widthRequested % 2 == 1 ? widthRequested : widthRequested + 1); + var height = heightRequested <= 1 ? 1 : (heightRequested % 2 == 1 ? heightRequested : heightRequested + 1); + + var rnd = new Random(Seed); + var weights = new float[width * height]; + for (var i = 0; i < weights.Length; i++) + weights[i] = ((float)rnd.NextDouble() - 0.5f) * 0.5f; + return new Kernel(width, height, width / 2, height / 2, weights); + } +} + diff --git a/bench/BMPConvolver.Benchmarks/Program.cs b/bench/BMPConvolver.Benchmarks/Program.cs new file mode 100644 index 0000000..d1153c1 --- /dev/null +++ b/bench/BMPConvolver.Benchmarks/Program.cs @@ -0,0 +1,12 @@ +using BenchmarkDotNet.Running; + +namespace BMPConvolver.Benchmarks; + +public static class Program +{ + public static int Main(string[] args) + { + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); + return 0; + } +} From cb4fbe424d586dbdcb5e1ee0a18a63c566cf69eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Thu, 30 Apr 2026 18:55:31 +0300 Subject: [PATCH 14/17] feat: add parallel realiztion usage to cli --- src/BMPConvolver.Cli/Program.cs | 36 ++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/BMPConvolver.Cli/Program.cs b/src/BMPConvolver.Cli/Program.cs index 004c736..2859e64 100644 --- a/src/BMPConvolver.Cli/Program.cs +++ b/src/BMPConvolver.Cli/Program.cs @@ -1,11 +1,12 @@ using BMPConvolver.Core; using BMPConvolver.Core.ImageSharp; using BMPConvolver.Cli.KernelParser; +using BMPConvolver.Core.WorkPartitioning; static int PrintUsage() { Console.Error.WriteLine("Usage:"); - Console.Error.WriteLine(" BMPConvolver.Cli [--border zero|clamp]"); + Console.Error.WriteLine(" BMPConvolver.Cli [--mode seq|par] [--partition pixels|rows|cols|grid] [--grid XxY] [--border zero|clamp]"); Console.Error.WriteLine(" [--kernel box3|sharpen|identity] [--kernel-text \"...\"] [--kernel-file path.txt]"); Console.Error.WriteLine(); Console.Error.WriteLine("Kernel text format:"); @@ -18,7 +19,11 @@ static int PrintUsage() var inputPath = args[0]; var outputPath = args[1]; +var mode = "seq"; +var partition = "rows"; var border = "zero"; +var gridX = 4; +var gridY = 4; string? kernelPreset = null; string? kernelText = null; string? kernelFile = null; @@ -26,15 +31,26 @@ static int PrintUsage() for (var i = 2; i < args.Length; i++) { var a = args[i]; + if (a == "--mode" && i + 1 < args.Length) { mode = args[++i]; continue; } + if (a == "--partition" && i + 1 < args.Length) { partition = args[++i]; continue; } if (a == "--border" && i + 1 < args.Length) { border = args[++i]; continue; } if (a == "--kernel" && i + 1 < args.Length) { kernelPreset = args[++i]; continue; } if (a == "--kernel-text" && i + 1 < args.Length) { kernelText = args[++i]; continue; } if (a == "--kernel-file" && i + 1 < args.Length) { kernelFile = args[++i]; continue; } + if (a == "--grid" && i + 1 < args.Length) + { + var s = args[++i]; + var parts = s.Split('x'); + if (parts.Length != 2 || !int.TryParse(parts[0], out gridX) || !int.TryParse(parts[1], out gridY)) + return PrintUsage(); + continue; + } return PrintUsage(); } BorderMode borderMode; +PartitioningMode partitionMode; Kernel kernel; try { @@ -45,6 +61,15 @@ static int PrintUsage() _ => throw new ArgumentException($"Unknown border mode: {border}") }; + partitionMode = partition.ToLowerInvariant() switch + { + "pixels" => PartitioningMode.Pixels, + "rows" => PartitioningMode.Rows, + "cols" or "columns" => PartitioningMode.Columns, + "grid" => PartitioningMode.Grid, + _ => throw new ArgumentException($"Unknown partition mode: {partition}") + }; + kernel = ResolveKernel(kernelPreset, kernelText, kernelFile); } catch (Exception e) @@ -83,7 +108,12 @@ static Kernel ResolveKernel(string? preset, string? text, string? file) GrayImage output; try { - output = Convolver.ConvolveSequential(input, kernel, borderMode); + output = mode.ToLowerInvariant() switch + { + "seq" => Convolver.ConvolveSequential(input, kernel, borderMode), + "par" => Convolver.ConvolveParallel(input, kernel, borderMode, partitionMode, gridX, gridY), + _ => throw new ArgumentException($"Unknown mode: {mode}") + }; } catch (Exception e) { @@ -93,5 +123,5 @@ static Kernel ResolveKernel(string? preset, string? text, string? file) timer.Stop(); GrayImageIo.SaveGrayAsBmp(output, outputPath); -Console.WriteLine($"Done. {input.Width}x{input.Height}, border={border}, elapsed={timer.ElapsedMilliseconds} ms"); +Console.WriteLine($"Done. {input.Width}x{input.Height}, mode={mode}, partition={partition}, border={border}, elapsed={timer.ElapsedMilliseconds} ms"); return 0; From de2c3fbdf48159a168bbaf0b123cacfc2a5b1d67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Thu, 30 Apr 2026 18:56:01 +0300 Subject: [PATCH 15/17] feat: add benchmarks to readme --- README.md | 149 ++++++++++++++++++++++ public/img/Border_mode_Clamp_Graphics.png | Bin 0 -> 39215 bytes public/img/Border_mode_Clamp_columnar.png | Bin 0 -> 21622 bytes public/img/Border_mode_Zero_Graphics.png | Bin 0 -> 37186 bytes public/img/Border_mode_Zero_columnar.png | Bin 0 -> 21361 bytes 5 files changed, 149 insertions(+) create mode 100644 public/img/Border_mode_Clamp_Graphics.png create mode 100644 public/img/Border_mode_Clamp_columnar.png create mode 100644 public/img/Border_mode_Zero_Graphics.png create mode 100644 public/img/Border_mode_Zero_columnar.png diff --git a/README.md b/README.md index 84f56c6..b554504 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +# Запуск Для запуска приложения выполните следующую команду: @@ -21,3 +22,151 @@ dotnet run --project src/BMPConvolver.Cli/BMPConvolver.Cli.csproj hK7ds>ZOD-8X86r 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%0D#f^#bg~m^~o5kPge_cR$|QSrWY* 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

Lcyy6aihrS&vtJT{aqL&bcS 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 w#en(PsxXs{{Rx2uPDA(gP&w3<82g=`~SlA_5}4 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 Q5XYl^LhpFa(JQzAA5olR{r~^~ literal 0 HcmV?d00001 diff --git a/public/img/Border_mode_Zero_Graphics.png b/public/img/Border_mode_Zero_Graphics.png new file mode 100644 index 0000000000000000000000000000000000000000..5fa5f354403335447816bde800788a35a49efe7b GIT binary patch literal 37186 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

  • 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%0DiDmGHT9MQ&PHTKqq+n*~YlL$n|iCv$W2 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 literal 0 HcmV?d00001 From 8d98c70df9c3b6e6815b22bd64b5a5ab6bf5c7de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Thu, 30 Apr 2026 18:59:08 +0300 Subject: [PATCH 16/17] test: add tests for parallel realization --- .../CoreTests/CoreUnitTests.cs | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/test/BMPConvolver.Tests/CoreTests/CoreUnitTests.cs b/test/BMPConvolver.Tests/CoreTests/CoreUnitTests.cs index 4ecea21..79e2c18 100644 --- a/test/BMPConvolver.Tests/CoreTests/CoreUnitTests.cs +++ b/test/BMPConvolver.Tests/CoreTests/CoreUnitTests.cs @@ -1,4 +1,5 @@ using BMPConvolver.Core; +using BMPConvolver.Core.WorkPartitioning; using OpenCvSharp; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; @@ -101,6 +102,111 @@ public void SequentialApplication_EqualsKernelComposition(int width, int height, AssertImagesEqualInterior(seq, one, marginX, marginY, eps: Epsilon); } + [Theory] + [InlineData(1, 1, BorderMode.Zero)] + [InlineData(2, 7, BorderMode.Zero)] + [InlineData(13, 9, BorderMode.Zero)] + [InlineData(64, 64, BorderMode.Zero)] + [InlineData(127, 33, BorderMode.Zero)] + [InlineData(128, 128, BorderMode.Zero)] + [InlineData(400, 200, BorderMode.Zero)] + [InlineData(1, 1, BorderMode.Clamp)] + [InlineData(2, 7, BorderMode.Clamp)] + [InlineData(13, 9, BorderMode.Clamp)] + [InlineData(64, 64, BorderMode.Clamp)] + [InlineData(127, 33, BorderMode.Clamp)] + [InlineData(128, 128, BorderMode.Clamp)] + [InlineData(400, 200, BorderMode.Clamp)] + public void Parallel_EqualsSequential_PixelsPartitioning(int width, int height, BorderMode borderMode) + { + var kernel = RandomKernelOdd(maxSize: 9); + + var img = RandomImage(width, height); + var seq = Convolver.ConvolveSequential(img, kernel, borderMode); + + var parPixel = Convolver.ConvolveParallel(img, kernel, borderMode, PartitioningMode.Pixels); + + AssertImagesEqual(seq, parPixel); + } + + [Theory] + [InlineData(1, 1, BorderMode.Zero)] + [InlineData(2, 7, BorderMode.Zero)] + [InlineData(13, 9, BorderMode.Zero)] + [InlineData(64, 64, BorderMode.Zero)] + [InlineData(127, 33, BorderMode.Zero)] + [InlineData(128, 128, BorderMode.Zero)] + [InlineData(400, 200, BorderMode.Zero)] + [InlineData(1, 1, BorderMode.Clamp)] + [InlineData(2, 7, BorderMode.Clamp)] + [InlineData(13, 9, BorderMode.Clamp)] + [InlineData(64, 64, BorderMode.Clamp)] + [InlineData(127, 33, BorderMode.Clamp)] + [InlineData(128, 128, BorderMode.Clamp)] + [InlineData(400, 200, BorderMode.Clamp)] + public void Parallel_EqualsSequential_ForRowsPartitioning(int width, int height, BorderMode borderMode) + { + var kernel = RandomKernelOdd(maxSize: 9); + + var img = RandomImage(width, height); + var seq = Convolver.ConvolveSequential(img, kernel, borderMode); + var parRows = Convolver.ConvolveParallel(img, kernel, borderMode, PartitioningMode.Rows); + + AssertImagesEqual(seq, parRows); + } + + [Theory] + [InlineData(1, 1, BorderMode.Zero)] + [InlineData(2, 7, BorderMode.Zero)] + [InlineData(13, 9, BorderMode.Zero)] + [InlineData(64, 64, BorderMode.Zero)] + [InlineData(127, 33, BorderMode.Zero)] + [InlineData(128, 128, BorderMode.Zero)] + [InlineData(400, 200, BorderMode.Zero)] + [InlineData(1, 1, BorderMode.Clamp)] + [InlineData(2, 7, BorderMode.Clamp)] + [InlineData(13, 9, BorderMode.Clamp)] + [InlineData(64, 64, BorderMode.Clamp)] + [InlineData(127, 33, BorderMode.Clamp)] + [InlineData(128, 128, BorderMode.Clamp)] + [InlineData(400, 200, BorderMode.Clamp)] + public void Parallel_EqualsSequential_ForColumnsPartitioning(int width, int height, BorderMode borderMode) + { + var kernel = RandomKernelOdd(maxSize: 9); + + var img = RandomImage(width, height); + var seq = Convolver.ConvolveSequential(img, kernel, borderMode); + var parCols = Convolver.ConvolveParallel(img, kernel, borderMode, PartitioningMode.Columns); + + AssertImagesEqual(seq, parCols); + } + + [Theory] + [InlineData(1, 1, BorderMode.Zero)] + [InlineData(2, 7, BorderMode.Zero)] + [InlineData(13, 9, BorderMode.Zero)] + [InlineData(64, 64, BorderMode.Zero)] + [InlineData(127, 33, BorderMode.Zero)] + [InlineData(128, 128, BorderMode.Zero)] + [InlineData(400, 200, BorderMode.Zero)] + [InlineData(1, 1, BorderMode.Clamp)] + [InlineData(2, 7, BorderMode.Clamp)] + [InlineData(13, 9, BorderMode.Clamp)] + [InlineData(64, 64, BorderMode.Clamp)] + [InlineData(127, 33, BorderMode.Clamp)] + [InlineData(128, 128, BorderMode.Clamp)] + [InlineData(400, 200, BorderMode.Clamp)] + public void Parallel_EqualsSequential_ForGridPartitioning(int width, int height, BorderMode borderMode) + { + var kernel = RandomKernelOdd(maxSize: 9); + + var img = RandomImage(width, height); + var seq = Convolver.ConvolveSequential(img, kernel, borderMode); + var parGrid = Convolver.ConvolveParallel(img, kernel, borderMode, PartitioningMode.Grid, gridX: 8, gridY: 8); + + AssertImagesEqual(seq, parGrid); + } + [Theory] [InlineData(64, 48)] [InlineData(128, 128)] From d9e6de0e4a1ad61db45d8f7bb039f9d3a00ab651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Tue, 12 May 2026 19:55:35 +0300 Subject: [PATCH 17/17] feat: add benchmarks error to readme --- README.md | 220 +++++++++++++++++++++++++++--------------------------- 1 file changed, 110 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index b554504..1fb94b8 100644 --- a/README.md +++ b/README.md @@ -48,61 +48,61 @@ dotnet run --project bench/BMPConvolver.Benchmarks/BMPConvolver.Benchmarks.cspro ![График_Zero](public/img/Border_mode_Zero_Graphics.png) ### Таблица -| Method | Width | Height | Mean | -|----------------- |------ |------- |-----------:| -| Sequential | 1024 | 1024 | 11.347 ms | -| Parallel.Pixels | 1024 | 1024 | 24.677 ms | -| Parallel.Rows | 1024 | 1024 | 2.510 ms | -| Parallel.Columns | 1024 | 1024 | 2.313 ms | -| Parallel.Grid | 1024 | 1024 | 2.247 ms | -| | | | | -| Sequential | 1024 | 2048 | 22.219 ms | -| Parallel.Pixels | 1024 | 2048 | 51.281 ms | -| Parallel.Rows | 1024 | 2048 | 4.961 ms | -| Parallel.Columns | 1024 | 2048 | 5.252 ms | -| Parallel.Grid | 1024 | 2048 | 4.517 ms | -| | | | | -| Sequential | 1024 | 4096 | 48.451 ms | -| Parallel.Pixels | 1024 | 4096 | 110.977 ms | -| Parallel.Rows | 1024 | 4096 | 9.724 ms | -| Parallel.Columns | 1024 | 4096 | 10.589 ms | -| Parallel.Grid | 1024 | 4096 | 8.937 ms | -| | | | | -| Sequential | 2048 | 1024 | 24.379 ms | -| Parallel.Pixels | 2048 | 1024 | 49.390 ms | -| Parallel.Rows | 2048 | 1024 | 4.982 ms | -| Parallel.Columns | 2048 | 1024 | 6.388 ms | -| Parallel.Grid | 2048 | 1024 | 5.264 ms | -| | | | | -| Sequential | 2048 | 2048 | 48.880 ms | -| Parallel.Pixels | 2048 | 2048 | 102.091 ms | -| Parallel.Rows | 2048 | 2048 | 9.650 ms | -| Parallel.Columns | 2048 | 2048 | 9.829 ms | -| Parallel.Grid | 2048 | 2048 | 9.884 ms | -| | | | | -| Sequential | 2048 | 4096 | 98.503 ms | -| Parallel.Pixels | 2048 | 4096 | 236.901 ms | -| Parallel.Rows | 2048 | 4096 | 18.314 ms | -| Parallel.Columns | 2048 | 4096 | 20.790 ms | -| Parallel.Grid | 2048 | 4096 | 19.169 ms | -| | | | | -| Sequential | 4096 | 1024 | 48.863 ms | -| Parallel.Pixels | 4096 | 1024 | 103.113 ms | -| Parallel.Rows | 4096 | 1024 | 8.819 ms | -| Parallel.Columns | 4096 | 1024 | 9.795 ms | -| Parallel.Grid | 4096 | 1024 | 9.115 ms | -| | | | | -| Sequential | 4096 | 2048 | 96.191 ms | -| Parallel.Pixels | 4096 | 2048 | 223.957 ms | -| Parallel.Rows | 4096 | 2048 | 16.587 ms | -| Parallel.Columns | 4096 | 2048 | 22.102 ms | -| Parallel.Grid | 4096 | 2048 | 20.067 ms | -| | | | | -| Sequential | 4096 | 4096 | 203.417 ms | -| Parallel.Pixels | 4096 | 4096 | 483.104 ms | -| Parallel.Rows | 4096 | 4096 | 37.907 ms | -| Parallel.Columns | 4096 | 4096 | 45.954 ms | -| Parallel.Grid | 4096 | 4096 | 38.499 ms | +| Method | Width | Height | Mean | Error | +|----------------- |------ |------- |-----------:|-----------:| +| Sequential | 1024 | 1024 | 11.347 ms | 0.1680 ms | +| Parallel.Pixels | 1024 | 1024 | 24.677 ms | 1.3195 ms | +| Parallel.Rows | 1024 | 1024 | 2.510 ms | 0.0188 ms | +| Parallel.Columns | 1024 | 1024 | 2.313 ms | 0.0259 ms | +| Parallel.Grid | 1024 | 1024 | 2.247 ms | 0.0162 ms | +| | | | | | +| Sequential | 1024 | 2048 | 22.219 ms | 0.0945 ms | +| Parallel.Pixels | 1024 | 2048 | 51.281 ms | 5.7572 ms | +| Parallel.Rows | 1024 | 2048 | 4.961 ms | 0.0466 ms | +| Parallel.Columns | 1024 | 2048 | 5.252 ms | 0.0444 ms | +| Parallel.Grid | 1024 | 2048 | 4.517 ms | 0.0601 ms | +| | | | | | +| Sequential | 1024 | 4096 | 48.451 ms | 0.1076 ms | +| Parallel.Pixels | 1024 | 4096 | 110.977 ms | 19.1888 ms | +| Parallel.Rows | 1024 | 4096 | 9.724 ms | 0.0335 ms | +| Parallel.Columns | 1024 | 4096 | 10.589 ms | 0.0641 ms | +| Parallel.Grid | 1024 | 4096 | 8.937 ms | 0.0491 ms | +| | | | | | +| Sequential | 2048 | 1024 | 24.379 ms | 0.1265 ms | +| Parallel.Pixels | 2048 | 1024 | 49.390 ms | 6.5355 ms | +| Parallel.Rows | 2048 | 1024 | 4.982 ms | 0.0440 ms | +| Parallel.Columns | 2048 | 1024 | 6.388 ms | 0.1713 ms | +| Parallel.Grid | 2048 | 1024 | 5.264 ms | 0.1405 ms | +| | | | | | +| Sequential | 2048 | 2048 | 48.880 ms | 0.2759 ms | +| Parallel.Pixels | 2048 | 2048 | 102.091 ms | 8.1502 ms | +| Parallel.Rows | 2048 | 2048 | 9.650 ms | 0.0547 ms | +| Parallel.Columns | 2048 | 2048 | 9.829 ms | 0.1386 ms | +| Parallel.Grid | 2048 | 2048 | 9.884 ms | 0.0655 ms | +| | | | | | +| Sequential | 2048 | 4096 | 98.503 ms | 0.8038 ms | +| Parallel.Pixels | 2048 | 4096 | 236.901 ms | 4.6947 ms | +| Parallel.Rows | 2048 | 4096 | 18.314 ms | 0.2121 ms | +| Parallel.Columns | 2048 | 4096 | 20.790 ms | 0.1510 ms | +| Parallel.Grid | 2048 | 4096 | 19.169 ms | 0.1776 ms | +| | | | | | +| Sequential | 4096 | 1024 | 48.863 ms | 0.2360 ms | +| Parallel.Pixels | 4096 | 1024 | 103.113 ms | 11.9344 ms | +| Parallel.Rows | 4096 | 1024 | 8.819 ms | 0.0642 ms | +| Parallel.Columns | 4096 | 1024 | 9.795 ms | 0.0708 ms | +| Parallel.Grid | 4096 | 1024 | 9.115 ms | 0.2993 ms | +| | | | | | +| Sequential | 4096 | 2048 | 96.191 ms | 3.7277 ms | +| Parallel.Pixels | 4096 | 2048 | 223.957 ms | 24.3798 ms | +| Parallel.Rows | 4096 | 2048 | 16.587 ms | 0.0625 ms | +| Parallel.Columns | 4096 | 2048 | 22.102 ms | 0.0916 ms | +| Parallel.Grid | 4096 | 2048 | 20.067 ms | 0.2590 ms | +| | | | | | +| Sequential | 4096 | 4096 | 203.417 ms | 3.6494 ms | +| Parallel.Pixels | 4096 | 4096 | 483.104 ms | 3.5202 ms | +| Parallel.Rows | 4096 | 4096 | 37.907 ms | 0.3498 ms | +| Parallel.Columns | 4096 | 4096 | 45.954 ms | 0.4596 ms | +| Parallel.Grid | 4096 | 4096 | 38.499 ms | 0.2745 ms | ## Border mode: Clamp @@ -115,58 +115,58 @@ dotnet run --project bench/BMPConvolver.Benchmarks/BMPConvolver.Benchmarks.cspro ### Таблица -| Method | Width | Height | Mean | -|----------------- |------ |------- |-----------:| -| Sequential | 1024 | 1024 | 10.899 ms | -| Parallel.Pixels | 1024 | 1024 | 25.133 ms | -| Parallel.Rows | 1024 | 1024 | 2.423 ms | -| Parallel.Columns | 1024 | 1024 | 2.210 ms | -| Parallel.Grid | 1024 | 1024 | 2.339 ms | -| | | | | -| Sequential | 1024 | 2048 | 23.711 ms | -| Parallel.Pixels | 1024 | 2048 | 49.684 ms | -| Parallel.Rows | 1024 | 2048 | 4.385 ms | -| Parallel.Columns | 1024 | 2048 | 4.687 ms | -| Parallel.Grid | 1024 | 2048 | 4.933 ms | -| | | | | -| Sequential | 1024 | 4096 | 43.248 ms | -| Parallel.Pixels | 1024 | 4096 | 102.942 ms | -| Parallel.Rows | 1024 | 4096 | 9.432 ms | -| Parallel.Columns | 1024 | 4096 | 10.499 ms | -| Parallel.Grid | 1024 | 4096 | 9.198 ms | -| | | | | -| Sequential | 2048 | 1024 | 22.027 ms | -| Parallel.Pixels | 2048 | 1024 | 47.339 ms | -| Parallel.Rows | 2048 | 1024 | 4.699 ms | -| Parallel.Columns | 2048 | 1024 | 6.117 ms | -| Parallel.Grid | 2048 | 1024 | 5.095 ms | -| | | | | -| Sequential | 2048 | 2048 | 47.986 ms | -| Parallel.Pixels | 2048 | 2048 | 97.947 ms | -| Parallel.Rows | 2048 | 2048 | 8.399 ms | -| Parallel.Columns | 2048 | 2048 | 9.672 ms | -| Parallel.Grid | 2048 | 2048 | 9.903 ms | -| | | | | -| Sequential | 2048 | 4096 | 90.954 ms | -| Parallel.Pixels | 2048 | 4096 | 233.673 ms | -| Parallel.Rows | 2048 | 4096 | 20.059 ms | -| Parallel.Columns | 2048 | 4096 | 21.500 ms | -| Parallel.Grid | 2048 | 4096 | 20.564 ms | -| | | | | -| Sequential | 4096 | 1024 | 43.379 ms | -| Parallel.Pixels | 4096 | 1024 | 111.441 ms | -| Parallel.Rows | 4096 | 1024 | 8.383 ms | -| Parallel.Columns | 4096 | 1024 | 9.556 ms | -| Parallel.Grid | 4096 | 1024 | 10.015 ms | -| | | | | -| Sequential | 4096 | 2048 | 98.364 ms | -| Parallel.Pixels | 4096 | 2048 | 213.736 ms | -| Parallel.Rows | 4096 | 2048 | 17.360 ms | -| Parallel.Columns | 4096 | 2048 | 22.656 ms | -| Parallel.Grid | 4096 | 2048 | 18.816 ms | -| | | | | -| Sequential | 4096 | 4096 | 190.988 ms | -| Parallel.Pixels | 4096 | 4096 | 470.957 ms | -| Parallel.Rows | 4096 | 4096 | 35.611 ms | -| Parallel.Columns | 4096 | 4096 | 47.020 ms | -| Parallel.Grid | 4096 | 4096 | 35.363 ms | +| Method | Width | Height | Mean | Error | +|----------------- |------ |------- |-----------:|-----------:| +| Sequential | 1024 | 1024 | 10.899 ms | 0.0522 ms | +| Parallel.Pixels | 1024 | 1024 | 25.133 ms | 1.8560 ms | +| Parallel.Rows | 1024 | 1024 | 2.423 ms | 0.0177 ms | +| Parallel.Columns | 1024 | 1024 | 2.210 ms | 0.0153 ms | +| Parallel.Grid | 1024 | 1024 | 2.339 ms | 0.0334 ms | +| | | | | | +| Sequential | 1024 | 2048 | 23.711 ms | 0.1069 ms | +| Parallel.Pixels | 1024 | 2048 | 49.684 ms | 5.6650 ms | +| Parallel.Rows | 1024 | 2048 | 4.385 ms | 0.0375 ms | +| Parallel.Columns | 1024 | 2048 | 4.687 ms | 0.0334 ms | +| Parallel.Grid | 1024 | 2048 | 4.933 ms | 0.0552 ms | +| | | | | | +| Sequential | 1024 | 4096 | 43.248 ms | 0.2635 ms | +| Parallel.Pixels | 1024 | 4096 | 102.942 ms | 13.7667 ms | +| Parallel.Rows | 1024 | 4096 | 9.432 ms | 0.0324 ms | +| Parallel.Columns | 1024 | 4096 | 10.499 ms | 0.1717 ms | +| Parallel.Grid | 1024 | 4096 | 9.198 ms | 0.0589 ms | +| | | | | | +| Sequential | 2048 | 1024 | 22.027 ms | 0.1667 ms | +| Parallel.Pixels | 2048 | 1024 | 47.339 ms | 5.4978 ms | +| Parallel.Rows | 2048 | 1024 | 4.699 ms | 0.1312 ms | +| Parallel.Columns | 2048 | 1024 | 6.117 ms | 0.1464 ms | +| Parallel.Grid | 2048 | 1024 | 5.095 ms | 0.0555 ms | +| | | | | | +| Sequential | 2048 | 2048 | 47.986 ms | 0.2603 ms | +| Parallel.Pixels | 2048 | 2048 | 97.947 ms | 9.1101 ms | +| Parallel.Rows | 2048 | 2048 | 8.399 ms | 0.1387 ms | +| Parallel.Columns | 2048 | 2048 | 9.672 ms | 0.1301 ms | +| Parallel.Grid | 2048 | 2048 | 9.903 ms | 0.2243 ms | +| | | | | | +| Sequential | 2048 | 4096 | 90.954 ms | 6.1812 ms | +| Parallel.Pixels | 2048 | 4096 | 233.673 ms | 26.1762 ms | +| Parallel.Rows | 2048 | 4096 | 20.059 ms | 0.3383 ms | +| Parallel.Columns | 2048 | 4096 | 21.500 ms | 0.2026 ms | +| Parallel.Grid | 2048 | 4096 | 20.564 ms | 0.3094 ms | +| | | | | | +| Sequential | 4096 | 1024 | 43.379 ms | 0.2542 ms | +| Parallel.Pixels | 4096 | 1024 | 111.441 ms | 20.5576 ms | +| Parallel.Rows | 4096 | 1024 | 8.383 ms | 0.0320 ms | +| Parallel.Columns | 4096 | 1024 | 9.556 ms | 0.0487 ms | +| Parallel.Grid | 4096 | 1024 | 10.015 ms | 0.0773 ms | +| | | | | | +| Sequential | 4096 | 2048 | 98.364 ms | 1.2503 ms | +| Parallel.Pixels | 4096 | 2048 | 213.736 ms | 31.6862 ms | +| Parallel.Rows | 4096 | 2048 | 17.360 ms | 0.0749 ms | +| Parallel.Columns | 4096 | 2048 | 22.656 ms | 0.1534 ms | +| Parallel.Grid | 4096 | 2048 | 18.816 ms | 0.2907 ms | +| | | | | | +| Sequential | 4096 | 4096 | 190.988 ms | 16.7715 ms | +| Parallel.Pixels | 4096 | 4096 | 470.957 ms | 4.9517 ms | +| Parallel.Rows | 4096 | 4096 | 35.611 ms | 0.3819 ms | +| Parallel.Columns | 4096 | 4096 | 47.020 ms | 0.2583 ms | +| Parallel.Grid | 4096 | 4096 | 35.363 ms | 0.5478 ms |