Ultralytics YOLO27: 대기자 명단에 등록하세요.
Get Started
강의중급

회전된 객체의 방향성 바운딩 박스

네 꼭짓점 OBB에 라벨을 붙이고, DOTA8에서 yolo26n-obb.pt를 학습하며, 각도 규칙에 혼동되지 않도록 xywhr 예측값을 안정적인 방향 축으로 변환합니다.

항구의 선박, 컨베이어 벨트 위의 택배, 위에서 내려다본 주차장의 자동차에는 공통적인 문제가 있습니다. 회전되고 촘촘하게 배치된 객체를 축 정렬 바운딩 박스로 표시하면 서로 겹치고 배경까지 포함하므로 NMS가 실제 객체를 삭제해 객체 수가 실제보다 적게 집계됩니다. 방향성 바운딩 박스는 각 객체에 회전된 직사각형을 맞춰 밀집된 장면에서 객체를 정확히 집계하고 별도의 비용 없이 각도까지 제공합니다. 다만 각도에는 거의 모든 첫 통합 작업을 헷갈리게 하는 규칙이 있습니다. 이 강의의 항구 이미지에서 첫 번째 선박의 회전 각도는 −15°로 표시되지만 선체는 75° 방향으로 놓여 있습니다.

학습 성과

dota8.yaml에서 yolo26n-obb.pt을 학습하고, result.obb에서 클래스별 탐지 수를 집계한 다음, 모든 예측의 xywhr을 0~180° 범위의 장변 축으로 변환합니다.

속성 과정
이미 사용법을 알고 계신다면, 간단히 요약해 드리겠습니다.
  1. 라벨 행: 클래스 다음에 네 개의 꼭짓점 x1 y1 … x4 y4을 둘레를 따라 순서대로 정규화해 지정합니다.

  2. yolo obb train data=dota8.yaml model=yolo26n-obb.pt epochs=1 imgsz=320.

  3. result.obb.xywhr(라디안 단위 회전) 또는 .xyxyxyxy(꼭짓점)을 읽습니다. OBB 결과에서는 result.boxes가 채워지지 않습니다.

  4. 방향을 장변 기준으로 정규화하려면 w < h일 때 90°를 더한 다음 각도를 180°로 나눈 나머지를 취합니다.

실습형

회전이 중요한 경우#

항공 사진 속 계류 중인 요트 세 척을 단단히 감싸는 Ultralytics YOLO 방향성 바운딩 박스. 각 바운딩 박스에는 신뢰도와 Ship 라벨이 표시되어 있음

포즈 추정 및 OBB에서는 탐지 대신 OBB를 선택해야 하는 경우를 설명합니다. 실용적인 판단 기준은 다음과 같습니다. 인접 객체를 둘러싼 축 정렬 바운딩 박스가 객체 자체보다 더 많이 겹치면 NMS가 실제 탐지 결과를 억제해 집계 수가 실제보다 낮아집니다. 항공 영상이 대표적인 사례이며, 따라서 사전 학습된 yolo26n-obb.pt은 imgsz=1024에서 DOTA로 학습됩니다.

축 정렬 바운딩 박스방향 지정 박스
45° 방향 선박에 맞춤배경이 약 절반 포함됨정밀함
촘촘하게 회전된 행바운딩 박스가 겹쳐 NMS가 실제 객체를 제거함바운딩 박스가 깔끔하게 분리됨
각도 제공아니요예, 축 형태로 제공(180° 기준 나머지)
결과 필드result.boxesresult.obb

xyxyxyxy 라벨 형식#

OBB 라벨 행은 클래스 인덱스 다음에 둘레를 따라 순서대로 정규화된 네 꼭짓점을 나열합니다. 실제 DOTA8 행(클래스 1은 ship)은 다음과 같습니다:

1 0.813477 0.523437 0.814453 0.504882 0.87207 0.504882 0.87207 0.524414

네 개의 점으로 읽으면 다음과 같습니다. (0.813, 0.523) → (0.814, 0.505) → (0.872, 0.505) → (0.872, 0.524). 이 선박은 거의 수평으로 놓여 있으며 이미지 너비의 약 0.059, 높이의 약 0.019를 차지합니다. 내부적으로 Ultralytics는 각 꼭짓점 집합에 최소 면적 직사각형을 맞춰 중심, 너비, 높이, 회전(xywhr)으로 변환하므로 주석 도구에서 만들어진 약간 직사각형이 아닌 사각형도 학습에 사용할 수 있습니다. OBB 데이터셋 가이드에서 형식을 설명하며, ultralytics.data.converter의 convert_dota_to_yolo_obb는 원본 DOTA 주석을 변환합니다.

