
가우시안(Gaussians)을 이용한 페인팅
요약
이미지의 에지 정보를 활용하여 브러시 스트로크를 생성하는 디지털 페인팅 도구에 관한 기술적 탐구입니다. 2D 가우시안 스플랫(Gaussian Splat)을 사용하여 붓질의 위치, 방향, 색상, 불투명도를 모델링함으로써 자연스러운 회화 효과를 구현합니다.
핵심 포인트
- 에지 인식 그리드를 활용해 이미지의 디테일과 윤곽선을 보존
- 2D 가우시안 스플랫을 브러시 스트로크의 계산 모델로 채택
- 공분산 행렬을 통해 붓질의 방향과 신장(elongation)을 표현
- 오버-컴포지팅 방식을 통해 유화와 유사한 층 쌓기 효과 구현
지난해 저는 이미지를 픽셀 아트(pixel art)로 변환하기 위해 에지 인식(edge-aware) 픽셀화 도구를 만들었습니다. 이는 사진 위에 단순히 고정된 그리드(grid)를 사용하는 대신, 이미지의 에지(edge)를 따라 그리드를 변형하는 방식입니다. 픽셀의 색상과 밝기를 결정하는 데 정보를 제공하도록 구부러진 에지 어댑터 그리드(edge adapter grid)를 사용한 것은 에지를 선명하게 유지하고 디테일을 보존하는 데 효과적이었습니다.
이후 저는 동일한 에지 정보가 디지털 페인팅(digital painting) 맥락에서도 적용될 수 있다는 것을 깨달았습니다. 페인팅 프로그램은 브러시 스트로크(brush strokes)를 어디에 배치할지, 크기는 어느 정도여야 할지, 그리고 어느 방향으로 흘러야 할지를 결정해야 합니다. 이러한 요소의 상당 부분은 에지가 영역 사이의 경계를 표시하기 때문에 이미 에지에 인코딩되어 있습니다. 이것들은 브러시가 따라가게 될 객체 영역 주변의 윤곽선(contours)이며, 윤곽선이 없다는 것은 일반적으로 몇 번의 넓은 스트로크만으로 충분한 평평한 영역임을 나타냅니다.
그래서 저는 이미지 구조에서 유도된 흔적들이 브러시 스트로크와 닮은 디지털 페인팅 스타일로 그려지는 프로그램을 만들 수 있을지 확인하기 위해 시작했습니다. 저의 목표는 슬라이더를 드래그하면 눈앞에서 페인팅이 스스로 재구성되는 인터랙티브(interactive) 도구를 만드는 것이었습니다. 또한 이 프로젝트는 Jolt를 테스트해 보고, 복잡한 프로젝트를 구축하는 데 얼마나 잘 작동하는지 확인해 볼 수 있는 좋은 기회였습니다.
이 포스트에서는 이 모든 것이 어떻게 결합되었는지 설명하겠습니다. 어떤 아이디어가 효과적이었고 어떤 것이 그렇지 않았는지 살펴볼 것입니다. 가장 중요한 것은, 최종 결과물이 실제로 페인팅과 유사한 형태를 띠게 되었는지 확인하는 것입니다.
왜 가우시안(Gaussians)인가?
우리가 고려해야 할 첫 번째 질문은 계산적 관점에서 브러시 스트로크(brush stroke)가 정확히 무엇인가 하는 점입니다. 유화나 아크릴 스트로크는 중심부의 색상이 가장자리로 갈수록 흐려지는 길쭉한 흔적이며, 그 방향은 캔버스 위를 가로지르는 브러시의 결과물입니다. 가장자리는 반투명하며, 스트로크들이 서로 겹쳐지기 때문에 화가는 먼저 넓은 색상 블록을 깔고, 그 위에 더 작고 반투명한 흔적들을 쌓아 올려 미세한 디테일을 추가할 수 있습니다.
2D 가우시안 스플랫 (2D Gaussian splat)이 이 아이디어에 놀라울 정도로 잘 부합한다는 사실이 밝혀졌습니다. 스플랫은 붓질이 놓이는 위치인 평균 (mean), 그것이 어떻게 늘어나고 회전했는지를 나타내는 공분산 행렬 (covariance matrix), 그리고 색상 (color)과 불투명도 (opacity)를 가집니다. 공분산은 붓의 방향과 신장 (elongation)을 인코딩하는 데 사용될 수 있으며, 이때 장축 (major axis)은 붓질의 방향을 따라 향하고 단축 (minor axis)은 그 가로질러 놓입니다. 각 스플랫이 자신의 알파 (alpha) 값으로 뒤에 있는 것을 가리는 표준 오버-컴포지팅 (over-compositing) 방식으로 스플랫 필드를 렌더링하면, 흔적들이 층을 이루고 서로 섞일 수 있게 하는 자연스러운 회화 모델과 유사한 효과를 얻을 수 있습니다. 물론 실제 물감과 같은 동일한 충실도 (fidelity)를 얻을 수는 없으므로, 그 효과는 GIMP나 Krita 같은 도구를 사용하는 디지털 페인팅에 더 가깝습니다.
이미 DrawingWithGaussians 및 2d-gaussian-splatting-Art와 같은 이 아이디어의 구현체들이 존재합니다. 하지만 두 방식 모두 무작위 스플랫을 씨앗 (seed)으로 심은 다음, 렌더링된 필드가 목표 이미지의 외형을 갖출 때까지 위치, 모양, 색상을 반복적으로 미세하게 조정하는 경사 하강법 (gradient descent) 접근 방식을 사용합니다. 이는 속도가 느리고 불투명한(opaque) 것으로 잘 알려진 방식입니다. 가장 안 좋은 점은 최종 결과물이 어떤 종류의 회화처럼 보이는 것이 아니라, 단순히 입력 이미지의 손실이 있는 재구성 (lossy reconstruction) 결과물이 되어버린다는 것입니다.
저는 이미 이미지에서 에지 (edge) 정보를 추출하는 솔루션을 가지고 있었기 때문에, 이미지를 맹목적으로 진화시킬 필요성을 느끼지 못했습니다. 대신, 추출된 에지는 디테일이 어디에 있는지와 붓질의 방향을 알려주기 때문에 페인팅 과정을 안내하는 데 사용할 수 있습니다. 디테일 밀도와 픽셀 색상 사이에서, 저는 경사 하강법에 의존하지 않고도 필요한 모든 정보를 가질 수 있었습니다. 기본적으로 기존 이미지를 따라 그리기만 하면 되는 셈입니다. 이게 정말 얼마나 어렵겠습니까?
그래서 저는 pixel = background + Σ(intensity × color)를 사용하는 가산 혼합 (additive blending) 방식의 참조 래스터라이저 (rasterizer)를 따르기 시작했습니다. 결과적으로, 이 접근 방식은 옵티마이저 (optimizer)가 중첩을 보상하는 색상을 학습하기 때문에 피팅 (fitting) 단계에서 잘 작동한다는 것을 알게 되었습니다. 불행히도, 수천 개의 스플랫 (splat)을 픽셀 색상으로부터 직접 시딩 (seeding)하고 이를 가산적으로 렌더링하면 훨씬 더 많은 중첩이 발생합니다. 64×64 이미지에 1,200개의 스플랫이 있을 때, 일부 픽셀의 합계는 22.06에 달했고, 이로 인해 이미지 전체에 순백색의 덩어리 (blobs)가 생겨났습니다. 다행히도, 알파 합성 (alpha compositing)의 표준 오버 연산자 (over-operator)를 사용하여 각 스플랫이 자신의 알파 (alpha) 값만큼 뒤에 있는 것을 가리도록(occlude) 함으로써, 합산된 색상이 1.0을 초과하지 않게 하여 이 문제를 해결할 수 있었습니다. 이 접근 방식의 또 다른 장점은 이미지에서 샘플링된 색상과 불투명도 (opacity)를 깔끔하게 분리할 수 있다는 점입니다.
에지(Edges)는 어디에 칠해야 할지를 알려줍니다
이 섹션의 모든 이미지는 동일한 사진을 동일한 파이프라인 (pipeline)에 통과시킨 것이며, 한 번에 하나의 아이디어만 끄거나 켭니다. 즉, 동일한 소스, 동일한 획 예산 (stroke budget), 동일한 기본 크기를 사용하여 각 단계가 해당 아이디어를 통해 무엇을 얻을 수 있는지를 정확히 보여줍니다.
저는 다음의 소스 사진을 사용하기 시작했으며, 각 아이디어가 무엇을 가져다주는지 설명하기 위해 앱을 개선해 나가는 과정을 기록했습니다. 새로운 기술들이 추가됨에 따라 페인팅이 어떻게 진화하는지 살펴봅시다.

