OpenCV 비전 실무4장 · 채널 처리와 히스토그램 분석18 / 21← 이전목차다음 →MetaContents

4장. 채널 처리와 히스토그램 분석

실습 5 — 컬러 히스토그램과 마스크 히스토그램

노을 그림에서 채널마다 히스토그램을 세어 세 줄로 겹쳐 그립니다. 그다음 해 둘레만 흰 사각형 마스크를 만들어, 그림 전체와 해 둘레의 R 채널 히스토그램을 견줍니다.

1단계. 채널마다 세어 세 줄로 그리기

Python
import cv2
import numpy as np
import matplotlib.pyplot as plt

plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False

img = cv2.imread("ch04_scenes/sunset.png")
centers = np.arange(32) * 8 + 4   # 구간 32개의 가운데 밝기: 4, 12, 20, ...

fig, ax = plt.subplots(figsize=(8, 3.6))
for ch, (name, color) in enumerate([("B", "blue"), ("G", "green"), ("R", "red")]):
    # 둘째 인자 [ch] 로 채널을 고른다. 도형 그림이라 뾰족하지 않게 구간 32개로 센다
    hist = cv2.calcHist([img], [ch], None, [32], [0, 256]).ravel()
    ax.plot(centers, hist, color=color, label=name)
    # 픽셀이 가장 많은 구간과 그 구간의 밝기 범위
    k = int(hist.argmax())
    print(f"{name} 채널: 가장 많은 구간 {k * 8}~{k * 8 + 7}, {int(hist[k]):,}칸")
ax.set_xlim(0, 256)
ax.set_xlabel("밝기")
ax.set_ylabel("픽셀 수")
ax.legend(loc="upper center", ncol=3)
ax.set_title("노을 그림의 컬러 히스토그램 (구간 32개)")
plt.tight_layout()
plt.show()
출력
B 채널: 가장 많은 구간 64~71, 28,781칸
G 채널: 가장 많은 구간 40~47, 11,443칸
R 채널: 가장 많은 구간 200~207, 7,680칸
노을 그림의 B, G, R 히스토그램을 파랑, 초록, 빨강 줄로 겹쳐 그린 그래프

2단계. 해 둘레만 흰 마스크 만들기

Python
# 그림과 같은 세로·가로의 검은 판(0)을 만든다. 채널은 하나, 자료형은 uint8
mask = np.zeros(img.shape[:2], dtype=np.uint8)
# 해 둘레: 세로 105~174, 가로 165~234 를 흰색(255)으로 칠한다
mask[105:175, 165:235] = 255
print("마스크 모양:", mask.shape, "/ 흰 칸 수:", int((mask == 255).sum()))

# cv2.bitwise_and(그림, 그림, mask=마스크): 마스크의 흰 칸만 남기고 나머지를 검정으로
# bitwise 는 "비트 단위로", and 는 "그리고" 라는 뜻이다. 셀 곳을 눈으로 확인하려고 만든다
shown = cv2.bitwise_and(img, img, mask=mask)

fig, axes = plt.subplots(1, 2, figsize=(8, 3))
axes[0].imshow(mask, cmap="gray")
axes[0].set_title("마스크")
axes[1].imshow(cv2.cvtColor(shown, cv2.COLOR_BGR2RGB))
axes[1].set_title("세는 곳만 남긴 모습")
for ax in axes:
    ax.axis("off")
plt.tight_layout()
plt.show()
출력
마스크 모양: (240, 320) / 흰 칸 수: 4900
흰 사각형 마스크와 마스크를 씌워 해 둘레만 남긴 노을 그림

3단계. 전체와 해 둘레의 R 채널 견주기

Python
# 같은 R 채널(2번)을 마스크 없이 한 번, 마스크를 주고 한 번 센다
whole = cv2.calcHist([img], [2], None, [32], [0, 256]).ravel()
part = cv2.calcHist([img], [2], mask, [32], [0, 256]).ravel()
print("전체에서 센 칸:", int(whole.sum()), "/ 해 둘레에서 센 칸:", int(part.sum()))

# cv2.mean 도 mask 를 받는다: 흰 칸만의 B, G, R 평균
print("전체 평균(B, G, R)   :", [round(v, 1) for v in cv2.mean(img)[:3]])
print("해 둘레 평균(B, G, R):", [round(v, 1) for v in cv2.mean(img, mask=mask)[:3]])

# 센 칸 수가 다르므로, 막대를 칸 수로 나눠 비율로 바꿔 한 그래프에서 견준다
fig, ax = plt.subplots(figsize=(8, 3.4))
ax.plot(centers, whole / whole.sum(), label="그림 전체")
ax.plot(centers, part / part.sum(), label="해 둘레(마스크)")
ax.set_xlim(0, 256)
ax.set_xlabel("R 채널 밝기")
ax.set_ylabel("칸의 비율")
ax.legend(loc="upper left")
ax.set_title("R 채널 히스토그램: 전체와 일부")
plt.tight_layout()
plt.show()
출력
전체에서 센 칸: 76800 / 해 둘레에서 센 칸: 4900
전체 평균(B, G, R)   : [61.1, 86.6, 185.5]
해 둘레 평균(B, G, R): [96.6, 173.9, 234.6]
노을 그림 전체와 해 둘레의 R 채널 히스토그램을 비율로 바꿔 겹쳐 그린 그래프

코드 설명

  • cv2.calcHist([img], [ch], None, [32], [0, 256]) 의 둘째 인자로 채널을 고릅니다. 같은 그림에서 0, 1, 2 를 차례로 주면 B, G, R 히스토그램이 나옵니다.
  • 노을 그림은 B 채널이 64~71 구간에 28,781칸으로 몰려 있고, R 채널은 200 이 넘는 밝은 쪽까지 넓게 퍼져 있습니다. 빨강이 밝고 많은 따뜻한 사진이라는 실습 3의 판정과 맞습니다.
  • 마스크는 그림과 세로·가로가 같은 uint8 판이고, 흰 칸 70×70 = 4,900칸만 셉니다. cv2.bitwise_and 는 세는 곳을 눈으로 확인하려고 만든 그림입니다.
  • 전체와 일부는 센 칸 수가 76,800 과 4,900 으로 달라서, whole / whole.sum() 처럼 비율로 바꿔야 한 그래프에서 견줄 수 있습니다. 해 둘레만 세면 R 채널 칸의 0.641 이 가장 밝은 248~255 구간에 몰리고, R 평균도 185.5 에서 234.6 으로 오릅니다. 전체 평균에서는 보이지 않던 해의 밝은 빨강이 마스크로 드러납니다.
← 이전 절4장 · 실습 4 — 흑백 히스토그램 그리기다음 절 →4장 · 프로젝트 1 — 수업 자료 사진 밝기 진단 도구