DOTA8 항공 이미지에서 테니스 코트, 소형 차량, 수영장 주변에 회전된 바운딩 박스가 표시된 Ultralytics Platform 주석 편집기

DOTA8으로 학습하기#

DOTA8은 파이프라인 테스트용으로 구축된 소규모 회전 박스 데이터셋입니다:

항공 타일의 DOTA8 학습 모자이크: 계류된 보트, 테니스 코트, 주차장, 계류장에 있는 비행기가 행별로 배치되어 있으며, 각 객체에는 회전 박스와 클래스 인덱스가 표시되어 있습니다

from ultralytics import YOLO

model = YOLO("yolo26n-obb.pt")
model.train(data="dota8.yaml", epochs=1, imgsz=320, device="cpu")
metrics = model.val()
print(f"OBB mAP50: {metrics.box.map50:.3f}  mAP50-95: {metrics.box.map:.3f}")
OBB mAP50: 0.995  mAP50-95: 0.832

OBB 메트릭은 metrics.box에 있으며, 해당 메트릭의 기반이 되는 IoU는 회전된 사각형 간에 계산됩니다.

DOTA8은 스모크 테스트용 데이터셋

DOTA8은 사전 학습 데이터의 일부이기 때문에 점수가 높습니다. 이 점수는 파이프라인이 실행된다는 사실만 증명할 뿐, 여러분의 이미지에 대해서는 아무것도 알려주지 않습니다.

예측 결과를 읽고 각도를 정규화하기#

import math
from collections import Counter

from ultralytics import YOLO

def long_side_axis(w, h, r):
    """Angle of the box's long side in degrees, in [0, 180), whichever side the model called width."""
    deg = math.degrees(r) + (90 if w < h else 0)
    return deg % 180

model = YOLO("yolo26n-obb.pt")  # trained on DOTAv1, 15 aerial classes
result = model("https://ultralytics.com/images/boats.jpg", verbose=False)[0]
obb = result.obb.cpu()

print("xywhr:", tuple(obb.xywhr.shape), " xyxyxyxy:", tuple(obb.xyxyxyxy.shape))
print(Counter(result.names[int(c)] for c in obb.cls.tolist()))
for (cx, cy, w, h, r), conf in list(zip(obb.xywhr.tolist(), obb.conf.tolist()))[:5]:
    print(f"{conf:.2f} center=({cx:.0f},{cy:.0f}) size={w:.0f}x{h:.0f} raw={math.degrees(r):4.0f} deg  axis={long_side_axis(w, h, r):4.0f} deg")
xywhr: (173, 5)  xyxyxyxy: (173, 4, 2)
Counter({'ship': 168, 'harbor': 5})
0.87 center=(619,850) size=33x108 raw= -15 deg  axis=  75 deg
0.86 center=(1650,937) size=116x33 raw=  21 deg  axis=  21 deg
0.86 center=(1105,571) size=124x35 raw=  21 deg  axis=  21 deg
0.86 center=(1596,1037) size=129x35 raw=  25 deg  axis=  25 deg
0.86 center=(825,940) size=34x99 raw= -15 deg  axis=  75 deg

각 줄의 의미:

  • xywhr: (173, 5) — 탐지 결과 173개로, 각 결과에는 픽셀 단위의 중심 x, 중심 y, 너비, 높이와 라디안 단위의 회전값이 포함됩니다. xyxyxyxy에는 동일한 박스가 꼭짓점 네 개로 표시되어 있으며, cv2.polylines에 바로 사용할 수 있습니다.
  • Counter는 result.obb.cls에서 항목을 셉니다. 배는 168척, 항구는 5곳입니다. OBB 모델에서 result.boxes은 None이므로, 탐지용으로 작성한 카운팅 코드는 필드를 바꿔야 합니다.
  • raw과 axis 비교. 첫 번째 배는 33 × 108이고 각도는 −15°입니다. 배의 w가 짧은 변이므로 선체 방향은 −15° + 90° = 75°입니다. 두 번째 배는 116 × 33이고 각도는 21°로, 긴 변이 먼저 표시되어 있습니다. 원시 각도를 읽으면 첫 번째 배의 축이 90° 잘못됩니다. 정규화하면 모든 배의 각도가 동일한 기준을 의미하므로 배와 프레임 간에 비교할 수 있습니다.

이 규칙은 설치된 소스 코드에서 확인할 수 있습니다. 예측 회전값은 [−π/4, 3π/4) 범위, 즉 대략 −45°에서 135° 사이에 있으며, 동일한 사각형을 (w, h, θ) 또는 (h, w, θ + 90°)로 나타낼 수 있습니다. w가 긴 변이라고 가정하지 마세요.