초기 렌더러 (renderer)를 사용했을 때는 균일한 모자이크처럼 보이는 다소 슬픈 결과물이 나왔습니다. 모든 스플랫이 동일한 크기, 종횡비 (aspect ratio), 회전 값을 가져서 동일한 덩어리들이 규칙적인 그리드 (grid) 형태로 나타났습니다. 아직까지는 딱히 페인팅처럼 보이지 않습니다.

실제 화가라면 하늘이나 매끄러운 표면 같은 평평한 영역에는 몇 번의 넓은 획을 사용하여 획을 변화시킬 것입니다. 그런 다음, 에지 (edges)를 따라가거나 눈이나 천과 같이 질감이 있는 영역에서는 더 작은 브러시를 사용하여 물체의 윤곽 (contours)을 따르는 수많은 미세한 획을 사용합니다.
이를 모방하는 한 가지 방법은 구조 텐서 (structure tensor)를 사용하여 이미지 기울기 (image gradient)를 계산함으로써, 각 픽셀에 대해 색상이 얼마나 그리고 어느 방향으로 변하는지를 인코딩하는 것입니다. 기울기의 외적 (outer product)으로부터 2×2 텐서가 형성되며, 주변 영역에 대해 블러 (blur) 처리가 됩니다. 중요한 점은, 이 텐서의 고유벡터 (eigenvectors)가 세 가지 핵심 정보를 알려준다는 것입니다. 주요 고유벡터 (major eigenvector)는 윤곽선을 가로질러 가리키며, 가장 강한 기울기의 방향을 제공합니다. 부차적 고유벡터 (minor eigenvector)는 가장자리를 따라 가리키며, 브러시 획 (brush stroke)의 방향을 제공합니다. 그리고 고유값 (eigenvalues)의 비율인 일관성 (coherence)은 해당 가장자리가 선명한 윤곽선인지 아니면 등방성 (isotropic)의 뭉개진 형태인지를 알려줍니다.
각 스플랫 (splat)은 자신의 위치에 있는 이 텐서로부터 고유한 공분산 (covariance)을 얻으며, 가장자리를 따라 길게 늘어납니다. 이때 늘어나는 정도는 일관성에 비례합니다. 평평한 영역은 둥근 상태를 유지하는 반면, 가장자리는 장면 내 물체의 윤곽을 따라가는 가늘고 방향성이 있는 획이 됩니다. 이는 Litwinowicz와 Hertzmann이 제안한 고전적인 회화적 렌더링 (painterly rendering) 기법입니다. 이것이 적용되자, 렌더링은 눈을 가늘게 뜨고 보면 브러시 작업처럼 보이는 무언가와 닮기 시작했습니다. 여기서 털과 모자 챙이 방향성을 갖게 되었고, 수염이 나타나기 시작했습니다.

