SonataSmooth2D.Tune · SmoothingConductor.cs · 전체 필터 완전 해설

Image Smoothing Filters
1D→2D · RGB · 8/16bit

Rectangular부터 Savitzky-Golay까지 6가지 필터의 수학적 원리, 커널 호버 수식, RGB 채널 독립 처리, 8-bit / 16-bit 구현을 코드와 다이어그램으로 완전 정리합니다.

Overview · Architecture

SmoothingConductor 전체 구조

6개 필터는 모두 하나의 진입점 ApplySmoothing()을 통해 호출됩니다. 비트심도(8/16-bit)에 따라 파이프라인이 분기되고, 각 필터는 RGB 3채널을 독립 처리합니다.

6가지 필터 선택 구조

Filter 1
Rectangular
Filter 2
Binomial Avg
Filter 3
Binom. Median
Filter 4
Gauss. Median
Filter 5
Gaussian
Filter 6
Savitzky-Golay
// Apply8() / Apply16() 내 분기
if (doRect)                 → ApplyRect8() / ApplyRectPlanes16()
if (doAvg)                  → ApplyBinomialAverage8() / ApplyBinomialAveragePlanes16()
if (doMed && !doGaussMed)   → ApplyWeightedMedian8(Binom) / ApplyWeightedMedianPlanes16()
if (doGaussMed)             → ApplyWeightedMedian8(Gauss) / ApplyWeightedMedianPlanes16()
if (doGauss)                → ApplyGaussian8() / ApplyGaussianPlanes16()
if (isSg)                   → ApplySavitzkyGolaySeparable8() / ApplySavitzkyGolaySeparablePlanes16()

🏗️ 전체 처리 흐름

ENTRY POINT
SmoothingConductor.ApplySmoothing()
Bitmap input · int r · BoundaryMode · filter flags · OutputBitDepth
OutputBitDepth.Bit8
Apply8()
① Ensure24bppFormat()
② LockBits → byte[] sBuffer
③ 픽셀 = sBuffer[p], [p+1], [p+2]
④ 필터 적용 (Parallel.For)
⑤ Marshal.Copy → dst 24bpp
BitDepth
⟵│⟶
OutputBitDepth.Bit16
Apply16()
① ExtractPlanes() → double[,] B,G,R
② 16-bit 픽셀값 (0~65535)
③ 필터 적용 (Parallel.For)
④ WritePlanes() → dst 48bpp
⑤ ClampToUShort() 정밀 출력
R 채널
sBuffer[p+2] / planes.R[y,x]
G 채널
sBuffer[p+1] / planes.G[y,x]
B 채널
sBuffer[p+0] / planes.B[y,x]
Bitmap dst 출력
R+G+B 재합성 · ClampToByte / ClampToUShort
공유 인프라 GetBinomial1D + 캐싱 ComputeGaussian1D GetIndex1D 경계처리 BoundaryMode 6종 Parallel.For 병렬화 ThreadLocal 버퍼

🔑 핵심 원리: 1D → 2D 외적(Outer Product)

모든 필터(SG 제외)는 1D 가중치 배열 하나만 생성하고, 루프 안에서 w = wY[wy+r] × wX[wx+r] 곱셈 한 번으로 2D 가중치를 즉시 계산합니다. 별도 2D 배열 할당 없음.

1D w = [1,4,6,4,1]
C(4,0)=11
C(4,1)=44
C(4,2)=66
C(4,3)=44
C(4,4)=11
전치(T)
1
4
6
4
1
=
2D 커널 (셀 호버 = 계산식)
1×1=11
1×4=44
1×6=66
1×4=44
1×1=11
4×1=44
4×4=1616
4×6=2424
4×4=1616
4×1=44
6×1=66
6×4=2424
6×6=36 (중앙)36
6×4=2424
6×1=66
4×1=44
4×4=1616
4×6=2424
4×4=1616
4×1=44
1×1=11
1×4=44
1×6=66
1×4=44
1×1=11
중앙 36 ÷ 모서리 1 = 36× 차이

🔬 같은 반경 r — 필터별 커널 가중치 변화 비교 (셀 호버 → 수식 표시)

반경 r을 고정한 채 필터를 바꾸면 같은 (2r+1)×(2r+1) 크기 안에서 가중치 분포가 어떻게 달라지는지 확인하세요.

2 → 커널 5×5 / 픽셀 수 25
SG (Savitzky-Golay)는 외적이 아닌 X→Y 순차 합성곱으로 처리됩니다. 표시된 값은 1D 계수이며, 음수 계수가 나타나는 것이 엣지 보존의 수학적 이유입니다.
Rectangular은 모든 픽셀이 동등하므로 가중치가 1/(2r+1)²로 균일합니다.

📊 6가지 필터 특성 비교

필터가중치 방식출력엣지 보존노이즈 강건성8-bit Median 알고리즘속도
Rectangular균일 (모두 1/(2r+1)²)평균낮음낮음★★★★★
Binomial Avg이항 계수 C(n-1,k)가중 평균보통보통★★★★☆
Binom. Median이항 계수가중 중앙값높음매우 높음Bucket[256] O(n+256)★★★☆☆
Gauss. Medianexp(−x²/2σ²)가중 중앙값높음매우 높음Bucket[256] O(n+256)★★★☆☆
Gaussianexp(−x²/2σ²)가중 평균보통보통★★★★☆
Savitzky-Golay다항 회귀(QR)최소제곱 추정매우 높음보통★★☆☆☆
Core Architecture · Channels

RGB 채널 독립 처리 & 8/16-bit 파이프라인

컬러 이미지는 R·G·B 3채널의 조합입니다. 모든 필터는 세 채널을 완전히 독립적으로 필터링합니다. 비트심도에 따라 메모리 레이아웃·데이터 타입·Median 알고리즘이 달라집니다.

🎨 메모리 안의 RGB — 24bpp vs 48bpp

8-BIT · Format24bppRgb
B
p+0
1 byte
G
p+1
1 byte
R
p+2
1 byte
B
p+3
next px
...
픽셀당 3 bytes · 범위 0~255 · int p = ny*stride + nx*3
16-BIT · Format48bppRgb
B
sp+0~1
2 bytes LE
G
sp+2~3
2 bytes LE
R
sp+4~5
2 bytes LE
...
픽셀당 6 bytes · 범위 0~65535 · b16 = buf[sp] | (buf[sp+1] << 8)
// 8-bit: 인터리브 byte 배열에서 직접 채널 추출
int p = (ny * sStride) + (nx * 3);
sumB += w * sBuffer[p];     // Blue  offset +0
sumG += w * sBuffer[p + 1]; // Green offset +1
sumR += w * sBuffer[p + 2]; // Red   offset +2

// 16-bit: ExtractPlanes()가 채널을 double[,]으로 사전 분리
planes.B[y, x] = (double)(sBuffer[sp]   | (sBuffer[sp+1] << 8)); // LE 2-byte → double
planes.G[y, x] = (double)(sBuffer[sp+2] | (sBuffer[sp+3] << 8));
planes.R[y, x] = (double)(sBuffer[sp+4] | (sBuffer[sp+5] << 8));
8-BIT PIPELINE — Apply8()
byte[] sBufferFormat24bppRgbMarshal.Copy
  • 픽셀값: 0 ~ 255 (byte)
  • 채널당 1 byte, 픽셀당 3 bytes
  • Median: Bucket[256] — O(n+256)
  • 출력: ClampToByte() → 24bpp
  • 스레드 버퍼: MedianThreadBuffers8
  • 속도: 가장 빠름
16-BIT PIPELINE — Apply16()
double[,] PlanesFormat48bppRgbExtractPlanes
  • 픽셀값: 0 ~ 65535 (double)
  • 채널당 2 bytes (LE), 픽셀당 6 bytes
  • Median: Array.Sort — O(n log n)
  • 출력: ClampToUShort() → 48bpp
  • 스레드 버퍼: MedianPlaneBuf16
  • 정밀도: 풀 16-bit 보존

⚡ Median 전략: 8-bit Bucket vs 16-bit Sort

8-BIT: 히스토그램 버킷 방식
Array.Clear(bucket, 0, 256);
for (i) { bucket[vals[i]] += w[i]; total += w[i]; }

double half = total / 2.0;
for (i = 0; i < 256; i++) {
  acc += bucket[i];
  if (acc > half + eps) return (byte)i;   // ✓
  if (acc >= half - eps)
    return 보간(i, nextNonEmpty); // (i+k+1)>>1
}
// O(n+256) — 정렬 불필요!
가중 히스토그램 (주황=중앙값):
0128255
16-BIT: 간접 정렬 방식
// 값 배열 불변, 인덱스 배열만 정렬
for (i) idx[i] = i;
comparer.SortValues = values;
Array.Sort(idx, 0, count, comparer); // O(n log n)

double half = total / 2.0, acc = 0;
for (i) {
  acc += weights[idx[i]];
  if (acc > half + eps)  return values[idx[i]];
  if (acc >= half - eps) return (lo + hi) / 2.0;
}
// 65536 버킷 없이 임의 정밀도 지원
Filter 1 / 6

Rectangular — 단순 균일 박스 평균

커널 내 모든 픽셀에 동일한 가중치를 부여합니다. 가장 단순하고 빠르지만 엣지가 흐릿해집니다. Box Filter라고도 합니다.

📐 수식 & 커널 (r=2 예시, 셀 호버 → 수식)

output(x,y) = 1/(2r+1)² · Σwy=-rr Σwx=-rr pixel(x+wx, y+wy)
r=2 · 5×5 · 균일 가중치 1/25
w(−2,−2)=1/251
w(−2,−1)=1/251
w(−2,0)=1/251
w(−2,+1)=1/251
w(−2,+2)=1/251
w(−1,−2)=1/251
w(−1,−1)=1/251
w(−1,0)=1/251
w(−1,+1)=1/251
w(−1,+2)=1/251
w(0,−2)=1/251
w(0,−1)=1/251
w(0,0)=1/25 (중앙)1
w(0,+1)=1/251
w(0,+2)=1/251
w(+1,−2)=1/251
w(+1,−1)=1/251
w(+1,0)=1/251
w(+1,+1)=1/251
w(+1,+2)=1/251
w(+2,−2)=1/251
w(+2,−1)=1/251
w(+2,0)=1/251
w(+2,+1)=1/251
w(+2,+2)=1/251
모든 가중치 동일 · count=(2r+1)²=25 · 각 픽셀 기여 = 1/25
r크기픽셀수각 기여
13×391/9 ≈ 11.1%
25×5251/25 = 4%
37×7491/49 ≈ 2%
49×9811/81 ≈ 1.2%
주파수 영역: sinc 함수 형태. 이상적이지 않아 링잉 (ringing) 아티팩트가 생길 수 있음.
8-BIT 구현 — ApplyRect8()
R
sumR += sBuffer[p+2]
G
sumG += sBuffer[p+1]
B
sumB += sBuffer[p+0]
// 범용 경로 (GetIndex1D 기반 경계 처리)
for (int wy=-r; wy<=r; wy++) {
  int ny = GetIndex1D(y+wy, height, mode);
  for (int wx=-r; wx<=r; wx++) {
    int nx = GetIndex1D(x+wx, width, mode);
    if (nx < 0 || ny < 0) {
      if (mode == ZeroPad) count++;
      continue;
    }
    int p = ny*sStride + nx*3;
    sumB += sBuffer[p];
    sumG += sBuffer[p+1];
    sumR += sBuffer[p+2];
    count++;
  }
}
// 반올림 포함 정수 나눗셈
dBuffer[d]   = (byte)((sumB + count/2) / count);
dBuffer[d+1] = (byte)((sumG + count/2) / count);
dBuffer[d+2] = (byte)((sumR + count/2) / count);
픽셀값 byte, 정수 누적합, ClampToByte 불필요 (범위 자동 보장). 16-bit와 달리 별도 Interior 분기 없이 모든 픽셀에 GetIndex1D() 호출.
16-BIT 구현 — ApplyRectPlanes16()
R
planes.R[ny,nx]
G
planes.G[ny,nx]
B
planes.B[ny,nx]
// fullCount 사전 계산 (Interior 공통)
int fullCount = (2*r+1) * (2*r+1);
bool yIn = y>=r && y<h-r;

if (yIn && x>=r && x<w-r) {
  // 내부: GetIndex1D() 호출 없음
  outB[y,x] = sumB / fullCount;
  outG[y,x] = sumG / fullCount;
  outR[y,x] = sumR / fullCount;
} else {
  // 경계: GetIndex1D() / Adaptive
  outB[y,x] = sumB / count; // 실제 count
}
double 누적, ClampToUShort()로 0~65535 보장

🔲 Adaptive 경계 — 윈도우 자동 축소

Adaptive 모드에서는 이미지 밖으로 나가는 대신 실제 가용 영역으로 윈도우를 축소하고, count를 재계산합니다.

내부
5×5
count=25
가장자리
4×5
count=20
모서리
3×3
count=9

🔢 8-bit 반올림 나눗셈 — (sumB + count/2) / count

8-bit Rectangular 평균은 정수 연산으로 수행됩니다. ClampToByte()가 불필요한 이유와 반올림 원리:

(sumB + ⌊count/2⌋) / count = ⌊sumB/count + 0.5⌋ = round(sumB/count)
// 예시: r=1, count=9, 픽셀합=1026
// 정확한 평균: 1026 / 9 = 114.0
// C# 정수 나눗셈: 1026 / 9 = 114 (내림)
// 반올림:   (1026 + 4) / 9 = 1030 / 9 = 114 ✓

// 예시: 픽셀합=1031
// 정확한 평균: 1031 / 9 = 114.56
// 내림:   1031 / 9 = 114
// 반올림: (1031 + 4) / 9 = 1035 / 9 = 115 ✓ (0.5 이상 올림)

dBuffer[d] = (byte)((sumB + count/2) / count);
// ↑ long 타입 sumB: 최대 255×81 = 20655 → byte 자동 보장
// 0 ≤ 평균 ≤ 255이므로 ClampToByte() 불필요
16-bit와의 차이: 16-bit는 double outB = sB / count 실수 나눗셈 후 ClampToUShort()로 범위 제한. 8-bit는 정수 연산만으로 범위가 자동 보장.

🧩 BoundaryMode별 count 계산 차이

Rectangular 평균에서 count(분모)는 경계 모드에 따라 다르게 계산됩니다. 이 차이가 경계 픽셀의 밝기에 직접 영향.

Mode범위 밖 처리count 영향결과
Symmetric거울 반사 → 유효 인덱스(2r+1)² 고정경계 데이터 반복 포함
Replicate가장자리 복제(2r+1)² 고정경계 픽셀 과다 반영
ZeroPadnx=-1 → 값 0, count++(2r+1)² 고정경계 어두워짐
ValidOnlynx=-1 → skipcount < (2r+1)²유효 픽셀만 평균
AdaptiveMaskskip (GetIndex1D 없음)count < (2r+1)²유효 픽셀만 평균
Adaptive윈도우 축소winW × winH축소된 영역만 평균
ZeroPad는 count에 포함하되 값은 0 → 분모 동일, 분자에 0 추가 → 경계 어두워짐. ValidOnly/AdaptiveMask는 count 자체가 줄어들어 밝기 왜곡 없음.

🔄 전체 처리 흐름 — 단계별 분석

Rectangular 필터의 처리 과정을 4단계로 분해합니다. 모든 필터 중 가장 단순하지만, 경계 처리와 8/16-bit 분기를 이해하는 데 중요합니다.

1
입력 준비 — 비트심도별 분기

8-bit: Ensure24bppFormat() → LockBits → byte[] sBuffer 추출. 16-bit: ExtractPlanes() → double[,] B/G/R 분리.

2
윈도우 순회 — 모든 픽셀의 합 계산

각 출력 픽셀에 대해 반경 r의 (2r+1)² 윈도우를 순회하며 R·G·B 채널별 독립 합산. 가중치 없이 단순 누적합.

3
경계 처리 — BoundaryMode별 분기

Adaptive: 윈도우 축소 + count 재계산. Symmetric/Replicate: GetIndex1D()로 인덱스 매핑. ZeroPad: 범위 밖 = 0, count 유지. ValidOnly: 범위 밖 skip, count 감소.

4
출력 — 나눗셈과 클램핑

8-bit: (sumB + count/2) / count 반올림 정수 나눗셈 → byte cast. 16-bit: sB / count 실수 나눗셈 → ClampToUShort().

⚡ Interior 빠른 경로 — 8-bit vs 16-bit 구현 차이

16-bit 파이프라인은 Interior(경계에서 r 이상 떨어진) 픽셀에 대해 GetIndex1D() 호출을 완전히 생략하는 빠른 경로를 가집니다. 8-bit는 모든 픽셀에 GetIndex1D()를 호출합니다.

