OpenCV 비전 실무18장 · OpenCV 전처리와 YOLO 연계16 / 21← 이전목차다음 →MetaContents

18장. OpenCV 전처리와 YOLO 연계

실습 3 — 학습한 모델로 부품 찾기

실습 2에서 학습한 out_ch18_runs/parts/weights/best.pt 를 불러와 검증용 사진에서 부품을 찾습니다. 찾은 상자와 17장의 정답 상자를 한 그림에 함께 그려 격자로 보고, 신뢰도 기준값 0.25 와 0.5 에서 찾은 개수를 견줍니다. 실습 2를 먼저 실행해야 합니다.

1단계. 학습한 모델로 한 장 추론하기

Python
import os
from pathlib import Path
os.environ["YOLO_VERBOSE"] = "False"
from ultralytics import YOLO
import cv2
import matplotlib.pyplot as plt

plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False

weights = Path("out_ch18_runs/parts/weights/best.pt")
if not weights.exists():
    raise SystemExit("best.pt 가 없습니다. 18차시 실습 2를 먼저 실행하세요")
model = YOLO("out_ch18_runs/parts/weights/best.pt")
print("이 모델의 클래스:", model.names)        # 결과: bolt, nut, washer 셋만 남는다

root = Path("out_ch17_yolo")
val_paths = sorted((root / "images" / "val").glob("*.png"))
img = cv2.imread(str(val_paths[0]))
r = model.predict(img, imgsz=320, conf=0.25, device="cpu", verbose=False)[0]
print(val_paths[0].name, "에서 찾은 물체:")
for box, p, c in zip(r.boxes.xyxy.cpu().numpy(), r.boxes.conf.cpu().numpy(), r.boxes.cls.cpu().numpy().astype(int)):
    print(f"  {r.names[int(c)]:6} 신뢰도 {float(p):.2f}  상자 {[int(v) for v in box]}")
출력
이 모델의 클래스: {0: 'bolt', 1: 'nut', 2: 'washer'}
part_001.png 에서 찾은 물체:
  washer 신뢰도 0.98  상자 [34, 5, 74, 46]
  bolt   신뢰도 0.95  상자 [175, 127, 228, 199]

2단계. 정답 상자와 찾은 상자를 함께 그리기

Python
def read_labels(label_path, w, h):
    """YOLO 라벨을 읽어 [(클래스번호, (x1, y1, x2, y2)), ...] 픽셀 상자로 돌려준다(17차시의 되돌리는 식)."""
    boxes = []
    for line in label_path.read_text(encoding="utf-8").split("\n"):
        if not line.strip():
            continue
        c, xc, yc, bw, bh = line.split()
        xc, yc, bw, bh = float(xc), float(yc), float(bw), float(bh)
        boxes.append((int(c), ((xc - bw / 2) * w, (yc - bh / 2) * h, (xc + bw / 2) * w, (yc + bh / 2) * h)))
    return boxes

def detect(model, img, conf=0.25):
    """추론해서 [(클래스번호, 신뢰도, (x1, y1, x2, y2)), ...] 로 돌려준다."""
    r = model.predict(img, imgsz=320, conf=conf, device="cpu", verbose=False)[0]
    return [(int(c), float(p), tuple(float(v) for v in b))
            for b, p, c in zip(r.boxes.xyxy.cpu().numpy(), r.boxes.conf.cpu().numpy(),
                               r.boxes.cls.cpu().numpy().astype(int))]

def draw(img, gts, preds):
    """정답은 초록 얇은 선, 찾은 상자는 빨강 굵은 선과 이름·신뢰도로 그린 사본을 돌려준다."""
    out = img.copy()
    for c, (x1, y1, x2, y2) in gts:
        cv2.rectangle(out, (round(x1), round(y1)), (round(x2), round(y2)), (0, 255, 0), 1)
    for c, p, (x1, y1, x2, y2) in preds:
        cv2.rectangle(out, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), 2)
        cv2.putText(out, f"{model.names[c]} {p:.2f}", (int(x1), max(int(y1) - 4, 12)),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255), 1)
    return out

