Ultralytics YOLO27 : Inscris-toi sur la liste d’attente.
Get Started
Leçonintermédiaire

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

Résultat

Entraîne yolo26n-obb.pt sur dota8.yaml, compte les détections par classe à partir de result.obb et convertis chaque xywhr prédite en un axe du grand côté, exprimé en degrés dans [0, 180).

Parcours accéléré
Si tu connais déjà les bases, voici la version courte.
  1. Ligne d’étiquette : classe, puis quatre coins x1 y1 … x4 y4, normalisés et ordonnés le long du périmètre.

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

  3. Lis result.obb.xywhr (rotation en radians) ou .xyxyxyxy (coins) ; les résultats OBB ne renseignent pas result.boxes.

  4. Normalise l’orientation par rapport au grand côté : ajoute 90° lorsque w < h, puis calcule l’angle modulo 180°.

Pratique

Quand la rotation compte#

Boîtes englobantes orientées Ultralytics YOLO ajustées précisément autour de trois yachts amarrés dans une photo aérienne, chacun étiqueté Ship avec son score de confiance

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 axesBoîte orientée
Ajustement sur un navire à 45°environ la moitié de l’image est de l’arrière-planajustement serré
Rangées denses et en rotationles boîtes se chevauchent ; NMS élimine de vrais objetsles boîtes sont bien séparées
Fournit un anglenonoui, sous forme d’axe (modulo 180°)
Champ de résultatresult.boxesresult.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.524414

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

Éditeur d’annotation d’Ultralytics Platform sur une image aérienne DOTA8, avec des boîtes en rotation autour de courts de tennis, de petits véhicules et d’une piscine

Entraîne-toi sur DOTA8#

DOTA8 est un petit jeu de données de boîtes orientées conçu pour tester les pipelines :

Mosaïque d’entraînement DOTA8 composée de tuiles aériennes : rangées de bateaux amarrés, courts de tennis, parking et avions sur une aire de trafic, chaque objet étant entouré d’une boîte orientée et associé à un indice de classe

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

Les métriques OBB se trouvent dans metrics.box, mais l’IoU est calculée entre des rectangles pivotés.

DOTA8 est un test rapide

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 deg

Ligne 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. xyxyxyxy contient les mêmes boîtes sous forme de quatre points d’angle, prêtes pour cv2.polylines.
  • Le compteur compte à partir de result.obb.cls : 168 bateaux et 5 ports. result.boxes correspond à None pour les modèles OBB ; le code de comptage écrit pour la détection doit donc changer de champs.
  • raw par rapport à axis. Le premier bateau mesure 33 × 108 à −15° : son w est 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 :

Image d’autoroute avec les détections obtenues par inférence sur l’image entière, qui manque de nombreux petits véhicules éloignés

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

La même image d’autoroute traitée par inférence par tuiles SAHI, qui détecte bien plus de petits véhicules éloignés

Pièges#

Ni avant ni arrière

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.

Comparer les courbes de validation brutes aux résultats de détection

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.

Essayer

Exécute obb.py, puis écris un script qui calcule l’axe du grand côté de chaque bateau dans l’image du port, trouve l’axe le plus fréquent arrondi aux 10° les plus proches et compte les bateaux qui s’en écartent de plus de 30°. N’oublie pas que 175° et 5° ne sont séparés que de 10°.

Terminé lorsque
Tu as terminé la leçon lorsque tous les éléments suivants sont réunis.
  • Ton script affiche le nombre de bateaux, l’axe le plus fréquent et le nombre de bateaux qui s’en écartent de plus de 30°.

  • La comparaison des axes tient compte du passage à 180° : 175° et 5° sont donc considérés comme séparés de 10°.

  • Tu peux expliquer pourquoi les OBB ne permettent pas de distinguer la proue de la poupe et ce que tu ajouterais si tu avais besoin d’un cap.

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 it

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

Et ensuite ?

Ensuite, nous abandonnons complètement la localisation et entraînons un classificateur pour des images entières et des recadrages.