OpenCV 비전 실무16장 · AI 데이터 정제와 데이터 증강15 / 21← 이전목차다음 →MetaContents

16장. AI 데이터 정제와 데이터 증강

실습 2 — 흐림 점수와 밝기 점수로 품질 표 만들기

실습 1의 관문을 통과한 사진마다 평균 밝기와 흐림 점수(Laplacian 분산)를 재어 품질 표를 만듭니다. 점수의 분포를 그래프로 그려 기준값을 정하고, 어두운 사진은 밝기를 고친 뒤 다시 잽니다. 품질 표는 out_ch16_quality.csv 로 저장합니다.

1단계. 손계산을 코드로 확인하기

Python
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

# 본문의 두 줄: 선명한 경계와 흐린 경계
sharp = np.array([50, 50, 50, 150, 150, 150], dtype=np.float64)
soft = np.array([50, 50, 70, 130, 150, 150], dtype=np.float64)
for label, row in [("선명", sharp), ("흐림", soft)]:
    second = row[:-2] + row[2:] - 2 * row[1:-1]       # 두 번 빼기
    diff = second - second.mean()                      # 평균과의 차이
    var = float((diff ** 2).mean())                    # 제곱의 평균 = 분산
    print(f"{label}: 두 번 빼기 {second.tolist()} → 분산 {var}, 표준편차 {round(float(np.sqrt(var)), 1)}")
출력
선명: 두 번 빼기 [0.0, 100.0, -100.0, 0.0] → 분산 5000.0, 표준편차 70.7
흐림: 두 번 빼기 [20.0, 40.0, -40.0, -20.0] → 분산 1000.0, 표준편차 31.6

2단계. 사진마다 밝기와 흐림 점수 재기

Python
folder = "ch16_raw"

def load_bgr(path):
    """저장된 그대로 읽은 뒤 세 채널로 맞춘다. 못 읽거나 너무 작으면 None."""
    img = cv2.imread(path, cv2.IMREAD_UNCHANGED) if os.path.getsize(path) > 0 else None
    if img is None or min(img.shape[:2]) < 64:
        return None
    if img.ndim == 2:
        return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
    if img.shape[2] == 4:
        return cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
    return img

def blur_score(img):
    """흑백으로 바꾸고 Laplacian 의 분산을 돌려준다. 클수록 선명하다."""
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # CV_64F: + 와 - 를 모두 담는 소수로 받는다(uint8 이면 음수가 잘린다)
    return float(cv2.Laplacian(gray, cv2.CV_64F).var())

rows, images = [], {}
for name in sorted(n for n in os.listdir(folder) if n != "answer.csv"):
    img = load_bgr(os.path.join(folder, name))
    if img is None:
        continue                                       # 실습 1에서 버린 파일은 건너뛴다
    images[name] = img
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    rows.append({"파일": name,
                 "평균": round(float(gray.mean()), 1),
                 "어두운칸": round(float((gray < 50).mean()), 3),     # 50 미만 비율(4차시)
                 "밝은칸": round(float((gray > 205).mean()), 3),      # 205 초과 비율(4차시)
                 "흐림점수": round(blur_score(img), 1)})
quality = pd.DataFrame(rows)
print("잰 사진:", len(quality))
print(quality.sort_values("흐림점수").head(8).to_string(index=False))
출력
잰 사진: 35
              파일    평균  어두운칸   밝은칸  흐림점수
    apple_09.jpg 142.4 0.000 0.065   3.2
tangerine_09.jpg 160.6 0.000 0.042   4.8
     lime_09.jpg 160.2 0.000 0.058   6.1
tangerine_10.png  23.9 1.000 0.000   7.9
     lime_11.png 228.5 0.000 1.000  39.6
     lime_10.png  62.7 0.174 0.000  67.8
tangerine_11.jpg 165.1 0.000 0.086 203.8
     lime_01.png 168.6 0.000 0.123 210.3

3단계. 점수 분포를 보고 기준값 정하기

Python
BLUR_MIN = 100   # 이보다 작으면 흐리다고 본다

ordered = quality.sort_values("흐림점수").reset_index(drop=True)
fig, ax = plt.subplots(figsize=(11, 3.6))
ax.bar(range(len(ordered)), ordered["흐림점수"])
# 기준선: 막대와 구별되도록 기본 색 순서의 둘째 색(C1)과 점선으로
ax.axhline(BLUR_MIN, color="C1", linestyle="--", label=f"기준값 {BLUR_MIN}")
# 점수가 3 에서 1,500 까지 퍼져 있어 세로축을 로그 눈금(10배마다 같은 간격)으로 그린다
ax.set_yscale("log")
ax.set_yticks([1, 10, 100, 1000], labels=["1", "10", "100", "1000"])
ax.minorticks_off()
ax.set_xticks(range(len(ordered)), labels=ordered["파일"], rotation=90, fontsize=8)
ax.set_ylabel("흐림 점수 (Laplacian 분산)")
ax.set_title("사진별 흐림 점수 (작은 것부터)")
ax.legend(loc="upper left")
plt.tight_layout()
plt.show()