하지만 여기서 또 다른 문제에 부딪혔습니다. 구조 텐서는 그레이스케일인 휘도 (luminance) 기울기를 사용하기 때문에, 창백한 피부 위의 붉은 입술이나 회색 벽 위의 파란색 표지판과 같이 휘도가 동일한 (isoluminant) 색상 가장자리에는 반응하지 못하기 때문입니다. 다행히 Di Zenzo 컬러 텐서 (color tensor)를 사용하여 RGB 채널별로 소벨 기울기 (Sobel gradients)를 계산할 수 있습니다. 이들의 외적을 하나의 텐서로 합치면, 휘도 가장자리만큼이나 강력하게 방향성을 유도하는 색도 (chroma) 가장자리를 얻을 수 있습니다.
가장자리만으로는 충분하지 않다
구조 텐서가 방향성을 파악하는 문제는 해결해주지만, 해당 영역의 밀도 (density)에 대해서는 아무것도 알려주지 않습니다. 밀도를 알지 못하면 해당 영역에 얼마나 많은 획이 들어가야 하는지, 그리고 그 획들이 얼마나 작아야 하는지를 파악하는 것이 불가능합니다.
단순히 에지 강도(edge strength)를 사용하여 이를 파악하고, 그에 따라 사용할 획의 수를 결정할 수 있다고 생각할지도 모릅니다. 하지만 그렇게 하면 물체의 질감(texture)을 완전히 놓치게 됩니다. 예를 들어, 자갈길은 일관된 에지 구조는 거의 없지만, 그 자체로 미세한 흔적이 필요한 고주파 디테일(high-frequency detail)이 매우 많습니다. 반면, 매끄러운 볼은 단일 윤곽선 에지(contour edge)를 가지며 내부 영역은 넓게 유지되어야 합니다. 반대로, 미세한 옷 주름이나 멀리 있는 나뭇가지와 같이 희미하지만 실제 존재하는 에지는 절대적인 그래디언트 크기(gradient magnitude)는 낮지만 여전히 렌더링될 필요가 있습니다. 따라서 에지 분석(edge analysis)에만 의존하는 것은 원본 이미지에 존재하는 많은 중요한 디테일을 재현하는 것을 불가능하게 만듭니다.
이 지점에서 제가 이 포스트에서 논의했던 Haar 웨이블릿(Haar wavelet)이 역할을 합니다. 휘도(luminance)에 다중 스케일 2D Haar 분해(multi-scale 2D Haar decomposition)를 실행하면, 각 셀에 대해 스케일 전반에 걸친 절대 디테일 계수(absolute detail coefficients)를 합산하여 디테일 에너지 맵(detail energy map)을 생성합니다. 이 맵은 질감이 있거나 에지가 있는 영역에서는 높은 값을 가지며, 디테일이 부족한 영역에서는 낮은 값을 가집니다. 하지만 가공되지 않은 웨이블릿 에너지는 여전히 절대적이라는 문제를 가지고 있습니다. 실제 질감이 있는 어두운 영역은 중간 정도의 질감을 가진 밝은 영역보다 적은 절대 에너지를 생성하여, 어두운 디테일이 씻겨 나가는(washed out) 결과를 초래합니다.
여기서 우리에게 다시 필요한 것은 각 셀의 에너지를 해당 지역의 평균 밝기(local mean brightness)와 전역 평균(global mean)의 일부로 나눈 휘도 상대적 디테일 맵(luma-relative detail map)입니다. 이렇게 하면 어두운 영역도 디테일을 유지할 수 있으며, 이 맵이 구조 텐서(structure tensor)로부터 얻은 지역 정규화된 에지 강도(locally-normalized edge strength)와 융합되기 때문에, 평탄한 영역에 있는 희미한 윤곽선도 여전히 획을 끌어당길 수 있게 됩니다.
그것이 바로 이 두 가지 기술이 여기서 가치 있는 이유입니다. 텐서 (Tensor)는 모든 획 (Stroke)의 방향성과 일관성 (Coherence)을 전달하는 반면, 웨이브렛 (Wavelet)은 적응형 배치 (Adaptive placement)를 구동하는 데 필요한 밀도 맵 (Density map)을 식별합니다. 이들은 함께 디테일이 어디에 존재하는지, 그리고 어떤 형태를 띠는지에 대한 질문에 완전한 해답을 제시합니다. 이 시점에서 요소들이 결합되기 시작하며, 소스 이미지로부터 단순한 알고리즘적 획 배치 (Naive algorithmic stroke placement)를 넘어설 수 있을 만큼 충분한 정보가 추출되었습니다.
이제 디테일 맵 (Detail map)은 작은 흔적들을 어디에 사용할지 결정하며, 초점이 맞지 않는 배경은 매끄러운 워시 (Wash)로 변하는 반면, 획들은 털이나 눈과 같은 디테일이 있는 영역에 모이게 됩니다.

