diff --git a/algorithms-modules/algorithms-numeric/src/main/java/com/baeldung/algorithms/fastgaussianblur/FastGaussianBlur.java b/algorithms-modules/algorithms-numeric/src/main/java/com/baeldung/algorithms/fastgaussianblur/FastGaussianBlur.java new file mode 100644 index 000000000000..42b2ef10ba2c --- /dev/null +++ b/algorithms-modules/algorithms-numeric/src/main/java/com/baeldung/algorithms/fastgaussianblur/FastGaussianBlur.java @@ -0,0 +1,100 @@ +package com.baeldung.algorithms.fastgaussianblur; + +/** + * Fast Gaussian Blur approximation using sliding window over rows and columns + */ +public class FastGaussianBlur { + /** + * Horizontal box blur over rows + * + * @param source Source image row. + * @param target Target image row. + * @param width Image width. + * @param height Image height. + * @param radius Blur radius. + */ + private static void horizontalBoxBlur(int[] source, int[] target, int width, int height, int radius) { + double scale = 1.0 / (radius * 2 + 1); + + for (int y = 0; y < height; y++) { + int windowSum = 0; + int offset = y * width; + + // 1. We initialize the sliding window for the first pixel in the row + for (int x = -radius; x <= radius; x++) { + int safeX = Math.min(Math.max(x, 0), width - 1); + windowSum += source[offset + safeX]; + } + + // 2. We slide the window across the row + for (int x = 0; x < width; x++) { + target[offset + x] = (int) Math.round(windowSum * scale); + + // 2a. We subtract the leaving pixel and add the entering pixel + int leftX = Math.max(x - radius, 0); + int rightX = Math.min(x + radius + 1, width - 1); + + // 2b. We update the sliding window + windowSum -= source[offset + leftX]; + windowSum += source[offset + rightX]; + } + } + } + + /** + * Vertical box blur over columns. It is identical in logic, but we just + * traverse columns instead of rows + * + * @param source Source image row. + * @param target Target image row. + * @param width Image width. + * @param height Image height. + * @param radius Blur radius. + */ + private static void verticalBoxBlur(int[] source, int[] target, int width, int height, int radius) { + double scale = 1.0 / (radius * 2 + 1); + + for (int x = 0; x < width; x++) { + int windowSum = 0; + + for (int y = -radius; y <= radius; y++) { + int safeY = Math.min(Math.max(y, 0), height - 1); + windowSum += source[safeY * width + x]; + } + + for (int y = 0; y < height; y++) { + target[y * width + x] = (int) Math.round(windowSum * scale); + + int topY = Math.max(y - radius, 0); + int bottomY = Math.min(y + radius + 1, height - 1); + + windowSum -= source[topY * width + x]; + windowSum += source[bottomY * width + x]; + } + } + } + + /** + * Main orchestrator to call Fast Gaussian 2D Blur by separating it into + * two 1D operations + * @param source Source image row. + * @param width Image width + * @param height Image height + * @param radius Blur radius + * @param numPasses Number of Passes to Gaussian Approximate + */ + public static int[] applyFastGaussianBlur(int[] source, int width, int height, int radius, int numPasses) { + int[] target = new int[source.length]; + int[] temp = new int[source.length]; + + // 1. Copy original image data to target + System.arraycopy(source, 0, target, 0, source.length); + + // 2. Run (numPasses = 5) iterations for the Gaussian approximation + for (int i = 0; i < numPasses; i++) { + horizontalBoxBlur(target, temp, width, height, radius); + verticalBoxBlur(temp, target, width, height, radius); + } + return target; + } +} diff --git a/algorithms-modules/algorithms-numeric/src/main/java/com/baeldung/algorithms/fastgaussianblur/FastGaussianBlurRealImageTester.java b/algorithms-modules/algorithms-numeric/src/main/java/com/baeldung/algorithms/fastgaussianblur/FastGaussianBlurRealImageTester.java new file mode 100644 index 000000000000..8e1d5a11f56c --- /dev/null +++ b/algorithms-modules/algorithms-numeric/src/main/java/com/baeldung/algorithms/fastgaussianblur/FastGaussianBlurRealImageTester.java @@ -0,0 +1,89 @@ +package com.baeldung.algorithms.fastgaussianblur; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.InputStream; +import javax.imageio.ImageIO; + +import javax.annotation.Nonnull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Fast Gaussian Blur approximation tester on a real RGB image + */ +public class FastGaussianBlurRealImageTester { + + private static final Logger LOGGER = LoggerFactory.getLogger(FastGaussianBlurRealImageTester.class); + + @Nonnull + public static BufferedImage blurRealImage(@Nonnull BufferedImage image, int radius, int numPasses) { + int width = image.getWidth(); + int height = image.getHeight(); + + // 1. We extract 1D array of 32-bit ARGB pixels + int[] pixels = image.getRGB(0, 0, width, height, null, 0, width); + + // 2. We define arrays to hold independent color channels + int[] a = new int[pixels.length]; + int[] r = new int[pixels.length]; + int[] g = new int[pixels.length]; + int[] b = new int[pixels.length]; + + // 3. We unpack the 32-bit integers into separate channels + for (int i = 0; i < pixels.length; i++) { + a[i] = (pixels[i] >> 24) & 0xff; + r[i] = (pixels[i] >> 16) & 0xff; + g[i] = (pixels[i] >> 8) & 0xff; + b[i] = pixels[i] & 0xff; + } + + // 4. We apply our O(n) FastGaussianBlur algorithm to each color channel independently + r = FastGaussianBlur.applyFastGaussianBlur(r, width, height, radius, numPasses); + g = FastGaussianBlur.applyFastGaussianBlur(g, width, height, radius, numPasses); + b = FastGaussianBlur.applyFastGaussianBlur(b, width, height, radius, numPasses); + + // 5. We repack the channels back into a 32-bit integer array + int[] resultPixels = new int[pixels.length]; + for (int i = 0; i < pixels.length; i++) { + resultPixels[i] = (a[i] << 24) | (r[i] << 16) | (g[i] << 8) | b[i]; + } + + // 6. We create a new BufferedImage and set the blurred pixels + BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + result.setRGB(0, 0, width, height, resultPixels, 0, width); + + // 7. We return the result + return result; + } + + public static void main(String[] args) throws Exception { + long startTime = System.currentTimeMillis(); + int radius = 1; + int numPasses = 5; + + // 1. We create a thread and read sapmple.jpg from ../src/man/resources + InputStream is = Thread.currentThread() + .getContextClassLoader() + .getResourceAsStream("sample.jpg"); + + if (is == null) { + throw new RuntimeException("Resource not found: sample.jpg"); + } + + BufferedImage originalImage = ImageIO.read(is); + assert originalImage != null; + + // 2. We run our Fast Blur algorithm + BufferedImage blurredImage = blurRealImage(originalImage, radius, numPasses); // Radius 10 + long endTime = System.currentTimeMillis(); + + LOGGER.debug("Blur completed in: " + (endTime - startTime) + " ms"); + + //3. We save result image sample_blurred.jpg under algorithms-numeric/target + File outputFile = new File("algorithms-numeric/target/sample_blurred.jpg"); + boolean status = ImageIO.write(blurredImage, "png", outputFile); + LOGGER.debug("Blur Operation: " + status + " Saved to: " + outputFile.getAbsolutePath()); + } +} \ No newline at end of file diff --git a/algorithms-modules/algorithms-numeric/src/main/resources/sample.jpg b/algorithms-modules/algorithms-numeric/src/main/resources/sample.jpg new file mode 100644 index 000000000000..e7c7b10812c5 Binary files /dev/null and b/algorithms-modules/algorithms-numeric/src/main/resources/sample.jpg differ diff --git a/algorithms-modules/algorithms-numeric/src/test/java/com/baeldung/algorithms/fastgaussianblur/FastGaussianBlurUnitTest.java b/algorithms-modules/algorithms-numeric/src/test/java/com/baeldung/algorithms/fastgaussianblur/FastGaussianBlurUnitTest.java new file mode 100644 index 000000000000..aa8c6ac83c42 --- /dev/null +++ b/algorithms-modules/algorithms-numeric/src/test/java/com/baeldung/algorithms/fastgaussianblur/FastGaussianBlurUnitTest.java @@ -0,0 +1,32 @@ +package com.baeldung.algorithms.fastgaussianblur; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/** + * Fast Gaussian Blur JUnit Test case + */ +class FastGaussianBlurUnitTest { + @Test + void givenSharpImage_whenAppliedBlur_thenCenterIsSmoothed() { + int width = 5; + int height = 5; + int numPasses = 5; + int[] image = new int[width * height]; + + // 1. We create an impulse image with all indices black image except one bright white pixel in the center. + image[12] = 255; + + int[] blurredImage = FastGaussianBlur.applyFastGaussianBlur(image, width, height, 1, numPasses); + + // 2. We expect the center pixel should lose intensity + // as it gets spread to its neighbors + assertTrue(blurredImage[12] < 255); + assertTrue(blurredImage[12] > 0); + + // 3. We expect that its immediate neighbors should have + // gained intensity + assertTrue(blurredImage[11] > 0); // Left neighbor + assertTrue(blurredImage[13] > 0); // Right neighbor + } +} \ No newline at end of file