5장. 영상 향상과 학습용 전처리
실습 1 — 밝기 더하고 빼기
4장의 거리 장면(ch04_exposure/normal.png)에 NumPy 덧셈과 cv2.add 로 100 을 더해 넘침과 포화를 견주고, 어두운 장면(dark.png)을 cv2.add 로 밝혀 히스토그램이 옆으로 밀리는 것을 봅니다.
1단계. 한 칸으로 넘침과 포화 견주기
import cv2
import numpy as np
# 흑백으로 읽는다. 밝기 조정은 채널 하나로 보면 숫자를 읽기 쉽다
gray = cv2.imread("ch04_exposure/normal.png", cv2.IMREAD_GRAYSCALE)
# 구름 칸(세로 30, 가로 80)과 도로 칸(세로 230, 가로 5)을 고른다
for name, (y, x) in [("구름", (30, 80)), ("도로", (230, 5))]:
v = int(gray[y, x])
# NumPy 덧셈: uint8 끼리 더해 255 를 넘으면 0 부터 다시 센다(넘침)
np_add = int((gray + 100)[y, x])
# cv2.add: 255 를 넘으면 255 에서 멈춘다(포화)
cv_add = int(cv2.add(gray, 100)[y, x])
print(f"{name}: 원래 {v} → NumPy {np_add}, cv2.add {cv_add}")구름: 원래 250 → NumPy 94, cv2.add 255
도로: 원래 31 → NumPy 131, cv2.add 1312단계. 넘친 칸 세기
np_bright = gray + 100 # NumPy 덧셈
cv_bright = cv2.add(gray, 100) # 포화 덧셈
# 156 이상인 칸은 100 을 더하면 256 이 넘는다. True 를 1 로 세어 더한다
over = int((gray >= 156).sum())
print(f"100 을 더하면 넘치는 칸: {over:,}칸 ({over / gray.size:.3f})")
# 평균을 견준다: 넘친 칸이 0 근처로 돌아가면 NumPy 쪽 평균은 덜 오른다
print("원본 평균:", round(float(gray.mean()), 1))
print("NumPy 평균:", round(float(np_bright.mean()), 1))
print("cv2.add 평균:", round(float(cv_bright.mean()), 1))
# 빼기도 같다: cv2.subtract 는 0 에서 멈춘다
print("100 을 빼면 0 이 되는 칸:", f"{int((cv2.subtract(gray, 100) == 0).sum()):,}칸")100 을 더하면 넘치는 칸: 30,756칸 (0.400)
원본 평균: 121.4
NumPy 평균: 118.9
cv2.add 평균: 204.5
100 을 빼면 0 이 되는 칸: 32,913칸3단계. 어두운 사진 밝히고 히스토그램 견주기
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
dark = cv2.imread("ch04_exposure/dark.png", cv2.IMREAD_GRAYSCALE)
# 어두운 사진에 80 을 더한다. 가장 밝은 칸도 255 를 넘지 않는지 먼저 본다
print("가장 밝은 칸:", int(dark.max()), "→ 80 을 더하면", int(dark.max()) + 80)
lifted = cv2.add(dark, 80)
for name, g in [("원본", dark), ("+80", lifted)]:
print(f"{name}: 평균 {round(float(g.mean()), 1)}, 표준편차 {round(float(g.std()), 1)}")
fig, axes = plt.subplots(2, 2, figsize=(9, 5.6))
for k, (title, g) in enumerate([("어두운 원본", dark), ("cv2.add(dark, 80)", lifted)]):
axes[0, k].imshow(g, cmap="gray", vmin=0, vmax=255) # 밝기 기준을 0~255 로 고정
axes[0, k].set_title(title)
axes[0, k].axis("off")
# 구간 32개(너비 8) 히스토그램
hist = cv2.calcHist([g], [0], None, [32], [0, 256]).ravel()
axes[1, k].bar(np.arange(32) * 8, hist, width=8, align="edge")
axes[1, k].set_xlim(0, 256)
axes[1, k].set_xlabel("밝기")
plt.tight_layout()
plt.show()가장 밝은 칸: 87 → 80 을 더하면 167
원본: 평균 41.9, 표준편차 23.8
+80: 평균 121.9, 표준편차 23.8
코드 설명
gray + 100은 NumPy 의uint8덧셈이라 255 를 넘으면 256 을 뺀 값이 남습니다. 구름 칸 250 은 350 - 256 = 94 가 되어 오히려 어두워졌고, 넘치지 않는 도로 칸 31 은 두 방법 모두 131 입니다.cv2.add(gray, 100)은 255 에서 멈춥니다(포화 연산).cv2.subtract는 0 에서 멈춥니다.- 156 이상이라 넘치는 칸이 30,756칸(0.400)이나 됩니다. 그래서 NumPy 쪽 평균은 121.4 에서 118.9 로 오히려 내려갔고,
cv2.add쪽은 204.5 로 올랐습니다. 100 을 빼면 32,913칸이 0 에서 멈춥니다. - 어두운 사진은 가장 밝은 칸이 87 이라 80 을 더해도 167 로 넘치지 않습니다. 평균은 41.9 에서 121.9 로 80 만큼 오르고 표준편차는 23.8 그대로입니다. 히스토그램에서도 막대 묶음이 모양 그대로 오른쪽으로 80 만큼 옮겨 갔습니다.