Chapter 06

전달 함수, 감마, 양자화

5장의 3×3 행렬은 선형광(linear light), 즉 빛의 양에 비례하는 RGB를 다룬다. 그러나 파일과 영상 신호, 그래픽 API가 주고받는 숫자는 거의 언제나 비선형으로 인코딩된 값이다. 이 장은 그 비선형 곡선(전달 함수)이 왜 필요한지, 표준 곡선의 정확한 식이 무엇인지, 인코딩된 값으로 계산하면 어떤 오류가 생기는지, 그리고 유한한 비트로 매끄러운 그라데이션을 만드는 법을 숫자로 다룬다.

왜 비선형으로 인코딩하는가

빛의 양(휘도 \(Y\))을 그대로 정수로 저장하면 가장 단순하다. 문제는 사람의 밝기 지각이 휘도에 비례하지 않는다는 데 있다. 19세기 Weber는 겨우 구별되는 차이 \(\Delta I\)가 기준 자극 \(I\)에 비례한다는 것을 관찰했다(Weber 법칙, \(\Delta I / I \approx\) 일정). 명소시의 넓은 시야에서 휘도의 Weber 비율은 대략 1~2%다. Fechner는 이를 적분해 감각이 자극의 로그에 비례한다고 보았다(Weber–Fechner 법칙). 20세기 중반 Stevens는 크기 추정 실험으로 감각이 자극의 거듭제곱에 비례한다는 멱법칙(Stevens' power law)을 제시했고, 어두운 배경 속 넓은 시야(약 5°) 표적의 밝기(brightness) 지수로 약 0.33을 보고했다.

$$\text{Fechner: } S = k\ln\frac{I}{I_0},\qquad \text{Stevens: } S = k\,I^{\,a}\;(a\approx 0.33)$$
로그와 세제곱근은 넓은 범위에서는 다르지만, 실제 영상의 휘도 범위(대비 수백:1)에서는 둘 다 "어두운 쪽을 확대하고 밝은 쪽을 압축하는" 오목한 곡선이다.

CIE 1976 명도 \(L^*\)는 반사 표면의 명도 척도(먼셀 명도)를 근사하도록 만든 세제곱근 식이다. 7장에서 색차를 다룰 때 다시 쓰지만, 여기서는 "지각적으로 균등한 밝기 축"의 기준으로 쓴다.

$$L^* = \begin{cases}116\,\left(Y/Y_n\right)^{1/3} - 16, & Y/Y_n > (6/29)^3 \approx 0.008856\\[2pt] (29/3)^3\,(Y/Y_n)\approx 903.3\,(Y/Y_n), & \text{otherwise}\end{cases}$$
\(Y_n\)은 기준 백색의 휘도. 18% 회색은 \(L^*=49.5\), 휘도 50%는 \(L^*=76.1\)이다. 휘도 절반이 "중간 밝기"가 아니라는 점이 이 장 전체의 출발점이다.
휘도 Y 등간격 (0, 0.1, …, 1.0) — 아래 숫자는 L* 0 38 52 62 69 76 82 87 92 96 100 L* 등간격 (0, 10, …, 100) — 아래 숫자는 휘도 Y 0.00 0.01 0.03 0.06 0.11 0.18 0.28 0.41 0.57 0.76 1.00
그림 6-1. 위: 휘도를 0.1씩 똑같이 늘린 11단계. 첫 칸과 둘째 칸 사이(L* 0→38)가 크게 벌어지고, 밝은 쪽 칸들은 거의 구별되지 않는다. 아래: L*를 10씩 늘린 단계는 눈에 고르게 보이지만, 휘도로는 0.01, 0.03, 0.06…처럼 어두운 쪽에 촘촘하다(sRGB 화면 기준으로 표시).

이제 8비트(256단계)로 휘도를 저장한다고 하자. 위 줄처럼 휘도에 균등하게 코드를 주면, 밝은 쪽 코드는 사람이 구별하지 못하는 차이에 낭비되고 어두운 쪽은 한 단계가 너무 커서 계단(밴딩(banding))이 보인다. 아래 줄처럼 지각에 균등하게 코드를 주면 같은 비트 수로 모든 밝기에서 단계를 비슷하게 작게 만들 수 있다. 이것이 비선형 인코딩의 본질적인 목적이다. 감마 곡선은 "어두운 영역을 확대하는 압축 코덱"이다.

감마의 역사: 두 개의 우연

CRT의 휘도는 구동 전압에 대해 지수 약 2.35~2.55의 멱함수에 가깝다(전자총 특성). 텔레비전은 수상기를 바꾸는 대신 송출 쪽(카메라)에서 역함수를 미리 곱했다. NTSC(1953)는 수상기 감마 2.2를 가정했다. 그런데 이 CRT 곡선의 역함수(지수 약 0.4~0.45)가 공교롭게도 사람의 밝기 지각(지수 약 1/3, 주변 조건에 따라 달라짐)과 비슷했다. 그 결과 전송 신호는 지각적으로 거의 균등한 척도가 되었고, 채널에 더해진 잡음도 디스플레이를 거치며 어두운 쪽에서 눌려 눈에 덜 띄었다. CRT가 사라진 지금도 감마 인코딩이 남은 이유는 바로 이 두 번째, 지각적 부호화 효과 때문이다. LCD·OLED는 원래 감마 특성이 없지만 구동 회로에서 일부러 CRT형 EOTF를 흉내 낸다(13장).

OETF, EOTF, OOTF — 용어 정리

"감마"라는 말은 문맥마다 뜻이 달라 혼란을 부른다. 현대 표준(특히 ITU-R BT.2100)은 곡선이 어디에서 어디로 가는지로 이름을 붙인다.

장면 빛L, RGB (선형) OETF카메라·인코더 신호 R′G′B′8/10/12비트 코드 EOTF디스플레이 화면 빛cd/m² OOTF = EOTF ∘ OETF (렌더링 의도, 보통 국소 지수 ≈ 1.1~1.3) 프라임(′) = 비선형으로 인코딩된 값. R′G′B′ → Y′CbCr(15장), Y′는 "루마"이지 휘도 Y가 아니다. 압축·전송·저장·편집은 대부분 R′G′B′ 영역에서 일어난다 — 그래서 "어느 영역의 숫자인가"를 늘 따져야 한다.
그림 6-2. 영상 체인의 세 전달 함수. 선형 값에는 프라임이 없고(\(R, G, B, Y\)), 인코딩된 값에는 프라임을 붙인다(\(R', G', B', Y'\)). 이 관례는 ITU-R 문서와 Poynton의 교재에서 일관되게 쓰인다.

OOTF가 1이 아닌 이유는 9장(색 외관)의 주제와 맞닿아 있다. 밝은 야외 장면(수천 cd/m²)을 어두운 거실의 100 cd/m² 화면으로 보면, 휘도가 줄고 주변이 어두워져 대비와 채도가 낮아 보인다(Stevens 효과, Bartleson–Breneman 효과). 이를 보상하려고 시스템 전체에 약간의 대비 증폭(국소 지수 1보다 큼)을 넣는다. 전통 HDTV는 BT.709 OETF(유효 지수 약 0.5)와 BT.1886 EOTF(2.4)의 조합으로 중간 톤에서 약 1.2의 시스템 감마를 얻고, BT.2100 HLG는 1,000 cd/m² 디스플레이에서 OOTF 감마 1.2를 명시한다(14장).

공학자 관점: 문서에서 "gamma 2.2"를 만났을 때

세 가지를 확인한다. ① 인코딩 지수인가 디코딩 지수인가(0.4545 = 1/2.2). ② 순수 멱함수인가 선형 구간이 있는 구간 함수인가. ③ OETF인가 EOTF인가. 예컨대 BT.709는 OETF만 정의하고 디스플레이 EOTF는 BT.1886이 따로 정의한다. sRGB는 디스플레이 기준으로 정의된 곡선이다. 이 셋을 섞어 쓰면 어두운 영역에서 수 배의 휘도 오차가 난다(다음 절).

sRGB 전달 함수와 표준 곡선들

sRGB(IEC 61966-2-1:1999)는 PC·웹의 기본 색공간이다. 원색과 백색점(D65)은 BT.709와 같고(5장), 전달 함수는 0 근처에 짧은 직선 구간을 둔 구간 함수(piecewise function)다. 기준 디스플레이 백색 휘도는 80 cd/m², 기준 주변 조도는 64 lx로 정해져 있다.

$$V' = \begin{cases}12.92\,L, & L \le 0.0031308\\ 1.055\,L^{1/2.4} - 0.055, & L > 0.0031308\end{cases} \qquad L = \begin{cases}V'/12.92, & V' \le 0.04045\\ \left(\dfrac{V'+0.055}{1.055}\right)^{2.4}, & V' > 0.04045\end{cases}$$
\(L\)은 선형 값(0~1), \(V'\)은 인코딩 값(0~1, 8비트 코드 = \(255V'\)). 임계값 \(0.04045 = 12.92\times 0.0031308\). 지수 2.4에 오프셋 0.055가 붙어, 전체 곡선은 순수 2.2 멱함수에 가깝게 보인다(최소자승으로 맞추면 약 2.23).