8-BIT — 단일 경로 (GetIndex1D 항상 호출)
// ApplyRect8: Adaptive 분기만 별도,
// 나머지는 모두 GetIndex1D() 경로

if (mode == BoundaryMode.Adaptive)
{
    // 윈도우 축소 → 직접 루프
    int left = Math.Min(r, x);
    int right = Math.Min(r, width-1-x);
    // ...
}
else
{
    // 모든 픽셀에 GetIndex1D() 호출
    for (wy) {
        int ny = GetIndex1D(y+wy, height, mode);
        for (wx) {
            int nx = GetIndex1D(x+wx, width, mode);
            // ← 내부 픽셀도 이 검사를 통과
        }
    }
}
8-bit는 byte[] sBuffer에서 직접 읽는 단순 구조. Interior 분기 없음 — 코드 단순성 우선.
16-BIT — 3-way 분기 (Interior 최적화)
int fullCount = (2*r+1) * (2*r+1);
bool yInterior = y >= r && y < height-r;

if (mode == Adaptive
    && !(yInterior && x >= r && x < width-r))
{
    // 경계 Adaptive: 윈도우 축소
}
else if (yInterior && x >= r && x < width-r)
{
    // ★ Interior 빠른 경로:
    // GetIndex1D() 호출 0회!
    double sB = 0;
    for (int wy = -r; wy <= r; wy++)
        for (int wx = -r; wx <= r; wx++)
            sB += planes.B[y+wy, x+wx];
    outB[y,x] = sB / fullCount;
    // ↑ 사전 계산된 fullCount 재사용
}
else
{
    // 경계: GetIndex1D() 경로
}
대부분의 픽셀(Interior)이 GetIndex1D() 없이 직접 인덱싱. fullCount도 루프 밖에서 1회만 계산.

🔍 Adaptive 경계 처리 — 코드 상세 (8-bit)

// ApplyRect8 — Adaptive 분기 전체 코드
if (mode == BoundaryMode.Adaptive)
{
    int left   = Math.Min(r, x);                // X 왼쪽 여유
    int right  = Math.Min(r, width - 1 - x);    // X 오른쪽 여유
    int top    = Math.Min(r, y);                // Y 위쪽 여유
    int bottom = Math.Min(r, height - 1 - y);  // Y 아래쪽 여유

    int winW = left + right + 1;   // 실제 윈도우 너비
    int winH = top + bottom + 1;   // 실제 윈도우 높이
    int startX = x - left;          // 윈도우 시작 X
    int startY = y - top;           // 윈도우 시작 Y

    long sumB = 0, sumG = 0, sumR = 0;
    int count = winW * winH;        // 축소된 count

    for (int yy = 0; yy < winH; yy++)
    {
        int rowOffset = (startY + yy) * sStride;
        for (int xx = 0; xx < winW; xx++)
        {
            int p = rowOffset + (startX + xx) * 3;
            sumB += sBuffer[p];       // Blue
            sumG += sBuffer[p + 1];   // Green
            sumR += sBuffer[p + 2];   // Red
        }
    }

    int d = y * dStride + x * 3;
    dBuffer[d]     = (byte)((sumB + (count / 2)) / count);
    dBuffer[d + 1] = (byte)((sumG + (count / 2)) / count);
    dBuffer[d + 2] = (byte)((sumR + (count / 2)) / count);
}
Adaptive vs 다른 모드의 차이: Adaptive는 GetIndex1D()를 호출하지 않고 윈도우 자체를 축소합니다. 따라서 모든 인덱스가 보장된 범위 내에 있어 경계 검사가 불필요하며, 축소된 count로 정확한 평균을 계산합니다. 다른 모드(Symmetric/Replicate 등)는 원본 크기 윈도우를 유지하고 GetIndex1D()로 범위 밖 인덱스를 매핑합니다.

⚡ 성능 특성 — Rectangular 필터

장점
  • 가장 빠른 필터 — 가중치 곱셈 없음 (w=1)
  • 정수 산술만 (8-bit) — FPU 미사용
  • ClampToByte() 불필요 — 범위 자동 보장
  • Parallel.For 행 단위 완전 병렬
단점
  • 엣지 블러링 최대 — 모든 픽셀 동등 취급
  • 주파수 영역: sinc 형태 → 링잉 아티팩트
  • 노이즈 강건성 최저 — 극단값에 취약
  • 8-bit에서 Interior 최적화 없음 (16-bit만)
Filter 2 / 6

Binomial Average — 이항 가중 평균

파스칼의 삼각형(이항 계수)으로 가중치를 생성합니다. 중앙 픽셀에 높은 가중치를 주어 가우시안과 유사하지만, 정수 기반으로 더 빠르고 Dictionary 캐싱이 가능합니다.

📐 수식 & 커널 (r=2 예시, 셀 호버 → 수식)

w[i] = C(n−1, i) = (n−1)! / (i! × (n−1−i)!)  재귀: w[i] = w[i−1] × (n−i) / i  (n = 2r+1)
r=2 · 2D = 외적 [1,4,6,4,1]ᵀ × [1,4,6,4,1]
C(4,0)²=1×1=11
C(4,0)×C(4,1)=1×4=44
1×C(4,2)=1×6=66
1×4=44
1×1=11
4×1=44
C(4,1)²=4×4=1616
4×6=2424
4×4=1616
4×1=44
6×1=66
6×4=2424
C(4,2)²=6×6=36 (중앙·최대)36
6×4=2424
6×1=66
4×1=44
4×4=1616
4×6=2424
4×4=1616
4×1=44
1×1=11
1×4=44
1×6=66
1×4=44
1×1=11
원시 합 = 16² = 256 · 중앙 36/256 = 14.1% · 모서리 1/256 = 0.4%
1D 계수 예시:
n (r)계수
3 (r=1)[1, 2, 1]4 = 2²
5 (r=2)[1,4,6,4,1]16 = 2⁴
7 (r=3)[1,6,15,20,15,6,1]64 = 2⁶
9 (r=4)[1,8,28,56,70,...]256 = 2⁸
합 = 2^(n−1) = 2^(2r) · Dictionary 캐싱으로 재계산 없음
// GetBinomial1D(n) — 캐싱 후 반환
var c = new double[n]; c[0] = 1.0;
for (int i = 1; i < n; i++) c[i] = c[i-1] * (n-i) / i;
// 2D 적용: w = coeff1D[wy+r] × coeff1D[wx+r]  ← 외적 즉시 계산
8-BIT 구현 — ApplyBinomialAverage8()
R
sumR += w × sBuffer[p+2]
G
sumG += w × sBuffer[p+1]
B
sumB += w × sBuffer[p+0]
double[] coeff = GetBinomial1D(windowSize);

for (int wy=-r; wy<=r; wy++) {
  double wyW = coeff[wy+r];        // 행 가중치
  for (int wx=-r; wx<=r; wx++) {
    double w = wyW * coeff[wx+r];  // 2D = 행×열
    sumB += w * sBuffer[p];
    sumG += w * sBuffer[p+1];
    sumR += w * sBuffer[p+2];
    denom += w;
  }
}
out = ClampToByte(sum / denom);
denom은 정규화된 이항 계수 합. 경계(Adaptive)에서는 winW×winH 축소 계수 재생성.
16-BIT 구현 — ApplyBinomialAveragePlanes16()
R
planes.R[ny,nx]
G
planes.G[ny,nx]
B
planes.B[ny,nx]
// fullDenom: Interior용 분모 1회 사전 계산
double fullDenom = 0;
for (wy) for (wx)
  fullDenom += coeff[wy+r] * coeff[wx+r];

if (yInterior && xInterior) {
  outB[y,x] = sB / fullDenom; // ← 최고속
  outG[y,x] = sG / fullDenom;
  outR[y,x] = sR / fullDenom;
} else {
  // 경계: GetBinomial1D(winW/H) 재생성
}
Interior는 fullDenom 재사용으로 나눗셈 1회. 경계는 비대칭 계수 별도 생성.

📊 denom(분모) 계산 — 경로별 차이

이항 가중 평균의 분모는 경계 모드에 따라 다르게 계산됩니다. 이 차이가 경계 픽셀의 밝기에 직접 영향.

경로denom 계산코드
Interior
(16-bit)
fullDenom (1회 사전계산) for(wy)for(wx) fullDenom += coeff[wy+r]*coeff[wx+r]
Adaptive sumWx × sumWy
(축소된 계수 합의 곱)
cx = GetBinomial1D(winW);
denom = sumWx * sumWy
AdaptiveMask 유효 픽셀 가중치만 누적 if ((uint)ny >= height) continue;
denom += w;
ZeroPad 전체 가중치 누적
(범위 밖도 denom에 포함)
if (nx<0) { denom+=w; continue; }
Symmetric/Replicate 전체 가중치 누적 모든 nx 유효 → denom += w
Adaptive vs AdaptiveMask: Adaptive는 윈도우를 축소하고 GetBinomial1D(winW)로 새 계수 생성. AdaptiveMask는 원래 윈도우 유지하되 범위 밖 스킵 → denom이 자동 감소하여 재정규화.

🔍 AdaptiveMask 경계 처리 — 코드 상세

// ApplyBinomialAverage8 — AdaptiveMask 분기
else if (mode == BoundaryMode.AdaptiveMask)
{
    double sumB = 0, sumG = 0, sumR = 0;
    double denom = 0;

    for (int wy = -r; wy <= r; wy++)
    {
        int ny = y + wy;
        if ((uint)ny >= (uint)height) continue;
        // ↑ unsigned 비교로 음수&초과를 한 번에 검사
        //   (uint)(-1) = 4294967295 >= height → skip

        double wyW = coeff1D[wy + r];

        for (int wx = -r; wx <= r; wx++)
        {
            int nx = x + wx;
            if ((uint)nx >= (uint)width) continue;

            double w = wyW * coeff1D[wx + r];
            int p = (ny * sStride) + (nx * 3);

            sumB += w * sBuffer[p];
            sumG += w * sBuffer[p + 1];
            sumR += w * sBuffer[p + 2];
            denom += w;  // 유효 픽셀만 반영
        }
    }
    if (denom <= 0) denom = 1;
    dBuffer[d]   = ClampToByte(sumB / denom);
    dBuffer[d+1] = ClampToByte(sumG / denom);
    dBuffer[d+2] = ClampToByte(sumR / denom);
}
(uint)ny >= (uint)height는 음수와 범위 초과를 한 번의 비교로 처리하는 최적화. unsigned 변환 시 음수는 매우 큰 수가 되어 자동으로 >= height. GetIndex1D() 호출보다 빠름.

📐 Adaptive 경계 — 축소 커널 재생성 상세

Adaptive 모드에서 경계 픽셀은 축소된 윈도우에 맞는 새로운 Binomial 계수를 생성합니다. GetBinomial1D(winW/H) 캐싱으로 동일 크기 재계산 없음.

// Adaptive 분기 핵심
int left  = Math.Min(r, x);
int right = Math.Min(r, width - 1 - x);
int winW = left + right + 1;  // 축소된 너비

var cx = GetBinomial1D(winW); // 새 Binomial 계수
var cy = GetBinomial1D(winH);

double sumWx = 0, sumWy = 0;
for (i) sumWx += cx[i];  // 축소 계수 합
for (i) sumWy += cy[i];
double denom = sumWx * sumWy; // 2D 외적 합

// 예: 모서리(x=0,y=0) r=2
// winW=1+2+1=3 → cx=[1,2,1] 합=4
// winH=1+2+1=3 → cy=[1,2,1] 합=4
// denom = 4×4 = 16 (원본 5×5: 16²=256)
내부 (5×5)
[1,4,6,4,1] 합=16
가장자리 (4×5)
[1,3,3,1] 합=8
모서리 (3×3)
[1,2,1] 합=4
축소된 Binomial 계수는 원본과 다른 분포입니다 (예: [1,3,3,1] ≠ [1,4,6,4,1]의 부분). 이것이 Adaptive가 단순 마스킹(AdaptiveMask)보다 수학적으로 정확한 이유.
Filter 3 / 6

Binomial Weighted Median — 이항 가중 중앙값

이항 계수를 가중치로 사용해 가중 중앙값을 구합니다. 평균과 달리 극단값(노이즈)에 강건하고 엣지를 보존합니다.

📐 수식 & 커널 (r=2 예시, 셀 호버 → 수식)

Find m : Σv≤m w(wy,wx) ≥ (Σall w) / 2  where w(wy,wx) = C(n−1,wy+r) × C(n−1,wx+r)
r=2 · 동일 이항 계수 커널 (Avg와 동일 가중치, 출력 방법만 다름)
1×1=1 (가중치 최소)1
1×4=44
1×6=66
1×4=44
1×1=11
4×1=44
4×4=1616
4×6=2424
4×4=1616
4×1=44
6×1=66
6×4=2424
6×6=36 → 중앙 픽셀 누적 36/256=14.1%36
6×4=2424
6×1=66
4×1=44
4×4=1616
4×6=2424
4×4=1616
4×1=44
1×1=11
1×4=44
1×6=66
1×4=44
1×1=11
Binomial Avg와 동일한 가중치. 차이: 가중합 평균이 아닌 가중 중앙값을 구함
Rectangular 평균 (노이즈 있을 때):
[100,102,99,103,240,101,98,100,102] / 9 = 127
노이즈 240에 크게 끌림
Binomial Weighted Median:
101
240은 가중 중앙값에 영향 없음

📐 Step 1 — GetBinomial1D(n) : 1D 이항 가중치 생성 & Dictionary 캐싱

파스칼의 삼각형 재귀로 이항 계수를 생성합니다. Dictionary<int, double[]> 캐싱으로 동일 windowSize 재호출 시 O(1) 반환. Gaussian의 매번 exp() 계산 대비 구조적 속도 우위입니다.

// SmoothingConductor.cs — GetBinomial1D(n)
private static readonly Dictionary<int, double[]> _binom1D
    = new Dictionary<int, double[]>();
private static readonly object _binom1DLock = new object();

private static double[] GetBinomial1D(int n)
{
    lock (_binom1DLock)
    {
        if (_binom1D.TryGetValue(n, out var cached))
            return cached; // 캐시 적중 → 즉시 반환

        var c = new double[n];
        c[0] = 1.0;
        for (int i = 1; i < n; i++)
            c[i] = c[i - 1] * (n - i) / i;
        // ↑ 파스칼 삼각형 재귀: C(n-1,i)
        // 정규화 없이 원시 정수 계수 생성

        _binom1D[n] = c;
        return c;
    }
}

예시: windowSize별 1D 이항 계수

n (r)계수
3 (r=1)[1, 2, 1]4 = 2²
5 (r=2)[1, 4, 6, 4, 1]16 = 2⁴
7 (r=3)[1, 6, 15, 20, 15, 6, 1]64 = 2⁶
9 (r=4)[1, 8, 28, 56, 70, ...]256 = 2⁸
합 = 2^(n−1) = 2^(2r) — 정수 합은 비트 시프트 정규화 가능. Dictionary에 키 = n으로 캐싱 → 같은 윈도우 크기 재호출 비효율성 제거.
1
4
6
4
1
r=2 · 중앙(6)은 가장자리(1)보다 6배 높은 가중치

🔑 Step 2 — 2D 외적 수집 : w = w1D[wy+r] × w1D[wx+r]

2D 커널을 명시적 배열로 생성하지 않고, 루프 안에서 행 가중치 × 열 가중치를 곱해 즉시 계산합니다. Median 전용 코드에서는 픽셀값(vals[])과 가중치(w[])를 함께 수집합니다.

// ApplyWeightedMedian8() — 범용 경로 (Symmetric/Replicate/ZeroPad 등)
double[] w1D = GetBinomial1D(windowSize);
int maxSamples = checked(windowSize * windowSize);

for (int wy = -r; wy <= r; wy++)
{
    int ny = GetIndex1D(y + wy, height, mode);

    for (int wx = -r; wx <= r; wx++)
    {
        int nx = GetIndex1D(x + wx, width, mode);
        double wt = w1D[wy + r] * w1D[wx + r];
        // ↑ 2D 가중치 = 행w × 열w (외적)

        if (nx < 0 || ny < 0)
        {
            if (mode == BoundaryMode.ZeroPad)
            {   // 값 0, 가중치 유지
                valsB[count] = 0;
                valsG[count] = 0;
                valsR[count] = 0;
                w[count] = wt;
                count++;
            }
            continue;
        }

        int p = (ny * sStride) + (nx * 3);
        valsB[count] = sBuffer[p];       // Blue
        valsG[count] = sBuffer[p + 1];   // Green
        valsR[count] = sBuffer[p + 2];   // Red
        w[count]     = wt;               // 가중치
        count++;
    }
}
// 각 채널 독립적으로 가중 중앙값 계산
byte b  = WeightedMedianDouble8(valsB, w, bucket, count);
byte g  = WeightedMedianDouble8(valsG, w, bucket, count);
byte rr = WeightedMedianDouble8(valsR, w, bucket, count);

