본문 바로가기
데이터사이언스/컴퓨터 비전 입문

CH02 밝기와 대비: 히스토그램이 말해 주는 것

by 카이스토 2026. 8. 30.

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 — 픽셀 값 하나만 보고 새 값을 정하는 변환. 위치를 보지 않는다
STEP 01

직관: 히스토그램은 사진의 요약이다

1장에서 사진이 숫자 격자임을 봤다. 그 262,144개의 숫자를 위치를 버리고 값별로 세기만 한 것이 히스토그램이다.

위치를 버렸으니 잃는 것이 많다. 히스토그램만 봐서는 무엇이 찍힌 사진인지 알 수 없다. 그래도 유용한 이유는, 밝기와 대비에 관한 질문은 위치와 무관하기 때문이다.

히스토그램 모양이 말해 주는 것

왼쪽에 몰려 있다 → 어두운 사진. 밝은 쪽 칸들이 놀고 있다.
오른쪽에 몰려 있다 → 과노출.
가운데 좁게 뭉쳐 있다 → 대비가 낮다. 흐리멍덩해 보인다.
양 끝에 벽처럼 서 있다 → 클리핑. 이미 잘려 나간 값이 있다 — 되돌릴 수 없다.

사진가가 카메라 뒷면에서 보는 그 히스토그램이 정확히 이것이다.

1-2. 이 장의 질문

어두운 사진이 있다. 밝게 고친다. 사진이 좋아졌다고 말할 수 있나?

"좋아졌다"를 정의해야 답할 수 있다. 두 가지 후보를 놓고 둘 다 재기로 했다.

① 정보량 — 픽셀 하나가 담은 비트 수(엔트로피). 클수록 사진이 256칸을 고르게 쓴다.
② 지역 대비 — 작은 조각 안에서 값이 얼마나 흔들리는가. 클수록 가까이서 볼 때 선명하다.

내 예상은 "둘 다 오른다"였다. 밝게 펴면 값이 넓게 퍼지니 정보량도 늘 것 같았다. 4절에서 이 예상이 어떻게 됐는지 나온다.

STEP 02

수식 읽는 법

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이다.

"픽셀 하나를 전달하려면 평균 몇 비트가 필요한가"로 읽으면 정확하다.

STEP 03

손으로 풀기: 픽셀 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장에도 이미 있었던 셈이다.

STEP 04

코드: 어두운 사진을 네 가지로 고쳐 본다

camera를 감마 2.2로 어둡게 만들어 입력으로 삼고, 네 가지 보정을 걸어 두 지표를 동시에 잰다.

ch02_code.pynumpy · skimage 엔트로피와 지역 대비
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 (타일별)
어두운 사진을 만들고 네 가지로 고쳐 본다 원본 평균 129.06 → 어둡게 만든 뒤 평균 60.13 방법 평균 표준편차 엔트로피 지역대비(8x8) 고유값수 원본 129.06 73.64 7.2317 11.4897 256 어둡게 60.13 48.03 6.1182 8.6146 164 감마보정 110.67 67.15 5.9843 10.7281 148 히스토그램평활화 131.60 69.24 5.9658 15.3721 106 2-98% 스트레칭 109.17 86.76 6.0211 15.0293 130 CLAHE 97.18 70.42 6.9531 22.9208 256 감마를 바꿔 가며 감마 평균 엔트로피 지역대비 0.4 118.71 5.9122 10.9345 0.6 92.67 6.0294 10.2197 0.8 73.89 6.0588 9.4168 1.0 60.13 6.1182 8.6146 1.4 40.01 5.4351 6.9052 2.0 22.90 4.9505 4.7805 3.0 9.61 4.0854 2.5612 → 엔트로피가 가장 큰 감마는 1.0 — 즉 '아무것도 안 하기'다 되돌릴 수 있는 변환과 없는 변환 방법 복원 RMSE 되돌아가나 감마 1/2.2 1.0979 거의 그렇다 ×1.5 밝기 1.3065 거의 그렇다 히스토그램평활화 74.7997 아니다

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이다. 되돌아가기는커녕 다른 사진이 된다.

실무 규칙 하나가 여기서 나온다 — 원본은 반드시 남겨 둔다. 감마·밝기는 순서를 바꿔도 되지만, 평활화는 파이프라인의 마지막에 한 번만 건다.

STEP 05

시각화

