Smooth blur: replace O(N*r) box blur with O(N) sliding window box blur and scale intensity linearly from 1 to 40

This commit is contained in:
2026-07-06 18:45:12 +07:00
parent fe095304cc
commit 7e21d37ae2
@@ -1664,7 +1664,7 @@ class MainActivity : AppCompatActivity() {
if (!isAiSupported) { if (!isAiSupported) {
if (blurIntensity > 0f) { if (blurIntensity > 0f) {
val radius = (25 * blurIntensity).toInt().coerceIn(1, 25) val radius = (40 * blurIntensity).toInt().coerceIn(1, 40)
return blurBitmap(capturedImage, radius) return blurBitmap(capturedImage, radius)
} }
return capturedImage return capturedImage
@@ -1723,7 +1723,7 @@ class MainActivity : AppCompatActivity() {
val bgPixels = IntArray(w * h) val bgPixels = IntArray(w * h)
var blurredBg: android.graphics.Bitmap? = null var blurredBg: android.graphics.Bitmap? = null
if (blurIntensity > 0f) { if (blurIntensity > 0f) {
val radius = (15 * blurIntensity).toInt().coerceIn(1, 25) val radius = (25 * blurIntensity).toInt().coerceIn(1, 25)
blurredBg = blurBitmap(baseBg, radius) blurredBg = blurBitmap(baseBg, radius)
blurredBg.getPixels(bgPixels, 0, w, 0, 0, w, h) blurredBg.getPixels(bgPixels, 0, w, 0, 0, w, h)
} else { } else {
@@ -1778,7 +1778,7 @@ class MainActivity : AppCompatActivity() {
} }
if (blurIntensity > 0f) { if (blurIntensity > 0f) {
val radius = (15 * blurIntensity).toInt().coerceIn(1, 15) val radius = (25 * blurIntensity).toInt().coerceIn(1, 25)
return blurBitmap(capturedImage, radius) return blurBitmap(capturedImage, radius)
} }
return capturedImage return capturedImage
@@ -1835,7 +1835,7 @@ class MainActivity : AppCompatActivity() {
val bgPixels = IntArray(w * h) val bgPixels = IntArray(w * h)
var blurredBg: android.graphics.Bitmap? = null var blurredBg: android.graphics.Bitmap? = null
if (blurIntensity > 0f) { if (blurIntensity > 0f) {
val radius = (25 * blurIntensity).toInt().coerceIn(1, 25) val radius = (40 * blurIntensity).toInt().coerceIn(1, 40)
blurredBg = blurBitmap(baseBg, radius) blurredBg = blurBitmap(baseBg, radius)
blurredBg.getPixels(bgPixels, 0, w, 0, 0, w, h) blurredBg.getPixels(bgPixels, 0, w, 0, 0, w, h)
} else { } else {
@@ -1890,7 +1890,7 @@ class MainActivity : AppCompatActivity() {
} }
if (blurIntensity > 0f) { if (blurIntensity > 0f) {
val radius = (25 * blurIntensity).toInt().coerceIn(1, 25) val radius = (40 * blurIntensity).toInt().coerceIn(1, 40)
return blurBitmap(capturedImage, radius) return blurBitmap(capturedImage, radius)
} }
return capturedImage return capturedImage
@@ -1950,74 +1950,84 @@ class MainActivity : AppCompatActivity() {
} }
private fun blurBitmap(src: android.graphics.Bitmap, radius: Int): android.graphics.Bitmap { private fun blurBitmap(src: android.graphics.Bitmap, radius: Int): android.graphics.Bitmap {
val scaleFactor = 4
val w = src.width val w = src.width
val h = src.height 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 pixels = IntArray(w * h)
val downscaledH = (h / scaleFactor).coerceAtLeast(1) src.getPixels(pixels, 0, w, 0, 0, w, h)
val downscaledBitmap = android.graphics.Bitmap.createScaledBitmap(src, downscaledW, downscaledH, true)
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 // Horizontal pass
val height = downscaledBitmap.height for (y in 0 until h) {
val pixels = IntArray(width * height) val rowOffset = y * w
downscaledBitmap.getPixels(pixels, 0, width, 0, 0, width, height) var rSum = 0
downscaledBitmap.recycle() var gSum = 0
var bSum = 0
val blurredPixels = IntArray(width * height)
// Initialize window sum
for (y in 0 until height) { for (dx in -r..r) {
for (x in 0 until width) { val px = dx.coerceIn(0, w - 1)
var rSum = 0 val pixel = pixels[rowOffset + px]
var gSum = 0 rSum += (pixel shr 16) and 0xFF
var bSum = 0 gSum += (pixel shr 8) and 0xFF
var count = 0 bSum += pixel and 0xFF
for (dx in -r..r) { }
val px = x + dx temp[rowOffset] = (0xFF shl 24) or ((rSum / windowSize) shl 16) or ((gSum / windowSize) shl 8) or (bSum / windowSize)
if (px in 0 until width) {
val color = pixels[y * width + px] // Slide window
rSum += android.graphics.Color.red(color) for (x in 1 until w) {
gSum += android.graphics.Color.green(color) val oldPixelX = (x - 1 - r).coerceIn(0, w - 1)
bSum += android.graphics.Color.blue(color) val newPixelX = (x + r).coerceIn(0, w - 1)
count++
} val oldPixel = pixels[rowOffset + oldPixelX]
} val newPixel = pixels[rowOffset + newPixelX]
val idx = y * width + x
blurredPixels[idx] = android.graphics.Color.rgb(rSum / count, gSum / count, bSum / count) 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) // Vertical pass
for (x in 0 until width) { for (x in 0 until w) {
for (y in 0 until height) { var rSum = 0
var rSum = 0 var gSum = 0
var gSum = 0 var bSum = 0
var bSum = 0
var count = 0 // Initialize window sum
for (dy in -r..r) { for (dy in -r..r) {
val py = y + dy val py = dy.coerceIn(0, h - 1)
if (py in 0 until height) { val pixel = temp[py * w + x]
val color = blurredPixels[py * width + x] rSum += (pixel shr 16) and 0xFF
rSum += android.graphics.Color.red(color) gSum += (pixel shr 8) and 0xFF
gSum += android.graphics.Color.green(color) bSum += pixel and 0xFF
bSum += android.graphics.Color.blue(color) }
count++ pixels[x] = (0xFF shl 24) or ((rSum / windowSize) shl 16) or ((gSum / windowSize) shl 8) or (bSum / windowSize)
}
} // Slide window
val idx = y * width + x for (y in 1 until h) {
finalPixels[idx] = android.graphics.Color.rgb(rSum / count, gSum / count, bSum / count) 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) output.setPixels(pixels, 0, w, 0, 0, w, h)
tinyBlurred.setPixels(finalPixels, 0, width, 0, 0, width, height) return output
val upscaledDest = android.graphics.Bitmap.createScaledBitmap(tinyBlurred, w, h, true)
tinyBlurred.recycle()
return upscaledDest
} }
override fun onDestroy() { override fun onDestroy() {