4장. 채널 처리와 히스토그램 분석
프로젝트 1 — 수업 자료 사진 밝기 진단 도구
강사가 수업 자료로 쓸 사진을 폴더 하나에 모았습니다. 그런데 어떤 사진은 너무 어둡고, 어떤 사진은 하얗게 날아가 있고, 어떤 사진은 뿌옇습니다. 사진을 하나씩 열어 보는 대신, 폴더의 사진마다 밝기를 재어 판정표를 찍고 사진과 히스토그램을 한 장의 보고서 그림으로 저장하는 도구를 만들어 보세요. 고치는 일은 5장에서 하므로, 이번에는 진단만 합니다.
진단할 폴더는 ch04_exposure/ 입니다. 같은 거리 장면 하나를 노출만 바꿔 만든 연습용 가상 그림입니다.
| 파일 | 내용 |
|---|---|
normal.png | 노출이 알맞은 거리 장면 (320×240, 컬러) |
dark.png | 같은 장면을 어둡게 만든 그림 |
bright.png | 같은 장면을 밝게 만든 그림 |
low_contrast.png | 같은 장면의 밝고 어두운 차이를 줄인 그림 |
판정은 아래 표를 위에서부터 차례로 따져, 처음 맞는 줄에서 멈춥니다.
| 순서 | 판정 | 조건 |
|---|---|---|
| 1 | 어두움 | 평균 밝기가 80 미만이거나, 아주 어두운 칸(50 미만) 비율이 0.4 초과 |
| 2 | 밝음 | 평균 밝기가 175 초과이거나, 아주 밝은 칸(205 초과) 비율이 0.4 초과 |
| 3 | 대비 낮음 | 표준편차가 35 미만 |
| 4 | 정상 | 위의 어디에도 맞지 않음 |
요구사항
ch04_exposure/의 png 파일을 이름순으로 모두 흑백으로 읽습니다.- 사진마다 평균 밝기, 표준편차, 아주 어두운 칸 비율(50 미만), 아주 밝은 칸 비율(205 초과)을 재어 표로 찍습니다. 평균과 표준편차는 소수 첫째 자리, 비율은 소수 셋째 자리까지 적습니다.
- 판정표에 따라 사진마다 "어두움, 밝음, 대비 낮음, 정상" 가운데 하나를 판정해 표에 더합니다.
- 위 줄에 사진, 아래 줄에 그 사진의 히스토그램을 놓은 격자 그림을
out_ch04_report.png로 저장하고 화면에 띄웁니다. 히스토그램 제목에 판정을 적습니다. - 판정마다 몇 장인지와, 5장에서 고칠 사진(정상이 아닌 사진)의 이름을 찍습니다.
힌트
- 파일 목록은
sorted(name for name in os.listdir(folder) if name.endswith(".png"))로 얻습니다. cv2.imread(경로, cv2.IMREAD_GRAYSCALE)로 읽으면 밝기 하나짜리 그림이 됩니다.- 비율은
(gray < 50).mean()처럼 True/False 배열의 평균으로 구하고,round(float(...), 3)으로 찍습니다. - 판정은
if를 위에서부터 차례로 쓰고return으로 멈추는 함수 하나로 만듭니다. - 격자는
plt.subplots(2, 사진 수)로 만들고,plt.savefig를plt.show()보다 먼저 부릅니다.
채점 기준
| 항목 | 기준 | 점수 |
|---|---|---|
| 읽기 | 폴더의 png 를 이름순으로 모두 흑백으로 읽는다 | 15 |
| 숫자 재기 | 네 가지 숫자를 정해진 자리수로 찍는다 | 25 |
| 판정 | 판정표의 순서대로 따져 네 장을 바르게 판정한다 | 25 |
| 보고서 그림 | 사진과 히스토그램을 격자로 놓고 판정을 적어 저장하고 띄운다 | 20 |
| 요약 | 판정별 개수와 고칠 사진의 이름을 찍는다 | 15 |
코드의 흐름
그림의 다섯 단계를 예시 풀이의 단계와 같은 순서로 짰습니다. 넷째 단계의 판정은 판정표를 위에서부터 따지므로, 어두우면서 대비도 낮은 사진은 "어두움"으로 판정됩니다. 밝기를 먼저 고쳐야 대비도 제대로 볼 수 있기 때문입니다.
실제 데이터로 해 보려면
실제 수업 자료 사진으로 해 보려면 사진이 든 폴더 이름만 바꾸면 됩니다. 공개 데이터로 연습하려면 AI Hub(aihub.or.kr)처럼 이미지 데이터를 공개한 곳에서 내려받습니다. 내려받기 전에 데이터마다 이용 약관(상업적 이용 가능 여부, 재배포 금지 등)을 확인하고, 사람의 얼굴이나 차량 번호가 찍힌 사진은 개인정보 처리 기준을 먼저 확인합니다. 실제 사진은 장면마다 원래 밝기가 다르므로(밤 풍경, 눈밭), 판정표의 기준값은 모은 사진의 숫자를 한 번 훑어본 뒤 알맞게 고칩니다.
결과 보기 (예시 풀이)
1단계. 사진마다 숫자 재기
import os
import cv2
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
folder = "ch04_exposure"
# 요구사항 1: png 파일을 이름순으로
names = sorted(name for name in os.listdir(folder) if name.endswith(".png"))
rows, grays = [], {}
for name in names:
gray = cv2.imread(os.path.join(folder, name), cv2.IMREAD_GRAYSCALE)
grays[name] = gray
# 요구사항 2: 네 가지 숫자
rows.append({
"파일": name,
"평균": round(float(gray.mean()), 1), # 막대가 모인 자리
"표준편차": round(float(gray.std()), 1), # 막대가 퍼진 너비
"어두운칸": round(float((gray < 50).mean()), 3), # 50 미만 비율
"밝은칸": round(float((gray > 205).mean()), 3), # 205 초과 비율
})
report = pd.DataFrame(rows)
print(report.to_string(index=False)) 파일 평균 표준편차 어두운칸 밝은칸
bright.png 203.9 30.4 0.000 0.472
dark.png 41.9 23.8 0.528 0.000
low_contrast.png 125.2 20.3 0.000 0.000
normal.png 121.4 68.3 0.235 0.0392단계. 판정표로 판정하기
def diagnose(mean, std, dark, bright):
"""판정표를 위에서부터 차례로 따져, 처음 맞는 줄의 판정을 돌려준다."""
if mean < 80 or dark > 0.4:
return "어두움"
if mean > 175 or bright > 0.4:
return "밝음"
if std < 35:
return "대비 낮음"
return "정상"
# 요구사항 3: 표의 줄마다 판정을 붙인다
report["판정"] = [diagnose(r.평균, r.표준편차, r.어두운칸, r.밝은칸) for r in report.itertuples()]
print(report.to_string(index=False)) 파일 평균 표준편차 어두운칸 밝은칸 판정
bright.png 203.9 30.4 0.000 0.472 밝음
dark.png 41.9 23.8 0.528 0.000 어두움
low_contrast.png 125.2 20.3 0.000 0.000 대비 낮음
normal.png 121.4 68.3 0.235 0.039 정상3단계. 사진과 히스토그램을 한 장에 저장하기
n = len(report)
fig, axes = plt.subplots(2, n, figsize=(3.2 * n, 5.4))
for k, row in enumerate(report.itertuples()):
gray = grays[row.파일]
# 위 줄: 사진. 흑백이므로 cmap="gray", 밝기 기준을 0~255 로 고정해 사진끼리 견줄 수 있게 한다
axes[0, k].imshow(gray, cmap="gray", vmin=0, vmax=255)
axes[0, k].set_title(row.파일)
axes[0, k].axis("off")
# 아래 줄: 구간 32개 히스토그램
hist = cv2.calcHist([gray], [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"{row.판정} (평균 {row.평균}, 편차 {row.표준편차})", fontsize=10)
axes[1, k].set_xlabel("밝기")
plt.tight_layout()
# 요구사항 4: 저장을 먼저 하고 띄운다(띄운 뒤에는 그림이 닫힐 수 있다)
plt.savefig("out_ch04_report.png", dpi=100)
print("저장: out_ch04_report.png")
plt.show()저장: out_ch04_report.png
4단계. 판정 요약
# 요구사항 5: 판정마다 몇 장인지
print("[판정별 장 수]")
for label, count in report["판정"].value_counts().items():
print(f" {label}: {int(count)}장")
# 정상이 아닌 사진은 5차시에서 고친다
to_fix = report.loc[report["판정"] != "정상", "파일"].tolist()
print("5차시에서 고칠 사진:", ", ".join(to_fix))[판정별 장 수]
밝음: 1장
어두움: 1장
대비 낮음: 1장
정상: 1장
5차시에서 고칠 사진: bright.png, dark.png, low_contrast.png판정표를 보면 네 장이 의도대로 갈렸습니다. dark.png 는 평균 41.9 에 아주 어두운 칸이 0.528 이라 첫 줄에서 "어두움", bright.png 는 평균 203.9 에 아주 밝은 칸이 0.472 라 둘째 줄에서 "밝음"입니다. low_contrast.png 는 평균이 125.2 로 정상과 비슷해 앞의 두 줄을 통과하지만, 표준편차가 20.3 으로 35 보다 작아 "대비 낮음"입니다. normal.png 는 평균 121.4, 표준편차 68.3 으로 어느 줄에도 걸리지 않아 "정상"입니다. bright.png 의 표준편차도 30.4 로 35 보다 작지만, 판정표를 위에서부터 따지므로 "밝음"에서 멈췄습니다. 보고서 그림의 아래 줄에서도 어두움은 막대가 왼쪽에, 밝음은 오른쪽에, 대비 낮음은 가운데에 좁게 모인 것이 보입니다. 정상이 아닌 세 장(bright.png, dark.png, low_contrast.png)은 5장에서 평활화와 CLAHE 로 고쳐 보고, 이 도구로 다시 진단해 판정이 바뀌는지 확인합니다.