Files
sodalive-backend-spring-boot/src/main/kotlin/kr/co/vividnext/sodalive/utils/ImageBlurUtil.kt
Klaus f8be99547a fix: ImageBlurUtil.kt
- 블러 radius 200 -> 240
2025-08-21 21:18:29 +09:00

174 lines
7.0 KiB
Kotlin

package kr.co.vividnext.sodalive.utils
import java.awt.image.BufferedImage
import java.awt.image.DataBufferInt
import java.util.stream.IntStream
import kotlin.math.exp
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
/**
* 가우시안 커널 기반 블러 유틸리티
* - 반경(radius)에 따라 커널 크기(2*radius+1) 생성
* - 시그마는 관례적으로 radius/3.0 적용
* - 수평/수직 분리 합성곱으로 품질과 성능 확보
*/
/**
* 고속 가우시안 블러 유틸
*
* - 원본 비율/해상도 그대로 두고 "큰 반경 블러"만 빠르게 적용하고 싶을 때 사용합니다.
* - 강한 익명화를 원하면(식별 불가 수준) 이 함수 대신
* "다운스케일 → 큰 반경 블러 → 원본 해상도로 업스케일"을 조합하세요.
* (예: ImageUtils.anonymizeStrongFast 처럼)
*/
object ImageBlurUtil {
/**
* 분리형(1D) 가우시안 블러(수평 → 수직 2패스), 배열 접근 기반 고속 구현.
*
* @param src 원본 이미지
* @param radius 가우시안 반경(>=1). 클수록 강하게 흐려짐. (권장 5~64)
* @param parallel true면 행/열 패스를 병렬 실행(ForkJoinPool). 멀티코어에서만 유효.
* @return 블러된 새 이미지 (TYPE_INT_ARGB)
*/
fun blurFast(src: BufferedImage, radius: Int = 240, parallel: Boolean = true): BufferedImage {
require(radius > 0) { "radius must be > 0" }
// 1) 프리멀티 알파로 변환 (경계 품질↑)
val s = toPremultiplied(src) // TYPE_INT_ARGB_PRE
val w = s.width
val h = s.height
// 2) 중간/최종 버퍼(프리멀티 유지)
val tmp = BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE)
val dst = BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE)
val srcArr = (s.raster.dataBuffer as DataBufferInt).data
val tmpArr = (tmp.raster.dataBuffer as DataBufferInt).data
val dstArr = (dst.raster.dataBuffer as DataBufferInt).data
// 3) 1D 가우시안 커널(정규화)
// sigma는 일반적으로 radius/3.0이 자연스러운 값
val sigma = radius / 3.0
val kernel = buildGaussian1D(radius, sigma)
// 4) 수평 패스 (y 라인별)
if (parallel) {
IntStream.range(0, h).parallel().forEach { y ->
convolveRow(srcArr, tmpArr, w, h, y, kernel, radius)
}
} else {
for (y in 0 until h) convolveRow(srcArr, tmpArr, w, h, y, kernel, radius)
}
// 5) 수직 패스 (x 컬럼별)
if (parallel) {
IntStream.range(0, w).parallel().forEach { x ->
convolveCol(tmpArr, dstArr, w, h, x, kernel, radius)
}
} else {
for (x in 0 until w) convolveCol(tmpArr, dstArr, w, h, x, kernel, radius)
}
// 6) 비프리멀티(일반 ARGB)로 변환해서 반환 (파일 저장/그리기 호환성↑)
return toNonPremultiplied(dst)
}
// ─────────────────────────────────────────────────────────────────────────────
// 내부 구현
// ─────────────────────────────────────────────────────────────────────────────
// 수평 합성곱: 경계는 replicate(클램프)
private fun convolveRow(src: IntArray, dst: IntArray, w: Int, h: Int, y: Int, k: DoubleArray, r: Int) {
val base = y * w
for (x in 0 until w) {
var aAcc = 0.0
var rAcc = 0.0
var gAcc = 0.0
var bAcc = 0.0
var i = -r
while (i <= r) {
val xx = clamp(x + i, 0, w - 1)
val argb = src[base + xx]
val a = (argb ushr 24) and 0xFF
val rr = (argb ushr 16) and 0xFF
val gg = (argb ushr 8) and 0xFF
val bb = argb and 0xFF
val wgt = k[i + r]
aAcc += a * wgt; rAcc += rr * wgt; gAcc += gg * wgt; bAcc += bb * wgt
i++
}
val a = aAcc.roundToInt().coerceIn(0, 255)
val rr = rAcc.roundToInt().coerceIn(0, 255)
val gg = gAcc.roundToInt().coerceIn(0, 255)
val bb = bAcc.roundToInt().coerceIn(0, 255)
dst[base + x] = (a shl 24) or (rr shl 16) or (gg shl 8) or bb
}
}
// 수직 합성곱: 경계 replicate(클램프)
private fun convolveCol(src: IntArray, dst: IntArray, w: Int, h: Int, x: Int, k: DoubleArray, r: Int) {
var idx = x
for (y in 0 until h) {
var aAcc = 0.0
var rAcc = 0.0
var gAcc = 0.0
var bAcc = 0.0
var i = -r
while (i <= r) {
val yy = clamp(y + i, 0, h - 1)
val argb = src[yy * w + x]
val a = (argb ushr 24) and 0xFF
val rr = (argb ushr 16) and 0xFF
val gg = (argb ushr 8) and 0xFF
val bb = argb and 0xFF
val wgt = k[i + r]
aAcc += a * wgt; rAcc += rr * wgt; gAcc += gg * wgt; bAcc += bb * wgt
i++
}
val a = aAcc.roundToInt().coerceIn(0, 255)
val rr = rAcc.roundToInt().coerceIn(0, 255)
val gg = gAcc.roundToInt().coerceIn(0, 255)
val bb = bAcc.roundToInt().coerceIn(0, 255)
dst[idx] = (a shl 24) or (rr shl 16) or (gg shl 8) or bb
idx += w
}
}
// 1D 가우시안 커널 (정규화)
private fun buildGaussian1D(radius: Int, sigma: Double): DoubleArray {
val size = radius * 2 + 1
val kernel = DoubleArray(size)
val sigma2 = 2.0 * sigma * sigma
var sum = 0.0
for (i in -radius..radius) {
val v = exp(-(i * i) / sigma2)
kernel[i + radius] = v
sum += v
}
for (i in 0 until size) kernel[i] /= sum
return kernel
}
private fun clamp(v: Int, lo: Int, hi: Int): Int = max(lo, min(hi, v))
// 프리멀티/비프리멀티 변환(빠른 방법: Graphics로 그리기)
private fun toPremultiplied(src: BufferedImage): BufferedImage {
if (src.type == BufferedImage.TYPE_INT_ARGB_PRE) return src
val out = BufferedImage(src.width, src.height, BufferedImage.TYPE_INT_ARGB_PRE)
val g = out.createGraphics()
g.drawImage(src, 0, 0, null)
g.dispose()
return out
}
private fun toNonPremultiplied(src: BufferedImage): BufferedImage {
if (src.type == BufferedImage.TYPE_INT_ARGB) return src
val out = BufferedImage(src.width, src.height, BufferedImage.TYPE_INT_ARGB)
val g = out.createGraphics()
g.drawImage(src, 0, 0, null)
g.dispose()
return out
}
}