5장. 영상 향상과 학습용 전처리
실습 2 — alpha·beta 대비 조정과 정규화
4장의 대비 낮은 거리 장면(ch04_exposure/low_contrast.png)을 cv2.convertScaleAbs 로 alpha 를 바꿔 가며 늘여 보고, cv2.normalize 의 NORM_MINMAX 로 가장 어두운 값과 가장 밝은 값을 0 과 255 에 맞춥니다.
1단계. 범위 재고 식을 한 칸에 넣어 보기
import cv2
import numpy as np
gray = cv2.imread("ch04_exposure/low_contrast.png", cv2.IMREAD_GRAYSCALE)
lo, hi = int(gray.min()), int(gray.max())
print(f"가장 어두운 값 {lo}, 가장 밝은 값 {hi}, 표준편차 {round(float(gray.std()), 1)}")
# 대비 조정 식 g = alpha × f + beta. 가운데(128)는 그대로 두려고 beta = 128 - alpha × 128 로 정한다
alpha = 2.0
beta = 128 - alpha * 128 # 128 - 256 = -128
f = int(gray[100, 150]) # 가운데 건물 벽 칸 하나(세로 100, 가로 150)
print(f"손계산: {alpha} × {f} + ({beta}) = {alpha * f + beta}")
# convertScaleAbs(src, alpha, beta): 곱하고 더한 뒤 절댓값을 내어 0~255 로 맞춘다
out = cv2.convertScaleAbs(gray, alpha=alpha, beta=beta)
print("convertScaleAbs 결과:", int(out[100, 150]))가장 어두운 값 97, 가장 밝은 값 164, 표준편차 20.3
손계산: 2.0 × 119 + (-128.0) = 110.0
convertScaleAbs 결과: 1102단계. alpha 를 바꿔 가며 견주기
import pandas as pd
rows = []
for alpha in [1.0, 2.0, 3.0, 4.0]:
beta = 128 - alpha * 128
# 음수가 절댓값으로 바뀌지 않게, 소수로 계산해 np.clip 으로 0~255 를 자른다
g = np.clip(alpha * gray.astype(np.float32) + beta, 0, 255).astype(np.uint8)
rows.append({"alpha": alpha, "beta": beta,
"평균": round(float(g.mean()), 1),
"표준편차": round(float(g.std()), 1),
"0인칸": round(float((g == 0).mean()), 3), # 끝에 몰린 칸의 비율
"255인칸": round(float((g == 255).mean()), 3)})
print(pd.DataFrame(rows).to_string(index=False)) alpha beta 평균 표준편차 0인칸 255인칸
1.0 0.0 125.2 20.3 0.0 0.000
2.0 -128.0 122.5 40.7 0.0 0.000
3.0 -256.0 119.7 61.0 0.0 0.000
4.0 -384.0 116.5 80.6 0.0 0.0253단계. 정규화로 양 끝 맞추기
# cv2.normalize(src, dst, alpha, beta, norm_type): NORM_MINMAX 면 최솟값→alpha, 최댓값→beta
norm = cv2.normalize(gray, # 바꿀 그림
None, # 결과를 새로 만든다
0, # 가장 어두운 값이 갈 자리
255, # 가장 밝은 값이 갈 자리
cv2.NORM_MINMAX) # 최솟값·최댓값 방식
print("정규화 뒤 범위:", int(norm.min()), "~", int(norm.max()))
print("정규화 뒤 평균·표준편차:", round(float(norm.mean()), 1), round(float(norm.std()), 1))
# 손계산과 견준다: (값 - 가장 어두운 값) ÷ (범위) × 255
f = int(gray[100, 150])
print(f"손계산: ({f} - {lo}) ÷ ({hi} - {lo}) × 255 = {round((f - lo) / (hi - lo) * 255, 1)}")
print("normalize 결과:", int(norm[100, 150]))정규화 뒤 범위: 0 ~ 255
정규화 뒤 평균·표준편차: 107.4 77.3
손계산: (119 - 97) ÷ (164 - 97) × 255 = 83.7
normalize 결과: 844단계. 그림과 히스토그램으로 견주기
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
alpha2 = np.clip(2.0 * gray.astype(np.float32) - 128, 0, 255).astype(np.uint8)
panels = [("원본 (대비 낮음)", gray), ("alpha 2, beta -128", alpha2), ("정규화 (NORM_MINMAX)", norm)]
fig, axes = plt.subplots(2, 3, figsize=(11, 5.4))
for k, (title, g) in enumerate(panels):
axes[0, k].imshow(g, cmap="gray", vmin=0, vmax=255)
axes[0, k].set_title(title)
axes[0, k].axis("off")
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_title(f"표준편차 {round(float(g.std()), 1)}", fontsize=10)
plt.tight_layout()
plt.show()
코드 설명
beta = 128 - alpha * 128은 밝기 128 인 칸이 그대로 128 에 남게 하는 beta 입니다. 가운데를 축으로 양쪽으로 당기는 셈입니다.- 가운데 건물 벽 칸 119 는 손계산 2.0 × 119 - 128 = 110 과
convertScaleAbs결과 110 이 같습니다. alpha 를 1, 2, 3, 4 로 올리면 표준편차가 20.3, 40.7, 61.0, 80.6 으로 거의 alpha 배만큼 커집니다. alpha 4 에서는 0.025 의 칸이 255 에 몰리기 시작합니다. - 정규화는 가장 어두운 값 97 을 0, 가장 밝은 값 164 를 255 로 늘였고, 표준편차가 77.3 이 됐습니다. 벽 칸 119 는 손계산 (119 - 97) ÷ 67 × 255 = 83.7 을 반올림한 84 가 나옵니다. 정규화 뒤 평균이 107.4 로 원본(125.2)보다 내려간 까닭은, 이 그림에서 어두운 칸이 0 쪽으로 더 많이 당겨졌기 때문입니다.
cv2.convertScaleAbs대신np.clip으로 계산한 까닭은, 음수가 된 칸이 절댓값으로 바뀌어 밝게 나오는 일을 막기 위해서입니다.