回転オブジェクト向けの回転バウンディングボックス
四隅で表す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.5244144つの点として読み取ります。(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には、同じボックスが4つの頂点として格納されており、cv2.polylinesにそのまま渡せます。- Counterは
result.obb.clsから数を集計します。船が168隻、港が5か所です。OBBモデルではresult.boxesがNoneにあたるため、検出用に書いたカウントコードではフィールドを切り替える必要があります。 rawとaxis。 1隻目の船は33 × 108、角度は−15°です。そのwは短辺なので、船体の向きは−15° + 90° = 75°です。2隻目は116 × 33、角度は21°で、すでに長辺が先になっています。生の角度を読むと、1隻目の軸は90°ずれてしまいます。正規化すれば、すべての船の角度が同じ意味を持ち、船同士やフレーム間で比較できます。
このルールは、インストールされたソースコードに基づいています。予測された回転角は[−π/4, 3π/4) — およそ−45°から135° — の範囲にあり、同じ矩形を(w, h, θ)または(h, w, θ + 90°)として表せます。wが長辺だと決めつけないでください。
小さな物体には縮小ではなくタイルを使う#
航空画像の物体は小さいため、学習時のimgszより小さなサイズで検証またはデプロイすると、再現率が急速に低下します。モデルの入力サイズより大きいsatelliteタイルは縮小せず、重なりを持たせて分割してください。そうしないと、30ピクセルの車が5ピクセルのぼやけた塊になってしまいます。SAHIのタイル推論ガイドで手法を説明しています。検出モデルを使って高速道路のフレーム全体を推論すると、遠くにある多くの車両を見逃します。

タイル推論では、これらの車両を検出できます。

よくある落とし穴#
症状: 「heading」がフレーム間で180°反転する、または船が後ろ向きに進んでいるように見えます。原因: 180°回転した長方形は同じ長方形なので、OBBが示すのは軸であり、方向ではありません。対処: 船やフォークリフトの向きを知る必要がある場合は、船首またはフォーク先端にposeキーポイントを追加するか、切り抜き画像に対する分類器を追加してください。
解答を表示
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では船首と船尾を区別できません。船首にポーズキーポイントを設定すれば、正確な向きを取得できます。