SonataSmooth2D.Tune · SmoothingConductor.cs · 전체 필터 심층 분석

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

Rectangular 필터부터 Savitzky-Golay 필터까지 여섯가지 필터의 수학적 원리, α 블렌딩, 경계 처리, RGB 채널 독립 처리, 8-bit / 16-bit 구현을 코드와 다이어그램으로 자세하게 심층 분석합니다.

Interactive · Visual Comparison

Filter Lab

여섯 가지 Smoothing 필터 × 8 가지 커널 반경을 실시간으로 비교합니다. 테스트 이미지를 선택하고 슬라이더를 움직이거나 필름 스트립을 클릭하세요.

JS 시뮬레이터 vs 실제 C# 구현 - 영역별 일치도

영역일치 여부비고
이항 계수 생성완전 일치파스칼의 삼각형 기반의 동일 알고리즘
가우시안 커널완전 일치exp(−x² / 2σ²) 정규화 동일
가중 중앙값 (Pixel Simulator)완전 일치Bucket 방식, TieBreak 포함
경계 모드 인덱싱완전 일치GetIndex1D() 로직 동일
SG 계수 (polyOrder = 2)수학적 동치근사 공식 ↔ QR 분해 (결과 동일)
분리형 합성곱 구조완전 일치X pass → Y pass 순서 동일
Filter Lab 가중 중앙값 2D보간 생략최대 ±1 픽셀 차이
Filter Lab 경계 모드Replicate 고정실제 : 6 가지 선택 가능
⚠ 교육용 시뮬레이션
Filter Lab 은 필터 특성의 개념적 비교를 위한 브라우저 내 시뮬레이션입니다. 실제 SonataSmooth 애플리케이션의 결과와 차이가 발생할 수 있습니다. 채널 처리 (BWMF · GWMF) 는 JS 단일 채널 vs 실제 R · G · B 3 채널 독립 처리 방식으로 가장 큰 차이가 있으며, 비트 심도는 JS 8-bit 전용 vs 실제 16-bit 파이프라인, 이미지 크기는 80 × 60px 합성 vs 고해상도 다채널입니다.
이미지
모드
r = 7
← 스크롤하여 모든 필터 확인 · 썸네일 클릭 → split 비교 뷰
Resources · Downloads

다운로드 및 외부 링크

SonataSmooth 핵심 애플리케이션 코드, .NET Standard 2.0 기반 Smoothing 라이브러리 NuGet 패키지, 그리고 라이브러리 기능을 직접 실험해볼 수 있는 실습용 Etude 애플리케이션을 아래 링크에서 확인하실 수 있습니다.

GitHub Repository happybono / SonataSmooth

SonataSmooth

SonataSmooth의 핵심 소스 코드 저장소입니다. C# .NET Windows Forms 기반으로 제작된 수치 데이터 노이즈 제거 및 평활화 애플리케이션으로, 수동 입력, 클립보드 붙여넣기, 드래그 앤 드롭 등 다양한 입력 방식과 강력한 유효성 검사를 지원합니다. Rectangular Mean, Binomial Average, Binomial Median, Gaussian Weighted Median, Gaussian, Savitzky-Golay 필터를 파라미터를 세부 조정하며 함께 적용할 수 있으며, 실시간 진행 피드백과 배치 편집 기능을 갖춘 반응형 UI 를 제공합니다.

머신러닝 · 딥러닝 전처리, IoT 센서 데이터 처리, 금융 시계열 필터링, 과학 실험 측정값 정제, 1D 신호 처리, 데이터 시각화 등 순차적 수치 신호의 전처리가 필요한 다양한 도메인에서 활용할 수 있습니다.

NAMING PHILOSOPHY

