19장. Vision 데이터 파이프라인
실습 3 — 추론 단계 붙이기
18장에서 학습한 모델로 다듬은 그림 18장을 추론하고, 한 행에 물체 하나인 물체 표를 만듭니다. 상자는 다듬기의 비율로 나눠 원래 그림의 좌표로 되돌립니다. 18장 실습 2와 이 장의 실습 2를 먼저 실행해야 합니다.
1단계. 모델 한 번 부르기
import os
from pathlib import Path
import cv2
import pandas as pd
os.environ["YOLO_VERBOSE"] = "False" # 긴 진행 글을 줄인다
from ultralytics import YOLO
if not Path("out_ch18_runs/parts/weights/best.pt").exists():
raise SystemExit("best.pt 가 없습니다. 18차시 실습 2를 먼저 실행하세요")
if not Path("out_ch19_prepped.csv").exists():
raise SystemExit("out_ch19_prepped.csv 가 없습니다. 실습 2를 먼저 실행하세요")
CONFIG = {"conf": 0.25, "size": [320, 240]}
# 반복문 밖에서 한 번만 부른다(본문 "추론 단계")
model = YOLO("out_ch18_runs/parts/weights/best.pt")
print("클래스:", model.names)
prepped = pd.read_csv("out_ch19_prepped.csv", keep_default_na=False)
print("추론할 그림:", len(prepped), "장")클래스: {0: 'bolt', 1: 'nut', 2: 'washer'}
추론할 그림: 18 장2단계. 한 장에 시험하기
img = cv2.imread("out_ch19_prepped/belt_018.png")
# predict: imgsz 학습 크기, conf 신뢰도 기준, device 계산 장치, verbose 진행 글
results = model.predict(img, imgsz=320, conf=CONFIG["conf"], device="cpu", verbose=False)
r = results[0] # 그림 한 장을 넣었으므로 결과도 하나
xyxy = r.boxes.xyxy.cpu().numpy() # 상자 모서리 두 점(픽셀)
conf = r.boxes.conf.cpu().numpy() # 신뢰도
cls = r.boxes.cls.cpu().numpy().astype(int) # 클래스 번호
for (x1, y1, x2, y2), c, k in zip(xyxy, conf, cls):
print(r.names[int(k)], round(float(c), 3), [round(float(v)) for v in (x1, y1, x2, y2)])nut 0.987 [100, 164, 141, 207]
washer 0.983 [229, 186, 268, 224]
bolt 0.973 [63, 27, 122, 133]
nut 0.941 [148, 34, 176, 65]
bolt 0.53 [63, 25, 115, 105]
bolt 0.293 [63, 26, 101, 70]3단계. 추론 단계 함수로 물체 표 만들기
def infer_stage(rows, model, config):
"""추론 단계: 다듬은 그림마다 predict 하고, 상자를 원래 좌표로 되돌려 물체 표를 돌려준다."""
found = []
for row in rows.itertuples():
img = cv2.imread(f"out_ch19_prepped/{row.saved}")
r = model.predict(img, imgsz=config["size"][0], conf=config["conf"], device="cpu", verbose=False)[0]
for (x1, y1, x2, y2), c, k in zip(r.boxes.xyxy.cpu().numpy(), r.boxes.conf.cpu().numpy(),
r.boxes.cls.cpu().numpy().astype(int)):
found.append({"file": row.file, "class": r.names[int(k)], "conf": round(float(c), 3),
# 줄인 그림의 좌표 ÷ 비율 = 원래 그림의 좌표
"x1": round(float(x1) / row.scale), "y1": round(float(y1) / row.scale),
"x2": round(float(x2) / row.scale), "y2": round(float(y2) / row.scale)})
return pd.DataFrame(found, columns=["file", "class", "conf", "x1", "y1", "x2", "y2"])
detections = infer_stage(prepped, model, CONFIG)
detections.to_csv("out_ch19_detections.csv", index=False, encoding="utf-8-sig")
print("찾은 물체:", len(detections), "개 → out_ch19_detections.csv")
print(detections.head(5).to_string(index=False))
print("640x480 그림의 상자:", detections[detections["file"] == "belt_016.png"][["class", "x1", "y1", "x2", "y2"]].values.tolist())찾은 물체: 57 개 → out_ch19_detections.csv
file class conf x1 y1 x2 y2
belt_001.png washer 0.989 211 28 253 71
belt_002.png nut 0.937 140 160 176 199
belt_002.png bolt 0.909 101 81 123 154
belt_002.png nut 0.886 93 184 118 212
belt_003.png washer 0.987 168 176 193 201
640x480 그림의 상자: [['bolt', 406, 117, 606, 209]]4단계. 그림별 개수를 정답과 견주기
# 그림마다 찾은 개수와 정답 개수를 나란히 놓는다(상자 위치까지 맞는지는 프로젝트에서 IoU 로 잰다)
answer = pd.read_csv("ch19_incoming/answer.csv")
got = detections.groupby("file").size()
want = answer[answer["filename"].isin(prepped["file"])].groupby("filename").size()
compare = pd.DataFrame({"찾음": got, "정답": want}).fillna(0).astype(int)
compare["차이"] = compare["찾음"] - compare["정답"]
print(compare.to_string())
print("개수가 다른 그림:", compare[compare["차이"] != 0].index.tolist())
print("클래스별 찾은 개수:", detections["class"].value_counts().to_dict()) 찾음 정답 차이
belt_001.png 1 1 0
belt_002.png 3 3 0
belt_003.png 3 3 0
belt_004.png 2 2 0
belt_005.png 2 2 0
belt_006.png 4 4 0
belt_007.png 3 3 0
belt_008.jpg 1 1 0
belt_009.png 4 4 0
belt_010.png 1 1 0
belt_012.png 4 4 0
belt_014.png 4 4 0
belt_015.png 5 5 0
belt_016.png 1 1 0
belt_017.png 2 2 0
belt_018.png 6 4 2
belt_019.png 4 4 0
belt_020.png 7 5 2
개수가 다른 그림: ['belt_018.png', 'belt_020.png']
클래스별 찾은 개수: {'washer': 25, 'nut': 16, 'bolt': 16}코드 설명
YOLO("out_ch18_runs/parts/weights/best.pt")는 반복문 밖에서 한 번만 부릅니다.predict에 다듬은 그림 배열을 그대로 넣고,imgsz는 CONFIG 의 가로(320)를 씁니다.infer_stage는out_ch19_prepped.csv의scale로 상자를 나눠 원래 그림의 좌표로 되돌립니다.belt_016.png의 상자 좌표는 640×480 기준으로 저장됩니다.- 물체가 하나도 없는 그림이 있어도 표의 열이 정해지도록
pd.DataFrame(found, columns=[...])처럼 열 이름을 미리 줍니다. - 18장 가운데 16장은 찾은 개수가 정답과 같았습니다. 손질한 세 장도 모두 맞았습니다. 감마로 밝힌
belt_004.png는 bolt·washer 2개, 블러를 건belt_008.jpg는 nut 1개를 찾았고,belt_016.png의 bolt 는 원래 좌표 (406, 117, 606, 209) 로 되돌아가 정답 (404, 118, 608, 214) 과 몇 칸 차이로 겹칩니다. 개수가 다른belt_018.png와belt_020.png는 둘 다 2개씩 많았는데, 진짜 bolt 위에 bolt 의 일부만 감싼 상자가 신뢰도 0.29~0.53 으로 겹쳐 나온 것입니다. 그래서 클래스별 개수도 bolt 가 정답(12개)보다 4개 많은 16개, nut 16개, washer 25개로 모두 57개입니다.