CH 02 컴퓨터 비전 입문 · Part 1 이미지라는 데이터
밝기와 대비: 히스토그램이 말해 주는 것
어두운 사진을 밝게 고치면 정보가 늘어날까? 이 장에서는 그렇게 예상하고 실험을 짰다가 정반대 결과를 받았다. 감마보정·히스토그램 평활화·CLAHE를 걸고 엔트로피와 지역 대비를 동시에 재면, 사진 보정이 정확히 무엇을 주고 무엇을 뺏는지가 드러난다.
- 01 직관
- 02 수식 읽는 법
- 03 손으로 풀기
- 04 코드
- 05 시각화
약어 및 기호 정의
- 히스토그램
- histogram — 각 밝기 값의 픽셀 개수를 센 막대그래프. 0~255면 막대 256개
- CDF
- Cumulative Distribution Function, 누적분포함수 — 히스토그램을 왼쪽부터 누적한 것
- 감마보정
- gamma correction — x' = 255·(x/255)γ. γ<1이면 밝아지고 γ>1이면 어두워진다
- 히스토그램 평활화
- histogram equalization — CDF를 그대로 사상으로 써서 밝기를 고르게 퍼뜨리는 것
- CLAHE
- Contrast Limited Adaptive Histogram Equalization — 사진을 작은 타일로 나눠 타일마다 평활화하고 과보정을 제한
- 엔트로피
- entropy — H = −Σ pi log2 pi. 픽셀 하나가 담은 정보량(비트). 8비트 사진의 최댓값은 8
- 지역 대비
- local contrast — 8×8 조각마다 표준편차를 구해 평균 낸 값. "가까이서 볼 때의 선명함"
- 점 사상
- point mapping — 픽셀 값 하나만 보고 새 값을 정하는 변환. 위치를 보지 않는다
직관: 히스토그램은 사진의 요약이다
1장에서 사진이 숫자 격자임을 봤다. 그 262,144개의 숫자를 위치를 버리고 값별로 세기만 한 것이 히스토그램이다.
위치를 버렸으니 잃는 것이 많다. 히스토그램만 봐서는 무엇이 찍힌 사진인지 알 수 없다. 그래도 유용한 이유는, 밝기와 대비에 관한 질문은 위치와 무관하기 때문이다.
히스토그램 모양이 말해 주는 것
왼쪽에 몰려 있다 → 어두운 사진. 밝은 쪽 칸들이 놀고 있다.
오른쪽에 몰려 있다 → 과노출.
가운데 좁게 뭉쳐 있다 → 대비가 낮다. 흐리멍덩해 보인다.
양 끝에 벽처럼 서 있다 → 클리핑. 이미 잘려 나간 값이 있다 — 되돌릴 수 없다.
사진가가 카메라 뒷면에서 보는 그 히스토그램이 정확히 이것이다.
1-2. 이 장의 질문
어두운 사진이 있다. 밝게 고친다. 사진이 좋아졌다고 말할 수 있나?
"좋아졌다"를 정의해야 답할 수 있다. 두 가지 후보를 놓고 둘 다 재기로 했다.
① 정보량 — 픽셀 하나가 담은 비트 수(엔트로피). 클수록 사진이 256칸을 고르게 쓴다.
② 지역 대비 — 작은 조각 안에서 값이 얼마나 흔들리는가. 클수록 가까이서 볼 때 선명하다.
내 예상은 "둘 다 오른다"였다. 밝게 펴면 값이 넓게 퍼지니 정보량도 늘 것 같았다. 4절에서 이 예상이 어떻게 됐는지 나온다.
수식 읽는 법
2-1. 감마보정
x' = 255 · ( x / 255 )γ
읽는 법 — 값을 0~1로 정규화하고, γ제곱하고, 다시 0~255로 되돌린다. 0~1 사이 수를 제곱하면 작아지고(어두워지고), 제곱근을 씌우면 커진다(밝아진다). 그래서 γ<1이 밝게, γ>1이 어둡게다.
왜 곱셈이 아니라 거듭제곱인가? 사람 눈이 밝기를 비율로 느끼기 때문이다. 10에서 20으로 가는 변화와 200에서 210으로 가는 변화는, 같은 +10인데도 전자가 훨씬 크게 느껴진다. 거듭제곱은 어두운 쪽을 크게 늘이고 밝은 쪽을 적게 늘인다.
2-2. 히스토그램 평활화
f(v) = round( (CDF(v) − CDFmin) / (N − CDFmin) × 255 )
읽는 법 — CDF(v)는 "값이 v 이하인 픽셀 수"다. N은 전체 픽셀 수. 분수 부분은 항상 0~1이고, 여기에 255를 곱해 새 값을 만든다.
왜 CDF인가? 픽셀이 많이 몰린 구간에서 CDF가 가파르게 오르기 때문이다. 가파른 곳은 넓게 벌어지고, 평평한 곳(픽셀이 없는 구간)은 좁게 눌린다. 결과적으로 히스토그램이 고르게 펴진다.
여기서 중요한 성질 하나
f는 값 하나를 받아 값 하나를 내놓는 함수다(점 사상). 픽셀의 위치를 보지 않는다.
그리고 f는 단조증가다 — CDF가 감소하지 않으니까. 즉 원래 밝던 픽셀은 여전히 밝다. 순서는 지켜진다.
그런데 round(·)가 붙어 있다. 서로 다른 v1 ≠ v2가 같은 값으로 반올림될 수 있다. 한 번 합쳐지면 다시 갈라놓을 수 없다.
이 한 문장이 4절 결과를 전부 설명한다. 256칸에서 256칸으로 가는 점 사상은 칸을 합칠 수는 있어도 쪼갤 수 없다. 그러므로 엔트로피는 절대 늘어날 수 없다. 이건 실험 결과가 아니라 정리다.
2-3. 엔트로피
H = − Σi=0255 pi · log2 pi
읽는 법 — pi는 밝기 i인 픽셀의 비율이다. 256칸을 완전히 고르게 쓰면 pi = 1/256이라 H = 8비트, 전부 한 값이면 H = 0이다.
"픽셀 하나를 전달하려면 평균 몇 비트가 필요한가"로 읽으면 정확하다.
손으로 풀기: 픽셀 12개짜리 사진
손풀이 A — 평활화를 끝까지 계산해 본다
0~7의 8단계를 쓰는 아주 작은 사진을 만든다.
원본 [2, 3, 3, 4, 4, 4, 5, 7, 7, 7, 7, 7] — 12픽셀
히스토그램 (값 0~7의 개수) [0, 0, 1, 2, 3, 1, 0, 5]
CDF (왼쪽부터 누적) [0, 0, 1, 3, 6, 7, 7, 12]
이제 식에 넣는다. CDFmin = 1(0이 아닌 최솟값), N = 12, 최대 단계 7.
| 원본 v | CDF(v) | 계산 | 결과 f(v) |
|---|---|---|---|
| 2 | 1 | (1−1)/(12−1)×7 = 0.00 | 0 |
| 3 | 3 | (3−1)/11×7 = 1.27 | 1 |
| 4 | 6 | (6−1)/11×7 = 3.18 | 3 |
| 5 | 7 | (7−1)/11×7 = 3.82 | 4 |
| 7 | 12 | (12−1)/11×7 = 7.00 | 7 |
결과 [0, 1, 1, 3, 3, 3, 4, 7, 7, 7, 7, 7]
원본은 2~7에 몰려 있었는데 0~7 전 범위로 퍼졌다. "대비가 커졌다"는 말이 정확히 이 뜻이다.
손풀이 B — 그런데 무엇이 늘었나
서로 다른 값의 개수를 세어 보자. 원본 5개(2,3,4,5,7), 결과도 5개(0,1,3,4,7). 이 예에서는 합쳐지지 않았다.
그렇다면 엔트로피는? 각 값의 개수가 [1,2,3,1,5]로 그대로다. 이름표만 바뀌었을 뿐이다. 엔트로피는 개수 분포로만 정해지므로 정확히 같다.
여기서 핵심이 보인다. 점 사상은 기껏해야 이름표를 바꾼다. 운이 좋으면 엔트로피가 유지되고, 반올림으로 두 값이 합쳐지면 줄어든다. 늘어나는 경우는 없다.
4절의 실제 사진에서는 164개 값이 106개로 합쳐진다. 256칸을 놓고 픽셀 26만 개를 굴리면 반올림 충돌이 반드시 일어나기 때문이다.
손풀이 C — 4비트로 저장한 사진의 엔트로피 상한
1장에서 4비트 양자화의 RMSE를 쟀다. 정보량 쪽에서 보면 어떤가?
4비트면 칸이 16개뿐이니 H ≤ log216 = 4비트다. 8비트 원본의 7.2317비트에서 절반 아래로 떨어진다.
그런데 1장에서 봤듯 보기에는 멀쩡했다. RMSE 4.56이었다. 정보량과 보기 좋음은 다른 축이라는 이 장의 결론이 1장에도 이미 있었던 셈이다.
코드: 어두운 사진을 네 가지로 고쳐 본다
camera를 감마 2.2로 어둡게 만들어 입력으로 삼고, 네 가지 보정을 걸어 두 지표를 동시에 잰다.
def ent(a): # 엔트로피 (비트/픽셀)
h = np.bincount(a.ravel(), minlength=256).astype(float)
p = h / h.sum(); p = p[p > 0] # 0인 칸은 log가 정의 안 됨
return -(p * np.log2(p)).sum()
def local_contrast(a, k=8): # 8×8 조각마다 표준편차 → 평균
h, w = a.shape[0]//k*k, a.shape[1]//k*k
b = a[:h, :w].reshape(h//k, k, w//k, k)
return b.std((1, 3)).mean()
dark = np.clip((cam/255.0)**2.2 * 255 * 0.75, 0, 255) # 어두운 입력을 만든다
gam = ((dark/255.0)**(1/2.2)) * 255 # 감마보정
eq = exposure.equalize_hist(dark) * 255 # 전역 평활화
st = exposure.rescale_intensity(dark, in_range=(p2, p98)) # 2~98% 스트레칭
cl = exposure.equalize_adapthist(dark, clip_limit=0.02)*255 # CLAHE (타일별)
4-1. 예상이 틀렸다 — 어떤 보정도 정보를 늘리지 못했다
"밝게 고치면 정보가 는다"는 모든 경우에 반증됐다
어두운 입력의 엔트로피가 6.1182비트다. 네 가지 보정 결과는 이렇다.
| 방법 | 엔트로피 | 입력 대비 | 고유값 수 |
|---|---|---|---|
| 어둡게(입력) | 6.1182 | — | 164 |
| 감마보정 | 5.9843 | −0.134 | 148 |
| 히스토그램 평활화 | 5.9658 | −0.152 | 106 |
| 2–98% 스트레칭 | 6.0211 | −0.097 | 130 |
| CLAHE | 6.9531 | +0.835 | 256 |
앞의 셋은 전부 줄었다. 감마 스윕에서도 엔트로피가 가장 큰 감마는 1.0, 즉 아무것도 안 하기였다.
2절에서 이미 답을 봤다. 256칸에서 256칸으로 가는 점 사상은 칸을 합칠 수는 있어도 쪼갤 수 없다. 고유값 개수가 164 → 148 → 130 → 106으로 줄어드는 것이 그 합쳐짐이다. 반올림으로 두 값이 한 칸에 들어가면 끝이다.
이것은 우연한 실험 결과가 아니라 증명 가능한 정리다. 어떤 전역 곡선을 쓰든 엔트로피는 입력을 넘지 못한다.
CLAHE만 예외인 이유 — 그것은 점 사상이 아니다
CLAHE는 6.9531로 올랐고, 고유값도 256개를 전부 되찾았다. 정리를 어긴 것인가?
아니다. CLAHE는 점 사상이 아니기 때문이다. 사진을 타일로 나눠 타일마다 다른 곡선을 쓴다. 그래서 같은 입력값 100이라도 어두운 구석에 있으면 140으로, 밝은 창가에 있으면 60으로 갈 수 있다.
즉 CLAHE는 값만 보는 것이 아니라 위치도 본다. 위치라는 추가 정보를 끌어와 값에 실었으니 칸이 쪼개진다.
공짜는 아니다. 그 대가로 절대 밝기가 사진 전체에서 일관되지 않게 된다. 의료 영상처럼 절대값이 의미를 갖는 경우엔 위험하다.
4-2. 그러면 보정은 왜 하나 — 지역 대비를 보면 답이 나온다
엔트로피는 줄었는데 지역 대비는 8.61 → 15.37로 올랐다
히스토그램 평활화는 정보를 잃었지만 지역 대비를 78% 올렸다. CLAHE는 22.92로 원본(11.49)의 두 배다.
사람이 "선명해졌다"고 느끼는 것은 엔트로피가 아니라 이쪽이다. 눈은 절대 밝기보다 가까이 붙은 두 점의 차이에 훨씬 민감하다.
그러니 이 장의 답은 이렇게 정리된다 — 보정은 정보를 만들지 않는다. 이미 있던 정보를 사람이 볼 수 있는 곳으로 옮긴다.
어두운 사진의 그림자 속 디테일은 값 10~30 사이에 눌려 있었다. 정보로는 거기 있었지만 사람 눈에는 전부 검정이었다. 보정은 그 구간을 넓게 펴서 보이게 만든다. 대신 밝은 쪽을 눌러 거기를 잃는다.
4-3. 되돌아가는 변환과 못 돌아가는 변환
평활화의 복원 RMSE 74.80 — 원본과 완전히 다른 사진이다
감마 1/2.2를 걸었다가 감마 2.2로 되돌리면 RMSE 1.10이다. 밝기를 1.5배 했다 나누면 1.31. 둘 다 거의 완전히 돌아온다 — 남은 오차는 0~255 밖으로 잘려 나간 클리핑뿐이다.
평활화를 두 번 걸면 RMSE 74.80이다. 되돌아가기는커녕 다른 사진이 된다.
실무 규칙 하나가 여기서 나온다 — 원본은 반드시 남겨 둔다. 감마·밝기는 순서를 바꿔도 되지만, 평활화는 파이프라인의 마지막에 한 번만 건다.
시각화





이것만 기억하자
- 히스토그램은 위치를 버리고 값만 센 것이다. 밝기·대비 질문에는 충분하고, 그 밖에는 쓸모없다.
- 전역 보정은 정보를 늘릴 수 없다. 실험이 아니라 정리다 — 256칸 → 256칸 점 사상은 칸을 합칠 뿐 쪼개지 못한다. 감마 스윕에서 최적 감마가 1.0(=아무것도 안 하기)으로 나온 것이 그 증거다.
- 그래도 보정을 하는 이유는 지역 대비 때문이다. 평활화는 8.61 → 15.37, CLAHE는 22.92. 정보를 만드는 것이 아니라 볼 수 있는 곳으로 옮기는 것이다.
흔한 오해
- "어두운 사진을 밝게 고치면 정보가 늘어난다" — 이 장에서 내가 틀린 부분이다. 감마·평활화·스트레칭이 전부 엔트로피를 떨어뜨렸다(6.1182 → 5.98 / 5.97 / 6.02). 고유값 개수도 164 → 148 / 106 / 130으로 줄었다.
- "히스토그램 평활화는 항상 좋다" — 고유값을 106개까지 날리고 되돌릴 수도 없다(복원 RMSE 74.80). 잡음이 많은 어두운 영역에서는 잡음까지 같이 증폭한다.
- "CLAHE는 평활화의 개선판이니 정리의 예외다" — 예외가 아니라 다른 종류다. 위치에 따라 곡선이 달라지는 비점 사상이라 칸을 쪼갤 수 있다. 대신 절대 밝기의 일관성을 잃는다.
- "표준편차가 크면 대비가 좋다" — 2–98% 스트레칭의 표준편차가 86.76으로 가장 크지만 지역 대비는 15.03으로 CLAHE(22.92)에 크게 못 미친다. 전역 표준편차는 "멀리서 본 대비", 지역 대비는 "가까이서 본 선명함"이다.
'데이터사이언스 > 컴퓨터 비전 입문' 카테고리의 다른 글
| CH06 스케일: 피라미드와 가우시안 스케일공간 (0) | 2026.08.30 |
|---|---|
| CH05 미분으로 경계 찾기: 소벨에서 캐니까지 (1) | 2026.08.30 |
| CH04 이웃을 섞다: 합성곱 필터와 블러 (0) | 2026.08.30 |
| CH03 색: RGB·HSV·Lab, 그리고 빨강의 경계 (0) | 2026.08.30 |
| CH01 이미지는 숫자 배열이다: 픽셀·채널·비트깊이 (0) | 2026.08.30 |
댓글