SonataSmooth은 "Sonata"와 "Smooth"의 합성어입니다. 소나타 (Sonata) 는 서로 다른 악장 (악장 = 알고리즘) 이 하나의 조화로운 전체로 어우러지는 음악 형식으로, 여러 Smoothing 알고리즘이 협주 (concert) 하면서 함께 작동한다는 철학을 담고 있습니다. "Smooth" 는 데이터에서 노이즈를 부드럽게 제거하는 평활화 기능을 강조합니다. SonataSmooth 는 다양한 기법을 조화롭게 적용하여 데이터를 음악처럼 매끄럽고 명확하게 처리한다는 철학을 구현합니다.

⚠ SPECIAL LICENSING NOTICE

대부분의 컴포넌트는 MIT 라이선스 적용. 단, 파스칼의 삼각형 계수 기반 Binomial Weighted Median 필터는 특허 출원 중 (patent-pending) 으로, 비상업적 연구 · 교육 · 개인 프로젝트에는 무료로 사용 가능하나 상업적 사용 · 재배포 · 제품 통합은 특허 보유자의 사전 서면 동의가 필요합니다.

C# / .NET Windows Forms Noise Reduction Signal Processing
↗ View Source on GitHub
NuGet Package SonataSmooth.Tune · v5.3.0 · .NET Standard 2.0

SonataSmooth.Tune

.NET Standard 2.0 을 기반으로 하는 고성능 1D 수치 신호 평활화 & 내보내기 툴킷 NuGet 패키지입니다. Rectangular (Moving Average), Binomial (Pascal), Weighted Median (Binomial Weights), Gaussian Weighted Median (GWMF), Gaussian, Savitzky-Golay Smoothing을 다양한 경계 처리 옵션과 병렬화 설정과 함께 제공하며, CSV 및 Excel (COM) 내보내기 Helper 로 여러 Smoothing 결과를 나란히 비교 · 차트화할 수 있습니다.

C# / .NET Standard 2.0 Signal Smoothing CSV · Excel Export Parallelization
$ dotnet add package SonataSmooth.Tune --version 5.3.0
⚠ SPECIAL LICENSING NOTICE

대부분의 컴포넌트는 MIT 라이선스 적용. 단, Pascal's Triangle 계수 기반 Binomial Weighted Median 필터는 특허 출원 중(patent-pending)으로, 비상업적 연구 · 교육 · 개인 프로젝트에는 무료로 사용 가능하나 상업적 사용 · 재배포 · 제품 통합은 특허 보유자의 사전 서면 동의가 필요합니다.

↗ Download on NuGet Gallery
GitHub Repository happybono / SonataSmooth.Tune.Etude

SonataSmooth.Tune.Etude

SonataSmooth.Tune 라이브러리의 기능을 직접 체험하고 학습할 수 있는 실습용 동반 애플리케이션입니다. 음악에서 에튀드 (Étude) 가 최종 작품을 위한 준비 연습곡이듯, 이 저장소는 본격적인 데이터 처리 애플리케이션을 구현하기 전에 SonataSmooth.Tune의 다양한 Smoothing 알고리즘을 실험하고 이해하기 위한 스케치이자 학습 공간입니다.

Rectangular, Binomial Average, Binomial Weighted Median, Gaussian Weighted Median (GWMF), Gaussian, Savitzky-Golay 등 다양한 필터의 동작을 샘플 데이터로 직접 실행해보고 결과를 비교할 수 있으며, 경계 처리 방식 선택, 병렬화 옵션, CSV / Excel 내보내기 등 실제 워크플로에 필요한 기능도 함께 다룹니다. 저장소에는 Samples/ 폴더와 doc/ 문서가 포함되어 있어 라이브러리 도입 전 검증 단계로 활용하기에 최적입니다.

NAMING PHILOSOPHY

SonataSmooth.Tune은 데이터를 악기 조율하듯 정밀하게 다듬는 C# / .NET Smoothing 라이브러리이며, SonataSmooth.Tune.Etude는 그 라이브러리를 위한 연습곡 (Étude) - 완성된 작품에 앞서 기법을 탐구하고 숙달하는 실습 공간입니다.

⚠ SPECIAL LICENSING NOTICE

