Boîtes englobantes orientées pour les objets en rotation
Annote des OBB à quatre coins, entraîne yolo26n-obb.pt sur DOTA8 et transforme les prédictions xywhr en un axe d’orientation stable sans te laisser piéger par les conventions d’angle.
Les navires dans un port, les colis sur un tapis roulant et les voitures dans un parking vus de dessus ont un problème commun : les boîtes alignées sur les axes autour d’objets en rotation et très rapprochés se chevauchent et englobent trop d’arrière-plan. NMS supprime alors des objets réels et les comptages sont trop faibles. Les boîtes englobantes orientées ajustent un rectangle en rotation à chaque objet, ce qui améliore le comptage dans les scènes denses et fournit gratuitement un angle. Mais les conventions d’angle sont un piège dans presque toute première intégration : sur l’image du port de cette leçon, le premier navire affiche une rotation de −15°, alors que sa coque est orientée à 75°.
Pratique
Quand la rotation compte#

Estimation de pose et OBB explique quand préférer les OBB à la détection. Le test pratique : si les boîtes englobantes alignées sur les axes autour d’objets voisins se chevauchent davantage que les objets eux-mêmes, NMS supprimera de vraies détections et les comptages seront trop faibles. L’imagerie aérienne est le cas classique, d’où l’entraînement du modèle préentraîné yolo26n-obb.pt sur DOTA avec imgsz=1024.
| Boîte alignée sur les axes | Boîte orientée | |
|---|---|---|
| Ajustement sur un navire à 45° | environ la moitié de l’image est de l’arrière-plan | ajustement serré |
| Rangées denses et en rotation | les boîtes se chevauchent ; NMS élimine de vrais objets | les boîtes sont bien séparées |
| Fournit un angle | non | oui, sous forme d’axe (modulo 180°) |
| Champ de résultat | result.boxes | result.obb |
Format d’étiquette xyxyxyxy#
Une ligne d’étiquette OBB contient l’indice de classe suivi des quatre coins normalisés, dans l’ordre autour du périmètre. Voici une vraie ligne de DOTA8 (la classe 1 correspond à ship) :
1 0.813477 0.523437 0.814453 0.504882 0.87207 0.504882 0.87207 0.524414Lis-la comme quatre points : (0.813, 0.523) → (0.814, 0.505) → (0.872, 0.505) → (0.872, 0.524). Ce navire est presque horizontal : sa largeur représente environ 0.059 de l’image et sa hauteur 0.019. En interne, Ultralytics convertit chaque ensemble de coins en centre, largeur, hauteur et rotation (xywhr) en ajustant le rectangle de surface minimale ; un quadrilatère légèrement irrégulier issu d’un outil d’annotation peut donc tout de même servir à l’entraînement. Le guide des jeux de données OBB décrit le format, et convert_dota_to_yolo_obb dans ultralytics.data.converter convertit les annotations DOTA d’origine.

Entraîne-toi sur DOTA8#
DOTA8 est un petit jeu de données de boîtes orientées conçu pour tester les pipelines :

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.832Les métriques OBB se trouvent dans metrics.box, mais l’IoU est calculée entre des rectangles pivotés.
Le score est élevé parce que DOTA8 est un extrait des données de préentraînement. Il prouve que le pipeline s'exécute ; il ne dit rien sur tes images.
Lire les prédictions et normaliser l’angle#
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 degLigne par ligne :
xywhr: (173, 5)— 173 détections, chacune définie par les coordonnées x et y du centre, la largeur et la hauteur en pixels, ainsi qu’une rotation en radians.xyxyxyxycontient les mêmes boîtes sous forme de quatre points d’angle, prêtes pourcv2.polylines.- Le compteur compte à partir de
result.obb.cls: 168 bateaux et 5 ports.result.boxescorrespond àNonepour les modèles OBB ; le code de comptage écrit pour la détection doit donc changer de champs. rawpar rapport àaxis. Le premier bateau mesure 33 × 108 à −15° : sonwest le petit côté, donc la coque est orientée à −15° + 90° = 75°. Le deuxième mesure 116 × 33 à 21° ; son grand côté est déjà indiqué en premier. Si tu lis les angles bruts, l’axe du premier bateau est décalé de 90° ; après normalisation, l’angle de chaque bateau a la même signification et peut être comparé entre bateaux et entre images.
La règle vient du code source installé : la rotation prédite se situe dans [−π/4, 3π/4) — soit environ −45° à 135° — et un même rectangle peut être (w, h, θ) ou (h, w, θ + 90°). Ne pars jamais du principe que w correspond au grand côté.
Pour les petits objets, utilise des tuiles plutôt que de réduire la taille#
Les objets vus du ciel sont petits, et valider ou déployer en dessous de la taille d’entraînement imgsz fait rapidement baisser le rappel. Découpe les tuiles satellitaires plus grandes que l’entrée du modèle en tuiles qui se chevauchent au lieu de les réduire ; sinon, une voiture de 30 pixels devient une tache de 5 pixels. Le guide d’inférence par tuiles SAHI présente cette technique ; sur une image d’autoroute traitée par un détecteur, l’inférence sur l’image entière manque de nombreux véhicules éloignés :

L’inférence par tuiles les détecte :

Pièges#
Symptôme : un « cap » qui bascule de 180° d'une image à l'autre, ou des navires qui semblent naviguer à reculons. Cause : un rectangle pivoté de 180° reste le même rectangle ; OBB donne donc un axe, pas un cap. Solution : si tu dois savoir dans quelle direction pointe un navire ou un chariot élévateur, ajoute un point clé de pose à la proue ou aux extrémités des fourches, ou un classificateur sur le recadrage.
Symptôme : les courbes précision-rappel OBB semblent différentes de celles de tes exécutions de détection sur des données similaires. Cause : la validation OBB utilise conf=0.01 par défaut, tandis que les autres tâches utilisent 0.001. Solution : compare les valeurs mAP ou définis explicitement conf lorsque tu as besoin de courbes précision-rappel concordantes.
Afficher la solution
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 plupart des bateaux sont parallèles, autour de 20° ; les 32 bateaux hors axe sont les valeurs aberrantes qu’une règle de surveillance portuaire signalerait pour vérification. Les OBB ne permettent pas de distinguer la proue de la poupe, car un rectangle est symétrique après une rotation de 180° ; un point clé de pose placé sur la proue fournirait le cap réel.