fig, axes = plt.subplots(2, 3, figsize=(11, 5.6), layout="constrained")
for ax, p in zip(axes.ravel(), val_paths[:6]):
    img = cv2.imread(str(p))
    h, w = img.shape[:2]
    gts = read_labels(root / "labels" / "val" / (p.stem + ".txt"), w, h)
    preds = detect(model, img)
    ax.imshow(cv2.cvtColor(draw(img, gts, preds), cv2.COLOR_BGR2RGB))
    ax.set_title(f"{p.name}  정답 {len(gts)} · 찾음 {len(preds)}", fontsize=11)
    ax.axis("off")
fig.suptitle("초록: 정답 상자, 빨강: 학습한 모델이 찾은 상자 (conf 0.25)")
plt.show()
검증용 그림 여섯 장에 정답 상자는 초록 얇은 선, 학습한 모델이 찾은 상자는 빨강 굵은 선과 이름, 신뢰도로 그린 격자

3단계. 신뢰도 기준값 0.25 와 0.5 견주기

Python
import pandas as pd

rows = []
for p in val_paths:
    img = cv2.imread(str(p))
    h, w = img.shape[:2]
    gts = read_labels(root / "labels" / "val" / (p.stem + ".txt"), w, h)
    # 0.25 로 한 번만 추론하고, 0.5 는 그 결과에서 신뢰도로 다시 거른다(결과가 같고 빠르다)
    preds = detect(model, img, conf=0.25)
    rows.append({"그림": p.name, "정답": len(gts),
                 "conf 0.25": len(preds),
                 "conf 0.5": sum(1 for _, s, _ in preds if s >= 0.5)})
table = pd.DataFrame(rows)
print(table.to_string(index=False))
print("합계:", table[["정답", "conf 0.25", "conf 0.5"]].sum().to_dict())
출력
          그림  정답  conf 0.25  conf 0.5
part_001.png   2          2         2
part_004.png   1          1         1
part_005.png   3          3         3
part_022.png   4          4         4
part_026.png   4          5         5
part_030.png   3          3         3
part_031.png   1          1         1
part_034.png   2          2         2
part_037.png   1          1         1
part_049.png   3          3         3
part_053.png   2          2         2
합계: {'정답': 26, 'conf 0.25': 27, 'conf 0.5': 27}

교안 PC 에서는 기준값 0.25 와 0.5 모두 27개를 찾아 정답 26개보다 하나 많았습니다. 그림별로 보면 part_026.png 만 정답 4개에 5개를 찾았고, 나머지 10장은 정답 수와 같습니다. 2단계 격자의 part_026.png 를 보면 옆으로 누운 긴 볼트 하나에 빨강 상자 두 개가 겹쳐 있습니다. 0.25 와 0.5 의 개수가 같다는 것은 찾은 상자가 모두 신뢰도 0.5 이상이라는 뜻입니다. 이 가상 그림에서는 모델이 자신 있게 답해서, 기준값을 0.5 로 올려도 놓치는 것이 늘지 않았습니다. 남는 상자가 헛것인지는 실습 4에서 IoU 로 짝지어 확인합니다.

코드 설명

  • YOLO("out_ch18_runs/parts/weights/best.pt") 는 실습 2에서 학습한 가중치를 불러옵니다. 이 모델의 names 에는 bolt, nut, washer 셋만 있습니다.
  • read_labels 는 17장에서 본 되돌리는 식(가운데 ± 크기의 절반, 곱하기 그림 크기)으로 정답을 픽셀 상자로 바꿉니다. 정답과 찾은 상자를 같은 꼴 (x1, y1, x2, y2) 로 맞춰야 견줄 수 있습니다.
  • detect 는 추론 결과를 (클래스, 신뢰도, 상자) 묶음의 목록으로 바꿉니다. 실습 4, 5와 프로젝트도 같은 꼴을 씁니다.
  • 기준값 0.5 의 개수는 0.25 로 추론한 결과에서 신뢰도 0.5 이상만 다시 세어 구했습니다. 신뢰도가 높은 상자는 기준값을 바꿔도 그대로 남으므로, 처음부터 0.5 로 추론한 결과와 같은 개수가 나옵니다.
  • 격자의 초록 얇은 선(정답)이 빨강 굵은 선(찾은 상자)에 거의 가려진 것은 두 상자가 잘 겹쳤다는 뜻입니다. 초록 선이 따로 보이면 놓친 것, 짝 없는 빨강 상자는 헛것일 수 있습니다.
← 이전 절18장 · 실습 2 — 부품 데이터셋으로 전이 학습다음 절 →18장 · 실습 4 — IoU 와 정밀도·재현율 계산기