int d = (y * dStride) + (x * 3);
dBuffer[d] = b; dBuffer[d+1] = g; dBuffer[d+2] = rr;

✅ On-the-fly 외적 (코드 방식)

  • 1D 배열 하나만 캐싱 (_binom1D)
  • 루프 내 곱셈 1회로 2D 가중치 즉시 생성
  • 별도 2D 배열 할당 없음 → 메모리 절약
  • Adaptive 모드에서도 축소 윈도우 즉시 대응
Median vs Average 수집 차이:
Average 필터: sumB += w × pixel → 가중합 누적만 (가중치 별도 저장 불필요)
Median 필터: vals[] + w[] 둘 다 저장 → 정렬/버킷 후 50% 탐색에 필요

🪣 Step 3 — Weighted Median 알고리즘 : Bucket 방식 (8-bit)

일반 중앙값은 값을 정렬해야 합니다(O(n log n)). 8-bit 이미지의 경우 픽셀값이 0~255 범위이므로, 크기 256짜리 버킷 배열에 가중치를 누적한 뒤 누적합이 전체의 절반을 넘는 지점을 찾는 것으로 O(n + 256) = O(n)에 해결합니다.

1
윈도우 내 픽셀 수집

반경 r의 (2r+1)² 픽셀을 순회하며 pixel 값(byte)과 2D Binomial 가중치(double)를 ThreadLocal 버퍼에 적재. R·G·B 채널별 독립 배열.

2
Bucket 누적 — bucket[value] += weight

크기 256 배열 초기화 후, 각 픽셀의 강도(0~255)를 인덱스로 하여 가중치를 누적합니다. B/G/R 채널마다 독립적으로 3회 수행.

3
절반 찾기 — acc > total / 2

버킷 0→255 순서로 누적합을 계산하여 전체 가중치의 절반(+ε)을 초과하는 첫 인덱스를 반환. ε = total × 10⁻¹² (부동소수점 허용 오차).

4
경계 보간 처리 (TieBreak)

누적합이 정확히 절반과 ε 이내이면, 다음 비어있지 않은 버킷과 보간하여 부드러운 중앙값 반환: (i + k + 1) >> 1

// SmoothingConductor.cs — WeightedMedianDouble8() 전체 코드
private static byte WeightedMedianDouble8(
    byte[] values, double[] weights,
    double[] bucket, int count)
{
    if (count <= 0) return 0;

    Array.Clear(bucket, 0, 256);          // ① 버킷 초기화

    double total = 0;
    for (int i = 0; i < count; i++)
    {
        double w = weights[i];
        bucket[values[i]] += w;           // ② 픽셀값 → 버킷에 가중치 누적
        total += w;
    }

    if (total <= 0) return 0;

    double half = total / 2.0;
    double eps  = total * 1e-12;         // 부동소수점 허용 오차
    double acc  = 0;

    for (int i = 0; i < 256; i++)        // ③ 0→255 순서 탐색
    {
        double w = bucket[i];
        if (w > 0)
        {
            acc += w;

            if (acc > half + eps)              // 중앙값 발견!
                return (byte)i;

            if (acc >= half - eps)             // ④ TieBreak 보간
            {
                for (int k = i + 1; k < 256; k++)
                {
                    if (bucket[k] > 0)
                        return (byte)((i + k + 1) >> 1);
                }
                return (byte)i;
            }
        }
    }
    return 255;
}
이항 계수는 정수값 double (1.0, 4.0, 6.0 등 — IEEE 754에서 정확히 표현)이므로, bucket 누적 연산이 수학적으로 정확합니다. Gaussian의 exp() 근사값 가중치와 달리 부동소수점 누적 오차 없음.

🧵 Step 4 — MedianThreadBuffers8 : ThreadLocal 스레드 안전 버퍼

Parallel.For 행 단위 병렬화에서 각 스레드가 독립 버퍼를 사용합니다. lock 없이 완전 병렬 처리.

// 스레드별 버퍼 구조체
private sealed class MedianThreadBuffers8
{
    public readonly byte[]   ValsB;  // Blue 픽셀값
    public readonly byte[]   ValsG;  // Green 픽셀값
    public readonly byte[]   ValsR;  // Red 픽셀값
    public readonly double[] W;      // 2D 가중치
    public readonly double[] Bucket; // [256] 히스토그램

    public MedianThreadBuffers8(int maxSamples)
    {
        ValsB  = new byte[maxSamples];
        ValsG  = new byte[maxSamples];
        ValsR  = new byte[maxSamples];
        W      = new double[maxSamples];
        Bucket = new double[256];
    }
}
// ThreadLocal 생성 & 사용
int maxSamples = checked(windowSize * windowSize);
// ↑ r=2 → 5×5=25, r=5 → 11×11=121

using (var threadBuffers =
    new ThreadLocal<MedianThreadBuffers8>(
        () => new MedianThreadBuffers8(maxSamples)))
{
    Parallel.For(0, height, y =>
    {
        var buf = threadBuffers.Value;
        // 스레드별 독립 버퍼 → 동기화 없음

        var valsB  = buf.ValsB;
        var valsG  = buf.ValsG;
        var valsR  = buf.ValsR;
        var w      = buf.W;
        var bucket = buf.Bucket;
        // bucket[256]은 WeightedMedianDouble8
        // 내부에서 Array.Clear 후 재사용
        // → GC 없음, 할당 없음

        for (int x = 0; x < width; x++)
        {
            // ... 2D 수집 + 중앙값 계산 ...
        }
        proxy?.StepRows(1);
    });
}
maxSamples = (2r+1)² 사전 계산. 모든 배열이 ThreadLocal 내에서 한 번만 할당되어 전체 이미지 처리 동안 재사용. 행(row) 단위 완전 병렬 — 출력 픽셀이 입력에만 읽기 접근하므로 경쟁 조건 없음.
8-BIT 구현 — ApplyWeightedMedian8() (useGaussianWeights=false)
R
WeightedMedianDouble8(valsR, w, bucket, count)
G
WeightedMedianDouble8(valsG, w, bucket, count)
B
WeightedMedianDouble8(valsB, w, bucket, count)
// 가중치 생성 (Binomial)
double[] w1D = GetBinomial1D(windowSize);

// 2D 수집: vals[] + weights[]
for (wy) for (wx) {
  double w = w1D[wy+r] * w1D[wx+r];
  valsB[k] = sBuffer[p]; weightsB[k] = w;
}
// Bucket 알고리즘 O(n+256)
bucket[vals[i]] += weights[i]; total += w;
// Find: 누적 >= total/2 인 첫 i → 중앙값
if (acc > half + eps) return (byte)i;
Bucket[256] 재사용 (ThreadLocal). 정렬 불필요. 경계 보간: (i+k+1)>>1
가중 히스토그램 (주황=중앙값):
0128255
16-BIT 구현 — ApplyWeightedMedianPlanes16() (useGaussianWeights=false)
R
WeightedMedianSorted16(buf.R, buf.W, ...)
G
WeightedMedianSorted16(buf.G, buf.W, ...)
B
WeightedMedianSorted16(buf.B, buf.W, ...)
// 동일 이항 가중치, 단 정렬 기반
double[] w1D = GetBinomial1D(windowSize);

for (i) idx[i] = i;
comparer.SortValues = values;
Array.Sort(idx, 0, count, comparer); // O(n log n)

for (i) {
  acc += weights[idx[i]];
  if (acc > half + eps) return values[idx[i]];
  if (acc >= half - eps) return (lo+hi)/2.0;
}
Bucket[65536] 대신 Array.Sort로 임의 정밀도(0~65535) 지원. 연속 선형 보간.

⚖️ TieBreak — 누적 가중치가 정확히 절반일 때의 보간 처리

가중 중앙값 탐색에서 누적 가중치가 정확히 total/2에 도달하면 두 인접 값 사이에 중앙값이 위치합니다. 이 경계 상황을 TieBreak라 하며, 8-bit와 16-bit에서 보간 방식이 다릅니다.

eps 허용 오차 밴드 — half ± eps

코드는 부동소수점 비교를 위해 eps = total × 10⁻¹²의 미세 허용 밴드를 설정합니다. 누적값(acc)이 이 밴드 안에 들어오면 TieBreak 보간을 수행합니다.

acc < half−eps → 계속 누적
TieBreak 구간
acc > half+eps → 즉시 반환
|← half−eps ─── half ─── half+eps →|
8-BIT TieBreak — (i + k + 1) >> 1
// acc가 [half−eps, half+eps] 구간에 진입
if (acc >= half - eps) {
  // 다음 비어있지 않은 버킷 k 탐색
  for (int k = i + 1; k < 256; k++) {
    if (bucket[k] > 0)
      return (byte)((i + k + 1) >> 1);
  }
  return (byte)i; // 뒤에 값 없으면 현재 반환
}
반올림 규칙: (i + k + 1) >> 1 = ⌊(i + k + 1) / 2⌋
이는 두 값의 산술 평균에 +0.5 반올림(round-up)을 적용한 것입니다.
i (현재)k (다음)(i+k+1)>>1설명
100101101연속 → (201+1)/2=101 정확
1001021011칸 갭 → (203)/2=101.5 → ⌊⌋=101
100104102큰 갭 → (205)/2=102.5 → ⌊⌋=102
200201201연속 → (402)/2=201 정확
byte 반환이므로 정수 보간만 가능. +1과 비트 시프트>>1로 반올림을 구현하여 나눗셈 없이 처리.
16-BIT TieBreak — (lo + hi) / 2.0
// acc가 [half−eps, half+eps] 구간에 진입
if (acc >= half - eps && i + 1 < count) {
  double lo = values[idx[i]];
  double hi = values[idx[i + 1]];
  return (lo + hi) / 2.0;
}
연속 보간: double 정밀도로 두 값의 정확한 산술 평균을 반환합니다.
정렬된 인덱스 배열에서 idx[i]idx[i+1]은 값 기준 인접 원소이므로 빈 버킷 탐색이 불필요합니다.
lohi(lo+hi)/2설명
30000.030002.030001.0정확한 중간값
30000.030001.030000.5소수점 정밀도 보존
ClampToUShort() 후 0~65535 정수로 출력되지만, 필터 내부 연산은 full double 정밀도를 유지.
8-bit vs 16-bit TieBreak 차이 요약:
8-bit: bucket[256] 히스토그램 기반 → 다음 비어있지 않은 버킷을 탐색해 (i+k+1)>>1 반올림 정수 보간. byte 출력이므로 정수 결과만 가능.
16-bit: 간접 정렬(Array.Sort) 기반 → 정렬 순서상 바로 다음 원소와 (lo+hi)/2.0 연속 보간. double 내부 연산 → ClampToUShort() 시 반올림 출력.

🎯 실제 사용 사례 — Binomial Weighted Median Filter

Binomial Weighted Median은 엣지와 원본 구조를 보존하면서 임펄스·랜덤 노이즈만 선택적으로 제거하는 것이 핵심 강점이며, 이 특성은 다양한 산업 및 연구 분야에서 실질적인 이점을 제공합니다.

USE CASE 1
OCR — 이미지 텍스트 노이즈 제거 및 선명도 보정

스캔 문서, 사진 촬영된 텍스트, 팩스 수신 이미지 등에서 문자 인식(OCR) 전 노이즈를 제거하면서 글자 획의 엣지를 보존해야 합니다.

왜 Binomial Weighted Median 인가
  • 획 엣지 보존: 가중 중앙값은 극단값(salt-pepper, 스캔 노이즈)을 제거하면서도 문자 획의 경계(흑→백 전환)를 유지합니다. 가중 평균 필터는 획을 뭉개어 세리프·획 두께 정보를 소실합니다.
  • 이항 계수의 중앙 집중 → 원본 텍스처 유지: 중앙 픽셀에 높은 가중치를 부여하므로 문자 내부의 미세 텍스처(잉크 농도 변화, 세리프 곡률)가 보존됩니다. 이는 OCR 엔진의 특징점 검출 정확도에 직접 기여합니다.
  • 이진화(Binarization) 전처리 호환: 엣지가 보존된 상태에서 Otsu/Adaptive 이진화를 적용하면 임계값 경계가 정확해집니다. 평균 필터 후 이진화 시 발생하는 획 끊김·병합 아티팩트가 억제됩니다.
수학적 근거: 문자 획 경계에서 밝기 분포는 bimodal(이봉 분포)입니다. 가중 중앙값은 이봉 분포에서 다수파(signal)의 대표값을 선택하여 엣지를 보존합니다. 가중 평균은 두 모드를 혼합하여 경계를 흐리게 합니다.
USE CASE 2
2D 이미지 & 동영상 보정 — 센서/카메라 노이즈 제거

CMOS/CCD 센서의 핫 픽셀, 읽기 노이즈, 양자화 노이즈가 포함된 이미지 및 동영상에서 원본 구조를 유지하면서 품질을 향상합니다.

왜 Binomial Weighted Median 인가
  • 임펄스 노이즈(핫 픽셀) 완전 제거: 센서의 핫/데드 픽셀은 주변과 극단적으로 다른 값을 가집니다. 가중 중앙값은 이 극단값이 50% 누적 기준을 넘지 못하게 하여 완벽히 제거합니다. Binomial 가중치의 2D 중앙 ≤ 25%(r=1)이므로 모든 반경에서 보장.
  • Cross-Hatching Artifact 개선: 균일 가중 중앙값(Rectangular Median)은 정사각 윈도우 내 모든 픽셀을 동등하게 취급하여 대각/수직 방향으로 격자 패턴(Cross-Hatching)이 발생합니다. 이항 가중치는 중앙에서 방사형으로 감쇠하는 bell-shape 분포로 이 방향성 편향을 억제합니다.
  • r 증가 시에도 원본 강건성 유지: Binomial의 중앙 집중도는 r이 커질수록 상대적으로 더 높아집니다(중앙/모서리 비 = C(2r,r) ≫ 1). 따라서 강한 노이즈 제거를 위해 r을 키워도 중앙 픽셀(원본 신호)의 영향력이 지배적으로 유지됩니다.
수학적 근거 — Cross-Hatching Artifact: Cross-Hatching은 사각형 윈도우와 중앙값(Median) 연산이 결합될 때 특히 두드러지는 아티팩트입니다. 가중 평균(Mean) 필터에서는 모든 픽셀값이 연속적으로 혼합되어 이 현상이 거의 나타나지 않지만, 중앙값 필터는 이산적인 투표(voting) 방식으로 출력값을 결정하기 때문에 커널 형태의 비등방성이 결과에 직접 반영됩니다. 균일 박스 커널(Rectangular)은 사각형 윈도우의 모든 꼭짓점에 동등 투표권을 부여하므로, 수평·수직·대각 방향에서 동일 수의 샘플이 참여하여 Median 결과가 방향에 따라 불연속적으로 변합니다. 이는 균일 가중 중앙값(Unweighted Median)뿐 아니라, 가중치가 균일한 모든 중앙값 기반 필터에서 공통으로 발생하는 구조적 문제입니다. Binomial의 bell-shape 가중치는 w(wy,wx) = C(n-1,wy+r)×C(n-1,wx+r)로 중앙에서 등방향(isotropic)으로 감쇠하여, 유효 기여 영역이 원형에 가까워지고 방향성 아티팩트가 구조적으로 억제됩니다.
USE CASE 3
FlatTop Beam 보정 & 레이저 빔 프로파일링

레이저 빔 프로파일러(CCD/CMOS 기반)가 캡처한 2D 강도 분포에서 센서 노이즈를 제거하면서 빔의 공간 프로파일(FlatTop, Gaussian, 도넛 등)을 정밀하게 보존해야 합니다.

왜 Binomial Weighted Median 인가
  • FlatTop 프로파일의 급격한 에지 보존: FlatTop 빔은 중앙 평탄 영역에서 가장자리로 급격히 감쇠하는 step-like edge를 가집니다. 가중 평균(Gaussian blur)은 이 경사를 완화하여 빔 직경(D4σ, 86.5% 등)과 edge steepness 측정값을 왜곡합니다. 가중 중앙값은 edge를 보존하여 정확한 프로파일 측정을 보장합니다.
  • 센서 핫 픽셀 → 빔 형상 왜곡 방지: CCD 핫 픽셀이 빔 에너지로 오인되면 빔 중심(centroid), M² 값, power-in-bucket 측정이 심각하게 왜곡됩니다. Binomial WM은 핫 픽셀을 모든 r에서 안정적으로 제거합니다.
  • 평탄 영역 내 미세 구조 보존: FlatTop 빔의 중앙 평탄 영역에도 레이저 간섭·회절에 의한 미세 강도 변동(ripple)이 존재합니다. 이항 가중치의 중앙 집중 특성은 이 ripple을 보존하면서 랜덤 노이즈만 제거하여, 빔 균일도(uniformity) 분석의 정밀도를 유지합니다.
