Riquadri di delimitazione orientati per oggetti ruotati
Etichetta OBB con quattro angoli, addestra yolo26n-obb.pt su DOTA8 e converti le previsioni xywhr in un asse di orientamento stabile senza incappare nelle convenzioni sugli angoli.
Navi in porto, pacchi su un nastro trasportatore e auto in un parcheggio viste dall'alto condividono lo stesso problema: i riquadri allineati agli assi attorno a oggetti ruotati e ravvicinati si sovrappongono e includono troppo sfondo, quindi NMS elimina oggetti reali e il conteggio risulta inferiore al reale. I riquadri di delimitazione orientati racchiudono ogni oggetto in un rettangolo ruotato, risolvendo i problemi di conteggio nelle scene dense e fornendo gratuitamente un angolo. Le convenzioni associate all'angolo traggono in inganno quasi tutte le prime integrazioni: nell'immagine del porto di questa lezione, la prima nave indica una rotazione di −15°, ma lo scafo è orientato a 75°.
Pratico
Quando la rotazione è importante#

Stima della posa e OBB spiega quando scegliere OBB invece del rilevamento. La verifica pratica: se i riquadri di delimitazione allineati agli assi attorno a oggetti vicini si sovrappongono più degli oggetti stessi, NMS sopprime le rilevazioni reali e il conteggio risulta inferiore al reale. Le immagini aeree sono il caso classico, ed è per questo che il modello preaddestrato yolo26n-obb.pt viene addestrato su DOTA a imgsz=1024.
| Riquadro allineato agli assi | Riquadro orientato | |
|---|---|---|
| Adattato a una nave a 45° | circa metà sfondo | aderente |
| File dense di oggetti ruotati | i riquadri si sovrappongono; NMS elimina oggetti reali | i riquadri sono ben separati |
| Fornisce un angolo | no | sì, come asse (modulo 180°) |
| Campo dei risultati | result.boxes | result.obb |
Il formato xyxyxyxy delle etichette#
Una riga di etichetta OBB contiene l'indice della classe seguito dai quattro angoli, normalizzati e disposti in ordine lungo il perimetro. Una riga reale di DOTA8 (la classe 1 è ship):
1 0.813477 0.523437 0.814453 0.504882 0.87207 0.504882 0.87207 0.524414Leggila come quattro punti: (0.813, 0.523) → (0.814, 0.505) → (0.872, 0.505) → (0.872, 0.524). Questa nave è quasi orizzontale: è larga circa 0.059 e alta 0.019 rispetto all'immagine. Internamente, Ultralytics converte ogni insieme di angoli in centro, larghezza, altezza e rotazione (xywhr) adattando il rettangolo di area minima, così è possibile addestrare anche su quadrilateri leggermente irregolari provenienti da uno strumento di annotazione. La guida ai dataset OBB descrive il formato, mentre convert_dota_to_yolo_obb in ultralytics.data.converter converte le annotazioni originali di DOTA.

Addestra su DOTA8#
DOTA8 è un piccolo dataset con bounding box orientate, creato per testare le pipeline:

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.832Le metriche OBB si trovano in metrics.box, ma la IoU alla loro base è calcolata tra rettangoli ruotati.
Il punteggio è alto perché DOTA8 è una porzione dei dati di preaddestramento. Dimostra che la pipeline viene eseguita, ma non dice nulla sulle tue immagini.
Leggi le predizioni e normalizza l'angolo#
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 degAnalisi riga per riga:
xywhr: (173, 5)— 173 rilevamenti, ciascuno con centro x, centro y, larghezza e altezza in pixel e una rotazione in radianti.xyxyxyxycontiene gli stessi riquadri come quattro punti d'angolo, pronti percv2.polylines.- Il contatore conta a partire da
result.obb.cls: 168 navi e 5 porti. Nei modelli OBB,result.boxesèNone, quindi il codice di conteggio scritto per il rilevamento deve usare campi diversi. rawrispetto aaxis. La prima nave misura 33 × 108 a −15°: il suowè il lato corto, quindi lo scafo è orientato a −15° + 90° = 75°. La seconda misura 116 × 33 a 21° e ha già il lato lungo per primo. Se leggi gli angoli grezzi, l'asse della prima nave è sbagliato di 90°; dopo la normalizzazione, l'angolo di ogni nave ha lo stesso significato e può essere confrontato tra navi e fotogrammi.
La regola deriva dal codice sorgente installato: la rotazione predetta è compresa nell'intervallo [−π/4, 3π/4) — all'incirca da −45° a 135° — e lo stesso rettangolo può essere (w, h, θ) oppure (h, w, θ + 90°). Non dare mai per scontato che w sia il lato lungo.
Per gli oggetti piccoli servono riquadri, non immagini ridotte#
Gli oggetti ripresi dall'alto sono piccoli e validare o distribuire con una imgsz inferiore a quella di addestramento riduce rapidamente il richiamo. Suddividi i riquadri satellitari più grandi dell'input del modello in riquadri sovrapposti, invece di ridimensionarli: un'auto di 30 pixel diventerebbe una macchia di 5 pixel. La guida all'inferenza a riquadri SAHI illustra la tecnica; in un fotogramma autostradale, un rilevatore che analizza l'intera immagine non rileva molti veicoli lontani:

mentre l'inferenza a riquadri li trova:

Insidie#
Sintomo: un «orientamento» che cambia di 180° tra un frame e l'altro, oppure navi che sembrano navigare all'indietro. Causa: un rettangolo ruotato di 180° è lo stesso rettangolo, quindi OBB fornisce un asse, non un orientamento. Soluzione: se ti serve sapere in che direzione è rivolto una nave o un carrello elevatore, aggiungi un keypoint di posa sulla prua o sulle punte delle forche, oppure un classificatore applicato al ritaglio.
Sintomo: curve precision-recall OBB diverse da quelle dei tuoi rilevamenti su dati simili. Causa: la validazione OBB usa conf=0.01 come valore predefinito, mentre gli altri task usano 0.001. Soluzione: confronta i valori mAP oppure imposta esplicitamente conf quando ti servono curve precisione-richiamo corrispondenti.
Mostra la soluzione
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 itLa maggior parte delle barche è allineata parallelamente, a circa 20°; le 32 navi fuori asse sono i casi anomali che una regola di monitoraggio del porto segnalerebbe per una verifica. OBB non può distinguere la prua dalla poppa perché un rettangolo è simmetrico rispetto a una rotazione di 180°; un keypoint di posa sulla prua fornirebbe la direzione effettiva.