그리드 제거하기 (Killing the Grid)
하지만 아직 우리가 이야기하지 않은 문제가 하나 남아 있습니다. 적응형 방향성 (Adaptive orientation)과 밀도 (Density)를 사용하더라도, 획들이 희미한 규칙성을 띠게 되기 때문에 결과물은 여전히 어색해 보입니다. 이들의 배치와 규칙적인 형태는 규칙적인 배치 그리드 (Placement grid)를 사용함으로써 발생하는 아티팩트 (Artifact)입니다.
이미지를 셀 (Cell)로 나누고 각 셀에 하나의 획을 배치하는 층화 그리드 (Stratified grid)를 사용하면 반드시 가장자리에서 아티팩트가 발생합니다. 참고로, 이는 높은 압축률의 JPEG 압축에서 보이는 것과 동일한 문제입니다. 격자 구조를 깨뜨리기 위해 셀 내에서 위치를 지터링 (Jitter)하거나 디테일 레벨별로 회전된 그리드를 사용할 수도 있지만, 이러한 임시방편들은 그리드가 규칙적이라는 근본적인 문제를 해결하지 못합니다.
따라서 그리드 방식은 버려야 하며, 대신 인덱스의 Wang avalanche hash에서 유도된 위치에 획을 배치해야 합니다. 이 방법은 주기성이 없는 진정한 화이트 노이즈 (White-noise) 좌표를 제공합니다. 하지만 여기에 미묘한 차이가 있는데, 단순한 선형 해시 (frac × i A)는 Marsaglia 초평면 (Hyperplanes) 위에 떨어지는 점들을 생성하여, 그리드 아티팩트보다 훨씬 더 심각한 대각선 줄무늬를 만들어내기 때문입니다. 각 입력 비트가 모든 출력 비트에 영향을 미치는 적절한 avalanche mix가 필요합니다.

