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

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

실습 3 — 픽셀 좌표와 YOLO 좌표 바꾸기

상자 좌표를 YOLO 형식으로 바꾸는 함수와 거꾸로 되돌리는 함수를 만듭니다. 본문의 손계산과 견주고, ch17_raw/annotations.csv 의 상자로 바꿨다 되돌렸을 때 얼마나 어긋나는지 잽니다.

1단계. 픽셀 → YOLO 함수와 손계산 견주기

Python
def to_yolo(x1, y1, x2, y2, img_w, img_h):
    """모서리 두 점(픽셀)을 YOLO 의 (xc, yc, w, h) 비율로 바꾼다."""
    xc = (x1 + x2) / 2 / img_w      # 가운데 x 를 그림 가로로 나눈다
    yc = (y1 + y2) / 2 / img_h      # 가운데 y 를 그림 세로로 나눈다
    w = (x2 - x1) / img_w
    h = (y2 - y1) / img_h
    return xc, yc, w, h

# 본문의 손계산: 가로 320, 세로 240, 상자 (40, 60, 120, 100) → 0.25, 0.333333, 0.25, 0.166667
result = to_yolo(40, 60, 120, 100, 320, 240)
print("함수:", [round(v, 6) for v in result])
print("손계산과 같은가:", [round(v, 6) for v in result] == [0.25, 0.333333, 0.25, 0.166667])
출력
함수: [0.25, 0.333333, 0.25, 0.166667]
손계산과 같은가: True

2단계. YOLO → 픽셀 함수로 되돌리기

Python
def to_pixel(xc, yc, w, h, img_w, img_h):
    """YOLO 비율을 모서리 두 점(픽셀, 실수)으로 되돌린다."""
    x1 = (xc - w / 2) * img_w
    y1 = (yc - h / 2) * img_h
    x2 = (xc + w / 2) * img_w
    y2 = (yc + h / 2) * img_h
    return x1, y1, x2, y2

back = to_pixel(*result, 320, 240)            # *result: 네 값을 풀어 차례로 넘긴다
print("되돌린 값:", [round(v, 3) for v in back])

# 그림을 두 배(640x480)로 키웠다고 보고, 같은 비율을 큰 그림 크기로 되돌린다
print("두 배 그림에서:", [round(v, 3) for v in to_pixel(*result, 640, 480)])
출력
되돌린 값: [40.0, 60.0, 120.0, 100.0]
두 배 그림에서: [80.0, 120.0, 240.0, 200.0]

3단계. 실제 주석으로 왕복 오차 재기

Python
import pandas as pd

ann = pd.read_csv("ch17_raw/annotations.csv")
# 실수가 섞인 줄을 피하려고, 그림 안에 있고 뒤집히지 않은 상자만 고른다
ok = ann[(ann.x1 >= 0) & (ann.y1 >= 0) & (ann.x2 <= 320) & (ann.y2 <= 240)
         & (ann.x1 < ann.x2) & (ann.y1 < ann.y2)]
print("고른 상자:", len(ok), "개")

def round_trip_error(digits):
    """비율을 소수 digits 자리로 적었다가 되돌렸을 때 가장 크게 어긋난 칸 수."""
    worst = 0.0
    for r in ok.itertuples():
        yolo = [round(v, digits) for v in to_yolo(r.x1, r.y1, r.x2, r.y2, 320, 240)]
        back = to_pixel(*yolo, 320, 240)
        worst = max(worst, max(abs(a - b) for a, b in zip(back, (r.x1, r.y1, r.x2, r.y2))))
    return worst

for digits in (6, 3, 2):
    print(f"소수 {digits}자리로 적으면 가장 큰 어긋남: {round_trip_error(digits):.4f}칸")
출력
고른 상자: 177 개
소수 6자리로 적으면 가장 큰 어긋남: 0.0002칸
소수 3자리로 적으면 가장 큰 어긋남: 0.2000칸
소수 2자리로 적으면 가장 큰 어긋남: 2.4000칸

4단계. 라벨 한 줄로 쓰고 다시 읽기

Python
CLASSES = ["bolt", "nut", "washer"]         # 차례가 곧 클래스 번호: bolt 0, nut 1, washer 2

r = ok.iloc[0]                               # 첫 상자 하나
xc, yc, w, h = to_yolo(r.x1, r.y1, r.x2, r.y2, 320, 240)
line = f"{CLASSES.index(r['class'])} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}"   # :.6f = 소수 여섯째 자리
print(r.filename, r["class"], (int(r.x1), int(r.y1), int(r.x2), int(r.y2)))
print("라벨 줄:", line)

# 거꾸로: 빈칸으로 나눠 숫자로 바꾸고, 픽셀로 되돌린다
parts = line.split()
cls, vals = int(parts[0]), [float(v) for v in parts[1:]]
print("다시 읽음:", CLASSES[cls], [round(v) for v in to_pixel(*vals, 320, 240)])
출력
part_001.png washer (34, 5, 75, 46)
라벨 줄: 2 0.170313 0.106250 0.128125 0.170833
다시 읽음: washer [34, 5, 75, 46]

코드 설명

  • to_yolo 는 가운데와 크기를 구한 뒤 x 쪽은 그림 가로, y 쪽은 그림 세로로 나눕니다. 본문의 손계산과 같은 값이 나옵니다.
  • to_pixel 은 반대로 가운데에서 크기의 절반을 빼고 더한 뒤 그림 크기를 곱합니다. 같은 비율을 640×480 에 되돌리면 좌표가 모두 두 배가 됩니다. 비율 하나로 어느 크기의 그림에든 상자를 그릴 수 있다는 뜻입니다.
  • 소수 자리를 줄이면 왕복 오차가 커집니다. YOLO 라벨을 소수 여섯째 자리까지 적는 까닭입니다.
  • 라벨 줄은 f-string 의 :.6f 로 쓰고, split() 과 int, float 로 다시 읽습니다. 클래스 번호는 CLASSES.index(이름) 으로 구합니다.
← 이전 절17장 · 실습 2 — 실패에 강한 일괄 처리다음 절 →17장 · 실습 4 — 주석 검사기로 실수 찾기