feat(chat-character-image): 캐릭터 이미지
- 등록시 블러 이미지를 생성하여 저장하는 기능 추가
This commit is contained in:
102
src/main/kotlin/kr/co/vividnext/sodalive/utils/ImageBlurUtil.kt
Normal file
102
src/main/kotlin/kr/co/vividnext/sodalive/utils/ImageBlurUtil.kt
Normal file
@@ -0,0 +1,102 @@
|
||||
package kr.co.vividnext.sodalive.utils
|
||||
|
||||
import java.awt.image.BufferedImage
|
||||
import kotlin.math.exp
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* 가우시안 커널 기반 블러 유틸리티
|
||||
* - 반경(radius)에 따라 커널 크기(2*radius+1) 생성
|
||||
* - 시그마는 관례적으로 radius/3.0 적용
|
||||
* - 수평/수직 분리 합성곱으로 품질과 성능 확보
|
||||
*/
|
||||
object ImageBlurUtil {
|
||||
fun blur(src: BufferedImage, radius: Int = 10): BufferedImage {
|
||||
require(radius > 0) { "radius must be > 0" }
|
||||
val w = src.width
|
||||
val h = src.height
|
||||
val dst = BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB)
|
||||
|
||||
// 가우시안 1D 커널 생성 및 정규화
|
||||
val sigma = radius / 3.0
|
||||
val kernel = gaussianKernel(radius, sigma)
|
||||
|
||||
// 중간 버퍼
|
||||
val temp = BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB)
|
||||
|
||||
// 수평 합성곱
|
||||
for (y in 0 until h) {
|
||||
for (x in 0 until w) {
|
||||
var aAcc = 0.0
|
||||
var rAcc = 0.0
|
||||
var gAcc = 0.0
|
||||
var bAcc = 0.0
|
||||
for (k in -radius..radius) {
|
||||
val xx = clamp(x + k, 0, w - 1)
|
||||
val rgb = src.getRGB(xx, y)
|
||||
val a = (rgb ushr 24) and 0xFF
|
||||
val r = (rgb ushr 16) and 0xFF
|
||||
val g = (rgb ushr 8) and 0xFF
|
||||
val b = rgb and 0xFF
|
||||
val wgt = kernel[k + radius]
|
||||
aAcc += a * wgt
|
||||
rAcc += r * wgt
|
||||
gAcc += g * wgt
|
||||
bAcc += b * wgt
|
||||
}
|
||||
val a = aAcc.toInt().coerceIn(0, 255)
|
||||
val r = rAcc.toInt().coerceIn(0, 255)
|
||||
val g = gAcc.toInt().coerceIn(0, 255)
|
||||
val b = bAcc.toInt().coerceIn(0, 255)
|
||||
temp.setRGB(x, y, (a shl 24) or (r shl 16) or (g shl 8) or b)
|
||||
}
|
||||
}
|
||||
|
||||
// 수직 합성곱
|
||||
for (x in 0 until w) {
|
||||
for (y in 0 until h) {
|
||||
var aAcc = 0.0
|
||||
var rAcc = 0.0
|
||||
var gAcc = 0.0
|
||||
var bAcc = 0.0
|
||||
for (k in -radius..radius) {
|
||||
val yy = clamp(y + k, 0, h - 1)
|
||||
val rgb = temp.getRGB(x, yy)
|
||||
val a = (rgb ushr 24) and 0xFF
|
||||
val r = (rgb ushr 16) and 0xFF
|
||||
val g = (rgb ushr 8) and 0xFF
|
||||
val b = rgb and 0xFF
|
||||
val wgt = kernel[k + radius]
|
||||
aAcc += a * wgt
|
||||
rAcc += r * wgt
|
||||
gAcc += g * wgt
|
||||
bAcc += b * wgt
|
||||
}
|
||||
val a = aAcc.toInt().coerceIn(0, 255)
|
||||
val r = rAcc.toInt().coerceIn(0, 255)
|
||||
val g = gAcc.toInt().coerceIn(0, 255)
|
||||
val b = bAcc.toInt().coerceIn(0, 255)
|
||||
dst.setRGB(x, y, (a shl 24) or (r shl 16) or (g shl 8) or b)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
private fun gaussianKernel(radius: Int, sigma: Double): DoubleArray {
|
||||
val size = 2 * radius + 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 kernel.indices) kernel[i] /= sum
|
||||
return kernel
|
||||
}
|
||||
|
||||
private fun clamp(v: Int, lo: Int, hi: Int): Int = max(lo, min(hi, v))
|
||||
}
|
Reference in New Issue
Block a user