대부분의 컴포넌트는 MIT 라이선스 적용. 단, Pascal's Triangle 계수 기반 Binomial Weighted Median 필터는 특허 출원 중(patent-pending)으로, 비상업적 연구 · 교육 · 개인 프로젝트에는 무료로 사용 가능하나 상업적 사용 · 재배포 · 제품 통합은 특허 보유자의 사전 서면 동의가 필요합니다.

C# / .NET Hands-on Study App Signal Processing Sample · Docs Included
↗ View on GitHub
Overview · Architecture

SmoothingConductor 전체 구조

여섯가지 필터는 모두 하나의 진입점 ApplySmoothing() 을 통해 호출됩니다. 비트심도 (8 / 16-bit) 에 따라 파이프라인이 분기되고, 각 필터는 R · G · B 각각의 세 가지 채널에 대해 독립적으로 처리합니다.

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()
1. Ensure24bppFormat()
2. LockBits → byte[] sBuffer
3. 픽셀 = sBuffer[p], [p+1], [p+2]
4. 필터 적용 (Parallel.For)
5. Marshal.Copy → dst 24bpp
BitDepth
⟵│⟶
OutputBitDepth.Bit16
Apply16()
1. ExtractPlanes() → double[,] B, G, R
2. 16-bit 픽셀값 (0 ~ 65535)
3. 필터 적용 (Parallel.For)
4. WritePlanes() → dst 48bpp
5. 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 Average - 단순 균일 박스 평균

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