페인팅처럼 보이게 만들기: 특징으로서의 노이즈 (Noise as a Feature)
이 시점에서 저는 전체 캔버스에 걸쳐 부드럽게 그려지는 적응형 밀도(adaptive densities)를 가진 가장자리 정렬된 스트로크(edge-aligned strokes)를 구현했지만, 결과물은 여전히 너무 규칙적으로 느껴졌습니다. 특정 가장자리의 모든 스트로크가 동일한 길이, 동일한 간격, 동일한 정렬을 가지고 있었는데, 이는 실제 붓질(brushwork)의 모습이 아니었습니다. 그려진 효과를 더 그럴듯하게 만들기 위해 손의 떨림과 압력 변화를 모방할 수 있는 방법이 필요했습니다.
그리고 미묘한 구조적 변화를 추가하는 데 Perlin noise(펄린 노이즈)를 사용하는 것보다 더 좋은 방법이 있을까요? 저는 최근에 Perlin noise를 사용하여 흐름 애니메이션(flow animation)을 만드는 방법을 보여주는 글을 작성한 적이 있습니다. 그래서 머릿속에 생생하게 남아 있던 이 방식을 자연스럽게 떠올리게 되었습니다. 상관관계가 없는(decorrelated) 두 개의 2D Perlin 채널을 사용하여 부드러운 벡터 흐름장(vector flow field)을 형성할 수 있습니다. 그런 다음, 각 스트로크의 방향은 구조 텐서(structure tensor)의 가장자리 각도와 Perlin 흐름 각도를 (1 − coherence)의 가중치로 혼합하여 도출할 수 있습니다. coherence(결합도)가 1에 가까운 강한 가장자리에서는 스트로크가 윤곽선을 충실히 따르는 반면, coherence가 0 근처인 평평한 영역에서는 더 유기적인 흐름장을 갖게 됩니다. 이 트릭을 사용하면 노이즈 요소에 의해 도입된 난류(turbulence) 덕분에 배경이 마치 흐르는 듯한 붓질처럼 보이게 됩니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 HN AI Posts의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기