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

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

실습 4 — 주석 검사기로 실수 찾기

ch17_raw/annotations.csv 를 검사해 일부러 섞어 둔 실수를 찾습니다. 줄마다 검사, 이름 검사, 짝 검사, 겹침 검사를 차례로 하고, 찾은 것을 정답 mistakes.csv 와 견줍니다.

1단계. 주석과 그림 크기 읽기

Python
from pathlib import Path
import cv2
import pandas as pd

ann = pd.read_csv("ch17_raw/annotations.csv")
# 그림마다 (세로, 가로) 를 사전에 담는다. 범위 검사에 그림 크기가 필요하다
sizes = {p.name: cv2.imread(str(p)).shape[:2] for p in sorted(Path("ch17_raw").glob("*.png"))}

print("주석 줄:", len(ann), "· 그림 파일:", len(sizes), "장")
print("클래스 이름:", ann["class"].value_counts().to_dict())
print("그림 크기 종류:", set(sizes.values()))
출력
주석 줄: 182 · 그림 파일: 60 장
클래스 이름: {'washer': 67, 'nut': 62, 'bolt': 51, 'blot': 1, 'Nut': 1}
그림 크기 종류: {(240, 320)}

2단계. 줄마다 검사하기

Python
CLASSES = ["bolt", "nut", "washer"]

def check_row(r):
    """주석 한 줄(사전)을 검사해 실수 갈래의 목록을 돌려준다."""
    if r["filename"] not in sizes:                     # 짝 검사: 그림 파일이 없다
        return ["그림 없는 주석"]
    h, w = sizes[r["filename"]]
    x1, y1, x2, y2 = r["x1"], r["y1"], r["x2"], r["y2"]
    found = []
    if r["class"] not in CLASSES:                      # 이름 검사
        found.append("모르는 클래스")
    if x1 > x2 or y1 > y2:
        found.append("뒤집힌 상자")
    if x1 == x2 or y1 == y2:
        found.append("크기 0 상자")
    if min(x1, x2) < 0 or min(y1, y2) < 0 or max(x1, x2) > w or max(y1, y2) > h:
        found.append("범위 밖")
    return found

problems = []
# to_dict("records"): 표를 줄마다 사전 하나인 목록으로 바꾼다(열 이름 class 를 그대로 쓰려고)
for row_no, r in enumerate(ann.to_dict("records")):
    for kind in check_row(r):
        problems.append({"kind": kind, "row": row_no, "filename": r["filename"]})
        print(f"{row_no:3}번 줄 {r['filename']} {r['class']:6} ({r['x1']}, {r['y1']}, {r['x2']}, {r['y2']}) → {kind}")
출력
 13번 줄 part_006.png washer (-7, 69, 20, 100) → 범위 밖
 21번 줄 part_008.png nut    (206, 97, 160, 142) → 뒤집힌 상자
 26번 줄 part_010.png nut    (161, 218, 194, 246) → 범위 밖
 39번 줄 part_014.png blot   (156, 96, 223, 128) → 모르는 클래스
 83번 줄 part_029.png washer (207, 168, 232, 143) → 뒤집힌 상자
101번 줄 part_036.png washer (169, 201, 169, 201) → 크기 0 상자
149번 줄 part_051.png Nut    (113, 181, 146, 211) → 모르는 클래스
180번 줄 part_061.png nut    (120, 80, 158, 116) → 그림 없는 주석
181번 줄 part_061.png bolt   (30, 150, 112, 172) → 그림 없는 주석

3단계. 겹침 검사와 짝 검사

Python
# duplicated(): 앞에 모든 칸이 같은 줄이 있으면 True. 둘째부터 중복으로 친다
for row_no in ann.index[ann.duplicated()]:
    problems.append({"kind": "중복 줄", "row": int(row_no), "filename": ann.loc[row_no, "filename"]})

# 폴더에는 있는데 주석에 한 번도 나오지 않는 그림
for name in sorted(set(sizes) - set(ann["filename"])):
    problems.append({"kind": "주석 없는 그림", "row": "", "filename": name})

found = pd.DataFrame(problems)
print(found["kind"].value_counts().to_dict())
found.to_csv("out_ch17_problems.csv", index=False, encoding="utf-8-sig")
print("찾은 실수", len(found), "건을 out_ch17_problems.csv 에 저장")
출력
{'범위 밖': 2, '뒤집힌 상자': 2, '모르는 클래스': 2, '그림 없는 주석': 2, '주석 없는 그림': 2, '크기 0 상자': 1, '중복 줄': 1}
찾은 실수 12 건을 out_ch17_problems.csv 에 저장

4단계. 정답과 견주기

Python
answer = pd.read_csv("ch17_raw/mistakes.csv", dtype={"row": str}).fillna("")

# (갈래, 줄 번호, 파일) 세 칸을 묶어 집합으로 만들어 견준다
def key_set(df):
    return {(k, str(r), f) for k, r, f in zip(df["kind"], df["row"], df["filename"])}

mine, truth = key_set(found), key_set(answer)
print("정답의 실수:", len(truth), "건")
print("맞힘:", len(mine & truth), "· 놓침:", len(truth - mine), "· 헛짚음:", len(mine - truth))
for item in sorted(truth - mine):
    print("  놓친 것:", item)
for item in sorted(mine - truth):
    print("  헛짚은 것:", item)
출력
정답의 실수: 12 건
맞힘: 12 · 놓침: 0 · 헛짚음: 0

코드 설명

  • sizes 사전에 그림마다 크기를 담아 두면, 줄마다 그림을 다시 읽지 않고 범위를 검사할 수 있습니다. 파일 이름이 사전에 없으면 그 자체가 "그림 없는 주석"입니다.
  • check_row 는 한 줄에 실수가 여럿이면 모두 돌려줍니다. 검사 규칙은 본문 "라벨 검사" 절의 표와 같습니다.
  • ann.duplicated() 는 둘째로 나온 같은 줄부터 True 이고, set(sizes) - set(ann["filename"]) 은 주석에 한 번도 나오지 않은 그림입니다.
  • 정답과 견줄 때는 (갈래, 줄 번호, 파일) 을 묶어 집합의 &(둘 다 있음)와 -(한쪽에만 있음)로 맞힘, 놓침, 헛짚음을 셉니다.
← 이전 절17장 · 실습 3 — 픽셀 좌표와 YOLO 좌표 바꾸기다음 절 →17장 · 실습 5 — YOLO 데이터셋 만들기