📐 수식 & 커널 (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 → 건너뜀count < (2r + 1)²유효 픽셀만 평균
AdaptiveMask건너뜀 (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 : 범위 밖 건너뜀, 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는 윈도우 너비는 그대로 두되, 범위 밖은 건너뛰어 분모가 감소하고 그에 따라 재정규화.

🔍 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 → 건너뜀

        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] 의 부분). 축소된 Binomial 계수는 원본과 다른 분포를 형성하므로, 단순 마스킹(AdaptiveMask)보다 Adaptive 방식이 수학적으로 더 정확합니다.
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 · 동일 이항 계수 커널 (Binomial Average 필터와 동일 가중치, 출력 방법만 다름)
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 Average 와 동일한 가중치. 차이 : 가중합 평균이 아닌 가중 중앙값을 구함
Rectangular Average (노이즈 있을 경우) :
[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);               // 1. 버킷 초기화

    double total = 0;
    for (int i = 0; i < count; i++)
    {
        double w = weights[i];
        bucket[values[i]] += w;                // 2. 픽셀 값 → 버킷에 가중치 누적
        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++)              // 3. 0 → 255 순서 탐색
    {
        double w = bucket[i];
        if (w > 0)
        {
            acc += w;

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

            if (acc >= half - eps)             // 4. 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) 단위 병렬 처리로 구현 - 출력 픽셀이 입력에만 읽기 접근하므로 쓰기 충돌 (race-condition) 이 발생하지 않습니다.
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 인가
  • 임펄스 노이즈(핫 픽셀) 완전 제거 : 센서의 Hot / Dead 픽셀은 주변과 극단적으로 다른 값을 가집니다. 가중 중앙값은 극단값이 50% 누적 기준을 넘지 못하게 하여 완벽하게 제거합니다. Binomial 가중치의 2D 중앙 ≤ 25% (r = 1) 이므로 모든 반경에서 보장됩니다.
  • Cross-Hatching Artifact 개선 : 균일 가중 중앙값 (Rectangular Median, Running 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 Weighted Median 필터는 핫 픽셀을 모든 r 에서 안정적으로 제거합니다.
  • 평탄 영역 내 미세 구조 보존 : FlatTop 빔의 중앙 평탄 영역에도 레이저 간섭·회절에 의한 미세 강도 변동 (ripple) 이 존재합니다. 이항 가중치의 중앙 집중 특성은 ripple 을 보존하면서 랜덤 노이즈만 제거하여, 빔 균일도 (uniformity) 분석의 정밀도를 유지합니다.
수학적 근거 - FlatTop Edge 보존 : FlatTop 빔의 이상적 1D 단면은 rect(x/w) 함수입니다. 가중 평균 (convolution with Gaussian) 은 엣지를 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 Weighted Median 필터의 엣지 보존 특성은 유지됩니다. 반면 Gaussian blur 전처리 후 augmentation (증강) 을 적용하면 이미 소실된 고주파 정보는 복원이 불가능합니다.
수학적 근거 - 학습 효율 : CNN 의 초기 레이어는 edge / texture 검출 필터 (Gabor-like) 를 학습합니다. 입력 이미지의 엣지가 보존되어 있으면 필터들의 gradient 신호가 명확하여 수렴 속도가 빨라집니다. 노이즈가 잔존하면 gradient 가 랜덤 방향으로 분산되어 학습이 느려지고, 엣지가 흐려지면 gradient 크기 자체가 감소하여 기울기 소실 (vanishing gradient) 문제가 발생합니다. Binomial Weighted Median 필터는 두 가지 문제점을 동시에 해결할 수 있는 필터입니다.
DEEP DIVE - Cross-Hatching Artifact 개선 원리

균일 중앙값 (Rectangular Median) 에서 발생하는 Cross-Hatching (격자) 왜곡 현상을 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) 응답에 근접합니다. 계산량이 많은 원형 커널을 사용하지 않고도 방향성이 왜곡되는 결과를 억제하는 구조적 해법입니다.
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 Weighted Median 필터가 충족하는 근거
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.0) · 정규화 후 합 = 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.0
중앙 집중
σ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 탐색
Binomial Median 16-bit 와 알고리즘 동일. 가중치 생성 함수만 ComputeGaussian1D 로 교체.

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

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

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

σ = windowSize / sigmaFactor로 σ를 계산한 뒤 exp(−x² / 2σ²) 생성. 합 = 1 정규화.
Binomial Weighted Median 의 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 Weighted 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 (Running Median) 과 유사
6 (기본)0.83~ 0.23~ 0.001~ 3σ = 2.5 ≈ r중앙 집중 + 먼 픽셀 무시 → 균형 잡힌 Smoothing
12 (좁음)0.42~ 0.48~ 10⁻⁶~ 3σ = 1.3 < r중앙 거의 단독 → 원본에 가까움
σF = 3 (넓은 σ)의 효과 :

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

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

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

Binomial Weighted Median 필터와의 차이 : 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 Weighted 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 Weighted Median vs Gaussian Weighted Median

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

1. 원리의 차이 - 가중치 분포 형태

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

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

2. 이미지 결과물 차이 - 시나리오별 분석

실제 픽셀 값 예시로 두 필터가 어떻게 다른 결과를 도출하는지 확인하세요. (픽셀값 0 ~ 255 기준)

7 × 7 윈도우 시뮬레이터 - sigmaFactor 조절
σ = windowSize / sigmaFactor = 7 / σF. sigmaFactor 가 작을수록 σ 값이 커져 가중치가 넓게 퍼지고, 클수록 σ 값이 작아져 중앙에 집중합니다. ⚠ r = 1 (3 × 3) 인 경우에 σF ≥ 3 이면 2D 중앙 > 50% → 필터 무효화. 이 시뮬레이터는 7 × 7 (r = 3) 이므로 σF = 6.0 에서도 정상 동작합니다.
픽셀값 입력 (클릭 또는 탭해서 수정) · ■ 중앙 · ■ 노이즈
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

3. 장단점 종합 비교

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

4. 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 Weighted Median 필터는 파스칼 삼각형의 수학적 특성이 자동으로 "합리적인" 가중치를 보장합니다. 사용자 오조작이 없어 사용이 직관적입니다.

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.0) 과 오차 <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 대신

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

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

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

