OpenCV 비전 실무17장 · Batch Processing 과 YOLO 데이터 준비15 / 21← 이전목차다음 →MetaContents

17장. Batch Processing 과 YOLO 데이터 준비

실습 2 — 실패에 강한 일괄 처리

여러 곳에서 받은 그림을 모은 ch17_inbox/ 를 처리합니다. 이 폴더에는 정상 그림 말고도 빈 파일, 깨진 파일, 너무 작은 그림, 그림이 아닌 메모, 하위 폴더가 섞여 있습니다. 어느 파일에서도 멈추지 않고, 파일마다 결과와 이유를 기록하게 만듭니다.

1단계. 폴더 둘러보고 그림만 고르기

Python
from pathlib import Path

inbox = Path("ch17_inbox")
# iterdir(): 폴더 바로 안의 항목을 하나씩 준다. 하위 폴더도 항목 하나로 나온다
for p in sorted(inbox.iterdir()):
    kind = "폴더" if p.is_dir() else f"{p.stat().st_size:,}바이트"   # stat().st_size: 파일 크기
    print(f"{p.name:13}{kind}")

# rglob("*"): 하위 폴더까지 훑는다. 확장자는 소문자로 바꿔 견준다(.PNG 도 잡힌다)
IMAGE_EXT = {".png", ".jpg", ".jpeg"}
targets = sorted(p for p in inbox.rglob("*") if p.is_file() and p.suffix.lower() in IMAGE_EXT)
# relative_to: inbox 를 기준으로 한 경로. as_posix: 사이를 / 로 적는다
print("처리할 파일:", [p.relative_to(inbox).as_posix() for p in targets])
출력
belt_a.png   7,001바이트
belt_b.jpg   4,641바이트
belt_c.PNG   3,499바이트
broken.png   120바이트
empty.png    0바이트
memo.txt     38바이트
old          폴더
tiny.png     135바이트
처리할 파일: ['belt_a.png', 'belt_b.jpg', 'belt_c.PNG', 'broken.png', 'empty.png', 'old/belt_old.png', 'tiny.png']

2단계. 이유를 돌려주는 한 장 처리 함수

Python
import cv2

out_dir = Path("out_ch17_inbox")
out_dir.mkdir(parents=True, exist_ok=True)
MIN_SIDE = 32          # 가로나 세로가 이보다 작으면 학습에 쓰기 어렵다고 본다

def process(path):
    """그림 한 장을 흑백 160x120 으로 저장하고 (결과, 이유) 를 돌려준다."""
    if path.stat().st_size == 0:
        return "실패", "빈 파일"
    img = cv2.imread(str(path))
    if img is None:                                   # 깨진 파일은 오류 대신 None
        return "실패", "읽기 실패"
    h, w = img.shape[:2]
    if min(h, w) < MIN_SIDE:
        return "건너뜀", f"너무 작음({w}x{h})"
    small = cv2.resize(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), (160, 120))
    target = out_dir / (path.stem + ".png")           # stem: 확장자를 뗀 이름. 결과는 모두 png
    if not cv2.imwrite(str(target), small):
        return "실패", "저장 실패"
    return "성공", f"{w}x{h} → 160x120"

# 정상 파일 하나와 깨진 파일 하나에 먼저 시험한다
print(process(inbox / "belt_a.png"))
print(process(inbox / "broken.png"))
출력
('성공', '320x240 → 160x120')
('실패', '읽기 실패')

3단계. 모든 파일에 돌리고 기록 저장하기

Python
import pandas as pd

rows = []
for k, path in enumerate(targets, start=1):
    try:
        status, reason = process(path)
    except Exception as e:                            # 뜻밖의 오류도 멈추지 않고 이름을 적는다
        status, reason = "실패", f"예외 {type(e).__name__}"
    rows.append({"파일": path.relative_to(inbox).as_posix(), "결과": status, "이유": reason})
    print(f"[{k}/{len(targets)}] {rows[-1]['파일']:17} {status} · {reason}")

log = pd.DataFrame(rows)
log.to_csv("out_ch17_inbox_log.csv", index=False, encoding="utf-8-sig")
print("요약:", log["결과"].value_counts().to_dict())
출력
[1/7] belt_a.png        성공 · 320x240 → 160x120
[2/7] belt_b.jpg        성공 · 200x150 → 160x120
[3/7] belt_c.PNG        성공 · 240x320 → 160x120
[4/7] broken.png        실패 · 읽기 실패
[5/7] empty.png         실패 · 빈 파일
[6/7] old/belt_old.png  성공 · 320x240 → 160x120
[7/7] tiny.png          건너뜀 · 너무 작음(24x18)
요약: {'성공': 4, '실패': 2, '건너뜀': 1}

4단계. 다시 돌려도 안전하게: 이미 한 것은 건너뛰기

Python
def process_once(path):
    """결과 파일이 이미 있으면 다시 만들지 않는다. 중간에 끊겨 다시 돌릴 때 쓸모 있다."""
    if (out_dir / (path.stem + ".png")).exists():
        return "건너뜀", "이미 처리함"
    return process(path)

again = [process_once(p) for p in targets]
print("다시 돌린 결과:", pd.Series([s for s, _ in again]).value_counts().to_dict())
for path, (status, reason) in zip(targets, again):
    if reason != "이미 처리함":
        print(" ", path.name, status, reason)
출력
다시 돌린 결과: {'건너뜀': 5, '실패': 2}
  broken.png 실패 읽기 실패
  empty.png 실패 빈 파일
  tiny.png 건너뜀 너무 작음(24x18)

코드 설명

  • p.suffix.lower() in IMAGE_EXT 는 확장자를 소문자로 바꿔 견주므로 belt_c.PNG 도 잡고, memo.txt 는 처음부터 뺍니다.
  • process 는 파일 크기, None 확인, 크기 기준, 저장 결과를 차례로 보고 실패하면 곧바로 (결과, 이유) 를 돌려줍니다. 이유가 기록에 남으므로 나중에 무엇을 손봐야 할지 압니다.
  • try / except Exception 은 process 가 미처 막지 못한 오류가 나도 다음 파일로 넘어가게 하는 안전망입니다. 이번 폴더에서는 예외가 한 번도 나지 않았습니다.
  • process_once 는 결과 파일이 있으면 건너뜁니다. 처리가 중간에 끊겨 다시 돌려도 이미 한 일을 되풀이하지 않습니다.
← 이전 절17장 · 실습 1 — 폴더 일괄 처리 기본다음 절 →17장 · 실습 3 — 픽셀 좌표와 YOLO 좌표 바꾸기