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

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

실습 5 — 씨앗 고정 랜덤 증강 함수로 한 장에서 여덟 장 만들기

변환마다 확률로 걸지 고르고, 걸면 세기를 뽑는 증강 함수를 만듭니다. 사과 사진 한 장에서 여덟 장을 만들어 무엇이 걸렸는지 표로 보고, 같은 씨앗이면 같은 결과가 나오는지 확인합니다. 격자는 out_ch16_aug8.png 로 저장합니다.

1단계. 증강 함수 만들기

Python
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

def augment(img, rng):
    """변환마다 확률로 걸지 정하고, 걸면 세기를 뽑는다. (증강한 그림, 건 변환 목록) 을 돌려준다."""
    out = img.copy()
    h, w = out.shape[:2]
    done = []
    # 1) 좌우 뒤집기: 절반의 확률
    if rng.random() < 0.5:
        out = cv2.flip(out, 1)
        done.append("뒤집기")
    # 2) 작은 회전: 0.7 의 확률, -15 ~ 15도. 빈 곳은 거울 반사로 채운다
    if rng.random() < 0.7:
        angle = rng.uniform(-15, 15)
        M = cv2.getRotationMatrix2D((w / 2, h / 2), angle, 1.0)
        out = cv2.warpAffine(out, M, (w, h), borderMode=cv2.BORDER_REFLECT)
        done.append(f"회전 {angle:+.0f}도")
    # 3) 자르고 키우기: 0.5 의 확률, 가로·세로의 80~95% 를 아무 자리에서 잘라 원래 크기로
    if rng.random() < 0.5:
        s = rng.uniform(0.8, 0.95)
        ch, cw = int(h * s), int(w * s)
        y0 = int(rng.integers(0, h - ch + 1))
        x0 = int(rng.integers(0, w - cw + 1))
        out = cv2.resize(out[y0:y0 + ch, x0:x0 + cw], (w, h), interpolation=cv2.INTER_LINEAR)
        done.append(f"자르기 {s:.2f}")
    # 4) 밝기·대비: 0.7 의 확률, alpha 0.8~1.2, beta -30~30
    if rng.random() < 0.7:
        alpha, beta = rng.uniform(0.8, 1.2), rng.uniform(-30, 30)
        out = np.clip(out.astype(np.float32) * alpha + beta, 0, 255).astype(np.uint8)
        done.append(f"밝기 ×{alpha:.2f}{beta:+.0f}")
    # 5) 색 흔들기: 0.3 의 확률, H 를 -3 ~ 3 만큼
    if rng.random() < 0.3:
        shift = int(rng.integers(-3, 4))
        hsv = cv2.cvtColor(out, cv2.COLOR_BGR2HSV)
        hsv[:, :, 0] = (hsv[:, :, 0].astype(np.int32) + shift) % 180
        out = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
        done.append(f"색상 {shift:+d}")
    # 6) 블러: 0.2 의 확률, 3×3 Gaussian
    if rng.random() < 0.2:
        out = cv2.GaussianBlur(out, (3, 3), 0)
        done.append("블러")
    # 7) 노이즈: 0.2 의 확률, 표준편차 5~12
    if rng.random() < 0.2:
        sigma = rng.uniform(5, 12)
        out = np.clip(out.astype(np.float64) + rng.normal(0, sigma, out.shape), 0, 255).astype(np.uint8)
        done.append(f"노이즈 {sigma:.0f}")
    return out, done

img = cv2.imread("ch16_raw/apple_04.png")
test_out, test_done = augment(img, np.random.default_rng(1610))
print("모양:", img.shape, "→", test_out.shape, "/ 건 변환:", test_done)
출력
모양: (150, 200, 3) → (150, 200, 3) / 건 변환: ['뒤집기', '회전 -13도']

2단계. 한 장에서 여덟 장 만들기

Python
rng = np.random.default_rng(1610)   # 씨앗 고정: 언제 돌려도 같은 여덟 장
outs, logs = [], []
for k in range(8):
    out, done = augment(img, rng)
    outs.append(out)
    logs.append({"번호": k + 1, "건 변환": ", ".join(done) if done else "(없음)", "개수": len(done)})
