diff --git a/src/ImageJCsharp.App/Form1.cs b/src/ImageJCsharp.App/Form1.cs index 2bb2691..6b487b2 100644 --- a/src/ImageJCsharp.App/Form1.cs +++ b/src/ImageJCsharp.App/Form1.cs @@ -70,6 +70,9 @@ private void BuildUi() var process = AddMenu(menu, "&Process"); AddActiveImageItem(process, "&Invert", ApplyInvert); AddActiveImageItem(process, "&Find Edges", ApplyFindEdges); + AddActiveImageItem(process, "&Gaussian Blur", ApplyGaussianBlur); + AddActiveImageItem(process, "&Median", ApplyMedianFilter); + AddActiveImageItem(process, "&Sharpen", ApplySharpen); AddActiveImageItem(process, "&Threshold...", ApplyThreshold); var analyze = AddMenu(menu, "&Analyze"); @@ -281,6 +284,39 @@ private void ApplyFindEdges() RefreshDisplay(); } + private void ApplyGaussianBlur() + { + if (_document is null) + { + return; + } + + _document.Image = ImageProcessor.GaussianBlur(_document.Image); + RefreshDisplay(); + } + + private void ApplyMedianFilter() + { + if (_document is null) + { + return; + } + + _document.Image = ImageProcessor.MedianFilter(_document.Image); + RefreshDisplay(); + } + + private void ApplySharpen() + { + if (_document is null) + { + return; + } + + _document.Image = ImageProcessor.Sharpen(_document.Image); + RefreshDisplay(); + } + private void ApplyThreshold() { if (_document is null) diff --git a/src/ImageJCsharp.Core/ImageProcessor.cs b/src/ImageJCsharp.Core/ImageProcessor.cs index 82c88c1..e64b107 100644 --- a/src/ImageJCsharp.Core/ImageProcessor.cs +++ b/src/ImageJCsharp.Core/ImageProcessor.cs @@ -76,4 +76,120 @@ public static GrayImage SobelEdges(GrayImage image) return result; } + + public static GrayImage GaussianBlur(GrayImage image) + { + if (image is null) + { + throw new ArgumentNullException(nameof(image)); + } + + int[,] kernel = + { + { 1, 2, 1 }, + { 2, 4, 2 }, + { 1, 2, 1 } + }; + + var result = new GrayImage(image.Width, image.Height); + for (var y = 0; y < image.Height; y++) + { + for (var x = 0; x < image.Width; x++) + { + var sum = 0; + for (var ky = -1; ky <= 1; ky++) + { + for (var kx = -1; kx <= 1; kx++) + { + sum += SampleClamped(image, x + kx, y + ky) * kernel[ky + 1, kx + 1]; + } + } + + result[x, y] = (ushort)Math.Round(sum / 16d); + } + } + + return result; + } + + public static GrayImage MedianFilter(GrayImage image) + { + if (image is null) + { + throw new ArgumentNullException(nameof(image)); + } + + var result = new GrayImage(image.Width, image.Height); + var values = new ushort[9]; + for (var y = 0; y < image.Height; y++) + { + for (var x = 0; x < image.Width; x++) + { + var index = 0; + for (var ky = -1; ky <= 1; ky++) + { + for (var kx = -1; kx <= 1; kx++) + { + values[index++] = SampleClamped(image, x + kx, y + ky); + } + } + + Array.Sort(values); + result[x, y] = values[4]; + } + } + + return result; + } + + public static GrayImage Sharpen(GrayImage image) + { + if (image is null) + { + throw new ArgumentNullException(nameof(image)); + } + + var result = new GrayImage(image.Width, image.Height); + for (var y = 0; y < image.Height; y++) + { + for (var x = 0; x < image.Width; x++) + { + var value = + (5 * image[x, y]) - + SampleClamped(image, x - 1, y) - + SampleClamped(image, x + 1, y) - + SampleClamped(image, x, y - 1) - + SampleClamped(image, x, y + 1); + + result[x, y] = ClampToUShort(value); + } + } + + return result; + } + + private static ushort SampleClamped(GrayImage image, int x, int y) + { + return image[Clamp(x, 0, image.Width - 1), Clamp(y, 0, image.Height - 1)]; + } + + private static int Clamp(int value, int minimum, int maximum) + { + return Math.Min(Math.Max(value, minimum), maximum); + } + + private static ushort ClampToUShort(int value) + { + if (value < 0) + { + return 0; + } + + if (value > ushort.MaxValue) + { + return ushort.MaxValue; + } + + return (ushort)value; + } } diff --git a/tests/ImageJCsharp.App.Tests/FormStartupTests.cs b/tests/ImageJCsharp.App.Tests/FormStartupTests.cs index fa50f42..e6b86f8 100644 --- a/tests/ImageJCsharp.App.Tests/FormStartupTests.cs +++ b/tests/ImageJCsharp.App.Tests/FormStartupTests.cs @@ -53,6 +53,9 @@ public void Form1DisablesImageCommandsWhenNoImageIsActive() ["File/Exit"] = FindMenuItem(form, "File", "Exit").Enabled, ["Process/Invert"] = FindMenuItem(form, "Process", "Invert").Enabled, ["Process/Find Edges"] = FindMenuItem(form, "Process", "Find Edges").Enabled, + ["Process/Gaussian Blur"] = FindMenuItem(form, "Process", "Gaussian Blur").Enabled, + ["Process/Median"] = FindMenuItem(form, "Process", "Median").Enabled, + ["Process/Sharpen"] = FindMenuItem(form, "Process", "Sharpen").Enabled, ["Process/Threshold"] = FindMenuItem(form, "Process", "Threshold...").Enabled, ["Analyze/Measure"] = FindMenuItem(form, "Analyze", "Measure").Enabled, ["Analyze/Histogram"] = FindMenuItem(form, "Analyze", "Histogram").Enabled, @@ -139,6 +142,9 @@ public void FileCloseClearsActiveImageState() InvokePrivateMethod(form, "CloseImage"); InvokePrivateMethod(form, "ApplyInvert"); InvokePrivateMethod(form, "ApplyFindEdges"); + InvokePrivateMethod(form, "ApplyGaussianBlur"); + InvokePrivateMethod(form, "ApplyMedianFilter"); + InvokePrivateMethod(form, "ApplySharpen"); InvokePrivateMethod(form, "MeasureCurrentRoi"); InvokePrivateMethod(form, "ShowHistogram"); } diff --git a/tests/ImageJCsharp.Core.Tests/CoreImageTests.cs b/tests/ImageJCsharp.Core.Tests/CoreImageTests.cs index 9898cdb..1b42905 100644 --- a/tests/ImageJCsharp.Core.Tests/CoreImageTests.cs +++ b/tests/ImageJCsharp.Core.Tests/CoreImageTests.cs @@ -194,6 +194,53 @@ public void SobelFindsVerticalEdge() Assert.Equal(0, edges[0, 0]); } + [Fact] + public void GaussianBlurUsesThreeByThreeWeightedKernel() + { + var image = GrayImage.FromPixels(3, 3, new ushort[] + { + 0, 0, 0, + 0, 160, 0, + 0, 0, 0 + }); + + var blurred = ImageProcessor.GaussianBlur(image); + + Assert.Equal(40, blurred[1, 1]); + Assert.Equal(20, blurred[0, 1]); + Assert.Equal(10, blurred[0, 0]); + } + + [Fact] + public void MedianFilterReplacesCenterWithNeighborhoodMedian() + { + var image = GrayImage.FromPixels(3, 3, new ushort[] + { + 10, 10, 10, + 10, 250, 10, + 10, 10, 10 + }); + + var filtered = ImageProcessor.MedianFilter(image); + + Assert.Equal(10, filtered[1, 1]); + } + + [Fact] + public void SharpenEnhancesCenterPixelWithFourNeighborKernel() + { + var image = GrayImage.FromPixels(3, 3, new ushort[] + { + 10, 10, 10, + 10, 20, 10, + 10, 10, 10 + }); + + var sharpened = ImageProcessor.Sharpen(image); + + Assert.Equal(60, sharpened[1, 1]); + } + [Fact] public void HistogramCountsKnownEightBitValues() {