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 } }