OpenCV 비전 실무19장 · Vision 데이터 파이프라인16 / 21← 이전목차다음 →MetaContents

19장. Vision 데이터 파이프라인

실습 3 — 추론 단계 붙이기

18장에서 학습한 모델로 다듬은 그림 18장을 추론하고, 한 행에 물체 하나인 물체 표를 만듭니다. 상자는 다듬기의 비율로 나눠 원래 그림의 좌표로 되돌립니다. 18장 실습 2와 이 장의 실습 2를 먼저 실행해야 합니다.

1단계. 모델 한 번 부르기

Python
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단계. 한 장에 시험하기

Python
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단계. 추론 단계 함수로 물체 표 만들기

Python
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단계. 그림별 개수를 정답과 견주기

Python
# 그림마다 찾은 개수와 정답 개수를 나란히 놓는다(상자 위치까지 맞는지는 프로젝트에서 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개입니다.
← 이전 절19장 · 실습 2 — 다듬기 함수와 단계 잇기다음 절 →19장 · 실습 4 — 시각화와 보고