수학적 근거 — FlatTop Edge 보존: FlatTop 빔의 이상적 1D 단면은 rect(x/w) 함수입니다. 가중 평균(convolution with Gaussian)은 edge를 erf(x/√2σ)로 확산시켜 transition width를 증가시킵니다. 반면 가중 중앙값은 edge 양쪽의 값 분포가 달라도 다수파를 선택하므로, 이론적으로 edge shift = 0이 됩니다. 이는 ISO 11146 기준에 따른 빔 폭 측정의 정확도에 직접적인 영향을 미칩니다.
USE CASE 4
이미지 학습 전처리 — ML/DL 데이터 정제

CNN/Vision Transformer 등의 이미지 분류·탐지 모델 학습 전 노이즈를 제거하여 학습 데이터 품질을 높이고, 모델이 노이즈가 아닌 실제 특징(feature)에 집중하도록 합니다.

왜 Binomial Weighted Median 인가
  • 특징 보존 노이즈 제거 → 학습 신호 대 잡음비 향상: 엣지·텍스처를 보존하면서 노이즈를 제거하므로, 모델이 학습해야 할 실제 특징(edge, corner, texture)은 유지되고 학습을 방해하는 랜덤 노이즈만 제거됩니다. 이는 특히 소규모 데이터셋에서 과적합 방지에 효과적입니다.
  • 파라미터 없는 일관성 → 배치 전처리 적합: r만 결정하면 되므로 수만 장의 학습 이미지에 동일한 전처리를 일관되게 적용할 수 있습니다. Gaussian Median의 sigmaFactor 튜닝은 이미지 특성에 따라 달라야 하므로 대규모 배치에 부적합합니다. Dictionary 캐싱으로 반복 처리 시 성능도 우수합니다.
  • Data Augmentation과의 호환: 회전·반전·크롭 등 기하학적 augmentation 후에도 Binomial WM의 엣지 보존 특성은 유지됩니다. 반면 Gaussian blur 전처리 후 augmentation을 적용하면 이미 소실된 고주파 정보는 복원 불가능합니다.
수학적 근거 — 학습 효율: CNN의 초기 레이어는 edge/texture 검출 필터(Gabor-like)를 학습합니다. 입력 이미지의 엣지가 보존되어 있으면 이 필터들의 gradient 신호가 명확하여 수렴 속도가 빨라집니다. 노이즈가 남아있으면 gradient가 랜덤 방향으로 분산되어 학습이 느려지고, 엣지가 블러되면 gradient 크기 자체가 감소하여 vanishing gradient 유사 현상이 발생합니다. Binomial WM은 이 두 문제를 동시에 해결합니다.
DEEP DIVE — Cross-Hatching Artifact 개선 원리

균일 중앙값(Rectangular Median)에서 발생하는 Cross-Hatching(격자) Artifact를 Binomial Weighted Median이 어떻게 구조적으로 개선하는지 분석합니다.

❌ Rectangular Median — 균일 가중치
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
  • 모든 25개 픽셀이 동등한 투표권
  • 모서리 4개 + 변 12개 + 내부 9개 = 동일 영향
  • 유효 기여 영역 = 정사각형
  • 대각·수직 방향 전환점에서 불연속적 중앙값 점프 발생
✅ Binomial Weighted Median — bell-shape 가중치
1
4
6
4
1
4
16
24
16
4
6
24
36
24
6
4
16
24
16
4
1
4
6
4
1
  • 중앙(36) >> 모서리(1) = 36배 차이
  • 모서리 4개 합(4) vs 중앙 1개(36) → 모서리 영향 무시 수준
  • 유효 기여 영역 ≈ 원형(등방향)
  • 방향 전환점에서도 부드러운 전이 유지
핵심 원리: Cross-Hatching은 사각형 커널의 비등방성(anisotropy)에서 발생합니다. 중앙에서 (r,0) 방향(축 방향)과 (r,r) 방향(대각 방향)의 유클리드 거리는 r vs √2·r로 41% 차이가 나지만, 균일 커널은 둘 다 동등하게 취급합니다. Binomial 가중치는 중앙 거리에 따라 지수적으로 감쇠(C(n-1,k) ∝ Gaussian 근사)하므로, 대각 모서리(거리=√2·r)의 기여가 축 방향(거리=r)보다 자연스럽게 작아져 등방향(isotropic) 응답에 근접합니다. 이는 원형 커널(computational cost가 높음)을 사용하지 않고도 방향성 아티팩트를 억제하는 구조적 해법입니다.
DEEP DIVE — r 증가 시 원본 유지 강건성

노이즈가 강할 때 r을 키워 더 넓은 영역을 참조하면, 대부분의 필터는 원본 구조도 함께 뭉개집니다. Binomial Weighted Median은 r을 키워도 원본 신호를 유지하는 독특한 강건성을 가집니다.

r커널 중앙 2D% 중앙/모서리 비 중앙+인접4 누적% 원본 영향력
수학적 근거 — Breakdown Point: Weighted Median의 breakdown point(필터가 유효한 최대 오염 비율)은 1 − max(wᵢ)에 비례합니다. Binomial의 2D 최대 가중치 = (C(2r,r)/2^(2r))²는 Stirling 근사에 의해 ≈ 1/(π·r)로 감쇠합니다.
따라서 r↑ → max(w)↓ → breakdown point↑ → 더 많은 극단값을 허용하면서도 필터가 유효합니다. 동시에 중앙 + 인접 픽셀의 누적 가중치는 여전히 높아 신호(원본) 영향력을 유지합니다.

실용적 의미: r=1에서 25% 미만의 픽셀이 오염되어야 유효하지만, r=3에서는 약 97% 수준까지 breakdown point가 상승합니다. 이는 극심한 소금-후추 노이즈에서도 안정적으로 동작함을 의미합니다.
사용 사례 핵심 요구사항 Binomial WM이 충족하는 근거
OCR 획 엣지 보존 + 노이즈 제거 Bimodal 분포에서 다수파 선택 → 엣지 shift=0, 이진화 호환
2D 이미지 보정 핫 픽셀 제거 + 방향 아티팩트 억제 Bell-shape 등방향 감쇠 → Cross-Hatching 억제, 모든 r에서 임펄스 제거 보장
레이저 빔 프로파일링 FlatTop edge 보존 + 센서 노이즈 제거 Median의 edge shift=0 특성 + 미세 ripple 보존(중앙 집중 가중치)
ML 전처리 특징 보존 + 배치 일관성 r만 파라미터 → 대규모 배치 일관 적용, 엣지 보존 → gradient 신호 유지
Filter 4 / 6

Gaussian Weighted Median — 가우시안 가중 중앙값

Binomial Median과 동일한 알고리즘에 가우시안 가중치를 사용합니다. sigmaFactor로 σ를 정밀 조절해 감쇠 강도를 제어합니다.

📐 수식 & 커널 (r=2, σ=windowSize/sigmaFactor 예시, 셀 호버 → 수식)

g[i] = exp(−(i−center)² / (2σ²))  정규화: g[i] /= Σg  2D: w(wy,wx) = g[wy+r] × g[wx+r]
r=2, σ≈0.83 (sigmaFactor=6) · 정규화 후 합=1
g(−2)²≈0.001
exp(−4/2σ²)²
.001
g(−2)×g(−1)≈0.006.006
g(−2)×g(0)≈0.013.013
g(−2)×g(1)≈0.006.006
g(−2)²≈0.001.001
g(−1)×g(−2)≈0.006.006
g(−1)²≈0.054.054
g(−1)×g(0)≈0.112.112
g(−1)²≈0.054.054
g(−1)×g(−2)≈0.006.006
g(0)×g(−2)≈0.013.013
g(0)×g(−1)≈0.112.112
g(0)²≈0.230 (중앙·최대).230
g(0)×g(1)≈0.112.112
g(0)×g(2)≈0.013.013
g(1)×g(−2)≈0.006.006
g(1)²≈0.054.054
g(1)×g(0)≈0.112.112
g(1)²≈0.054.054
g(1)×g(2)≈0.006.006
g(2)²≈0.001.001
g(2)×g(1)≈0.006.006
g(2)×g(0)≈0.013.013
g(2)×g(1)≈0.006.006
g(2)²≈0.001.001
합=1 정규화 · 중앙≈23.0% · 모서리≈0.07%
σF=3
넓고 평탄
σF=6
중앙 집중
σF=12
뾰족·원본
// ComputeGaussian1D(len, sigma) — sigma = windowSize / sigmaFactor
double twoSigmaSq = 2 * sigma * sigma;
for (int i = 0; i < len; i++) {
    int x = i - (len-1)/2;
    g[i] = Math.Exp(-(x*x) / twoSigmaSq);  // 가우시안 커브
    sum += g[i];
}
for (i) g[i] /= sum; // 합=1 정규화

🔄 Binomial Median과의 핵심 차이 — 단 한 줄

// useGaussianWeights 플래그 하나로 가중치 전략 전환
double[] w1D = useGaussianWeights
    ? ComputeGaussian1D(windowSize, windowSize / sigmaFactor)  // Gaussian
    : GetBinomial1D(windowSize);                               // Binomial
// ↑ 이후 2D 외적, 수집, Bucket/Sort 로직 완전 동일
8-BIT 구현 — ApplyWeightedMedian8() (useGaussianWeights=true)
R
WeightedMedianDouble8(valsR, w, bucket, n)
G
WeightedMedianDouble8(valsG, w, bucket, n)
B
WeightedMedianDouble8(valsB, w, bucket, n)
// Gaussian 1D 가중치 생성 (매번 exp 계산)
double[] w1D = ComputeGaussian1D(windowSize,
                 windowSize / sigmaFactor);

// 이후 Bucket 알고리즘 동일
for (wy) for (wx) {
  double w = w1D[wy+r] * w1D[wx+r];
  valsB[k] = sBuffer[p]; weightsB[k] = w;
}
// WeightedMedianDouble8() → Bucket[256]
// R·G·B 채널 독립 반복
Gaussian은 캐싱 없음. Adaptive 모드에서 경계마다 ComputeGaussian1D(winW, winW/σF) 재계산.
16-BIT 구현 — ApplyWeightedMedianPlanes16() (useGaussianWeights=true)
R
WeightedMedianSorted16(buf.R, buf.W, ...)
G
WeightedMedianSorted16(buf.G, buf.W, ...)
B
WeightedMedianSorted16(buf.B, buf.W, ...)
// Gaussian 가중치 + 16-bit Sort 조합
double[] w1D = ComputeGaussian1D(windowSize, sigma);

// planes.B/G/R[y,x]에서 수집 후
// 동일 WeightedMedianSorted16 호출
for (i) idx[i] = i;
Array.Sort(idx, 0, count, comparer);
// 누적 가중치 ≥ total/2 탐색
Binom Median 16-bit와 알고리즘 동일. 가중치 생성 함수만 ComputeGaussian1D로 교체.

🔄 전체 처리 흐름 — 단계별 분석

Gaussian Weighted Median은 Binomial Median과 완전히 동일한 파이프라인을 가집니다. 유일한 차이는 Step 1의 가중치 생성 함수입니다.

1
1D 가우시안 가중치 생성 — ComputeGaussian1D()

σ = windowSize / sigmaFactor로 σ를 계산한 뒤 exp(−x²/2σ²) 생성. 합=1 정규화. Binomial의 Dictionary 캐싱 없음 — 매번 exp() 호출.

2
2D 외적 수집 — vals[] + weights[] 동시 수집

w = g1D[wy+r] × g1D[wx+r]로 2D 가중치 생성. 픽셀값(byte/double)과 가중치를 ThreadLocal 버퍼에 적재. R·G·B 채널별 독립.

3
가중 중앙값 계산 — Bucket(8-bit) / Sort(16-bit)

8-bit: bucket[value] += weight → 누적합 50% 탐색 O(n+256). 16-bit: Array.Sort(idx) → 누적합 탐색 O(n log n). Binomial Median과 완전 동일한 알고리즘.

4
TieBreak 보간 & 출력

8-bit: (i+k+1)>>1 정수 보간. 16-bit: (lo+hi)/2.0 실수 보간. 동일 TieBreak 로직.

📐 sigmaFactor 영향 분석 — 가우시안 폭이 중앙값에 미치는 효과

sigmaFactor(σF)는 가우시안 커널의 유효 범위를 결정합니다. σF가 클수록 중앙에 집중, σF가 작을수록 넓게 분산됩니다. 이것이 중앙값 결과에 미치는 영향:

σFσ (r=2)중앙 가중치모서리 가중치유효 범위특성
3 (넓음)1.67~0.12~0.06~3σ=5.0 > r모든 픽셀 비중 유사 → Rectangular Median과 유사
6 (기본)0.83~0.23~0.001~3σ=2.5 ≈ r중앙 집중 + 먼 픽셀 무시 → 균형 잡힌 스무딩
12 (좁음)0.42~0.48~10⁻⁶~3σ=1.3 < r중앙 거의 단독 → 원본에 가까움
σF=3 (넓은 σ)의 효과:

모든 픽셀의 가중치가 비슷 → 단순 중앙값(Rectangular Median)과 유사. 노이즈 제거 강하지만 디테일도 소실. 넓은 영역의 소금-후추 노이즈 제거에 효과적.

σF=12 (좁은 σ)의 효과:

중앙 픽셀이 거의 단독으로 중앙값을 결정 → 원본 이미지에 가까움. 스무딩이 거의 없지만, 바로 인접한 극단 노이즈만 선택적으로 제거.

Binomial과의 차이: Binomial 가중치는 σF 조절이 불가능합니다. r이 같으면 항상 같은 분포. 따라서 가우시안 Median의 주된 장점은 σF를 통한 미세 튜닝이 가능하다는 점입니다.

🔍 Adaptive 경계 처리 — Gaussian Median 전용

Gaussian Median의 Adaptive 경계는 축소된 윈도우에 맞는 새 가우시안 커널을 재생성합니다. σ도 비례 축소됩니다.

// ApplyWeightedMedian8 — Adaptive + Gaussian
int left  = Math.Min(r, x);
int right = Math.Min(r, width - 1 - x);
int winW  = left + right + 1;
int winH  = top + bottom + 1;

// σ를 축소된 윈도우에 비례하여 재계산
double sigmaLocalX = winW / sigmaFactor;
double sigmaLocalY = winH / sigmaFactor;

// 새 가우시안 커널 생성 (캐싱 없음!)
var gx = ComputeGaussian1D(winW, sigmaLocalX);
var gy = ComputeGaussian1D(winH, sigmaLocalY);

// 2D 수집 (축소된 윈도우 내)
for (yy = 0; yy < winH; yy++) {
    for (xx = 0; xx < winW; xx++) {
        double w = gy[yy] * gx[xx]; // 축소 커널 외적
        valsB[k] = sBuffer[p];
        weights[k] = w;
        k++;
    }
}
// 동일 WeightedMedianDouble8() 호출

Binomial Median Adaptive

  • GetBinomial1D(winW) → Dictionary 캐싱
  • 동일 (left,right) 조합 재사용
  • O(1) 캐시 적중 후 커널 반환
  • 경계 픽셀 처리 빠름

Gaussian Median Adaptive

  • ComputeGaussian1D(winW, σ) → 매번 재계산
  • σ가 실수이므로 키로 캐싱 부적합
  • O(winW) exp() 호출 per 경계 픽셀
  • 경계 픽셀 처리 느림

⚡ 성능 최적화 포인트 — Gaussian Median

공유 인프라 (Binomial Median과 동일)
  • ThreadLocal<MedianThreadBuffers8> 독립 버퍼
  • Bucket[256] Array.Clear 재사용 (8-bit)
  • MedianPlaneBuf16 + Array.Sort (16-bit)
  • Parallel.For 행 단위 완전 병렬
Gaussian 전용 오버헤드
  • ComputeGaussian1D: 매번 exp() O(windowSize)
  • Adaptive 경계: winW/winH별 재계산
  • 캐싱 불가 — σ가 실수
  • Binomial 대비 ~10~20% 느림 (경계 집중)
Filter 3 vs Filter 4 · Deep Comparison

Binomial vs Gaussian Weighted Median

두 필터는 동일한 Weighted Median 알고리즘을 공유하되, 가중치 생성 방식이 다릅니다. 이 한 가지 차이가 이미지 결과물과 사용 시나리오에서 구체적으로 어떤 차이를 만드는지 깊이 분석합니다.

① 원리의 차이 — 가중치 분포 형태

두 필터의 가중치를 같은 r에서 시각적으로 비교합니다. 슬라이더로 반경을 조절해 어떻게 달라지는지 확인하세요.

