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

포즈 추정 및 OBB에서는 탐지 대신 OBB를 선택해야 하는 경우를 설명합니다. 실용적인 판단 기준은 다음과 같습니다. 인접 객체를 둘러싼 축 정렬 바운딩 박스가 객체 자체보다 더 많이 겹치면 NMS가 실제 탐지 결과를 억제해 집계 수가 실제보다 낮아집니다. 항공 영상이 대표적인 사례이며, 따라서 사전 학습된 yolo26n-obb.pt은 imgsz=1024에서 DOTA로 학습됩니다.
| 축 정렬 바운딩 박스 | 방향 지정 박스 | |
|---|---|---|
| 45° 방향 선박에 맞춤 | 배경이 약 절반 포함됨 | 정밀함 |
| 촘촘하게 회전된 행 | 바운딩 박스가 겹쳐 NMS가 실제 객체를 제거함 | 바운딩 박스가 깔끔하게 분리됨 |
| 각도 제공 | 아니요 | 예, 축 형태로 제공(180° 기준 나머지) |
| 결과 필드 | result.boxes | result.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으로 학습하기#
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.832OBB 메트릭은 metrics.box에 있으며, 해당 메트릭의 기반이 되는 IoU는 회전된 사각형 간에 계산됩니다.
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 타일 추론 가이드에서 이 기법을 설명합니다. 탐지기를 사용해 고속도로 프레임 전체를 추론하면 멀리 있는 차량을 많이 놓칩니다:

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

주의할 점#
증상: 프레임마다 180°씩 바뀌는 "방향" 또는 뒤로 항해하는 것처럼 보이는 선박이 나타납니다. 원인: 180° 회전한 직사각형은 원래 직사각형과 같으므로 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로 선수와 선미를 구분할 수 없습니다. 선수에 포즈 키포인트를 추가하면 실제 방향을 얻을 수 있습니다.