From 7e21d37ae2709274c3a2a1d3d19ead65c6710682 Mon Sep 17 00:00:00 2001 From: locphamtran Date: Mon, 6 Jul 2026 18:45:12 +0700 Subject: [PATCH] Smooth blur: replace O(N*r) box blur with O(N) sliding window box blur and scale intensity linearly from 1 to 40 --- .../java/com/photobooth/app/MainActivity.kt | 134 ++++++++++-------- 1 file changed, 72 insertions(+), 62 deletions(-) diff --git a/android-project/app/src/main/java/com/photobooth/app/MainActivity.kt b/android-project/app/src/main/java/com/photobooth/app/MainActivity.kt index aebcac2..9f4a584 100644 --- a/android-project/app/src/main/java/com/photobooth/app/MainActivity.kt +++ b/android-project/app/src/main/java/com/photobooth/app/MainActivity.kt @@ -1664,7 +1664,7 @@ class MainActivity : AppCompatActivity() { if (!isAiSupported) { if (blurIntensity > 0f) { - val radius = (25 * blurIntensity).toInt().coerceIn(1, 25) + val radius = (40 * blurIntensity).toInt().coerceIn(1, 40) return blurBitmap(capturedImage, radius) } return capturedImage @@ -1723,7 +1723,7 @@ class MainActivity : AppCompatActivity() { val bgPixels = IntArray(w * h) var blurredBg: android.graphics.Bitmap? = null if (blurIntensity > 0f) { - val radius = (15 * blurIntensity).toInt().coerceIn(1, 25) + val radius = (25 * blurIntensity).toInt().coerceIn(1, 25) blurredBg = blurBitmap(baseBg, radius) blurredBg.getPixels(bgPixels, 0, w, 0, 0, w, h) } else { @@ -1778,7 +1778,7 @@ class MainActivity : AppCompatActivity() { } if (blurIntensity > 0f) { - val radius = (15 * blurIntensity).toInt().coerceIn(1, 15) + val radius = (25 * blurIntensity).toInt().coerceIn(1, 25) return blurBitmap(capturedImage, radius) } return capturedImage @@ -1835,7 +1835,7 @@ class MainActivity : AppCompatActivity() { val bgPixels = IntArray(w * h) var blurredBg: android.graphics.Bitmap? = null if (blurIntensity > 0f) { - val radius = (25 * blurIntensity).toInt().coerceIn(1, 25) + val radius = (40 * blurIntensity).toInt().coerceIn(1, 40) blurredBg = blurBitmap(baseBg, radius) blurredBg.getPixels(bgPixels, 0, w, 0, 0, w, h) } else { @@ -1890,7 +1890,7 @@ class MainActivity : AppCompatActivity() { } if (blurIntensity > 0f) { - val radius = (25 * blurIntensity).toInt().coerceIn(1, 25) + val radius = (40 * blurIntensity).toInt().coerceIn(1, 40) return blurBitmap(capturedImage, radius) } return capturedImage @@ -1950,74 +1950,84 @@ class MainActivity : AppCompatActivity() { } private fun blurBitmap(src: android.graphics.Bitmap, radius: Int): android.graphics.Bitmap { - val scaleFactor = 4 val w = src.width val h = src.height + val output = android.graphics.Bitmap.createBitmap(w, h, android.graphics.Bitmap.Config.ARGB_8888) - val downscaledW = (w / scaleFactor).coerceAtLeast(1) - val downscaledH = (h / scaleFactor).coerceAtLeast(1) - val downscaledBitmap = android.graphics.Bitmap.createScaledBitmap(src, downscaledW, downscaledH, true) + val pixels = IntArray(w * h) + src.getPixels(pixels, 0, w, 0, 0, w, h) - val r = (radius / scaleFactor).coerceAtLeast(1) + val temp = IntArray(w * h) + val r = radius.coerceAtLeast(1) + val windowSize = r * 2 + 1 - val width = downscaledBitmap.width - val height = downscaledBitmap.height - val pixels = IntArray(width * height) - downscaledBitmap.getPixels(pixels, 0, width, 0, 0, width, height) - downscaledBitmap.recycle() - - val blurredPixels = IntArray(width * height) - - for (y in 0 until height) { - for (x in 0 until width) { - var rSum = 0 - var gSum = 0 - var bSum = 0 - var count = 0 - for (dx in -r..r) { - val px = x + dx - if (px in 0 until width) { - val color = pixels[y * width + px] - rSum += android.graphics.Color.red(color) - gSum += android.graphics.Color.green(color) - bSum += android.graphics.Color.blue(color) - count++ - } - } - val idx = y * width + x - blurredPixels[idx] = android.graphics.Color.rgb(rSum / count, gSum / count, bSum / count) + // Horizontal pass + for (y in 0 until h) { + val rowOffset = y * w + var rSum = 0 + var gSum = 0 + var bSum = 0 + + // Initialize window sum + for (dx in -r..r) { + val px = dx.coerceIn(0, w - 1) + val pixel = pixels[rowOffset + px] + rSum += (pixel shr 16) and 0xFF + gSum += (pixel shr 8) and 0xFF + bSum += pixel and 0xFF + } + temp[rowOffset] = (0xFF shl 24) or ((rSum / windowSize) shl 16) or ((gSum / windowSize) shl 8) or (bSum / windowSize) + + // Slide window + for (x in 1 until w) { + val oldPixelX = (x - 1 - r).coerceIn(0, w - 1) + val newPixelX = (x + r).coerceIn(0, w - 1) + + val oldPixel = pixels[rowOffset + oldPixelX] + val newPixel = pixels[rowOffset + newPixelX] + + rSum += ((newPixel shr 16) and 0xFF) - ((oldPixel shr 16) and 0xFF) + gSum += ((newPixel shr 8) and 0xFF) - ((oldPixel shr 8) and 0xFF) + bSum += (newPixel and 0xFF) - (oldPixel and 0xFF) + + temp[rowOffset + x] = (0xFF shl 24) or ((rSum / windowSize) shl 16) or ((gSum / windowSize) shl 8) or (bSum / windowSize) } } - val finalPixels = IntArray(width * height) - for (x in 0 until width) { - for (y in 0 until height) { - var rSum = 0 - var gSum = 0 - var bSum = 0 - var count = 0 - for (dy in -r..r) { - val py = y + dy - if (py in 0 until height) { - val color = blurredPixels[py * width + x] - rSum += android.graphics.Color.red(color) - gSum += android.graphics.Color.green(color) - bSum += android.graphics.Color.blue(color) - count++ - } - } - val idx = y * width + x - finalPixels[idx] = android.graphics.Color.rgb(rSum / count, gSum / count, bSum / count) + // Vertical pass + for (x in 0 until w) { + var rSum = 0 + var gSum = 0 + var bSum = 0 + + // Initialize window sum + for (dy in -r..r) { + val py = dy.coerceIn(0, h - 1) + val pixel = temp[py * w + x] + rSum += (pixel shr 16) and 0xFF + gSum += (pixel shr 8) and 0xFF + bSum += pixel and 0xFF + } + pixels[x] = (0xFF shl 24) or ((rSum / windowSize) shl 16) or ((gSum / windowSize) shl 8) or (bSum / windowSize) + + // Slide window + for (y in 1 until h) { + val oldPixelY = (y - 1 - r).coerceIn(0, h - 1) + val newPixelY = (y + r).coerceIn(0, h - 1) + + val oldPixel = temp[oldPixelY * w + x] + val newPixel = temp[newPixelY * w + x] + + rSum += ((newPixel shr 16) and 0xFF) - ((oldPixel shr 16) and 0xFF) + gSum += ((newPixel shr 8) and 0xFF) - ((oldPixel shr 8) and 0xFF) + bSum += (newPixel and 0xFF) - (oldPixel and 0xFF) + + pixels[y * w + x] = (0xFF shl 24) or ((rSum / windowSize) shl 16) or ((gSum / windowSize) shl 8) or (bSum / windowSize) } } - val tinyBlurred = android.graphics.Bitmap.createBitmap(width, height, android.graphics.Bitmap.Config.ARGB_8888) - tinyBlurred.setPixels(finalPixels, 0, width, 0, 0, width, height) - - val upscaledDest = android.graphics.Bitmap.createScaledBitmap(tinyBlurred, w, h, true) - tinyBlurred.recycle() - - return upscaledDest + output.setPixels(pixels, 0, w, 0, 0, w, h) + return output } override fun onDestroy() {