Ultralytics YOLO27: Trag dich in die Warteliste ein.
Get Started
LektionFortgeschrittene

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.

Lernergebnis

Trainiere yolo26n-obb.pt mit dota8.yaml, zähle die Erkennungen pro Klasse anhand von result.obb und wandle xywhr jeder Vorhersage in eine Achse entlang der langen Seite im Bereich [0, 180) Grad um.

Schnellkurs
Wenn du dich bereits auskennst, findest du hier die Kurzfassung.
  1. Label-Zeile: Klasse, gefolgt von den vier Ecken x1 y1 … x4 y4, normalisiert und in der Reihenfolge entlang des Umfangs.

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

  3. Lies result.obb.xywhr (Drehung in Radiant) oder .xyxyxyxy (Ecken); OBB-Ergebnisse füllen result.boxes nicht aus.

  4. Normalisiere die Ausrichtung anhand der langen Seite: Addiere 90°, wenn w < h, und berechne dann den Winkel modulo 180°.

Praxisnah

Wann die Drehung wichtig ist#

Ultralytics YOLO: Orientierte Bounding-Boxes liegen eng um drei festgemachte Yachten in einem Luftbild; jede ist mit „Ship“ und ihrer Konfidenz beschriftet

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 BoxOrientierte Box
An einem Schiff mit 45° angepasstetwa zur Hälfte Hintergrundeng anliegend
Dichte, gedrehte ReihenBoxen überlappen; NMS verwirft echte ObjekteBoxen sind klar voneinander getrennt
Liefert einen Winkelneinja, als Achse (Modulo 180°)
Ergebnisfeldresult.boxesresult.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.524414

Lies 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.

Annotationseditor der Ultralytics-Plattform mit einem DOTA8-Luftbild und gedrehten Boxen um Tennisplätze, kleine Fahrzeuge und ein Schwimmbecken

Mit DOTA8 trainieren#

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

Trainingsmosaik von DOTA8 mit Luftbildausschnitten: Reihen vertäuter Boote, Tennisplätze, ein Parkplatz und Flugzeuge auf einem Vorfeld, jedes Objekt mit einem orientierten Begrenzungsrahmen und Klassenindex

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-Metriken findest du in metrics.box, aber der IoU-Wert dahinter wird zwischen gedrehten Rechtecken berechnet.

DOTA8 ist ein Funktionstest

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 deg

Zeile 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. xyxyxyxy enthält dieselben Begrenzungsrahmen als vier Eckpunkte, bereit für cv2.polylines.
  • Der Zähler zählt anhand von result.obb.cls: 168 Schiffe und 5 Häfen. Bei OBB-Modellen ist result.boxes gleich None; daher muss Zählcode, der für die Objekterkennung geschrieben wurde, die Felder wechseln.
  • raw gegenüber axis. Das erste Schiff ist 33 × 108 bei −15°: Sein w ist 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:

Autobahnbild mit Erkennungen aus der Inferenz des Gesamtbilds, bei der viele kleine, weit entfernte Fahrzeuge übersehen werden

Die Inferenz mit Kacheln findet sie:

Dasselbe Autobahnbild mit gekachelter Inferenz durch SAHI, bei der deutlich mehr kleine, weit entfernte Fahrzeuge erkannt werden

Stolperfallen#

Keine Vorder- oder Rückseite

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.

Rohe Validierungskurven mit Erkennungsläufen vergleichen

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.

Ausprobieren

Führe obb.py aus und schreibe dann ein Skript, das die Längsachse jedes Schiffs im Hafenbild berechnet, die häufigste Achse auf die nächsten 10° rundet und die Schiffe zählt, deren Abweichung mehr als 30° beträgt. Denk daran, dass 175° und 5° nur 10° auseinanderliegen.

Abgeschlossen, wenn
Du hast die Lektion abgeschlossen, wenn alle folgenden Punkte zutreffen.
  • Dein Skript gibt die Anzahl der Schiffe, die häufigste Achse und die Anzahl der Schiffe aus, deren Abweichung mehr als 30° beträgt.

  • Beim Achsenvergleich wird der Winkel bei 180° umgebrochen, sodass 175° und 5° als 10° auseinanderliegend gezählt werden.

  • Du kannst erklären, warum OBB Bug und Heck nicht unterscheiden kann und was du hinzufügen würdest, wenn du eine Fahrtrichtung 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 it

Die 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.

Wie es weitergeht

Als Nächstes lassen wir die Lokalisierung ganz weg und trainieren einen Klassifikator für vollständige Bilder und Bildausschnitte.