# 기준값 아래와 위의 가장 가까운 점수: 틈이 넓을수록 기준을 정하기 쉽다
below = ordered.loc[ordered["흐림점수"] < BLUR_MIN, "흐림점수"]
above = ordered.loc[ordered["흐림점수"] >= BLUR_MIN, "흐림점수"]
print("기준 아래:", below.tolist())
print("기준 위의 가장 작은 점수:", float(above.min()))
출력
기준 아래: [3.2, 4.8, 6.1, 7.9, 39.6, 67.8]
기준 위의 가장 작은 점수: 203.8
사진 35장의 흐림 점수를 작은 것부터 막대로 그리고 기준값 100 에 점선을 그은 그래프

4단계. 밝기를 먼저 고친 뒤 다시 재고 판정하기

Python
def exposure(mean, dark, bright):
    """이 장의 밝기 판정표: 위에서부터 따져 처음 맞는 줄에서 멈춘다."""
    if mean < 40:
        return "아주 어두움"
    if mean < 80 or dark > 0.4:
        return "어두움"
    if mean > 175 or bright > 0.4:
        return "밝음"
    return "정상"

quality["밝기판정"] = [exposure(r.평균, r.어두운칸, r.밝은칸) for r in quality.itertuples()]

# 어두운 사진은 정규화(5차시)로 고친 뒤 흐림 점수를 다시 잰다
after = []
for r in quality.itertuples():
    img = images[r.파일]
    if r.밝기판정 == "어두움":
        img = cv2.normalize(img, None, 0, 255, cv2.NORM_MINMAX)
    after.append(round(blur_score(img), 1))
quality["고친뒤점수"] = after

def decide(r):
    """밝기 판정 → 흐림의 순서로 최종 처리를 정한다."""
    if r.밝기판정 in ("아주 어두움", "밝음"):
        return "버림(" + r.밝기판정 + ")"
    if r.고친뒤점수 < BLUR_MIN:
        return "버림(흐림)"
    return "고침(밝기)" if r.밝기판정 == "어두움" else "그대로"

quality["처리"] = [decide(r) for r in quality.itertuples()]
cols = ["파일", "평균", "어두운칸", "밝은칸", "흐림점수", "밝기판정", "고친뒤점수", "처리"]
print(quality.loc[quality["처리"] != "그대로", cols].to_string(index=False))
print(quality["처리"].value_counts().to_dict())

# 품질 표를 저장한다. 엑셀에서 한글이 깨지지 않게 utf-8-sig 로
quality.to_csv("out_ch16_quality.csv", index=False, encoding="utf-8-sig")
print("저장: out_ch16_quality.csv")
출력
              파일    평균  어두운칸   밝은칸  흐림점수   밝기판정  고친뒤점수         처리
    apple_09.jpg 142.4 0.000 0.065   3.2     정상    3.2     버림(흐림)
     lime_09.jpg 160.2 0.000 0.058   6.1     정상    6.1     버림(흐림)
     lime_10.png  62.7 0.174 0.000  67.8    어두움  639.4     고침(밝기)
     lime_11.png 228.5 0.000 1.000  39.6     밝음   39.6     버림(밝음)
tangerine_09.jpg 160.6 0.000 0.042   4.8     정상    4.8     버림(흐림)
tangerine_10.png  23.9 1.000 0.000   7.9 아주 어두움    7.9 버림(아주 어두움)
{'그대로': 29, '버림(흐림)': 3, '고침(밝기)': 1, '버림(밝음)': 1, '버림(아주 어두움)': 1}
저장: out_ch16_quality.csv

코드 설명

  • 1단계는 본문의 손계산을 그대로 옮긴 것입니다. 선명한 줄의 분산 5000 은 흐린 줄 1000 의 5배이고, 표준편차(70.7, 31.6)는 분산의 제곱근입니다.
  • 2단계에서 잰 사진은 실습 1의 관문을 통과한 35장입니다. 흐림 점수가 가장 낮은 셋(apple_09.jpg 3.2, tangerine_09.jpg 4.8, lime_09.jpg 6.1)이 일부러 흐리게 만든 사진입니다. 그다음의 7.9, 39.6, 67.8 은 흐린 사진이 아니라 아주 어둡거나(tangerine_10.png), 하얗게 날아가거나(lime_11.png), 어두운(lime_10.png) 사진입니다. 밝기 차이가 작으면 경계의 값도 작아지기 때문입니다.
  • 3단계 그래프에서 점수는 67.8 다음에 203.8 로 크게 뛰어오릅니다. 기준값 100 은 이 넓은 틈의 한가운데쯤에 있어, 조금 바꿔도 결과가 흔들리지 않습니다. 세로축을 로그 눈금으로 그려야 3 과 800 을 한 그래프에서 함께 볼 수 있습니다.
  • 4단계에서 lime_10.png 는 그대로 쟀다면 67.8 로 "흐림"이 되었을 것입니다. 정규화로 밝기를 고친 뒤 다시 재니 639.4 로 올라 남았습니다. 그래서 밝기 판정을 흐림보다 먼저 합니다. 최종 처리는 그대로 29, 버림 5(흐림 3, 밝음 1, 아주 어두움 1), 고침 1 입니다.
← 이전 절16장 · 실습 1 — 폴더 점검: 깨진·작은·채널이 다른 파일 찾기다음 절 →16장 · 실습 3 — 같은 사진 찾기: MD5 와 평균 해시