2 → 윈도우 5×5
BINOMIAL — C(n−1, k) / 2^(n−1)
중앙
GAUSSIAN — exp(−x²/2σ²) · σ = windowSize / sigmaFactor
중앙

② 이미지 결과물 차이 — 시나리오별 분석

실제 픽셀값 예시로 두 필터가 어떻게 다른 결과를 내는지 계산합니다. (픽셀값 0~255 기준)

7×7 윈도우 시뮬레이터 — sigmaFactor 조절
σ = windowSize / sigmaFactor = 7 / σF. sigmaFactor가 작을수록 σ가 커져 가중치가 넓게 퍼지고, 클수록 σ가 작아져 중앙에 집중합니다. ⚠ r=1(3×3)에서 σF ≥ 3이면 2D 중앙 > 50% → 필터 무효화. 이 시뮬레이터는 7×7(r=3)이므로 σF=6에서도 정상 동작합니다.
픽셀값 입력 (클릭해서 수정) · ■ 중앙 · ■ 노이즈
6
σ = windowSize/σF = 7/ = · σF↓ = 넓은 분포 · σF↑ = 좁은 분포
SCENARIO A · Salt-Pepper Noise
10010298 103240101 99100102
SCENARIO B · Sharp Edge
2020200 20110200 2020200
SCENARIO C · Uniform Region
128129127 130128127 129128130
SCENARIO D · Gradient
406080 507090 6080100

③ 장단점 종합 비교

Binomial Weighted Median
✅ 장점
  • 정수 계수 → Dictionary 캐싱 가능.
    동일 windowSize는 재계산 없이 즉시 반환. 반복 호출 시 Gaussian보다 캐시 히트율 높음.
  • 파라미터 없음 — 조정 오버헤드 제로.
    r만 결정하면 됨. sigmaFactor 튜닝이 불필요. 잘못된 파라미터로 인한 품질 저하 위험 없음.
  • 2의 거듭제곱 정규화 (2^(n−1)).
    합이 항상 2^(2r). 비트 시프트로 나눗셈 대체 가능 (최적화 여지).
  • σ 매칭 시 모든 r에서 구조적 우위.
    Platykurtic 분포(κ=3−1/r)로 n_eff가 최대 26% 높고, 정수값 double 산술로 bucket 오차 없음. r=1~2에서 σF=6 기준 Gaussian보다 명백히 우수.
  • 구현 단순성 — 수치 안정성 보장.
    exp() 함수 없음, 부동소수점 누적 오차 최소. 이항 계수가 정수값 double(1.0, 4.0, 6.0 등)이므로 WeightedMedianDouble8의 bucket 누적이 수학적으로 정확.
⚠️ 단점
  • 분포 형태 고정 — 뾰족함/평탄함 조절 불가.
    r에 따라 형태가 자동 결정되며, σF처럼 중앙 집중도를 독립적으로 조절하는 파라미터가 없음. 같은 r에서 넓거나 좁게 만들 수 없음.
  • 이산(discrete) 근사 — 연속 최적화 불가.
    정수 계수이므로 커널 형태의 미세 조정이 불가능. 특수 용도에서 실수 계수가 필요한 경우 Gaussian을 선택해야 함.
Gaussian Weighted Median
✅ 장점
  • sigmaFactor로 분포 형태 정밀 조절.
    σ를 크게 → 넓고 평탄, 작게 → 뾰족하고 중앙 집중. 같은 r에서도 용도별 최적 조율 가능. Binomial이 불가능한 미세 조율 지원.
  • 연속 함수 기반 — 수학적으로 잘 정의된 커널.
    exp(−x²/2σ²)는 주파수 영역에서 ringing이 없는 매끄러운 저역통과 특성을 가짐. Scale-space 이론에서 선형 스무딩(Mean 필터)의 유일한 커널이나, Median 연산에서는 이론적 최적성이 직접 적용되지 않음.
  • σF 조절로 r과 무관하게 분포 폭 자유 제어.
    r이 커져도 sigmaFactor를 낮추면 넓게 분산, 높이면 중앙 집중. 단 소형 커널(r=1,2)에서 σF 기본값(6)은 과도한 중앙 집중을 유발하므로 반드시 재조율 필요.
⚠️ 단점
  • exp() 기반 — 캐싱 효율이 Binomial보다 낮음.
    (len, σ) 쌍으로 캐싱은 가능하나 키 공간이 넓고, Adaptive 모드에서 경계마다 다른 윈도우 크기로 캐시 히트율 저하. Binomial의 단일 정수 키 대비 구조적으로 불리.
  • 파라미터 튜닝 필수 — 기본값 사용 위험.
    σF=6 기본값에서 r=1의 2D 중앙 가중치가 61.9% > 50%로 필터가 무효화됨. r=2 이상에서는 정상 동작하나, Binomial은 이 위험이 원천적으로 없음.
  • Median에서 이론적 최적성 부재.
    Gaussian Weighted Mean은 Gaussian 노이즈에 대해 L₂ MLE이나, Weighted Median은 이 최적성이 성립하지 않음. Mean과 Median을 혼동한 오류에 주의.
  • 부동소수점 정밀도 의존.
    exp() 수치 오차가 가중치에 영향. 620만 연산 중 0.01~0.1%에서 ±1 bucket 오차 가능. Binomial의 정수값 double 산술 대비 수치 분산이 큼.

④ Binomial Weighted Median의 특장점 — 왜 좋은 선택인가

SPEED · 속도 우위

GetBinomial1D()는 Dictionary 캐싱으로 동일 windowSize에서 재계산 없이 O(1) 반환합니다. 반면 ComputeGaussian1D()는 매번 exp()를 n번 호출해야 합니다.

// Binomial: 캐싱 적중 시 O(1)
_binom1D.TryGetValue(n, out var c) // ← 즉시 반환

// Gaussian: 항상 O(n) exp() 연산
g[i] = Math.Exp(-(x*x) / twoSigmaSq) // 매번
SIMPLICITY · 파라미터 없음

r 하나만 결정하면 됩니다. Gaussian은 최적 σ를 찾아야 하지만, Binomial은 파스칼 삼각형의 수학적 특성이 자동으로 "합리적인" 가중치를 보장합니다. 사용자 오조작이 없습니다.

r=1 → [1,2,1] r=2 → [1,4,6,4,1] r=3 → [1,6,15,20,15,6,1]
NOISE REJECTION · 동등한 노이즈 제거

Weighted Median 출력 방식에서는 가중치의 상대적 비율이 중앙값 탐색에 영향을 줍니다. r≤3에서 이항 계수의 중앙 집중도는 가우시안(σF=6)과 오차 <2% 수준으로 거의 동일합니다.

POWER OF TWO · 정수 최적화

이항 계수의 2D 외적 합은 2^(4r)로, 비트 시프트 정규화가 가능합니다. 고성능 환경에서 나눗셈 대신 >> 4r 연산으로 대체할 수 있습니다.

// r=2: 합=16² = 256 = 2^8
normalized = raw >> 8; // /256 대신
// r=3: 합=64² = 4096 = 2^12
normalized = raw >> 12; // /4096 대신

⑤ 커널 크기(r) 증가 시 — Gaussian이 왜곡되고 Binomial이 원본에 가까운 이유

Weighted Median의 핵심 조건은 단 하나입니다: 중앙 픽셀의 누적 가중치가 50% 미만이어야 주변 픽셀들이 노이즈를 억제합니다. 50% 이상이면 가중 중앙값 탐색이 노이즈 픽셀에서 멈춰 노이즈가 그대로 출력됩니다.

가중 중앙값 탐색 메커니즘 — 중앙 노이즈 픽셀 시나리오
Binomial r=1 · 2D 중앙 가중치 25.0%
중앙에 노이즈(240), 주변 8픽셀은 신호(100). 오름차순 누적:
100 × (8개 합산 75.0%) 누적 75.0%
240 × 25.0% 누적 100%
→ 50% 기준: 100에서 달성 ✓ 출력 = 100 (노이즈 제거)
Gaussian r=1, σF=6 · σ=3/6=0.5 · 2D 중앙 가중치 61.9%
중앙에 노이즈(240), 주변 8픽셀은 신호(100). 오름차순 누적:
100 × (8개 합산 38.1%) 누적 38.1%
240 × 61.9% 누적 100%
→ 50% 기준: 240에서 달성 ✗ 출력 = 240 (노이즈 통과!)
수치 증거: sigmaFactor=6 고정, r 증가 시 2D 중앙 가중치 변화
r커널 Binomial
2D 중앙%
노이즈 제거 Gaussian
2D 중앙% (σF=6)
노이즈 제거 우위
왜 이런 현상이 발생하나?
Binomial의 2D 중앙 가중치 = (C(n−1,r) / 2^(n−1))² — r=1부터 25%로 시작해 r이 커질수록 계속 낮아집니다. 항상 50% 미만이므로 모든 r에서 노이즈 제거 보장됩니다.

Gaussian(σF=6)의 2D 중앙 가중치 = g(0)² — σ = n/σF가 매우 작을 때 exp(0)²=1이 전체 합을 압도해 r=1에서 61.9%까지 치솟습니다. r=1에서만 50%를 초과해 노이즈가 통과되며, r=2부터는 23.0%로 정상 동작합니다. 즉 sigmaFactor=6 고정 시, r=1 소형 커널에서만 Gaussian Median이 Salt-Pepper 노이즈를 원본 그대로 통과시킵니다.
⚠️ 역설 — "r=1에서 GWMF가 원본에 더 가까워 보인다"는 주장의 진실
r=1, σF=6 기준으로 Gaussian Median의 2D 중앙 가중치는 61.9% (> 50%)에 달합니다.
이 상태에서 중앙 픽셀의 값이 거의 그대로 출력됩니다. 즉 보정을 거의 하지 않은 것과 동일합니다.

시각적으로는 "원본에 가깝다 = 덜 뭉개졌다 = 더 좋다"처럼 보일 수 있지만,
이는 노이즈도 그대로 통과되고 있는 상태입니다. 이미지 보정 필터로서는 실패입니다.

올바른 해석: r=1에서 σF=6인 GWMF가 "더 좋아 보이는" 이유는 블러가 적어서가 아니라, 필터가 사실상 작동하지 않고 있기 때문입니다. σF를 2~3으로 낮추면 Gaussian도 정상 작동하지만, 그 결과는 Binomial과 거의 동일해집니다. r=2부터는 σF=6에서도 2D 중앙 가중치가 23.0%로 정상 동작합니다.
Gaussian Median 해결책 — sigmaFactor를 함께 낮춰야
r을 키울 때 sigmaFactor를 함께 낮추면(σ를 키우면) 가중치가 넓게 분산되어 중앙 과집중 문제가 해소됩니다. 하지만 이 추가 튜닝 부담 자체가 Binomial에 없는 단점입니다. 게다가 σ를 완벽히 맞춰도 아래에 설명된 구조적 이유로 Binomial WM이 미세하게 우위입니다.
r=1, σF=6 → 중앙 62% ✗ 노이즈 통과 r=1, σF=3 → 중앙 ~20% ✓ 작동 r=1, σF=2 → 중앙 ~15% ✓ 작동

🔬 σ를 완벽히 일치시켜도 Binomial WM이 미세하게 우위인 수학적 근거

핵심 명제: "σ가 일치된 조건에서, weighted median 연산에 한정하여, 모든 실용적 r(1~15)에서 Binomial WM이 미세하게 우위이다." — 이는 수학적으로 성립하는 구조적 사실입니다.

근거 ① — 분포 형태(Kurtosis) 차이

σ(2차 모멘트)를 일치시켜도 4차 모멘트(첨도, kurtosis)는 여전히 다릅니다. Binomial B(2r, 0.5)의 첨도 κ = 3 − 1/r은 Gaussian의 κ=3보다 항상 작습니다. 이는 CLT 수렴이 완료되지 않은 유한 r의 구조적 성질이며 반례가 없습니다.

rBinomial κGaussian κBinom 2D n_effGauss 2D n_effBinomial 우위
유효 샘플 수 공식: n_eff = (Σwᵢ)² / Σwᵢ²  ·  Platykurtic 분포(납작)는 가중치를 더 고르게 분산 → n_eff ↑ → 추정 분산 ↓
Weighted Median 분산 ≈ π/(2·n_eff) · σ²_noise  →  n_eff +10%이면 추정 분산 10% 감소, 노이즈 제거 효율 ~0.4 dB 향상
근거 ② — 정수값 double vs exp() 근사 산술
두 필터 모두 동일한 WeightedMedianDouble8() 함수를 공유합니다. 차이는 입력 가중치의 수학적 성질입니다: Binomial 가중치는 정수값 double(1.0, 4.0, 6.0 등 — IEEE 754에서 정확히 표현됨)이고, Gaussian 가중치는 exp() 결과(무리수의 근사값)입니다.
Binomial → WeightedMedianDouble8 — 가중치가 정수값 double
double half = total / 2.0;    // 이항 계수=정수값 → 정확
double eps = total * 1e-12;  // 범용 코드 공유 (Binomial엔 불필요)
if (acc > half + eps) return i; // 정수값끼리 비교 → 결과 정확
Gaussian: WeightedMedianDouble8 — 근사
double half = total / 2.0;   // 누적 오차 존재
double eps = total * 1e-12;  // 보정값 도입 필요
if (acc > half + eps) return i; // 근사 비교
Gaussian 한정: 1920×1080×3채널 = 약 620만 median 연산 중 0.01~0.1% (600~6,000 픽셀)에서 exp() 가중치의 누적 오차로 half 경계 근처 ±1 bucket 오차 발생 가능. 영향: PSNR ~0.01–0.05 dB
Binomial: 가중치가 정수값 double이므로 bucket 누적 연산이 수학적으로 정확. 동일 함수를 사용하지만 오차 없음.
근거 ③ — 유한 지지(Finite Support) vs 강제 절단
Binomial r=2: [1, 4, 6, 4, 1] — 윈도우 경계가 분포의 정의 자체. 절단 불필요.
Gaussian r=2 (σ≈1): [0.054, 0.244, 0.403, 0.244, 0.054] | 절단 | — 실제 테일(0.011, 0.0003…)을 버리고 재정규화.
재정규화는 원래 분포 형태를 미세하게 변형시킵니다. Binomial은 이 문제가 원천적으로 없습니다.
근거 ④ — 중앙 노이즈 저항력 (σ 매칭 기준)

Weighted Median은 가중 투표 시스템입니다. 노이즈가 중앙에 위치할 때 주변 signal 픽셀들이 outvote하려면 중앙 가중치가 낮을수록 유리합니다. Platykurtic인 Binomial은 σ를 맞춰도 항상 중앙 가중치가 낮습니다.

rmatch σ Binom 중앙2D Gauss 중앙2D Binom Signal 우위비 Gauss Signal 우위비 저항력 차이
왜 반례가 없는가 — 4가지 근거 모두 구조적
우위 근거반례 가능성이유
Platykurtic → n_eff ↑없음κ = 3−1/r < 3 은 모든 유한 r에서 수학적으로 항상 성립
정수값 double 정밀성없음이항 계수는 정수값 double로 누적 시 오차 없음, exp() 근사 오차와 구조적 차이
유한 지지 (절단 없음)없음Binomial의 정의역 = 윈도우, 이는 분포의 구조적 성질
중앙 noise 저항력 ↑없음중앙 비중 < Gaussian은 같은 σ에서 모든 유한 r에서 항상 성립
결론: r → ∞ (CLT 수렴 완료)이 되어야 비로소 차이가 0에 수렴합니다. 실용적 r 범위(1~15)에서는 예외 없이 Binomial WM이 미세하게 우위입니다. 단순히 "더 빠르고 간단한 대안"이 아니라, weighted median 연산 자체에서 수학적으로 더 정확한 선택입니다.

⑥ 선택 가이드 — 언제 어떤 필터를?