올바른 해석 : r = 1에서 σF = 6.0.0 인 GWMF가 "더 좋아 보이는" 이유는 블러가 적어서가 아니라, 필터가 사실상 작동하지 않고 있기 때문입니다. σF 를 2 ~ 3으로 낮추면 Gaussian도 정상 작동하지만, 그 결과는 Binomial Weighted Median 필터와 거의 동일해집니다. r = 2 부터는 σF = 6.0 에서도 2D 중앙 가중치가 23.0% 로 정상 동작합니다.
Gaussian Median 해결책 - sigmaFactor를 함께 낮춰야
r 값을 증가시킬 때 sigmaFactor 를 동시에 낮추면 (σ 계수를 키우면) 가중치가 넓게 분산되어 중앙 과집중 문제가 해소되지만, 추가 튜닝에 대한 성능에 영향을 주며 계산량 부담이 커집니다. σ 를 완벽히 맞춘다고 하더라도 아래에 기술할 구조적인 이유로 Binomial Weighted Median 필터가 미세한 우위를 점합니다.
r=1, σF = 6.0 → 중앙 62% ✗ 노이즈 통과 r=1, σF = 3 → 중앙 ~ 20% ✓ 작동 r=1, σF = 2 → 중앙 ~ 15% ✓ 작동

🔬 σ를 완벽히 일치시켜도 Binomial Weighted Median 필터가 미세하게 우위인 수학적 근거

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

근거 1. - 분포 형태 (Kurtosis) 차이

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

