Orientierte Bounding-Boxes für gedrehte Objekte
Labele OBBs mit vier Ecken, trainiere yolo26n-obb.pt mit DOTA8 und wandle xywhr-Vorhersagen in eine stabile Ausrichtungsachse um, ohne über die verschiedenen Winkelkonventionen zu stolpern.
Schiffe in einem Hafen, Pakete auf einem Förderband und Autos auf einem Parkplatz aus der Vogelperspektive haben dasselbe Problem: Achsenparallele Boxen um gedrehte, dicht gepackte Objekte überlappen einander und nehmen zu viel Hintergrund auf. Daher löscht NMS echte Objekte, und die Zählungen fallen zu niedrig aus. Orientierte Bounding-Boxes passen jedes Objekt in ein gedrehtes Rechteck ein. Das verbessert die Zählung in dichten Szenen und liefert nebenbei einen Winkel. Dabei gelten Winkelkonventionen, die bei fast jeder ersten Integration für Probleme sorgen – beim ersten Schiff im Hafenbild dieser Lektion wird eine Drehung von −15° angegeben, obwohl der Rumpf entlang eines Winkels von 75° liegt.
Praxisnah
Wann die Drehung wichtig ist#

Pose-Schätzung und OBB behandelt, wann OBB gegenüber der Erkennung vorzuziehen ist. Der Praxistest: Wenn sich achsenparallele Bounding-Boxes benachbarter Objekte stärker überlappen als die Objekte selbst, unterdrückt NMS echte Erkennungen und die Zählwerte fallen zu niedrig aus. Luftbilder sind der klassische Anwendungsfall. Deshalb wird das vortrainierte Modell yolo26n-obb.pt mit DOTA auf imgsz=1024 trainiert.
| Achsenparallele Box | Orientierte Box | |
|---|---|---|
| An einem Schiff mit 45° angepasst | etwa zur Hälfte Hintergrund | eng anliegend |
| Dichte, gedrehte Reihen | Boxen überlappen; NMS verwirft echte Objekte | Boxen sind klar voneinander getrennt |
| Liefert einen Winkel | nein | ja, als Achse (Modulo 180°) |
| Ergebnisfeld | result.boxes | result.obb |
Das xyxyxyxy-Label-Format#
Eine OBB-Label-Zeile besteht aus dem Klassenindex, gefolgt von den vier Ecken, normalisiert und in der Reihenfolge entlang des Umfangs. Eine echte DOTA8-Zeile (Klasse 1 ist ship):
1 0.813477 0.523437 0.814453 0.504882 0.87207 0.504882 0.87207 0.524414Lies die Zeile als vier Punkte: (0.813, 0.523) → (0.814, 0.505) → (0.872, 0.505) → (0.872, 0.524). Dieses Schiff liegt fast waagerecht und ist etwa 0.059 der Bildbreite und 0.019 der Bildhöhe groß. Ultralytics wandelt jeden Satz von Ecken intern in Mittelpunkt, Breite, Höhe und Drehung (xywhr) um, indem es das Rechteck mit der kleinsten Fläche einpasst. Daher lassen sich auch leicht unrunde Vierecke aus einem Annotationswerkzeug für das Training verwenden. Der OBB-Datensatz-Leitfaden dokumentiert das Format, und convert_dota_to_yolo_obb in ultralytics.data.converter konvertiert ursprüngliche DOTA-Annotationen.

Mit DOTA8 trainieren#
DOTA8 ist ein kleiner Datensatz mit orientierten Begrenzungsrahmen, der zum Testen von Pipelines entwickelt wurde:

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-Metriken findest du in metrics.box, aber der IoU-Wert dahinter wird zwischen gedrehten Rechtecken berechnet.
Der Wert ist hoch, weil DOTA8 eine Teilmenge der Vortrainingsdaten ist. Er belegt, dass die Pipeline läuft, sagt aber nichts über deine Bilder aus.
Vorhersagen auslesen und den Winkel normalisieren#
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 degZeile für Zeile:
xywhr: (173, 5)— 173 Erkennungen, jeweils mit Mittelpunkt-x, Mittelpunkt-y, Breite und Höhe in Pixeln sowie einer Drehung in Radiant.xyxyxyxyenthält dieselben Begrenzungsrahmen als vier Eckpunkte, bereit fürcv2.polylines.- Der Zähler zählt anhand von
result.obb.cls: 168 Schiffe und 5 Häfen. Bei OBB-Modellen istresult.boxesgleichNone; daher muss Zählcode, der für die Objekterkennung geschrieben wurde, die Felder wechseln. rawgegenüberaxis. Das erste Schiff ist 33 × 108 bei −15°: Seinwist die kurze Seite, daher verläuft der Rumpf entlang −15° + 90° = 75°. Das zweite Schiff ist 116 × 33 bei 21° und hat bereits zuerst die lange Seite. Liest du die Rohwinkel aus, ist die Achse des ersten Schiffs um 90° falsch; nach der Normalisierung bedeutet der Winkel bei jedem Schiff dasselbe und lässt sich zwischen Schiffen und Einzelbildern vergleichen.
Die Regel stammt aus dem installierten Quellcode: Die vorhergesagte Drehung liegt im Bereich [−π/4, 3π/4) — ungefähr −45° bis 135° — und dasselbe Rechteck kann (w, h, θ) oder (h, w, θ + 90°) sein. Gehe niemals davon aus, dass w die lange Seite ist.
Kleine Objekte brauchen Kacheln, keine Verkleinerung#
Luftbildobjekte sind klein, und die Validierung oder Bereitstellung mit einer geringeren Auflösung als der beim Training verwendeten imgsz lässt den Recall schnell sinken. Teile Satelliten-Kacheln, die größer als die Modelleingabe sind, in überlappende Kacheln auf, statt sie zu verkleinern; sonst wird ein 30 Pixel großes Auto zu einem 5 Pixel großen Fleck. Der Leitfaden zur gekachelten Inferenz mit SAHI zeigt die Methode; bei einem Autobahnbild übersieht ein Detektor bei der Inferenz des Gesamtbilds viele weit entfernte Fahrzeuge:

Die Inferenz mit Kacheln findet sie:

Stolperfallen#
Symptom: Eine „Ausrichtung“, die sich zwischen Bildern um 180° dreht, oder Schiffe, die scheinbar rückwärts fahren. Ursache: Ein um 180° gedrehtes Rechteck ist dasselbe Rechteck. OBB liefert daher eine Achse, keine Ausrichtung. Lösung: Wenn du wissen musst, in welche Richtung ein Schiff oder Gabelstapler zeigt, füge einen Pose-Schlüsselpunkt am Bug oder an den Gabelspitzen hinzu oder verwende einen Klassifikator für den Zuschnitt.
Symptom: OBB-Präzisions-Recall-Kurven sehen anders aus als bei deinen Erkennungsläufen mit ähnlichen Daten. Ursache: Die OBB-Validierung verwendet standardmäßig conf=0.01, während andere Aufgaben 0.001 verwenden. Lösung: Vergleiche die mAP-Werte oder lege conf explizit fest, wenn du übereinstimmende Präzisions-Recall-Kurven benötigst.
Lösung anzeigen
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 itDie meisten Boote liegen etwa parallel bei 20°; die 32 Schiffe außerhalb der Achse sind Ausreißer, die eine Regel zur Hafenüberwachung zur Überprüfung markieren würde. OBB kann Bug und Heck nicht unterscheiden, da ein Rechteck bei einer Drehung um 180° symmetrisch bleibt; ein Pose-Schlüsselpunkte am Bug würde die tatsächliche Fahrtrichtung liefern.