4장. 채널 처리와 히스토그램 분석
실습 4 — 흑백 히스토그램 그리기
노출이 알맞은 거리 장면(ch04_exposure/normal.png)을 흑백으로 읽어 cv2.calcHist 로 히스토그램을 세고, np.histogram 과 결과가 같은지 확인합니다. 구간 수를 256개와 32개로 바꿔 그려 보고, 평균과 표준편차도 잽니다.
1단계. calcHist 로 세기
import cv2
import numpy as np
# IMREAD_GRAYSCALE: 읽으면서 흑백으로 바꾼다(2차시)
gray = cv2.imread("ch04_exposure/normal.png", cv2.IMREAD_GRAYSCALE)
print("흑백 모양:", gray.shape)
# cv2.calcHist(images, channels, mask, histSize, ranges): calculate histogram
hist = cv2.calcHist([gray], # 셀 그림(목록으로 감싼다)
[0], # 0번 채널(흑백은 하나뿐)
None, # 마스크 없음: 전체를 센다
[256], # 구간 256개: 밝기 1 마다 한 구간
[0, 256]) # 0 이상 256 미만
print("결과 모양:", hist.shape, "/ 자료형:", hist.dtype)
print("막대를 모두 더하면:", int(hist.sum()), "/ 픽셀 수:", gray.size)흑백 모양: (240, 320)
결과 모양: (256,) / 자료형: float32
막대를 모두 더하면: 76800 / 픽셀 수: 768002단계. np.histogram 과 견주기
# np.histogram: 도수와 구간 경계를 함께 돌려준다. bins=256, range=(0, 256) 으로 같은 구간을 준다
counts, edges = np.histogram(gray, bins=256, range=(0, 256))
print("np.histogram 모양:", counts.shape, "/ 구간 경계 수:", len(edges))
# calcHist 결과는 소수(float32)다. 판에 따라 (256, 1) 모양이기도 하므로 한 줄로 펴고(ravel) 정수로 바꿔 견준다
print("두 결과가 같은가:", np.array_equal(hist.ravel().astype(int), counts))
# 픽셀이 가장 많은 밝기 세 곳: argsort 로 작은 순서의 자리를 얻어 뒤의 셋을 큰 순서로
top3 = np.argsort(counts)[::-1][:3]
for v in top3:
print(f"밝기 {int(v):3}: {int(counts[v]):,}칸")np.histogram 모양: (256,) / 구간 경계 수: 257
두 결과가 같은가: True
밝기 199: 9,007칸
밝기 47: 8,651칸
밝기 101: 7,675칸3단계. 구간 256개와 32개로 그리기
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
# 구간 32개: 밝기 8 씩 묶는다
hist32 = cv2.calcHist([gray], [0], None, [32], [0, 256]).ravel()
fig, axes = plt.subplots(1, 3, figsize=(12, 3.4))
axes[0].imshow(gray, cmap="gray", vmin=0, vmax=255)
axes[0].set_title("흑백 그림")
axes[0].axis("off")
# 구간 256개: 막대 하나의 너비가 1
axes[1].bar(range(256), hist.ravel(), width=1)
axes[1].set_title("구간 256개 (너비 1)")
# 구간 32개: 막대 하나가 밝기 8칸을 맡으므로 왼쪽 끝 자리를 0, 8, 16, ... 으로 둔다
axes[2].bar(np.arange(32) * 8, hist32, width=8, align="edge", edgecolor="white")
axes[2].set_title("구간 32개 (너비 8)")
for ax in axes[1:]:
ax.set_xlim(0, 256)
ax.set_xlabel("밝기")
ax.set_ylabel("픽셀 수")
plt.tight_layout()
plt.show()
4단계. 숫자로 밝기 분포 말하기
# 평균: 막대가 모인 자리, 표준편차: 막대가 퍼진 너비
print("평균 밝기:", round(float(gray.mean()), 1))
print("표준편차:", round(float(gray.std()), 1))
# 아주 어두운 칸(50 미만)과 아주 밝은 칸(205 초과)의 비율. True 를 1 로 세어 평균을 낸다
print("아주 어두운 칸 비율:", round(float((gray < 50).mean()), 3))
print("아주 밝은 칸 비율:", round(float((gray > 205).mean()), 3))평균 밝기: 121.4
표준편차: 68.3
아주 어두운 칸 비율: 0.235
아주 밝은 칸 비율: 0.039코드 설명
cv2.calcHist([gray], [0], None, [256], [0, 256])의 결과는 교안 PC 의 OpenCV 5.0 에서 (256,) 모양의 float32 입니다(4.x 에서는 (256, 1)). 막대를 모두 더하면 픽셀 수 76,800 과 같습니다.np.histogram(gray, bins=256, range=(0, 256))은 도수 256개와 구간 경계 257개를 돌려줍니다. calcHist 결과를ravel()로 펴고 정수로 바꾸면 두 결과가 같습니다.- 구간 256개 그래프는 몇몇 밝기에만 막대가 뾰족하게 솟습니다. 도형을 한 색으로 칠해 만든 그림이라 같은 밝기의 칸이 몰려 있기 때문입니다(가장 많은 밝기 199 에 9,007칸). 32개로 묶으면 어두운 쪽 무리와 밝은 쪽 무리가 한눈에 보입니다.
(gray < 50).mean()은 50 미만인 칸의 비율입니다. 이 그림은 평균 121.4, 표준편차 68.3 이고, 아주 어두운 칸이 0.235(23.5%), 아주 밝은 칸이 0.039 입니다. 막대가 왼쪽 끝에서 오른쪽 끝까지 넓게 퍼진 고른 사진입니다.