17장. Batch Processing 과 YOLO 데이터 준비
실습 2 — 실패에 강한 일괄 처리
여러 곳에서 받은 그림을 모은 ch17_inbox/ 를 처리합니다. 이 폴더에는 정상 그림 말고도 빈 파일, 깨진 파일, 너무 작은 그림, 그림이 아닌 메모, 하위 폴더가 섞여 있습니다. 어느 파일에서도 멈추지 않고, 파일마다 결과와 이유를 기록하게 만듭니다.
1단계. 폴더 둘러보고 그림만 고르기
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단계. 이유를 돌려주는 한 장 처리 함수
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단계. 모든 파일에 돌리고 기록 저장하기
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단계. 다시 돌려도 안전하게: 이미 한 것은 건너뛰기
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는 결과 파일이 있으면 건너뜁니다. 처리가 중간에 끊겨 다시 돌려도 이미 한 일을 되풀이하지 않습니다.