17장. Batch Processing 과 YOLO 데이터 준비
실습 4 — 주석 검사기로 실수 찾기
ch17_raw/annotations.csv 를 검사해 일부러 섞어 둔 실수를 찾습니다. 줄마다 검사, 이름 검사, 짝 검사, 겹침 검사를 차례로 하고, 찾은 것을 정답 mistakes.csv 와 견줍니다.
1단계. 주석과 그림 크기 읽기
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단계. 줄마다 검사하기
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단계. 겹침 검사와 짝 검사
# 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단계. 정답과 견주기
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"])은 주석에 한 번도 나오지 않은 그림입니다.- 정답과 견줄 때는 (갈래, 줄 번호, 파일) 을 묶어 집합의
&(둘 다 있음)와-(한쪽에만 있음)로 맞힘, 놓침, 헛짚음을 셉니다.