r Binomial κ Gaussian κ Binom 2D n_eff Gauss 2D n_eff Binomial 우위
유효 샘플 수 공식: n_eff = (Σwᵢ)² / Σwᵢ²  ·  Platykurtic 분포 (납작) 는 가중치를 더 고르게 분산 → n_eff ↑ → 추정 분산 ↓
Weighted Median 분산 ≈ π / (2 · n_eff) · σ²_noise   →   n_eff + 10% 이면 추정 분산 10% 감소, 노이즈 제거 효율 ~ 0.4 dB 향상
근거 2. - 정수값 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 누적 연산이 수학적으로 정확. 동일 함수를 사용하지만 오차 없음.
근거 3. - 유한 지지(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 필터의 경우 이 문제가 원천적으로 없습니다.
근거 4. - 중앙 노이즈 저항력 (σ 매칭 기준)

Weighted Median 은 가중치 기반 중앙값 산출 방식입니다. 노이즈가 중앙에 위치할 때 주변 신호 값 혹은 픽셀 값들이 우세하게 작용하려면 중앙 가중치가 낮을수록 유리합니다. Platykurtic 특성을 가진 Binomial Weighted Median 필터는 σ 값을 동일하게 맞춘 조건에서도 상대적으로 중앙 가중치가 낮습니다.

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

6. 선택 가이드 - 언제 어떤 필터를?

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

7. 수식 나란히 비교 - 같은 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.0 : σ = 5 / 6.0 ≈ 0.83
// 중앙 기여(1D) : ≈ 47.9% (σF = 6.0 기준)
// 최외곽(1D) : ≈ 2.7% (σF = 6.0 기준)
// σ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.0 · Gaussian Weighted 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% · Gaussian Weighted Median 과 완전히 동일 커널 - 출력 방법 (평균 vs 중앙값) 만 다름
Gaussian Average vs Gaussian Weighted Median :
  • 동일 가우시안 가중치
  • Average : 가중합 / 분모 (연속 평균)
  • Median : 50% 누적 탐색 (노이즈 강건)
Binomial Average vs Gaussian Average :
  • 동일 출력 방법 (가중 평균)
  • Binomial : σ 없음, 캐싱 가능
  • 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 Average vs Binomial Average - 가중 평균 필터 비교

Binomial Average

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

Gaussian Average

  • 가중치 : exp(−x² / 2σ²) 연속 함수
  • sigmaFactor (σ) 로 분포 폭 정밀 제어
  • 매번 exp() 계산 (캐싱 효율 낮음)
  • 주파수 영역 : 완벽한 Gaussian 저역통과
  • σ 로 미세 조정 가능, 단 튜닝 필요
선택 기준 : 가우시안 노이즈에 대한 L₂ 최적 Smoothing이 필요하면 Gaussian Average. 파라미터 없이 빠르면서도 직관적인 사용성을 선호하면 Binomial Average. 두 필터 모두 가중 평균 을 산출하므로 극단값 (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 이 되도록 정규화되므로 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 - 다항 회귀 Smoothing

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

📐 수식 & 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 Smoothing 계수 (외적 아님, 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 (Smoothing) 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 : Smoothing만 (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 : Gradient 출력 (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 (Smoothing) 과 derivOrder > 0 (Gradient) 에서 처리 경로가 완전히 다릅니다. Smoothing 은 2-pass, 그래디언트는 12-pass (채널 × 4) 입니다.

derivOrder = 0 (Smoothing) · totalPasses = 2
1. coeffSmooth = ComputeSavitzkyGolayCoefficients(n, polyOrder, 0, 1.0)
2. X-pass : tmp[y, x] = Convolve1D_X(plane, coeffSmooth)
3. Y-pass : out[y, x] = Convolve1D_Y(tmp, coeffSmooth)
4. R · G · B 채널 독립적으로 반복 (각각 2-pass)
5. WritePlanes(dst, outB, outG, outR)
derivOrder > 0 (Gradient) · totalPasses = 12
1. coeffSmooth + coeffDeriv 둘 다 생성
2. gx 계산 (4-pass) :
tmp = Convolve1D_X(src, coeffDeriv) → gx = Convolve1D_Y(tmp, coeffSmooth)
3. gy 계산 :
tmp = Convolve1D_X(src, coeffSmooth) → gy = Convolve1D_Y(tmp, coeffDeriv)
4. 합성 : dst = √(gx² + gy²)
5. R · G · B 채널 독립 × 4-pass = 12-pass
// ApplySavitzkyGolaySeparable8 — totalPasses 계산
int totalPasses;
if (isSg)
totalPasses = (derivOrder == 0) ? 2 : 12;  // Smoothing 2 / Gradient 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 필터의 고유한 특성은 음수 계수의 존재이며, 다른 모든 필터 (Rectangular, Binomial Average, Binomial Weighted Median, Gaussian Weighted Median, 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) 경로.

1. 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 회.
2. 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]>). 동일 경계 형태 재계산 없음.
3. 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 에 캐싱됩니다.

// Smoothing 계수 캐시
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 방향 Smoothing
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 방향 Smoothing
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 방향으로 Smoothing 한 결과고, gy 는 반대로, 분리 가능 합성곱의 미분 확장입니다.

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

특성SGRectBinomial AverageGaussian
가중치 생성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 → 건너뜀
        case AdaptiveMask: return (idx < 0 || idx >= n) ? -  1 : idx; // -1 → 건너뜀
        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 마다 재생성Binomial / Gaussian 경계 품질
AdaptiveMask없음보통불필요 (자동)마스크 영역 처리
ZeroPad어두워짐빠름불필요주파수 / 신호 처리
ValidOnly없음보통SG : 비대칭 계수SG + 엣지 감지

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

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

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

🔬 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)           // Smoothing 캐시
: 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

공통 최적화 포인트

1. Binomial 계수 캐싱

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

2. ThreadLocal 버퍼

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

3. Parallel.For 행 단위 병렬화

각 행 (row) 을 완전히 독립적으로 처리, 읽기 전용 접근이므로 동시 접근 충돌없이 안정적으로 병렬화됩니다. Parallel.For(0, height, y => { ... })

4. Interior 빠른 경로

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

5. SG 비대칭 계수 캐싱

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

6. 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²) 계산 방식에 비해 효율적이지만, 1. QR 분해 사전 연산 부담 2. 경계 비대칭 계수 계산 3. 16-bit 분기에서 derivOrder > 0 시 12-pass 4. double[,] 임시 배열 할당 과정에서 성능적 부하를 유발할 수 있습니다.

💾 메모리 할당 패턴

GC (Garbage Collection) 친화적인 구조 (불필요한 할당 억제)
  • 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)