상황 Binomial Median 추천 Gaussian Median 추천
소형 커널 r=1
(기본 σF=6)
✓ 명백히 우위
2D 중앙 25% → 노이즈 제거 보장
✗ 필터 무효화
2D 중앙 61.9% > 50% → 중앙 노이즈 통과, σF 재조율 필수
소형 커널 r=2
(기본 σF=6)
✓ 미세 우위
2D 중앙 14.1% → 노이즈 제거, 구조적 n_eff 우위
△ 정상 동작
2D 중앙 23.0% < 50% → 노이즈 제거됨. Binomial보다 중앙 집중도 약간 높으나 실용적으로 허용 범위
소형 커널 r=1
(σF를 2 이하로 낮춘 경우)
파라미터 없이 동등 수준 σF≤2로 튜닝 시 정상 동작 (2D 중앙 <50%), 단 추가 설정 필요
Salt-Pepper 노이즈 제거
(r=2 이상)
모든 r에서 안정적 제거 r≥2부터 σF=6에서도 정상 동작, 그러나 Binomial이 파라미터 없이 더 간단
노이즈 강도가 다양한 이미지들의 배치 처리 동일 r 재호출 시 캐시 효과 exp() 기반 — 캐싱 효율이 Binomial보다 낮음
텍스처가 복잡한 이미지 정밀 처리 r만 조절 가능, 유연성 낮음 σ로 세밀 조율 가능 (단 r=1에서 σF > 2 사용 시 필터 무효화 주의)
r이 큰 경우 (r≥4) 강한 스무딩 전체 윈도우 고르게 활용, 캐싱 효과 극대화 σF=6 고정 시 r↑ → σ=n/6 비례 증가 → 중앙 집중도 감소 (r=4: 2D 중앙 7.1%). r≥2부터 모두 정상 동작하나 Binomial(r=4: 2D 중앙 1.5%)보다 중앙 집중도 여전히 높음
가우시안 노이즈 모델 최적화
(Weighted Mean 한정)
Weighted Median에서는 Gaussian과 동등 — 커널 형태가 L₁ 최적성에 무관 ※ 주의 Gaussian Weighted Mean이 Gaussian 노이즈에 L₂ MLE이나, Weighted Median에서는 이 최적성 미성립. Mean/Median 혼동 주의
파라미터 없이 빠른 적용 필요 시 r만 설정, 즉시 사용 — 모든 r에서 안정 σF 기본값(6)은 r=1에서 필터 무효화. r에 맞게 반드시 σF≤2(r=1), σF≤4(r=2) 조율
Adaptive 경계 모드 + 반복 스무딩 경계마다 캐시에서 조회 경계마다 exp() 재계산, 캐시 히트율 낮음
결론
r=1 구간: Binomial 명백한 우위
σF=6 기준 Gaussian의 2D 중앙 가중치 61.9% > 50%로 필터 무효화. Binomial은 파라미터 없이 모든 r에서 안정적으로 노이즈 제거.
r≥2: 성능 수렴, Binomial 미세 구조적 우위 유지
r≥2부터 σF=6에서도 Gaussian이 정상 동작. 단 Binomial은 Platykurtic 특성(n_eff 우위), 정수 산술, 파라미터 불필요 등 실용적 우위 유지.

⑦ 수식 나란히 비교 — 같은 r에서 각 픽셀의 기여도

w_B[i] = C(n−1, i) / Σ C(n−1,k)
n=2r+1 · 합=2^(n−1) · 정수 계수
// r=2: [1,4,6,4,1] / 16
// 중앙 기여: 6/16 = 37.5%
// 최외곽: 1/16 = 6.25%
// 비율: 6× 차이
w_G[i] = exp(−(i−r)²/2σ²) / Σ exp(−k²/2σ²)
σ = windowSize/σF · 연속 근사 · float 계수
// r=2, σF=6: σ=5/6≈0.83
// 중앙 기여(1D): ≈47.9% (σF=6 기준)
// 최외곽(1D): ≈2.7% (σF=6 기준)
// σF 변화로 비율 연속 조절 가능
핵심 인사이트: 두 필터가 공유하는 Weighted Median 알고리즘에서 가중치는 "어떤 픽셀값이 중앙값 탐색에 더 큰 영향을 미치는가"를 결정합니다. 이항과 가우시안 모두 중앙 픽셀에 더 높은 가중치를 주기 때문에 결과가 유사합니다. 결정적 차이는 꼬리(tail) 부분의 감쇠 속도입니다 — 가우시안은 exp 함수로 더 빠르게 감쇠하고, 이항 계수는 다항식적으로 감쇠합니다.
Filter 5 / 6

Gaussian Average — 가우시안 가중 평균

가우시안 가중치로 가중 평균을 계산합니다. Median이 아닌 평균이라 연속적이고 부드러운 결과를 냅니다. 이미지 처리에서 가장 널리 쓰이는 블러 방식입니다.

📐 수식 & 커널 (r=2, σ≈0.83, 셀 호버 → 수식)

output(x,y) = Σ g(wy)·g(wx)·pixel(x+wx,y+wy) / Σ g(wy)·g(wx)  (σ = windowSize/sigmaFactor)
r=2, sigmaFactor=6 · Gauss Median과 동일 커널, 출력 방법이 평균
g(−2)²≈0.001
가중 평균에 기여 0.07%
.001
g(−2)×g(−1)≈0.006.006
g(−2)×g(0)≈0.013.013
g(−2)×g(1)≈0.006.006
g(−2)²≈0.001.001
g(−1)×g(−2)≈0.006.006
g(−1)²≈0.054.054
g(−1)×g(0)≈0.112.112
g(−1)²≈0.054.054
g(−1)×g(−2)≈0.006.006
g(0)×g(−2)≈0.013.013
g(0)×g(−1)≈0.112.112
g(0)²≈0.230
중앙 픽셀 기여 23.0% (최대)
.230
g(0)×g(1)≈0.112.112
g(0)×g(2)≈0.013.013
g(1)×g(−2)≈0.006.006
g(1)²≈0.054.054
g(1)×g(0)≈0.112.112
g(1)²≈0.054.054
g(1)×g(2)≈0.006.006
g(2)²≈0.001.001
g(2)×g(1)≈0.006.006
g(2)×g(0)≈0.013.013
g(2)×g(1)≈0.006.006
g(2)²≈0.001.001
합=1 정규화 · 중앙≈23.0% · Gauss Median과 완전히 동일 커널 — 출력 방법(평균 vs 중앙값)만 다름
Gaussian Avg vs Gauss Median:
  • 동일 가우시안 가중치
  • Avg: 가중합/분모 (연속 평균)
  • Median: 50% 누적 탐색 (노이즈 강건)
Binomial Avg vs Gaussian Avg:
  • 동일 출력 방법 (가중 평균)
  • Binom: σ 없음, 캐싱 가능
  • Gaussian: sigmaFactor로 σ 조절
8-BIT 구현 — ApplyGaussian8()
R
sumR += w × sBuffer[p+2]
G
sumG += w × sBuffer[p+1]
B
sumB += w × sBuffer[p+0]
double[] g1D = ComputeGaussian1D(windowSize, sigma);
double sumB=0,sumG=0,sumR=0,denom=0;

for (wy) {
  double wyW = g1D[wy+r];
  for (wx) {
    double w = wyW * g1D[wx+r];
    sumB += w * sBuffer[p];
    sumG += w * sBuffer[p+1];
    sumR += w * sBuffer[p+2];
    denom += w;
  }
}
outB = ClampToByte(sumB / denom);
outG = ClampToByte(sumG / denom);
outR = ClampToByte(sumR / denom);
denom은 실제 가우시안 합. 경계(Adaptive)마다 새 g1D 생성.
16-BIT 구현 — ApplyGaussianPlanes16()
R
planes.R[ny,nx]
G
planes.G[ny,nx]
B
planes.B[ny,nx]
// fullDenom 1회 사전 계산 (Interior 공통)
double fullDenom = 0;
for (wy) for (wx)
  fullDenom += g1D[wy+r] * g1D[wx+r];

if (yInterior && xInterior) {
  // GetIndex1D() 없음 + fullDenom 재사용
  outB[y,x] = sB / fullDenom; // ← 최고속
  outG[y,x] = sG / fullDenom;
  outR[y,x] = sR / fullDenom;
} else {
  // 경계: 새 denom 누적
  outB[y,x] = sB / localDenom;
}
Interior 픽셀은 나눗셈 분모가 항상 동일 → fullDenom 재사용으로 per-pixel 재계산 없음.

📐 σ(sigma) 계산 — sigmaFactor의 역할

σ = windowSize / sigmaFactor = (2r+1) / σF

sigmaFactor(σF)는 가우시안 커널의 을 제어합니다. σF가 클수록 σ가 작아져 중앙에 집중, σF가 작을수록 σ가 커져 넓게 분산.

rwindowSizeσF=3 (넓음)σF=6 (기본)σF=12 (좁음)
13σ=1.00σ=0.50 ⚠ 과집중σ=0.25
25σ=1.67σ=0.83 ✓σ=0.42
37σ=2.33σ=1.17 ✓σ=0.58
511σ=3.67σ=1.83 ✓σ=0.92
// 코드에서의 구현 — ApplyGaussian8() 시작 부분
int windowSize = checked(2 * r + 1);
double sigma = windowSize / sigmaFactor;  // σ = (2r+1) / σF
var g1D = ComputeGaussian1D(windowSize, sigma);

// Adaptive 경계: 축소된 윈도우에 맞는 σ를 재계산
double sigmaLocalX = winW / sigmaFactor; // winW < windowSize
double sigmaLocalY = winH / sigmaFactor;
var gx = ComputeGaussian1D(winW, sigmaLocalX);
var gy = ComputeGaussian1D(winH, sigmaLocalY);
Adaptive 모드에서 경계 픽셀마다 σ가 달라지므로 ComputeGaussian1D()를 매번 호출. Binomial의 GetBinomial1D(winW) Dictionary 캐싱과 대비되는 성능 차이 포인트.

🔄 Gaussian Avg vs Binomial Avg — 가중 평균 필터 비교

Binomial Average

  • 가중치: 정수 계수 C(n−1,k)
  • σ 파라미터 없음 — r이 자동 결정
  • Dictionary 캐싱 → 재호출 O(1)
  • 합 = 2^(n−1) → 비트 시프트 정규화 가능
  • r만 조절, 분포 형태 미세 조정 불가

Gaussian Average

  • 가중치: exp(−x²/2σ²) 연속 함수
  • sigmaFactor로 분포 폭 정밀 제어
  • 매번 exp() 계산 (캐싱 효율 낮음)
  • 주파수 영역: 완벽한 Gaussian 저역통과
  • σ로 미세 조정 가능, 단 튜닝 필요
선택 기준: 가우시안 노이즈에 대한 L₂ 최적 스무딩이 필요하면 Gaussian Avg. 파라미터 없이 빠른 적용이면 Binomial Avg. 두 필터 모두 가중 평균 출력이므로 극단값(noise)에 취약 — 노이즈 강건성이 필요하면 Median 필터 사용.

⚡ fullDenom 사전 계산 최적화 (16-bit)

16-bit 파이프라인에서는 Interior 픽셀의 분모를 1회만 사전 계산하여 모든 내부 픽셀에 재사용합니다.

// ApplyGaussianPlanes16 시작 부분
double fullDenom = 0;
for (int wy = -r; wy <= r; wy++) {
    double wyW = g1D[wy + r];
    for (int wx = -r; wx <= r; wx++)
        fullDenom += wyW * g1D[wx + r]; // 1회 O((2r+1)²)
}

// Parallel.For 내부
if (yInterior && x >= r && x < width - r) {
    outB[y,x] = sB / fullDenom;  // 재사용 — per-pixel 재계산 없음
    outG[y,x] = sG / fullDenom;
    outR[y,x] = sR / fullDenom;
} else {
    // 경계: denom을 루프에서 직접 누적
    outB[y,x] = sB / localDenom;
}
fullDenom은 정규화된 가우시안의 2D 외적 합 ≈ 1.0이지만, 부동소수점 누적 순서에 의한 미세 오차를 방지하기 위해 정확한 합을 사용. 8-bit에서도 동일 패턴이 적용되지만 byte 연산은 ClampToByte로 처리.

🔄 전체 처리 흐름 — 단계별 분석

Gaussian Average 필터의 처리 과정을 Binomial Average와 비교하며 4단계로 분해합니다.

1
1D 가우시안 커널 생성 — ComputeGaussian1D()

σ = windowSize / sigmaFactor로 σ를 결정하고, exp(−x²/2σ²)를 계산한 뒤 합=1로 정규화. Binomial의 Dictionary 캐싱과 달리 매번 재계산.

2
2D 외적 수집 — w = g1D[wy+r] × g1D[wx+r]

Binomial Average와 동일한 외적 패턴. 행 가중치 × 열 가중치를 곱해 2D 가중치를 on-the-fly 생성. 별도 2D 배열 할당 없음.

3
가중 평균 계산 — sumB/denom

가중합(sumB = Σ w×pixel)과 가중치 합(denom = Σ w)을 누적한 뒤 나눗셈. R·G·B 채널 독립 처리.

4
출력 — ClampToByte / ClampToUShort

8-bit: ClampToByte(sumB / denom) → 24bpp. 16-bit: 실수 결과 → ClampToUShort() → 48bpp.

📐 ComputeGaussian1D() — 가우시안 커널 생성 상세

이항 계수(정수 재귀)와 달리 가우시안은 매번 exp() 함수를 호출합니다. 합=1 정규화가 핵심이며, 캐싱되지 않습니다.

// ComputeGaussian1D(len, sigma) 전체 코드
private static double[] ComputeGaussian1D(
    int len, double sigma)
{
    var w = (len - 1) / 2;       // = r
    var g = new double[len];

    double sum = 0.0;
    double twoSigmaSq = 2 * sigma * sigma;

    for (int i = 0; i < len; i++)
    {
        int x = i - w;           // 중앙 기준 오프셋
        double v = Math.Exp(
            -(x * x) / twoSigmaSq);
        g[i] = v;
        sum += v;                // 정규화용 합
    }

    // ★ 합=1 정규화 — 밝기 보존
    for (int i = 0; i < len; i++)
        g[i] /= sum;

    return g;
}

Binomial vs Gaussian 커널 생성 비교

항목BinomialGaussian
생성 함수GetBinomial1D(n)ComputeGaussian1D(len, σ)
핵심 연산c[i]=c[i-1]*(n-i)/iexp(−x²/2σ²)
캐싱Dictionary<int,double[]> ✓없음 ✗
파라미터n (windowSize만)len + sigma
정규화원시 정수 계수 (외부 denom)내부 합=1 정규화
Adaptive 경계GetBinomial1D(winW) 캐싱매번 재계산
Gaussian은 σ가 실수이므로 같은 windowSize라도 σ가 다르면 다른 커널. Dictionary 키로 쓰기 부적합.

🔍 AdaptiveMask 경계 처리 — 코드 상세

AdaptiveMask는 원래 가우시안 커널을 유지하되 범위 밖 픽셀만 건너뛰고, denom을 유효 픽셀 가중치만으로 재정규화합니다.

// ApplyGaussian8 — AdaptiveMask 분기
else if (mode == BoundaryMode.AdaptiveMask)
{
    double sumB = 0, sumG = 0, sumR = 0;
    double denom = 0;

    for (int wy = -r; wy <= r; wy++)
    {
        int ny = y + wy;
        if ((uint)ny >= (uint)height) continue;
        // ↑ unsigned 비교로 음수&초과 한 번에 검사

        double wyW = g1D[wy + r]; // 원본 가중치 유지

        for (int wx = -r; wx <= r; wx++)
        {
            int nx = x + wx;
            if ((uint)nx >= (uint)width) continue;

            double w = wyW * g1D[wx + r]; // 2D 외적
            int p = (ny * sStride) + (nx * 3);

            sumB += w * sBuffer[p];
            sumG += w * sBuffer[p + 1];
            sumR += w * sBuffer[p + 2];
            denom += w;  // 유효 픽셀만 denom에 반영
        }
    }
    if (denom <= 0) denom = 1;
    dBuffer[d]   = ClampToByte(sumB / denom);
    dBuffer[d+1] = ClampToByte(sumG / denom);
    dBuffer[d+2] = ClampToByte(sumR / denom);
}
AdaptiveMask vs Adaptive: Adaptive는 축소된 윈도우에 맞는 새 가우시안 커널(ComputeGaussian1D(winW, winW/σF))을 매번 재계산. AdaptiveMask는 원본 커널 유지 + 유효 픽셀만 수집 → denom 자동 감소로 재정규화. 결과가 미세하게 다릅니다.

📊 BoundaryMode별 denom(분모) 계산 경로

Gaussian Average에서 분모(denom)는 경계 모드에 따라 다르게 계산됩니다. Binomial Average와 동일한 패턴입니다.

경로denom 계산코드
Interior
(16-bit)
fullDenom (1회 사전계산) for(wy)for(wx) fullDenom += g1D[wy+r]*g1D[wx+r]
Adaptive 축소 커널의 2D 합 gx=ComputeGaussian1D(winW, winW/σF)
denom=Σgy[yy]*gx[xx]
AdaptiveMask 유효 픽셀 가중치만 누적 if((uint)ny>=height) continue;
denom += w;
ZeroPad 전체 가중치 누적 (밖도 포함) if(nx<0){denom+=w; continue;}
Symmetric/Replicate 전체 가중치 누적 모든 nx 유효 → denom += w
Gaussian은 합=1 정규화되므로 Interior의 fullDenom ≈ 1.0. 하지만 부동소수점 누적 순서에 의한 미세 오차(~10⁻¹⁵)를 방지하기 위해 정확한 합을 사용합니다.

📐 Adaptive 경계 — 축소 가우시안 커널 재생성