log = pd.DataFrame(logs)
print(log.to_string(index=False))
print("한 장에 평균", round(float(log["개수"].mean()), 2), "가지")
출력
 번호                                   건 변환  개수
  1                           뒤집기, 회전 -13도   2
  2          회전 -4도, 자르기 0.89, 밝기 ×0.92+22   3
  3          회전 -5도, 자르기 0.84, 밝기 ×0.97+16   3
  4                            밝기 ×0.90+27   1
  5              뒤집기, 자르기 0.85, 밝기 ×1.13-3   3
  6                       회전 +7도, 자르기 0.83   2
  7 뒤집기, 회전 +4도, 밝기 ×0.96-14, 색상 +2, 노이즈 7   5
  8           회전 -8도, 자르기 0.84, 밝기 ×0.87-0   3
한 장에 평균 2.75 가지

3단계. 원본과 여덟 장을 격자로 저장하기

Python
fig, axes = plt.subplots(3, 3, figsize=(9, 7.2))
panels = [("원본", img)] + [(f"{k + 1}번", out) for k, out in enumerate(outs)]
for ax, (title, out) in zip(axes.flat, panels):
    ax.imshow(cv2.cvtColor(out, cv2.COLOR_BGR2RGB))
    ax.set_title(title)
    ax.axis("off")
plt.tight_layout()
plt.savefig("out_ch16_aug8.png", dpi=80)
print("저장: out_ch16_aug8.png")
plt.show()
출력
저장: out_ch16_aug8.png
사과 사진의 원본과 씨앗 1610 으로 랜덤 증강한 여덟 장을 3행 3열로 놓은 격자

4단계. 같은 씨앗이면 같은 결과인지 확인하기

Python
def make_eight(seed):
    """씨앗 하나로 여덟 장을 만든다."""
    r = np.random.default_rng(seed)
    return [augment(img, r)[0] for _ in range(8)]

again = make_eight(1610)
other = make_eight(1611)
# np.array_equal: 모양과 모든 값이 같으면 True
print("씨앗 1610 다시:", all(np.array_equal(a, b) for a, b in zip(outs, again)))
print("씨앗 1611     :", all(np.array_equal(a, b) for a, b in zip(outs, other)))
# 여덟 장이 서로 다른지: 같은 그림이 두 번 나오지 않았는지 센다
distinct = len({out.tobytes() for out in outs})
print("서로 다른 그림:", distinct, "장 / 8장")
출력
씨앗 1610 다시: True
씨앗 1611     : False
서로 다른 그림: 8 장 / 8장

코드 설명

  • augment 는 변환마다 rng.random() < 확률 로 걸지 고르고, 건다면 rng.uniform·rng.integers 로 세기를 뽑습니다. 무엇을 걸었는지 done 목록에 적어 돌려주므로, 이상한 사진이 나오면 어떤 변환 때문인지 바로 압니다.
  • 씨앗 1610 으로 만든 여덟 장은 한 장에 평균 2.75 가지 변환이 걸렸습니다. 가장 적게는 1가지(4번, 밝기만), 가장 많게는 5가지(7번)입니다. 확률을 0.5, 0.7 처럼 정해 두면 사진마다 걸리는 변환의 조합이 달라집니다.
  • 격자를 보면 6번은 자르기(0.83)로 사과의 위쪽 잎이 그림 끝에 걸렸습니다. 증강 결과를 눈으로 확인해야 하는 까닭입니다. 과일이 더 많이 잘리는 일이 잦다면 자르는 비율의 아래 끝(0.8)을 올립니다.
  • 4단계에서 같은 씨앗 1610 으로 다시 만들면 여덟 장이 모두 같고(True), 씨앗 1611 이면 달라집니다(False). 여덟 장은 서로 모두 달랐습니다. 씨앗만 적어 두면 다른 PC 에서도 같은 증강 사진을 다시 만들 수 있습니다.
← 이전 절16장 · 실습 4 — 증강을 한 장씩 격자로 보기다음 절 →16장 · 프로젝트 1 — 학습용 묶음 만들기: 정제 → 크기 통일 → 증강