왜 이렇게 복잡한가? 순수 멱함수 \(L^{1/2.2}\)는 0에서 기울기가 무한대라, 0 근처 카메라 잡음이 엄청나게 증폭되고 역함수를 수치적으로 다루기도 어렵다. 직선 구간은 기울기를 유한(12.92)하게 묶는다. 오프셋 0.055는 직선과 곡선이 만나는 점에서 값이 이어지도록 고른 상수다(기울기는 12.92와 약 12.70으로 거의 이어진다).

그 대가로 sRGB 곡선과 순수 2.2 곡선은 어두운 영역에서 크게 다르다. 코드 128에서는 21.59%와 21.95%로 2%도 차이 나지 않지만, 코드 20에서는 1.9배, 코드 10에서는 3.8배, 코드 5에서는 8.7배 차이 난다. sRGB 파일을 순수 2.2로 보정된 디스플레이에 띄우면 그림자 디테일이 눌리고, 그 반대면 그림자가 들뜬다.

SIMULATOR

전달 함수 비교: sRGB · 2.2 · 2.4 · BT.709 · BT.1886 · L*

sRGB순수 γ 2.2γ 2.4 (BT.1886, Lb=0)BT.1886 (Lb 반영, 점선)BT.709 OETFL* (점선)
그래프
신호 V′—
sRGB EOTF 휘도—
순수 2.2 휘도—
BT.1886 휘도—
sRGB / 2.2 비—
국소 지수 sRGB · OOTF—
해볼 것: ① EOTF + 로그 축에서 코드값을 1~30으로 움직이며 sRGB와 2.2의 비를 보자. 코드 10에서 약 3.8배다. ② "국소 지수" \(d\ln L/d\ln V'\)를 보면 sRGB는 직선 구간에서 1, 중간에서 약 2.16, 백색 근처에서 2.27로 변한다 — "sRGB 감마 = 2.2"는 평균적인 말일 뿐이다. ③ OOTF 모드에서 BT.709 OETF → BT.1886의 합성 곡선을 선형 재현(점선)과 비교하고, Lb를 올려 블랙이 들뜨는 모습을 보자.

표준마다 곡선이 조금씩 다르다. 아래 표는 이 책 전체에서 쓰는 SDR 전달 함수를 정리한 것이다. HDR 곡선(PQ, HLG)은 14장에서 다룬다.

곡선문서방향식(요약)직선 구간비고
sRGBIEC 61966-2-1인코딩/디코딩\(1.055L^{1/2.4}-0.055\)\(L\le0.0031308\): ×12.92백색 80 cd/m². Display P3도 같은 곡선
BT.709ITU-R BT.709-6OETF\(1.099L^{0.45}-0.099\)\((Lt 0.018)\): ×4.5카메라용. 디스플레이 곡선 아님
BT.2020ITU-R BT.2020-2OETF\(\alpha L^{0.45}-(\alpha-1)\)\(L<\beta\): ×4.510비트 α=1.099, β=0.018; 12비트 α=1.0993, β=0.0181
BT.1886ITU-R BT.1886EOTF\(a\max(V'+b,0)^{2.4}\)없음블랙 레벨 \(L_b\) 반영, HD 마스터링
Adobe RGBAdobe RGB (1998)디코딩\(V'^{563/256}\)없음지수 2.19921875
DCI-P3 (영화)SMPTE RP 431-2디코딩\(V'^{2.6}\)없음어두운 극장, 48 cd/m²
L*CIE 015지각 척도\(116Y^{1/3}-16\)\(Y\le0.008856\): ×903.3일부 ICC 작업 공간(ECI-RGB v2)의 TRC
정밀 상수와 0.03928

BT.2020의 12비트 상수를 곡선이 값과 기울기 모두 연속이 되도록 풀면 \(\alpha = 1.09929682680944\), \(\beta = 0.018053968510807\)이다. BT.709의 1.099/0.018은 이를 반올림한 값이라 경계에서 아주 작은 불연속(0.0810 대 0.0812)이 있다. 오래된 sRGB 문헌에 나오는 디코딩 임계값 0.03928은 표준 이전 초안의 값이며, IEC 표준은 0.04045를 쓴다. 8비트에서는 코드 10(0.0392)이 두 경우 모두 직선 구간, 코드 11(0.0431)이 모두 곡선 구간이라 결과가 같다. 그러나 16비트나 부동소수점 파이프라인에서는 IEC 값을 써야 왕복 변환 오차가 최소가 된다.

sRGB 디스플레이의 EOTF는 무엇인가?

IEC 61966-2-1은 구간 함수를 RGB 값과 XYZ 사이의 변환으로 정의한다. 그런데 실제 모니터 다수는 순수 2.2 멱함수로 보정되어 출하되고, "sRGB 디스플레이는 구간 함수로 디코딩해야 하는가, 2.2로 디코딩해야 하는가"는 업계에서 지금도 논쟁 중이다. 실무 결론: 색 변환 계산(행렬, 블렌딩, ICC)에서는 표준의 구간 함수를 쓰고, 어두운 영역의 표시 결과를 따질 때는 실제 디스플레이의 EOTF를 측정해서 확인한다(13장 GOG 모델).

BT.709 OETF와 BT.1886 EOTF, 시스템 감마

방송 체인에서는 카메라 곡선과 디스플레이 곡선이 따로 정의된다. BT.709 OETF는 카메라가 받은 정규화된 장면 빛 \(L\)을 신호 \(V'\)로 바꾼다.

$$V' = \begin{cases}4.5\,L, & 0 \le L < 0.018\\ 1.099\,L^{0.45} - 0.099, & 0.018 \le L \le 1\end{cases}$$
18% 회색 카드는 \(V' = 0.409\)이 되고, 8비트 리미티드 레인지(16~235, 15장)로는 코드 약 106이다. 지수 0.45에 오프셋이 더해져 유효 인코딩 지수는 약 0.5다.

BT.1886(2011)은 CRT의 실제 특성을 모델링해 HDTV 마스터링 디스플레이의 EOTF를 정했다. 순수 2.4 멱함수에 디스플레이의 블랙 레벨 \(L_b\)를 반영하는 오프셋을 더한 형태다.

$$L = a\,\max(V'+b,\,0)^{\gamma},\quad \gamma = 2.4,\quad a = \left(L_W^{1/\gamma}-L_B^{1/\gamma}\right)^{\gamma},\quad b = \frac{L_B^{1/\gamma}}{L_W^{1/\gamma}-L_B^{1/\gamma}}$$
\(L_W\), \(L_B\): 화면 백색과 블랙의 휘도(cd/m²). \(V'=0\)이면 \(L=L_B\), \(V'=1\)이면 \(L=L_W\). \(L_B=0\)이면 순수 \(L_W V'^{2.4}\). HDTV 프로그램 평가의 기준 백색은 100 cd/m²(ITU-R BT.2035)다.

두 곡선을 합성한 OOTF의 국소 지수를 계산하면(\(L_W=100\), \(L_B=0\)): 장면 휘도 0.9에서 1.19, 0.5에서 1.23, 18% 회색에서 1.34, 5%에서 1.65다. 즉 OETF와 EOTF가 서로의 역함수가 아니어서, 중간 톤에서 장면 대비가 약 1.2승으로 강조된다. 이것이 흔히 말하는 "시스템 감마 1.2"이며, 어두운 시청 환경(dim surround)에서 화면이 실제 장면처럼 보이도록 하는 의도적인 설계다. 시뮬레이터의 OOTF 모드에서 확인할 수 있다.

공학자 관점: 카메라 영상을 "선형화"할 때

BT.709로 녹화된 영상을 합성·VFX용으로 선형화할 때는 OETF의 역함수를 써야 장면의 빛이 나온다. 이것을 BT.1886(2.4)으로 디코딩하면 디스플레이의 빛, 즉 OOTF가 이미 적용된 "화면에 보이는 빛"이 나온다. 둘은 중간 톤에서 약 1.2승만큼 다르다. 어느 쪽이 맞는지는 목적에 달렸다. 장면 조명을 재현하는 CG 합성이라면 장면 기준(scene-referred), 화면에 보이는 그대로 맞추는 그래픽이라면 디스플레이 기준(display-referred)이다. ACES·OCIO 파이프라인은 이 구분을 명시적으로 다룬다(16장).

선형광에서 해야 하는 연산

빛은 더해진다. 두 광원이 한 점을 비추면 휘도가 더해지고(3장 그라스만 법칙), 픽셀 하나에 두 물체가 반씩 걸치면 센서는 두 빛의 평균을 받는다. 따라서 빛의 물리적 혼합을 흉내 내는 연산은 모두 선형 값에서 해야 한다.

인코딩된 값으로 평균을 내면 무엇이 잘못되는지 가장 간단한 예로 보자. 검정(0)과 흰색(255) 픽셀 두 개를 섞는다.

0 , 255 인코딩 값 평균: (0+255)/2 = 128 디코딩하면 선형 0.216 (빛의 21.6%) L* 53.6 — 실제 평균보다 어둡다 선형 평균: (0+1)/2 = 0.5 → 인코딩 코드 188 (187.5) — 빛의 50% L* 76.1 — 멀리서 본 체커와 같은 밝기 빛 기준 오차: 0.216 / 0.5 = 0.43 → 57% 어둡게 계산됨
그림 6-3. 0과 255를 평균한 128은 빛으로 21.6%에 불과하다. 검정·흰색이 반씩 섞인 영역(1픽셀 체커)을 멀리서 보면 빛의 50%이며, 이는 sRGB 코드 188이다. 이미지 축소·블러·폰트 안티에일리어싱이 인코딩 값에서 수행되면 고대비 경계가 어둡게 변한다.

색이 있으면 문제가 더 두드러진다. 빨강 (255,0,0)과 초록 (0,255,0)을 인코딩 값으로 반반 섞으면 (128,128,0)이고, 휘도는 \(Y = (0.2126+0.7152)\times0.2159 = 0.200\), \(L^*=51.9\)다. 양 끝인 빨강(\(L^*=53.2\)), 초록(\(L^*=87.7\))보다 어둡다. 그라데이션 한가운데 탁하고 어두운 띠가 생기는 이유다. 선형 혼합은 (188,188,0), \(Y=0.464\), \(L^*=73.8\)이다.

SIMULATOR

그라데이션: 감마 공간 혼합 vs 선형광 혼합 vs OKLab

감마(인코딩 값) 혼합선형광 혼합OKLab 혼합
프리셋
감마 혼합—
선형 혼합—
OKLab 혼합—
감마 vs 선형 ΔE00—
해볼 것: ① 빨강–초록에서 t=50%: 감마 혼합의 L*가 양 끝 어느 쪽보다도 낮아지는 "어두운 띠"를 그래프로 확인하자. ② 검정–흰색에서는 선형 혼합의 L*가 처음에 급히 올라간다 — 선형광 혼합은 물리적으로 옳지만 지각적으로 균등한 그라데이션은 아니다. 균등한 램프가 목적이면 OKLab(7장)처럼 지각 공간을 쓴다. ③ 파랑–노랑에서 세 방식의 중간색 차이(ΔE00)가 얼마나 큰지 보자.
목적에 따라 공간을 고른다

"선형광이 항상 옳다"는 말은 반만 맞다. 물리 현상의 시뮬레이션(블러, 알파 커버리지, 조명, 축소, 렌즈 디포커스)은 선형광에서 해야 한다. 사람이 보기에 고른 변화를 원하는 디자인 그라데이션, 컬러맵, 색 보간 애니메이션은 지각 균등 공간(OKLab, CIELAB; 7장·18장)이 더 낫다. 인코딩된 sRGB 값에서의 보간은 어느 쪽에도 맞지 않으며, 그저 계산이 싸고 역사적으로 기본값이었을 뿐이다.

축소·블러에서의 밝기 변화

이미지 축소는 박스·바이큐빅·란초스 같은 필터로 이웃 픽셀을 가중 평균하는 연산이다. 고대비 미세 패턴이 있는 영역에서 인코딩 값을 평균하면, 위의 128 대 188 오류가 그대로 생긴다. 가는 흰 선, 어두운 하늘의 별, 반짝이는 하이라이트, 나뭇잎 사이 하늘처럼 밝은 점이 어두운 배경에 흩어진 영역이 가장 크게 어두워진다. 반대로 흰 바탕의 가는 검은 선(텍스트)은 상대적으로 덜 변한다.

SIMULATOR

이미지 축소: 인코딩 값 평균 vs 선형광 평균

축소 배율 (박스 필터)

영역(왼쪽부터): ① 흑백 1px 세로 줄무늬 ② 빨강/초록 1px 체커 ③ 어두운 하늘의 별(4%) ④ 흰 바탕 검은 격자. 축소 결과는 원본과 같은 크기로 확대해 보여 준다.

① 줄무늬 Y: 원본 → 감마 / 선형—
② R/G 체커 Y—
③ 별 Y—
④ 격자 Y—
해볼 것: 화면에서 조금 떨어져 눈을 가늘게 뜨고 원본 줄과 두 축소 줄의 밝기를 비교하자. 선형 축소 줄이 원본과 같은 밝기로 보이고, 감마 축소 줄은 ①·②·③ 영역이 눈에 띄게 어둡다. 수치는 영역 평균 상대 휘도 Y(선형)이다. 브라우저 확대 배율이 100%가 아니면 원본 줄무늬가 브라우저에 의해 다시 리샘플링되어 비교가 흐려질 수 있다.

박스 필터 1/2 축소에서 ①은 원본 Y=0.500이 감마 평균으로 0.216(−57%)이 되고, ②의 빨강/초록 체커는 0.464가 0.200이 된다. 선형 평균은 모든 영역에서 원본 평균 휘도를 정확히 보존한다. 선형 박스 필터는 정의상 영역의 빛 총량을 보존하는 연산이기 때문이다. 바이큐빅·란초스처럼 음의 로브가 있는 필터는 선형광에서 쓰면 고대비 경계 주변에 링잉이 더 눈에 띄는 경향이 있어, 실무에서는 선형광 + 클램핑, 또는 시그모이드 공간에서 처리하는 절충을 쓰기도 한다.

흔한 실수: 썸네일과 밉맵

많은 이미지 라이브러리와 브라우저의 이미지 축소는 기본적으로 인코딩된 값을 그대로 평균한다. ImageMagick에서는 -colorspace RGB -resize 25% -colorspace sRGB처럼 선형 공간으로 변환한 뒤 축소하는 방법이 문서화되어 있다. GPU 밉맵도 sRGB 텍스처 포맷으로 선언해야 하드웨어가 선형광에서 필터링한다(10절). 밉맵이 감마 공간에서 만들어지면 멀리 있는 고대비 텍스처(울타리, 잔디 하이라이트)가 거리에 따라 어두워진다.

양자화: 비트 깊이와 Weber 비율

\(n\)비트 인코딩은 \(N = 2^n - 1\)개의 단계를 갖는다. 코드 \(k\)의 디코딩 휘도를 \(L_k = \mathrm{EOTF}(k/N)\)라 하면, 코드 하나가 바뀔 때의 상대 휘도 변화, 즉 Weber 비율은 다음과 같다.

$$\frac{\Delta L}{L}\bigg|_k = \frac{L_{k+1}-L_k}{L_k} \approx \frac{1}{N}\,\frac{d\ln L}{dV'} = \frac{\gamma_\text{local}}{N\,V'}$$
선형 인코딩(\(L=V'\))이면 \(\Delta L/L = 1/k\): 코드 10에서 10%, 코드 100에서 1%. 감마 인코딩은 \(\gamma/(NV')\)로, 어두운 쪽으로 갈수록 커지긴 하지만 훨씬 완만하다.

이 비율이 시각의 대비 감도 임계(중간~밝은 휘도에서 대략 1% 부근)보다 크면 매끄러운 그라데이션에 계단이 보인다. 시각의 임계 자체도 휘도에 따라 변한다. 어두울수록 임계가 커지는데(드브리스–로즈 영역), 이를 정량화한 Barten 대비 감도 모델과 그 위에 만든 PQ 곡선은 14장에서 다룬다. 여기서는 1%를 기준선으로 삼는다.

각 눈금 = 4비트 코드 하나 (16단계). 가로축은 지각 명도 L* 0 → 100 선형 4비트 sRGB 4비트 L* 4비트 빈 구간 L* 0→31 050100 (L*)
그림 6-4. 같은 16단계를 지각 명도 축에 찍으면 차이가 분명하다. 선형 인코딩은 첫 단계가 L* 0에서 31로 건너뛰고 밝은 쪽은 촘촘하다. sRGB는 거의 균등하며(직선 구간 탓에 첫 간격만 조금 좁다), L* 인코딩은 정의상 완전히 균등하다.
SIMULATOR

비트 깊이별 Weber 비율 ΔL/L과 밴딩 램프

선형sRGBγ 2.2L*
램프 인코딩
램프 범위 (백색 대비)
램프 안 서로 다른 코드 수—
ΔL/L > 1%인 구간—
첫 비영 코드 휘도—
18% 회색에서 ΔL/L—
해볼 것: ① 8비트·선형·0–5% 램프: 백색의 5%까지를 겨우 13단계로 그리므로 굵은 계단이 보인다. sRGB로 바꾸면 같은 범위에 64단계가 들어간다. ② 비트를 10, 12로 올리며 선형 곡선이 1% 선 아래로 내려오는 지점을 보자. 선형으로 sRGB 8비트의 어둠부 분해능을 맞추려면 약 12비트가 필요하다. ③ 램프는 최종적으로 8비트 sRGB로 화면에 표시되므로, 9비트 이상 차이는 이 램프로 볼 수 없다(그래프로 판단).

계산 결과를 표로 정리하면 다음과 같다(해당 휘도에 가장 가까운 코드에서의 \(\Delta L/L\)).

비트인코딩Y = 1%Y = 10%Y = 50%첫 비영 코드 (백색 대비)
8선형33.3%3.85%0.78%0.39%
sRGB6.26%2.35%1.19%0.030%
γ 2.27.23%2.46%1.19%0.0005%
L*4.78%2.21%1.28%0.043%
10선형10.0%0.98%0.20%0.098%
sRGB1.52%0.58%0.30%0.0076%
12선형2.44%0.24%0.05%0.024%
sRGB0.38%0.15%0.07%0.0019%

몇 가지를 읽어 낼 수 있다. 첫째, 선형 8비트는 어둠부에서 처참하다. 백색 80 cd/m²에서 첫 비영 코드가 0.31 cd/m²이고, 그 아래는 전부 검정이다. 코드 1~99(백색의 39% 이하, 약 31 cd/m² 이하)가 모두 1%를 넘는다. 둘째, sRGB 8비트도 거의 전 구간에서 1%를 약간 넘는다. 그래서 하늘이나 비네팅 같은 아주 매끄러운 그라데이션에서 8비트 밴딩이 "겨우 보이는" 경계에 있고, 실무에서 디더링이나 10비트 파이프라인을 쓴다. 셋째, 순수 2.2는 0 근처 코드가 지나치게 촘촘해(코드 1 = 백색의 0.0005%) 어떤 디스플레이로도 재현되지 않는 단계에 코드를 낭비한다. 이 점에서 sRGB의 직선 구간은 합리적이다.

공학자 관점: 영상 파이프라인의 비트 예산

카메라 RAW는 선형 12~14비트(ADC)이고, ISP가 감마/로그 곡선을 적용한 뒤 8~10비트로 줄인다. 같은 이유로 렌더링 엔진의 중간 버퍼는 선형 16비트 부동소수점(half float, 지수부 덕분에 상대 정밀도가 거의 일정)을 쓰고, 출력 직전에만 sRGB 8비트로 인코딩한다. "선형으로 저장하려면 부동소수점, 정수로 저장하려면 지각 곡선"이 원칙이다. 리미티드 레인지(16~235)는 8비트에서 실제 단계를 219개로 줄인다는 점도 기억하자(15장).

디더링으로 밴딩 지우기

비트 수를 늘릴 수 없을 때는 양자화 오차를 공간에 퍼뜨린다. 디더링(dithering)은 양자화 직전에 신호에 작은 변동을 더하거나(임계값 변조), 앞 픽셀의 오차를 이웃에 넘겨서(오차 확산), 국소 평균이 원래 값과 같아지게 한다. 눈은 공간적으로 적분하므로, 고주파 잡음 한 겹을 대가로 계단(저주파의 구조적 오차)이 사라진다.

Bayer 4×4 (임계값 = (값+0.5)/16) 08210 124146 31119 157135 재귀: M₂ₙ = [4Mₙ, 4Mₙ+2; 4Mₙ+3, 4Mₙ+1] Floyd–Steinberg 오차 확산 처리됨현재 e 7/16 3/165/161/16 e = (값 + 받은 오차) − 양자화 레벨 가중치 합 = 1 → 오차가 사라지지 않고 이웃으로 옮겨져 국소 평균이 보존된다 스캔: 왼→오, 위→아래 (지그재그 스캔이면 방향성 감소)
그림 6-5. 왼쪽: 4×4 Bayer 행렬. 인접한 임계값이 공간적으로 최대한 떨어지게 배치되어 패턴이 고주파가 된다. 오른쪽: Floyd–Steinberg 가중치. 현재 픽셀의 양자화 오차를 오른쪽과 아래 세 픽셀에 나눈다.

한 가지 미묘한 점이 있다. 디더링이 보존하는 "국소 평균"은 어느 영역의 평균인가? 인코딩 값에서 디더링하면 인코딩 값의 평균이 보존되고, 눈이 실제로 적분하는 것은 빛이므로 밝기가 틀어진다. 극단적으로 1비트(흑/백)로 인코딩 값 0.5(코드 128) 회색을 디더링하면 흰 픽셀이 50%가 되어, 멀리서 보면 빛의 50%, 즉 코드 188처럼 밝게 보인다. 정확한 톤 재현이 필요하면 레벨 판단과 오차 계산을 선형광에서 해야 한다. 비트 수가 6 이상이면 두 방식의 차이는 작지만, 1~3비트(전자종이, 프린터 하프톤, 저가 LCD 패널의 6비트+FRC)에서는 중요하다.

SIMULATOR

비트 수를 줄인 그라데이션과 디더링 비교

방법
원본
채널당 레벨 수—
RMS 오차 (8비트 코드)—
멀리서 본 톤 오차 (평균 |ΔL*|)—
처리 시간—
해볼 것: ① 3비트, "없음"에서 8개의 굵은 띠가 보인다. F–S로 바꾸면 띠가 사라지고 RMS 오차는 오히려 커진다 — 디더링은 오차를 줄이는 것이 아니라 보이지 않는 주파수로 옮기는 것이다. ② 1비트·회색 램프에서 셋째 줄(선형광에서 흐린 "멀리서 본 모습")의 톤 오차를 보고, "선형광에서" 옵션을 켜서 비교하자. ③ Bayer와 랜덤의 질감 차이를 픽셀 확대 3~4배로 관찰하자.
공학자 관점: 디더는 마지막 양자화 바로 앞에서 한 번

디더 잡음의 진폭은 최종 양자화 한 단계(1 LSB) 수준이면 충분하다. 고비트 렌더 버퍼(부동소수점)를 8비트로 출력할 때 셰이더 마지막에 ±0.5 LSB의 청색 잡음 텍스처나 삼각 분포 잡음을 더하는 것이 게임 엔진의 표준 기법이다. 디더한 결과를 다시 압축(JPEG, H.264)하면 고주파 잡음이 양자화로 지워지거나 비트레이트를 잡아먹으므로, 디더는 인코더 앞단이 아니라 디스플레이 앞단에서 하는 것이 원칙이다.

내 화면의 감마 확인하기

디더 원리를 거꾸로 쓰면 디스플레이의 EOTF를 눈으로 잴 수 있다. 검정과 흰색이 1픽셀씩 번갈아 놓인 패턴은 디스플레이의 EOTF와 무관하게 백색 휘도의 정확히 50%를 낸다(검정 픽셀의 빛 0과 흰 픽셀의 빛 \(L_W\)의 평균). 그 옆에 균일한 회색 코드 \(c\)를 놓고 같은 밝기가 될 때까지 조절하면 \( (c/255)^{\gamma} = 0.5 \)에서 \(\gamma = \ln 0.5 / \ln(c/255)\)를 얻는다. sRGB 디스플레이라면 \(c \approx 188\), 순수 2.2면 186, 2.4면 191이다.

SIMULATOR

체커보드 감마 확인 패턴

패턴
밝은 픽셀 비율
밝은 픽셀 코드
패턴의 상대 휘도—
예측 코드 sRGB · 2.2 · 2.4—
맞춘 값에서 추정 감마—
해볼 것: 화면에서 1~2 m 떨어지거나 눈을 가늘게 뜨고, 가운데 사각형이 주변 패턴에 녹아들 때까지 슬라이더를 움직이자. 초기값 128은 확연히 어둡다 — "128이 중간 회색"이라는 직관이 틀렸다는 증거다. 밝은 픽셀 코드를 128로 바꾸면 중간 이하 톤의 감마를 따로 잴 수 있다. 주의: 브라우저 확대 배율과 OS 배율이 정수배가 아니면(예: 125%) 패턴이 리샘플링되어(그것도 대개 감마 공간에서!) 결과가 틀어진다. 가로줄 패턴이 체커보다 LCD 응답·크로스토크에 덜 민감하다.

이 방법은 디스플레이가 픽셀 사이에 간섭이 없고(이웃 픽셀의 구동이 서로 영향을 주지 않음) 휘도가 공간적으로 가산적이라는 가정에 기댄다. OLED는 대체로 잘 맞고, 일부 LCD는 1픽셀 체커에서 화소 간 크로스토크나 극성 반전(인버전) 패턴과 간섭해 오차가 생긴다. 정밀한 특성화는 측색기로 램프를 측정해 GOG 모델을 맞추는 방법을 쓴다(13장).

실무 함정: API의 기본값은 감마 공간

대부분의 그래픽·이미지 API는 역사적 이유로 숫자를 있는 그대로, 즉 인코딩된 값으로 다룬다. 선형광 처리를 원하면 명시적으로 요청해야 한다.

환경기본 동작선형광으로 처리하려면
CSS 그라데이션·color-mix()레거시 색(#hex, rgb()) 그라데이션은 인코딩 sRGB 값에서 보간CSS Color 4 보간 공간 지정: linear-gradient(in srgb-linear, …), in oklab; color-mix(in srgb-linear, red, lime)
Canvas 2DglobalAlpha 합성, drawImage 축소가 인코딩 값에서 수행되는 것이 일반적getImageData로 직접 디코딩→연산→인코딩
WebGL / OpenGL ESRGBA8 텍스처는 샘플링해도 디코딩하지 않음. 기본 드로잉 버퍼는 sRGB 인코딩을 하지 않음텍스처를 SRGB8_ALPHA8로 업로드(샘플링·필터링이 선형), 셰이더 끝에서 sRGB 인코딩 또는 sRGB 렌더 타깃 사용
Vulkan / D3D / Metal포맷이 결정: *_UNORM은 그대로, *_SRGB는 읽을 때 디코딩·쓸 때 인코딩색 텍스처·스왑체인은 _SRGB, 노멀맵·러프니스 등 데이터 텍스처는 _UNORM
이미지 라이브러리 (Pillow, OpenCV 등)리사이즈·블러·블렌딩이 저장된 값 그대로float로 변환 후 디코딩→처리→인코딩, 또는 선형 공간 옵션 사용
PNGgAMA 청크: 파일 인코딩 지수 × 100000 (예: 45455 = 1/2.2). sRGB 청크가 있으면 sRGB 곡선이 우선색 관리 디코더는 iCCP/sRGB/gAMA 순으로 해석. 청크가 없으면 sRGB로 가정
텍스처 (sRGB)SRGB8_ALPHA8 샘플러 디코딩필터링도 선형 셰이딩 (선형)조명·톤 매핑 블렌딩 (선형)sRGB 렌더 타깃 인코딩 출력+ 디더 각 경계에서 한 번씩만 변환한다. 두 번 디코딩(너무 어둡고 대비 강함)이나 디코딩 누락(밝고 물 빠진 그림자, 조명 경계가 딱딱함)이 가장 흔한 버그다.
그림 6-6. 선형 워크플로의 GPU 파이프라인. 색 텍스처만 sRGB로 선언하고, 노멀맵·마스크 같은 데이터 텍스처는 디코딩하면 안 된다.
흔한 실수 모음

① 감마 인코딩된 RGB를 평균해 "평균 색"이나 "평균 밝기"를 계산한다(이 장의 핵심 오류). ② 셰이더에서 sRGB 텍스처를 하드웨어가 이미 디코딩했는데 pow(c, 2.2)를 한 번 더 한다. ③ 데이터 텍스처(노멀맵)를 sRGB 포맷으로 올린다. ④ \(Y = 0.2126R'+0.7152G'+0.0722B'\)처럼 인코딩 값에 휘도 계수를 곱하고 "휘도"라 부른다 — 그것은 루마 \(Y'\)이며 휘도가 아니다(15장). ⑤ 알파를 곱해 둔(premultiplied) 색을 다시 인코딩한 뒤 합성한다 — 알파 곱도 선형 값에서 해야 일관된다. ⑥ 0.5 회색을 sRGB 코드 128로 만든다 — 빛의 50%는 188이다.

WCAG 대비비 계산(18장)과 CB.relLum은 정확히 이 원칙을 따른다. 8비트 값을 먼저 sRGB 디코딩한 뒤 0.2126/0.7152/0.0722를 곱해 상대 휘도를 얻는다. 이 책의 모든 색 계산 헬퍼도 내부적으로 선형 값에서 행렬 연산을 하고, 화면에 칠할 때만 인코딩한다.

핵심 정리

  1. 밝기 지각은 휘도에 대해 오목하다(Weber 비율 약 1%, Stevens 지수 약 1/3, \(L^* = 116Y^{1/3}-16\)). 비선형 인코딩은 제한된 코드를 지각적으로 고르게 배분하는 압축이다.
  2. OETF는 장면 빛→신호, EOTF는 신호→화면 빛, OOTF는 그 합성이다. 프라임(′)은 인코딩된 값을 뜻한다. 전통 HDTV의 OOTF는 중간 톤에서 국소 지수 약 1.2다.
  3. sRGB: 직선 구간 12.92 (\(L\le0.0031308\), \(V'\le0.04045\)), 곡선 \(1.055L^{1/2.4}-0.055\). 순수 2.2와는 중간 톤에서 2% 이내지만 코드 10에서 3.8배 다르다.
  4. BT.709 OETF: \(4.5L\) / \(1.099L^{0.45}-0.099\) (경계 0.018). BT.1886 EOTF: \(a\max(V'+b,0)^{2.4}\)로 블랙 레벨을 반영한다. BT.2020 12비트 상수는 1.0993/0.0181.
  5. 블렌딩·알파 합성·축소·블러·조명·평균은 선형광에서 한다. 0과 255의 평균은 128(빛 21.6%)이 아니라 188(빛 50%)이며, 빨강–초록 감마 혼합의 중간은 빨강보다 어둡다(L* 51.9).
  6. 코드당 \(\Delta L/L\): 선형 8비트는 백색의 39% 이하에서 1%를 넘고, 첫 단계가 백색의 0.39%다. sRGB 8비트는 전 구간에서 약 1~6%로 경계선에 있으며, 선형으로 같은 어둠부 분해능을 얻으려면 약 12비트가 필요하다.
  7. 디더링은 오차를 고주파로 옮겨 밴딩을 없앤다. 랜덤·Bayer·Floyd–Steinberg(7/16, 3/16, 5/16, 1/16) 순으로 품질이 좋아지며, 저비트에서 정확한 톤을 원하면 선형광에서 디더링한다.

확인 퀴즈

8비트 영상에 감마(비선형) 인코딩을 쓰는 가장 근본적인 이유는?

시각의 Weber 비율은 대략 일정하므로 어두운 곳일수록 작은 절대 단계가 필요하다. 감마 곡선은 어둠부에 코드를 더 주어 모든 밝기에서 단계를 비슷한 지각 크기로 만든다. CRT 호환은 역사적 계기였을 뿐이다.

sRGB 코드 128(회색)이 내는 빛은 백색의 약 몇 %인가?

\(((128/255+0.055)/1.055)^{2.4} = 0.2159\). 빛의 50%는 sRGB 인코딩 0.7354, 즉 코드 188이다.

sRGB 구간 함수와 순수 γ 2.2 멱함수의 차이가 가장 큰 곳은?

sRGB는 0 근처에 기울기 12.92의 직선 구간이 있어 어둠부 디코딩 값이 2.2보다 크다. 코드 10에서 3.8배, 코드 5에서 8.7배 차이 나며, 코드 128에서는 2% 이내다.

sRGB 빨강 (255,0,0)과 초록 (0,255,0)을 저장된 값 그대로 50:50 섞은 (128,128,0)에 대해 옳은 것은?

코드 128의 선형 값은 0.2159이므로 \(Y = 0.2159\times(0.2126+0.7152)=0.200\). 선형 혼합 (0.5, 0.5, 0)의 \(Y=0.464\)(코드 188,188,0)와 크게 다르다. 그라데이션 중간의 어두운 띠가 이것이다.

BT.709 OETF와 BT.1886 EOTF를 합성한 OOTF가 1이 아닌(중간 톤 국소 지수 약 1.2) 이유로 가장 적절한 것은?

장면보다 훨씬 어두운 화면을 어두운 주변에서 보면 대비·채도가 낮아 보인다(Stevens, Bartleson–Breneman 효과). 시스템 감마 1.2 정도의 대비 증폭으로 이를 보상한다. BT.2100 HLG도 1,000 cd/m²에서 OOTF 감마 1.2를 쓴다.

선형 8비트 인코딩에서 코드 한 단계의 \(\Delta L/L\)이 1%를 넘는 범위는?

선형에서 \(\Delta L/L = 1/k\)이므로 \(k < 100\)이면 1%를 넘는다. 코드 99는 백색의 38.8%다. 첫 비영 코드도 백색의 0.39%로, 80 cd/m² 화면에서 0.31 cd/m² 아래는 모두 검정이 된다.

디더링에 대한 설명으로 옳은 것은?

디더는 국소 평균을 맞추는 대신 픽셀 단위 잡음을 더한다. 눈의 공간 적분(저역 통과) 때문에 고주파 잡음은 덜 보이고 구조적인 띠는 사라진다. F–S는 아직 처리하지 않은 오른쪽·아래 이웃에 7/16, 3/16, 5/16, 1/16로 나눈다. Bayer는 고정 임계 행렬이다.

1픽셀 흑/백 체커보드와 같은 밝기로 보이는 균일 회색이 코드 186이었다. 이 디스플레이의 중간 톤 감마로 가장 가까운 것은?

체커는 EOTF와 무관하게 백색의 50% 빛을 낸다. \((186/255)^\gamma = 0.5\)에서 \(\gamma = \ln0.5/\ln(0.7294) = 2.2\). sRGB 구간 함수라면 188, 2.4라면 191이다.

WebGL2에서 색 텍스처를 SRGB8_ALPHA8 포맷으로 업로드했다. 셰이더에서 올바른 처리는?

sRGB 텍스처 포맷은 하드웨어가 디코딩하므로 다시 pow를 하면 이중 디코딩이 된다. 노멀맵 같은 데이터는 UNORM이어야 하며, 기본 드로잉 버퍼에 쓸 때는 셰이더에서 sRGB 인코딩을 해야 한다(또는 sRGB 렌더 타깃 사용).

1비트(흑/백)로 인코딩 값 0.5(코드 128) 회색 영역을 인코딩 값 기준 오차 확산으로 디더링했다. 멀리서 보면?

인코딩 값 평균을 보존하는 디더는 흰 픽셀을 50% 배치한다. 눈은 빛을 적분하므로 결과는 빛 50%(코드 188 상당)다. 정확한 톤을 원하면 선형광에서 디더링해 흰 픽셀을 21.6%로 만들어야 한다.