ch02-hist
그림 1. 히스토그램이 무엇인지부터. 왼쪽이 원본(평균 129.1), 가운데가 감마 2.2로 어둡게 만든 입력(평균 60.1)이다. 오른쪽에 두 히스토그램을 겹쳐 놓았다 — 어두운 쪽은 왼쪽 벽에 뭉쳐 있고 오른쪽 절반이 텅 비어 있다. 256칸 중 절반을 안 쓰고 있다는 뜻이다. 여기서 자연스럽게 나오는 생각이 "비어 있는 칸을 쓰게 만들면 정보가 늘겠다"이고, 이 장은 그 생각이 틀렸음을 보이는 과정이다. 히스토그램은 위치를 완전히 버린 요약이라는 점도 기억해 두자 — 픽셀을 아무렇게나 섞어도 히스토그램은 똑같다.
ch02-fix
그림 2. 네 가지 보정과 각각의 히스토그램. 감마보정은 곡선을 완만하게 끌어올려 히스토그램을 통째로 오른쪽으로 민다. 히스토그램 평활화는 전 구간에 고르게 펴는 대신 빗살 모양의 빈틈이 생긴다 — 저 빈틈 하나하나가 합쳐져서 사라진 값이고, 고유값이 164→106으로 준 이유다. CLAHE는 히스토그램이 매끄럽게 채워져 있다. 타일마다 다른 곡선을 쓰므로 빗살이 생기지 않는다. 사진을 보면 CLAHE만 카메라 삼각대의 질감과 잔디의 결이 살아 있다 — 지역 대비 22.92가 그것이다.
ch02-entropy
그림 3. 이 장의 핵심 두 막대. 왼쪽 — 어떤 보정도 원본 점선(7.2317)을 넘지 못한다. 그뿐 아니라 감마·평활화·스트레칭은 입력 6.1182보다도 낮다. 예상은 정확히 반대였다. 이유는 정리로 설명된다 — 256칸 → 256칸 점 사상은 칸을 합칠 뿐 쪼개지 못한다. CLAHE만 6.9531로 오르는데, 그것은 CLAHE가 위치에 따라 다른 곡선을 쓰는 비(非)점 사상이기 때문이다. 오른쪽 — 그런데 지역 대비 축에서는 순위가 뒤집힌다. 평활화가 15.37, CLAHE가 22.92로 원본(11.49)을 크게 넘는다. 보정은 정보를 만드는 것이 아니라, 있던 정보를 사람이 볼 수 있는 구간으로 옮기는 것이다.
ch02-gamma
그림 4. 왼쪽 — 감마를 0.4에서 3.0까지 쓸며 평균 밝기엔트로피를 동시에 잰다. 평균 밝기는 118.7에서 9.6까지 마음대로 움직인다. 그런데 엔트로피는 γ=1.0에서 최댓값이고 양쪽 어디로 가도 떨어진다. γ=1.0은 아무것도 하지 않은 상태다 — 즉 "가장 좋은 감마는 없다"가 이 그래프의 답이다. 오른쪽 — 되돌려 보기. 감마밝기 곱하기는 역변환을 걸면 RMSE 1.10 / 1.31로 거의 완전히 복구된다(남은 오차는 클리핑뿐). 평활화74.80 — 아예 다른 사진이다. 실무 규칙이 여기서 나온다: 원본을 남기고, 평활화는 마지막에 한 번만.
ch02-hand
그림 5. 손계산을 그림으로. 왼쪽 — 12픽셀 장난감 사진의 원본 히스토그램은 값 2~7에 몰려 있는데, 평활화 후에는 0~7 전 범위로 퍼졌다. 막대의 높이(개수)는 그대로이고 위치만 옮겨 갔다는 것에 주목하라 — 그래서 이 예에서는 엔트로피가 정확히 보존된다. 오른쪽 — 평활화의 정체. CDF(누적합)를 그대로 사상으로 쓴다. 픽셀이 많이 몰린 구간에서 CDF가 가파르게 오르고, 가파른 곳이 넓게 벌어진다. 4→3, 5→4처럼 이웃한 두 값이 이웃한 칸으로 갈 수도 있고, 사진이 커지면 같은 칸으로 갈 수도 있다 — 그 순간 정보가 사라진다.

이것만 기억하자

  1. 히스토그램은 위치를 버리고 값만 센 것이다. 밝기·대비 질문에는 충분하고, 그 밖에는 쓸모없다.
  2. 전역 보정은 정보를 늘릴 수 없다. 실험이 아니라 정리다 — 256칸 → 256칸 점 사상은 칸을 합칠 뿐 쪼개지 못한다. 감마 스윕에서 최적 감마가 1.0(=아무것도 안 하기)으로 나온 것이 그 증거다.
  3. 그래도 보정을 하는 이유는 지역 대비 때문이다. 평활화는 8.61 → 15.37, CLAHE는 22.92. 정보를 만드는 것이 아니라 볼 수 있는 곳으로 옮기는 것이다.

흔한 오해

  1. "어두운 사진을 밝게 고치면 정보가 늘어난다"이 장에서 내가 틀린 부분이다. 감마·평활화·스트레칭이 전부 엔트로피를 떨어뜨렸다(6.1182 → 5.98 / 5.97 / 6.02). 고유값 개수도 164 → 148 / 106 / 130으로 줄었다.
  2. "히스토그램 평활화는 항상 좋다" — 고유값을 106개까지 날리고 되돌릴 수도 없다(복원 RMSE 74.80). 잡음이 많은 어두운 영역에서는 잡음까지 같이 증폭한다.
  3. "CLAHE는 평활화의 개선판이니 정리의 예외다" — 예외가 아니라 다른 종류다. 위치에 따라 곡선이 달라지는 비점 사상이라 칸을 쪼갤 수 있다. 대신 절대 밝기의 일관성을 잃는다.
  4. "표준편차가 크면 대비가 좋다" — 2–98% 스트레칭의 표준편차가 86.76으로 가장 크지만 지역 대비는 15.03으로 CLAHE(22.92)에 크게 못 미친다. 전역 표준편차는 "멀리서 본 대비", 지역 대비는 "가까이서 본 선명함"이다.

댓글