Adaptive 모드에서 경계 픽셀은 축소된 윈도우에 맞는 새로운 가우시안 커널을 생성합니다. σ도 축소된 윈도우에 비례하여 재계산됩니다.

// ApplyGaussian8 — Adaptive 분기
int left  = Math.Min(r, x);
int right = Math.Min(r, width - 1 - x);
int winW  = left + right + 1;

// ★ σ도 축소된 윈도우에 비례하여 재계산
double sigmaLocalX = winW / sigmaFactor;
double sigmaLocalY = winH / sigmaFactor;
var gx = ComputeGaussian1D(winW, sigmaLocalX);
var gy = ComputeGaussian1D(winH, sigmaLocalY);

// 예: 모서리(x=0, y=0) r=2
// winW=3, σX=3/6=0.5 → 더 뾰족한 가우시안
// winH=3, σY=3/6=0.5 → 마찬가지

// denom 재계산 (합=1 정규화이므로 ~1.0)
double denom = 0;
for (yy) for (xx) denom += gy[yy] * gx[xx];
if (denom <= 0) denom = 1;
내부 (5×5, σ=0.83)
가장자리 (4×5, σ=0.67)
모서리 (3×3, σ=0.50)
Binomial Adaptive와의 차이: Binomial은 GetBinomial1D(winW)로 다른 차수의 이항 계수를 생성하여 Dictionary에 캐싱. Gaussian은 σ=winW/σF로 다른 σ의 가우시안을 매번 exp()로 재계산. 경계 픽셀이 많을수록 Gaussian의 성능 저하가 더 큽니다.
Filter 6 / 6

Savitzky-Golay — 다항 회귀 스무딩

윈도우 내 픽셀에 다항식을 최소제곱 피팅하여 중앙값을 추정합니다. 피크·엣지를 가장 잘 보존하며, derivOrder>0이면 그래디언트(엣지 감지)도 출력합니다.

📐 수식 & 1D 계수 (r=2, polyOrder=2, 셀 호버 → 수식)

Fit p(x)=a₀+a₁x+...+aₘxᵐ (최소제곱) → output = p(0) [smoothing] or p⁽ⁿ⁾(0) [derivative]
r=2, polyOrder=2 · 1D SG 스무딩 계수 (외적 아님, X→Y 순차 적용)
h(−2)≈−0.086
음수: 먼 픽셀 억제
−.09
h(−1)≈+0.343
가까운 픽셀 기여
+.34
h(0)≈+0.486
중앙 픽셀 최대 기여
+.49
h(+1)≈+0.343+.34
h(+2)≈−0.086
음수: 엣지 보존 원리
−.09
양수: 픽셀값 반영   음수: 고주파 성분 억제로 엣지 보존   2D는 X pass → Y pass 순차 적용

SG는 외적 대신 2-Pass 분리 합성곱을 사용합니다:

원본
plane
X축
tmp[]
Y축
out[]
음수 계수는 QR분해로 계산된 최소제곱 해의 자연스러운 결과.
8-BIT 구현 — ApplySavitzkyGolaySeparable8()
R
X-pass planes.R → tmpR → Y-pass → outR
G
X-pass planes.G → tmpG → Y-pass → outG
B
X-pass planes.B → tmpB → Y-pass → outB
// ExtractPlanes(src, false) → double[,] planes
// derivOrder=0 (스무딩) Parallel.For 2회
Parallel.For(0, h, y => {
  for (x) {
    tmpB[y,x] = Convolve1D_X(planes.B, y, x,
                  w, r, coeffSmooth, mode, polyOrder, 0);
  }
});
Parallel.For(0, h, y => {
  for (x) outB[y,x] = Convolve1D_Y(tmpB, y, x,
                  h, r, coeffSmooth, mode, polyOrder, 0);
});
// R·G·B 채널 독립 반복 (tmpR, tmpG, tmpB 별도)
ExtractPlanes(src, false)로 byte→double 변환 후 합성곱. WritePlanes()에서 ClampToByte로 출력. 음수 계수로 인한 범위 초과는 ClampToByte가 처리.
16-BIT 구현 — ApplySavitzkyGolaySeparablePlanes16()
R
Convolve1D_X(planes.R) → tmpR → Convolve1D_Y(tmpR) → outR
G
planes.G → tmpG → outG
B
planes.B → tmpB → outB
// derivOrder=0: 스무딩만 (totalPasses=2)
Parallel.For(0, h, y => {
  for (x) tmpB[y,x] = Convolve1D_X(planes.B,
             y, x, coeffSmooth, r, BoundaryMode);
});
Parallel.For(0, h, y => {
  for (x) outB[y,x] = Convolve1D_Y(tmpB,
             y, x, coeffSmooth, r, BoundaryMode);
});

// derivOrder>0: 그래디언트 출력 (totalPasses=12)
// gx = X-deriv × Y-smooth
// gy = X-smooth × Y-deriv
// out = √(gx²+gy²) per channel
계수는 ComputeSgCoefficientsViaQR() — Householder QR 분해. 경계는 GetAsymmetricSg() 캐시.

🔬 전체 처리 흐름 — derivOrder에 따른 분기

SG 필터는 derivOrder=0(스무딩)과 derivOrder>0(그래디언트)에서 처리 경로가 완전히 다릅니다. 스무딩은 2-pass, 그래디언트는 12-pass(채널×4)입니다.

derivOrder = 0 (스무딩) · totalPasses = 2
① coeffSmooth = ComputeSavitzkyGolayCoefficients(n, polyOrder, 0, 1.0)
X-pass: tmp[y,x] = Convolve1D_X(plane, coeffSmooth)
Y-pass: out[y,x] = Convolve1D_Y(tmp, coeffSmooth)
④ R·G·B 채널 독립 반복 (각 2-pass)
⑤ WritePlanes(dst, outB, outG, outR)
derivOrder > 0 (그래디언트) · totalPasses = 12
① coeffSmooth + coeffDeriv 둘 다 생성
gx 계산 (4-pass):
  tmp = Convolve1D_X(src, coeffDeriv) → gx = Convolve1D_Y(tmp, coeffSmooth)
gy 계산:
  tmp = Convolve1D_X(src, coeffSmooth) → gy = Convolve1D_Y(tmp, coeffDeriv)
합성: dst = √(gx² + gy²)
⑤ R·G·B 채널 독립 × 4-pass = 12-pass
// ApplySavitzkyGolaySeparable8 — totalPasses 계산
int totalPasses;
if (isSg)
    totalPasses = (derivOrder == 0) ? 2 : 12;  // 스무딩 2 / 그래디언트 12
else
    totalPasses = 1;

var proxy = new ProgressProxy(progress, checked(totalPasses * height));
// ↑ 총 진행 단위 = pass 수 × 이미지 높이

🔬 Step 1 — SG 계수 계산 : Householder QR 분해 완전 해설

SG 필터의 핵심은 다항식 최소제곱 피팅의 계수를 합성곱 가중치로 변환하는 것입니다. 이 과정은 Vandermonde 행렬 → QR 분해 → 역대입으로 이루어집니다.

1
Vandermonde 행렬 A 구성

A[i,j] = xᵢʲ (xᵢ = −r,...,0,...,+r). 각 행은 윈도우 내 한 점의 다항식 기저값. windowSize × (polyOrder+1) 크기.

2
Householder QR 분해: A = QR (in-place)

작업 복사본 W에 열 단위 Householder 반사 적용. Q는 명시 저장하지 않고 Householder 벡터(vecs[k])만 보관하여 메모리 절약. 부호 선택으로 catastrophic cancellation 방지.

3
전진 대입: Rᵀ·z = e_derivOrder

R의 전치에 대해 단위 벡터 e_derivOrder를 풀어 z 벡터를 구합니다. derivOrder=0이면 e₀=[1,0,...,0].

4
역 반사: h = Q·z (Householder 벡터 역순 적용)

[z; 0] 벡터에 Householder 반사를 역순(k=cols-1 → 0)으로 적용하여 최종 합성곱 계수 h를 복원합니다.

5
정규화 / 스케일링

derivOrder=0: 계수 합=1로 정규화 (밝기 보존). derivOrder>0: derivOrder!/δ^derivOrder로 스케일링 (미분 값 보정).

📐 Vandermonde 행렬 A (r=2, polyOrder=2 예시)

윈도우 내 각 위치 x에 대해 다항식 기저 [1, x, x²]를 행으로 구성합니다. 비대칭 윈도우(경계)에서는 x 범위가 [−left, +right]로 조정됩니다.

xx⁰ = 1
−21−24
−11−11
0100
+11+11
+21+24
5×3 행렬 · A[i+half, j] = (i)ʲ
// ComputeSavitzkyGolayCoefficients()
int m = polyOrder; // = 2
int half = windowSize / 2; // = r

var A = new double[windowSize, m+1];
for (int i = -half; i <= half; i++)
{
    double x = i;
    double pow = 1.0;
    for (int j = 0; j <= m; j++)
    {
        A[i + half, j] = pow; // xʲ
        pow *= x;
    }
}
// → ComputeSgCoefficientsViaQR(
//     A, windowSize, m+1,
//     derivOrder, delta)
비대칭 경계: ComputeSavitzkyGolayCoefficientsAsymmetric(left, right, polyOrder, derivOrder, delta)에서는 x 범위가 [−left, +right]로 변경됩니다. polyOrder도 localWindow−1 이하로 자동 축소됩니다.

🔧 ComputeSgCoefficientsViaQR() 핵심 코드

일반적인 (AᵀA)⁻¹Aᵀ 정규 방정식이 아닌 Householder QR 경로를 사용합니다. AᵀA의 조건수 = A의 조건수²이므로, 높은 polyOrder에서 수치적 안정성을 위해 QR이 필수적입니다.

STEP 2 — Householder QR 분해
// 작업 복사본 W = A
var W = new double[windowSize, cols];
var vecs = new double[cols][];

for (int k = 0; k < cols; k++)
{
    int len = windowSize - k;
    var v = new double[len];
    for (int i = 0; i < len; i++)
        v[i] = W[k + i, k];

    double norm = Math.Sqrt(sigma);
    // 부호 선택: catastrophic cancellation 방지
    double alpha = v[0] >= 0
                 ? -norm : norm;
    v[0] -= alpha;

    // v 정규화
    double vnorm2 = 0;
    for (i) vnorm2 += v[i] * v[i];
    double inv = 1.0 / Math.Sqrt(vnorm2);
    for (i) v[i] *= inv;
    vecs[k] = v;

    // 나머지 열 반사:
    // W[k:,j] -= 2·v·(vᵀ·W[k:,j])
    for (int j = k; j < cols; j++)
    {
        double dot = 0;
        for (i) dot += v[i] * W[k+i,j];
        dot *= 2.0;
        for (i) W[k+i,j] -= dot * v[i];
    }
}
// W[0..cols-1, 0..cols-1] → R (상삼각)
STEP 3 — 전진 대입 Rᵀz = e_d
var z = new double[cols];
for (int i = 0; i < cols; i++)
{
    double rhs = (i == derivOrder)
                 ? 1.0 : 0.0;
    for (int j = 0; j < i; j++)
        rhs -= W[j, i] * z[j];
    // Rᵀ[i,j] = R[j,i] = W[j,i]
    z[i] = rhs / W[i, i];
}
STEP 4 — h = Q·z (역 Householder)
var h = new double[windowSize];
for (j < cols) h[j] = z[j];
// h[cols..] 은 0 유지

for (int k = cols-1; k >= 0; k--)
{
    var v = vecs[k];
    int len = windowSize - k;
    double dot = 0;
    for (i) dot += v[i] * h[k+i];
    dot *= 2.0;
    for (i) h[k+i] -= dot * v[i];
}

// STEP 5: 정규화 / 스케일링
if (derivOrder == 0) {
    double s = Σh[i];
    for (i) h[i] /= s; // 합=1 보존
} else {
    double scale =
        FactorialAsDouble(derivOrder)
        / Math.Pow(delta, derivOrder);
    for (i) h[i] *= scale;
}

🔍 음수 계수의 수학적 의미 — 엣지 보존 원리

SG 필터의 고유한 특성은 음수 계수의 존재입니다. 이것이 다른 모든 필터(Rect, Binomial, Gaussian)와 근본적으로 다른 점입니다.

SG (r=2, p=2) 1D 계수
−.086
+.343
+.486
+.343
−.086
합 = 1.0 · 음수가 먼 픽셀의 기여를 억제
Binomial (r=2) 1D (비교용)
.063
.250
.375
.250
.063
합 = 1.0 · 모든 양수 → 모든 픽셀이 양의 기여
엣지 보존 원리

SG는 윈도우 내 데이터에 다항식을 피팅합니다:

  • 평탄 영역: 다항식이 상수 → 단순 평균과 유사
  • 엣지: 다항식이 경사를 추적 → 경사를 보존하면서 노이즈만 제거
  • 피크: 2차 이상이 곡률 보존 → 피크 높이 유지

음수 계수는 피팅 과정에서 자연 발생합니다. 먼 픽셀의 값을 빼는 것으로 엣지와 피크 형태를 복원합니다.

🔄 Convolve1D_X / Convolve1D_Y — 1D 합성곱 상세 분석

각 1D 합성곱 함수는 3가지 경로로 분기됩니다: Interior 빠른 경로, Adaptive/ValidOnly/AdaptiveMask 비대칭 SG 경로, 기존 경계 모드(Symmetric/Replicate/ZeroPad) 경로.

경로 ① Interior 빠른 경로
// x >= r && x < width - r
// GetIndex1D() 호출 없음!
double accFast = 0.0;
for (int k = -r; k <= r; k++)
    accFast += coeff[k + r] * src[y, x + k];
return accFast;
// ↑ 단순 내적 — 경계 검사 없음
// 대부분의 픽셀이 이 경로
이미지의 (width−2r) × (height−2r) 내부 영역 전체가 이 경로를 탑니다. GetIndex1D() 호출 0회.
경로 ② Adaptive/ValidOnly/AdaptiveMask
// 경계: 비대칭 SG 계수 재계산
int left  = Math.Min(r, x);
int right = Math.Min(r, width-1-x);
int start = x - left;

// polyOrder/derivOrder 축소
int localWindow = left + right + 1;
int localPoly = Math.Min(
    polyOrder, localWindow - 1);
int localDeriv = Math.Min(
    derivOrder, localPoly);

var h = (localDeriv == 0)
    ? GetAsymmetricSg(left, right, localPoly)
    : GetAsymmetricSgDeriv(
        left, right, localPoly, localDeriv);

double sum = 0;
for (int i = 0; i < h.Length; i++)
    sum += h[i] * src[y, start + i];
return sum;
비대칭 계수는 Dictionary 캐싱 (키: Tuple<left, right, polyOrder[, derivOrder]>). 동일 경계 형태 재계산 없음.

경로 ③ Symmetric / Replicate / ZeroPad

// 기존 경계 모드: GetIndex1D()로 인덱스 매핑
double acc = 0.0, denom = 0.0;
for (int k = -r; k <= r; k++)
{
    int nx = GetIndex1D(x + k, width, mode);
    double c = coeff[k + r];

    if (nx < 0) { if (mode == ZeroPad) denom += c; continue; }

    acc += c * src[y, nx];
    denom += c;
}

// derivOrder > 0: 미분 계수 합≈0이므로 acc 그대로 반환
if (derivOrder > 0) return acc;

// ZeroPad derivOrder=0: denom으로 재정규화
if (mode == BoundaryMode.ZeroPad) {
    if (Math.Abs(denom) < 1e-12) return src[y, x];
    return acc / denom;
}
return acc; // Symmetric/Replicate: denom≈1
Symmetric/Replicate는 모든 인덱스가 유효하므로 denom ≈ 계수 합(=1). ZeroPad에서만 재정규화가 필요합니다.

🗃️ 비대칭 SG 계수 캐싱 — GetAsymmetricSg / GetAsymmetricSgDeriv

경계 픽셀에서 좌/우(또는 상/하) 가용 폭이 다르므로 비대칭 Vandermonde 행렬로 계수를 재계산합니다. 동일한 (left, right, polyOrder) 조합은 Dictionary에 캐싱됩니다.

// 스무딩 계수 캐시
private static readonly
  Dictionary<Tuple<int,int,int>, double[]>
    _sgAsymSmoothCache;

private static double[] GetAsymmetricSg(
    int left, int right, int polyOrder)
{
    var key = Tuple.Create(
        left, right, polyOrder);
    lock (_sgAsymSmoothCacheLock) {
        if (_sgAsymSmoothCache
            .TryGetValue(key, out var c))
            return c; // 캐시 적중
        c = ComputeSGAsymmetric(
            left, right, polyOrder,
            derivOrder:0, delta:1.0);
        _sgAsymSmoothCache[key] = c;
        return c;
    }
}
// 미분 계수 캐시 (4-tuple 키)
private static readonly
  Dictionary<Tuple<int,int,int,int>, double[]>
    _sgAsymDerivCache;

