Ориентированные рамки для повёрнутых объектов
Размечай OBB по четырём углам, обучи yolo26n-obb.pt на DOTA8 и преобразуй предсказания xywhr в устойчивую ось ориентации, не запутавшись в соглашениях об углах.
У кораблей в гавани, посылок на ленте и автомобилей на парковке, снятых сверху, одна и та же проблема: осевые рамки повёрнутых и плотно расположенных объектов перекрывают друг друга и захватывают фон, поэтому NMS удаляет реальные объекты, а итоговые количества занижаются. Ориентированные рамки точно охватывают каждый объект повёрнутым прямоугольником, что упрощает подсчёт на плотных сценах и сразу даёт угол. Но с углами связаны соглашения, которые сбивают с толку почти всех при первой интеграции: на изображении гавани из этого урока поворот первого корабля равен −15°, хотя его корпус расположен под углом 75°.
Практические занятия
Когда важен поворот#

В материале Оценка позы и OBB рассказывается, когда стоит выбрать OBB вместо детекции. Практическая проверка: если осевые рамки соседних объектов перекрываются сильнее, чем сами объекты, NMS подавляет истинные детекции, и подсчёт оказывается заниженным. Классический пример — аэрофотоснимки; поэтому предобученная модель yolo26n-obb.pt обучена на DOTA с параметром imgsz=1024.
| Осевая рамка | Ориентированная рамка | |
|---|---|---|
| Рамка корабля под углом 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, а convert_dota_to_yolo_obb в ultralytics.data.converter преобразует исходные аннотации 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.832Метрики OBB находятся в 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.- Счётчик подсчитывает объекты из
result.obb.cls: 168 кораблей и 5 гаваней. Для моделей OBBresult.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 корабля, отклонившиеся от этого направления, — выбросы, которые правило мониторинга гавани отметило бы для проверки. OBB не позволяет отличить нос от кормы, поскольку прямоугольник симметричен при повороте на 180°; ключевая точка позы на носу позволила бы определить истинное направление.