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

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

실습 4 — 증강을 한 장씩 격자로 보기

사과 사진 한 장(apple_04.png)에 기하 증강 네 가지와 값 증강 다섯 가지를 하나씩 걸어 격자로 봅니다. 마지막 단계에서는 정답이 바뀌는 변환을 직접 만들어 보고, 귤의 색상을 크게 돌리면 H 값이 라임에 가까워지는지 숫자로 확인합니다. 격자 그림은 out_ch16_aug_grid.png 로 저장합니다.

1단계. 기하 증강 네 가지

Python
import cv2
import numpy as np
import matplotlib.pyplot as plt

plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False

img = cv2.imread("ch16_raw/apple_04.png")
h, w = img.shape[:2]

geo = {}
# 좌우 뒤집기: flipCode 1(14차시)
geo["좌우 뒤집기"] = cv2.flip(img, 1)
# 15도 회전: 빈 곳은 BORDER_REFLECT(가장자리를 거울처럼 비춰 채우기)
M = cv2.getRotationMatrix2D((w / 2, h / 2), 15, 1.0)
geo["15도 회전"] = cv2.warpAffine(img, M, (w, h), borderMode=cv2.BORDER_REFLECT)
# 자르고 키우기: 가로·세로의 85% 를 가운데에서 잘라 원래 크기로 키운다
ch, cw = int(h * 0.85), int(w * 0.85)
y0, x0 = (h - ch) // 2, (w - cw) // 2
geo["85% 자르고 키우기"] = cv2.resize(img[y0:y0 + ch, x0:x0 + cw], (w, h), interpolation=cv2.INTER_LINEAR)
# 옮기기: 오른쪽 25, 위로 10(2×3 표의 셋째 칸, 14차시)
T = np.float32([[1, 0, 25], [0, 1, -10]])
geo["옮기기"] = cv2.warpAffine(img, T, (w, h), borderMode=cv2.BORDER_REFLECT)

for name, out in geo.items():
    # 자리만 옮겼는지 보려고 크기와 평균 밝기를 견준다
    print(f"{name:10} 크기 {out.shape}, 평균 {round(float(out.mean()), 1)} (원본 {round(float(img.mean()), 1)})")
출력
좌우 뒤집기     크기 (150, 200, 3), 평균 144.4 (원본 144.4)
15도 회전     크기 (150, 200, 3), 평균 145.0 (원본 144.4)
85% 자르고 키우기 크기 (150, 200, 3), 평균 139.0 (원본 144.4)
옮기기        크기 (150, 200, 3), 평균 151.3 (원본 144.4)

2단계. 밝기·색·노이즈 증강 다섯 가지

Python
val = {}
# 밝기 +40: 소수로 더하고 0~255 로 자른다(5차시)
val["밝기 +40"] = np.clip(img.astype(np.float32) + 40, 0, 255).astype(np.uint8)
# 대비 0.7배: 128 을 기준으로 차이를 줄인다
val["대비 0.7배"] = np.clip((img.astype(np.float32) - 128) * 0.7 + 128, 0, 255).astype(np.uint8)
# 색상 +3: HSV 의 H 에 더하고 180 으로 나눈 나머지(3차시)
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
hsv[:, :, 0] = (hsv[:, :, 0].astype(np.int32) + 3) % 180
val["색상 +3"] = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
# 블러: 3×3 Gaussian(6차시)
val["블러 3×3"] = cv2.GaussianBlur(img, (3, 3), 0)
# 노이즈: 평균 0, 표준편차 10, 씨앗 1610(6차시와 같은 꼴)
rng = np.random.default_rng(1610)
val["노이즈 10"] = np.clip(img.astype(np.float64) + rng.normal(0, 10, img.shape), 0, 255).astype(np.uint8)

gray_mean = lambda x: round(float(cv2.cvtColor(x, cv2.COLOR_BGR2GRAY).mean()), 1)
for name, out in val.items():
    # 사과 한가운데(세로 75, 가로 100)의 BGR 값이 어떻게 바뀌었는지 본다
    print(f"{name:8} 평균 밝기 {gray_mean(out):6} / 사과 한가운데 {[int(v) for v in out[75, 100]]}")
print(f"{'원본':8} 평균 밝기 {gray_mean(img):6} / 사과 한가운데 {[int(v) for v in img[75, 100]]}")
출력
밝기 +40   평균 밝기  180.4 / 사과 한가운데 [85, 85, 240]
대비 0.7배  평균 밝기  136.6 / 사과 한가운데 [69, 69, 178]
색상 +3    평균 밝기  141.9 / 사과 한가운데 [44, 60, 200]
블러 3×3   평균 밝기  140.7 / 사과 한가운데 [45, 45, 200]
노이즈 10   평균 밝기  140.3 / 사과 한가운데 [52, 48, 202]
원본       평균 밝기  140.7 / 사과 한가운데 [45, 45, 200]

3단계. 격자로 띄우고 저장하기