private static double[] GetAsymmetricSgDeriv(
    int left, int right,
    int polyOrder, int derivOrder)
{
    var key = Tuple.Create(
        left, right, polyOrder, derivOrder);
    lock (_sgAsymDerivCacheLock) {
        if (cache.TryGetValue(key, out var c))
            return c;
        c = ComputeSGAsymmetric(
            left, right, polyOrder,
            derivOrder, delta:1.0);
        cache[key] = c;
        return c;
    }
}
내부 (x=50, r=2)
left=2, right=2
대칭 계수 사용
가장자리 (x=1, r=2)
left=1, right=2
비대칭 계수 캐싱
모서리 (x=0, r=2)
left=0, right=2
localPoly ≤ 2 유지
polyOrder 자동 축소: 경계에서 윈도우가 줄면 localPoly = Math.Min(polyOrder, localWindow − 1)로 축소되어 Vandermonde 행렬이 특이(singular)해지는 것을 방지합니다. ValidateSmoothingParameters()에서 사전 검증도 수행됩니다.

📐 derivOrder > 0 — 그래디언트 크기(Gradient Magnitude) 계산

derivOrder>0이면 X/Y 방향 미분을 합성하여 엣지 강도 맵을 생성합니다. 채널당 4-pass × 3채널 = 12-pass.

PASS 1
X-deriv
Convolve1D_X(src, coeffDeriv)
PASS 2
Y-smooth
Convolve1D_Y(tmp, coeffSmooth)
결과
gx[y,x]
PASS 3
X-smooth
Convolve1D_X(src, coeffSmooth)
PASS 4
Y-deriv
Convolve1D_Y(tmp, coeffDeriv)
결과
gy[y,x]
dst[y,x] = √(gx[y,x]² + gy[y,x]²)
// ComputeGradientMagnitudeForChannel() — 채널당 4-pass
var tmp = new double[h, w];
var gx  = new double[h, w];

// Pass 1: X 방향 미분
Parallel.For(0, h, y => {
    for (x) tmp[y,x] = Convolve1D_X(src, y, x, w, r, coeffDeriv, mode, polyOrder, derivOrder);
    proxy?.StepRows(1);
});
// Pass 2: Y 방향 스무딩
Parallel.For(0, h, y => {
    for (x) gx[y,x] = Convolve1D_Y(tmp, y, x, h, r, coeffSmooth, mode, polyOrder, 0);
    proxy?.StepRows(1);
});
// Pass 3: X 방향 스무딩
Parallel.For(0, h, y => {
    for (x) tmp[y,x] = Convolve1D_X(src, y, x, w, r, coeffSmooth, mode, polyOrder, 0);
    proxy?.StepRows(1);
});
// Pass 4: Y 방향 미분 + 합성
Parallel.For(0, h, y => {
    for (x) {
        double gyVal = Convolve1D_Y(tmp, y, x, h, r, coeffDeriv, mode, polyOrder, derivOrder);
        double gxVal = gx[y,x];
        dst[y,x] = Math.Sqrt(gxVal*gxVal + gyVal*gyVal);
    }
    proxy?.StepRows(1);
});
왜 X-deriv→Y-smooth와 X-smooth→Y-deriv 두 조합인가? 2D 그래디언트는 X/Y 방향 편미분의 벡터 크기입니다. gx = ∂f/∂x·smooth_y는 X 방향 미분을 Y 방향으로 스무딩한 것이고, gy는 그 반대입니다. 분리 가능 합성곱의 미분 확장.

📊 SG 필터 vs 다른 필터 — 특성 비교

특성SGRectBinomial AvgGaussian
가중치 생성QR 분해 (다항 회귀)균일 1/(2r+1)²파스칼 삼각형exp(−x²/2σ²)
음수 계수있음 ← 핵심없음없음없음
2D 적용X→Y 분리 합성곱2D 직접 루프2D 외적 곱셈2D 외적 곱셈
엣지 보존최고최저보통보통
피크 보존최고최저보통보통
미분 출력지원 (derivOrder)미지원미지원미지원
경계 처리비대칭 SG 재계산GetIndex1D축소 Binomial축소 Gaussian
계산 복잡도O(r) per pixel (1D)O(r²)O(r²)O(r²)
파라미터r + polyOrder + derivOrderrrr + sigmaFactor
SG의 계산 효율: 2-pass 분리 합성곱이므로 픽셀당 O(r) × 2 = O(r). 다른 필터의 2D 직접 루프 O(r²)보다 이론적으로 효율적이지만, QR 분해 사전 연산 부담과 경계 비대칭 계수 계산 오버헤드가 있습니다.
Shared Infrastructure · GetIndex1D()

BoundaryMode — 경계 처리 6가지

커널이 이미지 경계를 벗어날 때 범위 밖 픽셀을 어떻게 처리할지 결정합니다. X·Y 방향에 GetIndex1D()가 독립 적용됩니다.

🔍 왜 경계 처리가 필요한가? — 문제 원리

반경 r인 커널을 이미지 가장자리 픽셀에 적용하면, 커널의 일부가 이미지 밖(-1, -2, ... 또는 w, w+1, ...) 좌표를 참조합니다. 이 범위 밖 픽셀에 어떤 값을 할당하느냐가 BoundaryMode입니다.

경계 처리 인터랙티브 시각화
1 r = 2, 이미지 폭 = 9 픽셀
원본 이미지 (1D 행 예시)
커널 윈도우 [x−2 … x+2] — 모드별 범위 밖 처리
문제: 경계 픽셀 (x=1, r=2)
커널 범위: x = -1, 0, 1, 2, 3
이미지 범위: 0 ~ w-1
x=-1, x=0 → 범위 밖!
x=1,2,3 → 유효
GetIndex1D() 반환값 비교 (idx=-1, n=9)
Mode반환의미
Symmetric0idx=0 (거울)
Replicate0가장자리 복제
ZeroPad-1→ 값 0 사용
AdaptiveMask-1→ 스킵, 재정규화
Adaptive0Symmetric과 동일 (필터에서 별도 분기)

GetIndex1D() — 모든 필터가 공유하는 경계 인덱스 함수

private static int GetIndex1D(int idx, int n, BoundaryMode mode) {
    switch (mode) {
        case Symmetric:    return idx < 0 ? -idx-1 : idx >= n ? 2*n-idx-1 : idx;
        case Replicate:    return idx < 0 ? 0 : idx >= n ? n-1 : idx;
        case ZeroPad:      return (idx < 0 || idx >= n) ? -1 : idx; // -1 → 값 0
        case ValidOnly:    return (idx < 0 || idx >= n) ? -1 : idx; // -1 → skip
        case AdaptiveMask: return (idx < 0 || idx >= n) ? -1 : idx; // -1 → skip
        case Adaptive:     return idx < 0 ? -idx-1 : idx >= n ? 2*n-idx-1 : idx; // Symmetric과 동일 (필터에서 별도 분기)
    }
}
반환값 −1의 의미: ZeroPad는 −1을 "값 0을 사용하라"는 신호로 해석, ValidOnly/AdaptiveMask는 "이 샘플을 완전히 스킵하라"는 신호로 해석합니다. GetIndex1D()X축과 Y축에 독립적으로 호출되어 2D 경계를 처리합니다.

Symmetric 거울 반사

c b a
a b c d e
e d c

가장 자연스러운 연속성. idx=−1→0, idx=−2→1. (경계 픽셀 중복)

Replicate 가장자리 복제

a a a
a b c d e
e e e

경계 픽셀 반복. 경계 밝기 평탄화 효과.

ZeroPad 0 패딩

0 0 0
a b c d e
0 0 0

범위 밖=0, denom에 포함. 경계 어두워짐.

Adaptive 윈도우 축소

winW = left+right+1;
// GetBinomial1D(winW) 재생성

실제 가용 영역만 사용. 경계 효과 없음.

AdaptiveMask 경계 스킵

if ((uint)ny >= (uint)h) continue;
if ((uint)nx >= (uint)w) continue;

유효 픽셀만 누적 → denom 자동 재정규화.

ValidOnly 유효 영역

// SG에서 GetAsymmetricSg() 호출
// 경계 비대칭 계수를 캐시에서 로드

SG 전용: 경계마다 비대칭 계수 계산 후 캐시.

모드 선택 가이드

Mode경계 왜곡속도가중치 재계산권장 용도
Symmetric거의 없음빠름불필요일반 용도 (기본값)
Replicate약간빠름불필요경계 밝기 평탄화
Adaptive없음보통winW/H마다 재생성Binom/Gauss 경계 품질
AdaptiveMask없음보통불필요(자동)마스크 영역 처리
ZeroPad어두워짐빠름불필요주파수/신호 처리
ValidOnly없음보통SG: 비대칭 계수SG + 엣지 감지

📊 필터별 경계 처리 방식 차이

동일한 BoundaryMode라도 각 필터가 경계를 처리하는 방식은 다릅니다. 특히 Adaptive 모드에서 가중치 재계산 방식이 필터마다 크게 다릅니다.

필터Symmetric/ReplicateAdaptiveAdaptiveMaskValidOnly/ZeroPad
Rectangular GetIndex1D → 균일 합산
count=(2r+1)² 고정
윈도우 축소
count=winW×winH
uint 범위 검사
count 감소
범위 밖 skip/0
count 변동
Binomial Avg GetIndex1D → 이항 가중합
denom=Σw 전체
GetBinomial1D(winW)
축소 계수 재생성 (캐싱)
uint 범위 검사
denom=유효 w만
범위 밖 skip/0
denom 변동
Binom. Median GetIndex1D → Bucket
전체 가중치
GetBinomial1D(winW)
축소 계수 + Bucket
uint 검사 → Bucket
유효 샘플만
범위 밖 skip
count 감소
Gauss. Median GetIndex1D → Bucket
전체 가중치
ComputeGaussian1D(winW,σ)
σ도 비례 축소 (캐싱 없음)
uint 검사 → Bucket
유효 샘플만
범위 밖 skip
count 감소
Gaussian Avg GetIndex1D → 가중합
denom=Σw 전체
ComputeGaussian1D(winW,σ)
σ 비례 축소 (캐싱 없음)
uint 검사
denom=유효 w만
범위 밖 skip/0
denom 변동
Savitzky-Golay GetIndex1D → 계수 내적
denom≈1 (합=1 정규화)
GetAsymmetricSg(l,r,p)
비대칭 QR 분해 (캐싱)
비대칭 SG 계수
캐싱
비대칭 SG 계수
캐싱
핵심 차이: SG는 경계에서 "비대칭 QR 분해"로 완전히 새로운 계수를 계산합니다. Binomial/Gaussian은 "축소된 1D 커널"을 재생성합니다. Rectangular은 단순히 count만 재계산합니다. 이 복잡도의 차이가 경계 품질과 성능의 트레이드오프를 결정합니다.

🔬 SG 전용 — 비대칭 경계 계수의 특수성

SG 필터는 경계에서 다른 필터와 근본적으로 다른 접근을 합니다. 단순 축소가 아닌 비대칭 Vandermonde 행렬로 QR 분해를 수행합니다.

내부 (x=50, r=2) — 대칭 계수
−.086
+.343
+.486
+.343
−.086
좌우 대칭 · 합=1
경계 (x=1, r=2) — 비대칭 계수 (left=1, right=2)
−.114
+.571
+.371
+.171
비대칭 · 합=1 · 4개 계수
// Convolve1D_X — Adaptive/ValidOnly 경계 경로
int left  = Math.Min(r, x);
int right = Math.Min(r, width - 1 - x);
int localWindow = left + right + 1;

// polyOrder/derivOrder 자동 축소 (과적합 방지)
int localPoly  = Math.Min(polyOrder, localWindow - 1);
int localDeriv = Math.Min(derivOrder, localPoly);

// 비대칭 SG 계수 (Dictionary 캐싱)
double[] h = (localDeriv == 0)
    ? GetAsymmetricSg(left, right, localPoly)           // 스무딩 캐시
    : GetAsymmetricSgDeriv(left, right, localPoly, localDeriv); // 미분 캐시

// 비대칭 계수로 합성곱
double sum = 0;
int start = x - left;
for (int i = 0; i < h.Length; i++)
    sum += h[i] * src[y, start + i];
return sum;
다른 필터와의 차이: Binomial/Gaussian은 경계에서 축소된 윈도우에 맞는 "같은 종류의 더 짧은 커널"을 사용합니다. SG는 비대칭 Vandermonde 행렬에서 QR 분해를 다시 수행하여 "완전히 새로운 최소제곱 해"를 계산합니다. 이 때문에 SG의 경계 처리가 가장 수학적으로 정확하지만, 계산 성능 부담도 가장 높습니다.

📝 GetBoundaryMethodText() — 사용자 표시용 텍스트

public static string GetBoundaryMethodText(BoundaryMode mode) {
    switch (mode) {
        case Symmetric:    return "Symmetric (Mirror)";
        case Replicate:    return "Replicate (Edge Clamp)";
        case ZeroPad:      return "Zero Padding";
        case Adaptive:     return "Adaptive (Window Shrink)";
        case AdaptiveMask: return "Adaptive Mask (Skip Invalid)";
        case ValidOnly:    return "Valid Only";
    }
}
UI에 표시되는 경계 모드 이름. SmoothingConductor.ApplySmoothing()의 boundaryMode 파라미터와 1:1 대응.

Performance · Optimization

공통 최적화 포인트

① Binomial 계수 캐싱

private static readonly Dictionary<int, double[]> _binom1D;
lock (_lock) {
  if (_binom1D.TryGetValue(n, out var c)) return c;
  // 동일 windowSize 재계산 없음
}

② ThreadLocal 버퍼

using var tBufs = new ThreadLocal<MedianThreadBuffers8>(
  () => new MedianThreadBuffers8(maxSamples));
// 스레드별 독립 버퍼 → lock 없음
// bucket[256] 재사용 → GC 없음

③ Parallel.For 행 단위 병렬화

각 행(row)이 완전히 독립적. 입력에 읽기 전용 접근이므로 경쟁 없이 완전 병렬화됩니다. Parallel.For(0, height, y => { ... })

④ Interior 빠른 경로

bool yIn = y>=r && y<h-r;
if (yIn && x>=r && x<w-r) {
  // GetIndex1D() 없음!
  // fullDenom 재사용!
}

⑤ SG 비대칭 계수 캐싱

// Dictionary<Tuple<int,int,int>, double[]>
// 동일 (left,right,polyOrder) 재사용
lock (_sgAsymSmoothCacheLock) {
  if (cache.TryGetValue(key, out var c))
    return c; // 캐시 적중
}

⑥ ProgressProxy 진행 보고

// 원자적 진행률 업데이트
var acc = new ProgressAccumulator(
    progress, totalPasses * height);
// Interlocked.Add → 정확한 %
// 1% 변경시에만 Report() 호출

📊 필터별 성능 특성 비교

필터픽셀당 복잡도메모리 패턴경계 오버헤드상대 속도
RectangularO(r²) 정수합byte[] 직접GetIndex1D 또는 Adaptive★★★★★
Binomial AvgO(r²) 실수합+곱byte[] + 1D 캐시GetBinomial1D 캐싱★★★★☆
Binom. MedianO(r²+256) BucketThreadLocal 버퍼GetBinomial1D 캐싱★★★☆☆
Gauss. MedianO(r²+256) BucketThreadLocal 버퍼ComputeGaussian1D 재계산★★★☆☆
Gaussian AvgO(r²) 실수합+곱byte[] + g1DComputeGaussian1D 재계산★★★★☆
Savitzky-GolayO(r)×2 pass 1D 합성곱double[,] Planes + tmpGetAsymmetricSg + QR 캐싱★★☆☆☆
SG가 느린 이유: 1D 합성곱 O(r)×2는 이론적으로 다른 필터의 O(r²)보다 효율적이지만, ① QR 분해 사전 연산 부담 ② 경계 비대칭 계수 계산 ③ 16-bit에서 derivOrder>0 시 12-pass ④ double[,] 임시 배열 할당이 오버헤드를 만듭니다.

💾 메모리 할당 패턴

GC 프렌들리 (할당 최소화)
  • MedianThreadBuffers8: using ThreadLocal → Dispose 보장
  • Bucket[256]: Array.Clear 후 재사용
  • _binom1D Dictionary: 전역 캐시, GC Root
  • _sgAsymSmoothCache: static readonly → GC 안정
주요 할당 지점
  • SG: double[,] tmp — height×width 임시 배열
  • 16-bit: Planes(double[,]×3) — ExtractPlanes
  • Gaussian: ComputeGaussian1D — 매번 new double[]
  • SG gradient: gx[h,w] 추가 배열 (derivOrder>0)