174 lines
7.0 KiB
Kotlin
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
|
|
}
|
|
}
|