Python
panels = [("원본", img)] + list(geo.items()) + list(val.items())
fig, axes = plt.subplots(2, 5, figsize=(12, 4.6))
for ax, (name, out) in zip(axes.flat, panels):
    ax.imshow(cv2.cvtColor(out, cv2.COLOR_BGR2RGB))
    ax.set_title(name)
    ax.axis("off")
plt.tight_layout()
# 저장을 먼저 하고 띄운다
plt.savefig("out_ch16_aug_grid.png", dpi=80)
print("저장: out_ch16_aug_grid.png")
plt.show()
출력
저장: out_ch16_aug_grid.png
사과 사진의 원본과 기하 증강 네 가지, 값 증강 다섯 가지를 2행 5열로 놓은 격자

4단계. 정답이 바뀌는 변환 확인하기

Python
# 오른쪽 화살표 그림을 그린다: arrowedLine(그림, 시작점, 끝점, 색, 굵기, 선 종류, tipLength=화살촉 길이 비율)
arrow = np.full((120, 160, 3), 235, np.uint8)
cv2.arrowedLine(arrow, (30, 60), (130, 60), (40, 40, 200), 12, cv2.LINE_AA, tipLength=0.4)
# 숫자 6 을 그린다
six = np.full((120, 160, 3), 235, np.uint8)
cv2.putText(six, "6", (58, 95), cv2.FONT_HERSHEY_SIMPLEX, 3.0, (40, 40, 40), 6, cv2.LINE_AA)

# 귤의 색상을 30 돌리면 H 가 어디로 가나: 과일 칸(채도가 높은 칸)의 H 중앙값을 본다
def fruit_hue(bgr):
    hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV)
    mask = hsv[:, :, 1] > 150                      # S(채도)가 높은 칸 = 과일 몸통
    return int(np.median(hsv[:, :, 0][mask]))

tang = cv2.imread("ch16_raw/tangerine_07.png")
hsv = cv2.cvtColor(tang, cv2.COLOR_BGR2HSV)
hsv[:, :, 0] = (hsv[:, :, 0].astype(np.int32) + 30) % 180
moved = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
lime = cv2.imread("ch16_raw/lime_08.png")
print("귤의 H:", fruit_hue(tang), "→ 색상 +30:", fruit_hue(moved), "/ 라임의 H:", fruit_hue(lime))

fig, axes = plt.subplots(2, 3, figsize=(9, 4.6))
pairs = [(arrow, cv2.flip(arrow, 1), "오른쪽 화살표", "좌우 뒤집기"),
         (six, cv2.rotate(six, cv2.ROTATE_180), "숫자 6", "180도 돌리기"),
         (tang, moved, "귤", "색상 +30")]
for k, (before, after, t1, t2) in enumerate(pairs):
    axes[0, k].imshow(cv2.cvtColor(before, cv2.COLOR_BGR2RGB))
    axes[0, k].set_title(t1)
    axes[1, k].imshow(cv2.cvtColor(after, cv2.COLOR_BGR2RGB))
    axes[1, k].set_title(t2)
for ax in axes.flat:
    ax.axis("off")
plt.tight_layout()
plt.show()
출력
귤의 H: 15 → 색상 +30: 45 / 라임의 H: 47
오른쪽 화살표와 좌우로 뒤집은 왼쪽 화살표, 숫자 6 과 180도 돌린 9, 귤과 색상을 30 돌려 라임 색이 된 귤을 2행 3열로 놓은 그림

코드 설명

  • 1단계의 기하 증강은 모두 크기 (150, 200, 3) 를 지킵니다. 좌우 뒤집기는 칸을 옮기기만 해서 평균(144.4)이 원본과 똑같습니다. 회전과 옮기기는 빈 곳을 반사로 채우고, 자르기는 가장자리를 버리므로 평균이 조금씩 달라집니다.
  • 2단계에서 사과 한가운데 칸은 원본 [45, 45, 200] 이 밝기 +40 에서 [85, 85, 240], 대비 0.7배에서 [69, 69, 178] 이 됩니다. 대비는 128 에서 먼 값일수록 많이 당겨지므로 빨강(200)이 가장 많이 줄었습니다. 색상 +3 은 [44, 60, 200] 으로 G 가 올라 주황빛이 조금 돌았습니다.
  • 3단계의 격자는 plt.savefig 를 plt.show() 앞에 불러 out_ch16_aug_grid.png 로 저장합니다. 위 줄은 자리가 바뀌고 아래 줄은 값이 바뀐 것을 한눈에 견줄 수 있습니다.
  • 4단계의 fruit_hue 는 채도(S)가 150 을 넘는 칸, 곧 과일 몸통의 H 중앙값을 잽니다. 귤은 15 인데 색상을 30 돌리면 45 가 되어, 라임의 47 과 거의 같아집니다. 사진은 귤인데 색은 라임이니, 색으로 과일을 가르는 데이터에서 이 변환은 정답을 흔듭니다.
← 이전 절16장 · 실습 3 — 같은 사진 찾기: MD5 와 평균 해시다음 절 →16장 · 실습 5 — 씨앗 고정 랜덤 증강 함수로 한 장에서 여덟 장 만들기