작은 객체에는 축소가 아니라 타일이 필요합니다#

항공 이미지의 객체는 작으며, 학습 imgsz보다 작은 크기로 검증하거나 배포하면 재현율이 빠르게 떨어집니다. 축소하는 대신 모델 입력보다 큰 위성 이미지를 겹치는 타일로 분할하세요. 그렇지 않으면 30픽셀 자동차가 5픽셀 얼룩이 됩니다. SAHI 타일 추론 가이드에서 이 기법을 설명합니다. 탐지기를 사용해 고속도로 프레임 전체를 추론하면 멀리 있는 차량을 많이 놓칩니다:

전체 이미지 추론 결과를 표시한 고속도로 프레임으로, 작고 멀리 있는 차량을 많이 놓쳤습니다

타일 추론을 사용하면 다음과 같이 차량을 찾아냅니다:

SAHI 타일 추론을 적용한 동일한 고속도로 프레임으로, 작고 멀리 있는 차량을 훨씬 많이 탐지합니다

주의할 점#

앞면과 뒷면을 구분할 수 없음

증상: 프레임마다 180°씩 바뀌는 "방향" 또는 뒤로 항해하는 것처럼 보이는 선박이 나타납니다. 원인: 180° 회전한 직사각형은 원래 직사각형과 같으므로 OBB는 방향이 아니라 축을 제공합니다. 해결 방법: 선박이나 지게차가 향하는 방향을 알아야 한다면 선수나 포크 끝에 포즈 키포인트를 추가하거나 크롭에 분류기를 적용하세요.

원시 검증 곡선과 탐지 실행 결과 비교

증상: 유사한 데이터에서 OBB 정밀도-재현율 곡선이 탐지 실행 결과와 다르게 나타납니다. 원인: OBB 검증은 기본값으로 conf=0.01을 사용하지만, 다른 작업은 0.001을 사용합니다. 해결 방법: mAP 값을 비교하거나, 곡선을 일치시켜야 할 때는 conf을 명시적으로 설정합니다(정밀도-재현율 곡선).

사용해 보기

obb.py을 실행한 다음, 항구 이미지에 있는 모든 배의 긴 변 축을 계산하고 가장 흔한 축을 10° 단위로 반올림하여 구한 뒤, 해당 축에서 30° 넘게 벗어난 배의 수를 세는 스크립트를 작성하세요. 175°와 5°는 불과 10° 차이라는 점을 기억하세요.

완료 조건
다음 조건을 모두 충족하면 레슨을 완료한 것입니다.
  • 스크립트에는 배의 수, 가장 흔한 축, 해당 축에서 30° 넘게 벗어난 배의 수가 출력됩니다.

  • 축 비교는 180°에서 순환하므로 175°와 5°는 10° 차이로 계산됩니다.

  • OBB로 선수와 선미를 구분할 수 없는 이유와, 선수 방향이 필요한 경우 추가할 항목을 설명할 수 있습니다.

정답 보기
import math
from collections import Counter

from ultralytics import YOLO

result = YOLO("yolo26n-obb.pt")("https://ultralytics.com/images/boats.jpg", verbose=False)[0]
axes = []
for (cx, cy, w, h, r), cls in zip(result.obb.xywhr.cpu().tolist(), result.obb.cls.tolist()):
    if result.names[int(cls)] == "ship":
        axes.append((math.degrees(r) + (90 if w < h else 0)) % 180)  # long-side axis in [0, 180)

def axis_gap(a, b):
    """Smallest difference between two axes; 175 and 5 degrees are only 10 apart."""
    d = abs(a - b) % 180
    return min(d, 180 - d)

common = Counter(round(a / 10) * 10 % 180 for a in axes).most_common(1)[0][0]
off = [a for a in axes if axis_gap(a, common) > 30]
print(f"{len(axes)} ships, most common axis ~{common} deg, {len(off)} more than 30 deg off it")
168 ships, most common axis ~20 deg, 32 more than 30 deg off it

대부분의 보트는 약 20° 방향으로 나란히 놓여 있으며, 축에서 벗어난 32척은 항구 모니터링 규칙이 검토 대상으로 표시할 이상치입니다. 사각형은 180° 회전해도 대칭이므로 OBB로 선수와 선미를 구분할 수 없습니다. 선수에 포즈 키포인트를 추가하면 실제 방향을 얻을 수 있습니다.

다음 단계

다음으로 위치 추정을 완전히 제외하고 전체 이미지와 크롭을 분류하는 분류기를 학습합니다.