OpenCV 비전 실무5장 · 영상 향상과 학습용 전처리14 / 21← 이전목차다음 →MetaContents

5장. 영상 향상과 학습용 전처리

실습 1 — 밝기 더하고 빼기

4장의 거리 장면(ch04_exposure/normal.png)에 NumPy 덧셈과 cv2.add 로 100 을 더해 넘침과 포화를 견주고, 어두운 장면(dark.png)을 cv2.add 로 밝혀 히스토그램이 옆으로 밀리는 것을 봅니다.

1단계. 한 칸으로 넘침과 포화 견주기

Python
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 131

2단계. 넘친 칸 세기

Python
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단계. 어두운 사진 밝히고 히스토그램 견주기

Python
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
어두운 거리 장면과 80 을 더한 그림, 각각의 히스토그램

코드 설명

  • 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 만큼 옮겨 갔습니다.
← 이전 절5장 · 실습 지도다음 절 →5장 · 실습 2 — alpha·beta 대비 조정과 정규화