Structure du Projet
L'organisation des fichiers de notre projet de détection d'objets sur VisDrone avec YOLOv26 se présente comme suit :
visdrone-yolo26/
├── data/
│ ├── visdrone.yaml # Configuration des données
│ ├── telecharger_visdrone.py # Script de téléchargement
│ └── decouper_images.py # Découpage des images pour petits objets
├── models/
│ ├── yolo26-mini.yaml # Configuration du modèle adaptée aux petits objets
│ └── tete_detection_mini.py # Module de tête de détection pour petits objets
├── entrainer.py # Script d'entraînement
├── valider.py # Script de validation
├── deployer.py # Script de déploiement et d'inférence
└── exporter.py # Script d'exportation du modèle
Phase 1 : Préparation et Prétraitement des Données
1.1 Acquisition du Jeu de Données et Conversion de Format
Le jeu de données VisDrone nécessite une conversion de son format original vers le format YOLO. Les identifiants de classe, qui commencent à 1 dans VisDrone, doivent être ajustés pour démarrer à 0 pour YOLO.
# data/telecharger_visdrone.py
import os
from pathlib import Path
import shutil
import zipfile
from PIL import Image
import requests
from tqdm import tqdm
def recuperer_fichier(url_source, destination_chemin):
"""Télécharge un fichier avec une barre de progression."""
reponse = requests.get(url_source, stream=True)
taille_totale = int(reponse.headers.get('content-length', 0))
with open(destination_chemin, 'wb') as fichier_local, tqdm(
desc=destination_chemin.name,
total=taille_totale,
unit='iB',
unit_scale=True,
unit_divisor=1024,
) as barre_progression:
for donnees_tronc in reponse.iter_content(chunk_size=1024):
taille_ecrite = fichier_local.write(donnees_tronc)
barre_progression.update(taille_ecrite)
def decompresser_archive(chemin_zip, repertoire_cible):
"""Décompresse une archive ZIP et supprime le fichier source."""
with zipfile.ZipFile(chemin_zip, 'r') as ref_zip:
ref_zip.extractall(repertoire_cible)
chemin_zip.unlink() # Suppression du fichier ZIP
def convertir_visdrone_en_yolo(chemin_source_dataset, partie_jeu):
"""
Transforme les annotations VisDrone en format YOLO.
Format VisDrone: <bbox_left>,<bbox_top>,<bbox_width>,<bbox_height>,<score>,<category>,<truncation>,<occlusion>
Format YOLO: <class_id> <x_center> <y_center> <width> <height>
"""
chemin_source_dataset = Path(chemin_source_dataset)
repertoire_visdrone = chemin_source_dataset / f"VisDrone2019-DET-{partie_jeu}"
repertoire_images_cible = chemin_source_dataset / "images" / partie_jeu
repertoire_labels_cible = chemin_source_dataset / "labels" / partie_jeu
repertoire_images_cible.mkdir(parents=True, exist_ok=True)
repertoire_labels_cible.mkdir(parents=True, exist_ok=True)
# Déplacement des fichiers images
repertoire_images_visdrone = repertoire_visdrone / "images"
if repertoire_images_visdrone.exists():
for chemin_img in repertoire_images_visdrone.glob("*.jpg"):
shutil.move(str(chemin_img), str(repertoire_images_cible / chemin_img.name))
# Conversion des annotations
repertoire_annotations = repertoire_visdrone / "annotations"
if not repertoire_annotations.exists():
print(f"Avertissement: Le répertoire d'annotations {repertoire_annotations} est introuvable.")
return
for fichier_ann in tqdm(list(repertoire_annotations.glob("*.txt")), desc=f"Conversion des annotations pour {partie_jeu}"):
nom_image = fichier_ann.stem + ".jpg"
chemin_image = repertoire_images_cible / nom_image
if not chemin_image.exists():
continue
# Récupération des dimensions de l'image
with Image.open(chemin_image) as img:
largeur_img, hauteur_img = img.size
lignes_yolo = []
with open(fichier_ann, 'r', encoding='utf-8') as f_ann:
for ligne in f_ann:
elements = ligne.strip().split(',')
if len(elements) < 8:
continue
# Analyse du format VisDrone
coord_gauche = int(elements[0])
coord_haut = int(elements[1])
largeur_bbox = int(elements[2])
hauteur_bbox = int(elements[3])
score_obj = int(elements[4]) # 0 signifie région ignorée
categorie_id = int(elements[5]) # Classes 1-10
troncation = int(elements[6])
occlusion = int(elements[7])
# Ignorer les régions définies comme à ignorer ou fortement tronquées
if score_obj == 0 or troncation > 50:
continue
# Conversion de l'ID de catégorie VisDrone (1-10) à YOLO (0-9)
id_classe_yolo = categorie_id - 1
# Calcul des coordonnées YOLO normalisées
centre_x = (coord_gauche + largeur_bbox / 2) / largeur_img
centre_y = (coord_haut + hauteur_bbox / 2) / hauteur_img
largeur_norm = largeur_bbox / largeur_img
hauteur_norm = hauteur_bbox / hauteur_img
# S'assurer que les valeurs sont dans l'intervalle [0, 1]
centre_x = max(0, min(1, centre_x))
centre_y = max(0, min(1, centre_y))
largeur_norm = max(0, min(1, largeur_norm))
hauteur_norm = max(0, min(1, hauteur_norm))
lignes_yolo.append(f"{id_classe_yolo} {centre_x:.6f} {centre_y:.6f} {largeur_norm:.6f} {hauteur_norm:.6f}\n")
# Écriture des annotations au format YOLO
fichier_label_cible = repertoire_labels_cible / (fichier_ann.stem + ".txt")
with open(fichier_label_cible, 'w', encoding='utf-8') as f_label:
f_label.writelines(lignes_yolo)
def procedure_principale_telechargement():
"""Déclenche le téléchargement et la conversion des données VisDrone."""
url_base_visdrone = "https://github.com/VisDrone/VisDrone-Dataset/raw/master/VisDrone2019-DET"
sections_dataset = {
'train': 'VisDrone2019-DET-train.zip',
'val': 'VisDrone2019-DET-val.zip',
'test': 'VisDrone2019-DET-test-dev.zip'
}
repertoire_dataset_racine = Path("./datasets/VisDrone")
repertoire_dataset_racine.mkdir(parents=True, exist_ok=True)
# Boucle de téléchargement et de décompression
for section, nom_archive in sections_dataset.items():
chemin_archive_local = repertoire_dataset_racine / nom_archive
if not chemin_archive_local.exists():
url_complete = f"{url_base_visdrone}/{nom_archive}"
print(f"Téléchargement de {nom_archive}...")
try:
recuperer_fichier(url_complete, chemin_archive_local)
print(f"Décompression de {nom_archive}...")
decompresser_archive(chemin_archive_local, repertoire_dataset_racine)
# Conversion au format YOLO
convertir_visdrone_en_yolo(repertoire_dataset_racine, section)
# Nettoyage des répertoires originaux extraits
repertoire_extrait = repertoire_dataset_racine / f"VisDrone2019-DET-{section}"
if repertoire_extrait.exists():
shutil.rmtree(repertoire_extrait)
except Exception as e:
print(f"Une erreur est survenue lors du traitement de la section {section}: {e}")
print("Préparation du jeu de données VisDrone terminée !")
if __name__ == "__main__":
procedure_principale_telechargement()
1.2 Optimisation pour Petits Objets : Découpage d'Images (Stratégie SAHI)
Les images VisDrone, avec leurs résolutions allant jusqu'à 2000×1500 pixels et une forte densité de petits objets, bénéficient grandement du découpage en tuiles plus petites pour l'entraînement.
# data/decouper_images.py
import os
from pathlib import Path
from PIL import Image
import numpy as np
from tqdm import tqdm
from collections import defaultdict
class DecoupageVisDrone:
"""
Implémente la logique de découpage d'images de haute résolution VisDrone
en tuiles plus petites, avec gestion du chevauchement et des annotations.
"""
def __init__(self, repertoire_img_src, repertoire_lbl_src, repertoire_dest,
taille_tuile=640, taux_chevauchement=0.2):
self.repertoire_img_src = Path(repertoire_img_src)
self.repertoire_lbl_src = Path(repertoire_lbl_src)
self.repertoire_dest = Path(repertoire_dest)
self.taille_tuile = taille_tuile
self.taux_chevauchement = taux_chevauchement
self.pas_decoupage = int(taille_tuile * (1 - taux_chevauchement))
self.images_sortie = self.repertoire_dest / "images"
self.labels_sortie = self.repertoire_dest / "labels"
self.images_sortie.mkdir(parents=True, exist_ok=True)
self.labels_sortie.mkdir(parents=True, exist_ok=True)
def decouper_image_unique(self, chemin_image, chemin_label):
"""Procède au découpage d'une seule image et de ses annotations."""
image_originale = Image.open(chemin_image)
largeur_img_orig, hauteur_img_orig = image_originale.size
# Si l'image est déjà plus petite que la taille de la tuile, on la saute
if largeur_img_orig <= self.taille_tuile and hauteur_img_orig <= self.taille_tuile:
return
# Lecture des annotations originales
boites_originales = []
if chemin_label.exists():
with open(chemin_label, 'r') as f_lbl:
for ligne in f_lbl:
elements = ligne.strip().split()
if len(elements) == 5:
id_cls = int(elements[0])
cx, cy, l, h = map(float, elements[1:])
# Conversion en coordonnées pixel (x1, y1, x2, y2)
x1 = (cx - l/2) * largeur_img_orig
y1 = (cy - h/2) * hauteur_img_orig
x2 = (cx + l/2) * largeur_img_orig
y2 = (cy + h/2) * hauteur_img_orig
boites_originales.append([id_cls, x1, y1, x2, y2])
# Détermination des positions des tuiles
liste_tuiles = []
for y_start in range(0, hauteur_img_orig - self.taille_tuile + 1, self.pas_decoupage):
for x_start in range(0, largeur_img_orig - self.taille_tuile + 1, self.pas_decoupage):
liste_tuiles.append((x_start, y_start, min(x_start + self.taille_tuile, largeur_img_orig),
min(y_start + self.taille_tuile, hauteur_img_orig)))
# Ajout des tuiles pour les bords si nécessaire
if largeur_img_orig > self.taille_tuile and (largeur_img_orig - self.taille_tuile) % self.pas_decoupage != 0:
x_start_border = largeur_img_orig - self.taille_tuile
for y_start in range(0, hauteur_img_orig - self.taille_tuile + 1, self.pas_decoupage):
liste_tuiles.append((x_start_border, y_start, largeur_img_orig, min(y_start + self.taille_tuile, hauteur_img_orig)))
if hauteur_img_orig > self.taille_tuile and (hauteur_img_orig - self.taille_tuile) % self.pas_decoupage != 0:
y_start_border = hauteur_img_orig - self.taille_tuile
for x_start in range(0, largeur_img_orig - self.taille_tuile + 1, self.pas_decoupage):
liste_tuiles.append((x_start, y_start_border, min(x_start + self.taille_tuile, largeur_img_orig), hauteur_img_orig))
# Pour éviter les doublons dus aux boucles de bord, on peut utiliser un set de tuples
liste_tuiles = sorted(list(set(liste_tuiles)))
# Découpage effectif et enregistrement
for indice, (x1_tuile, y1_tuile, x2_tuile, y2_tuile) in enumerate(liste_tuiles):
# Recadrage de l'image
tuile_recadree = image_originale.crop((x1_tuile, y1_tuile, x2_tuile, y2_tuile))
nom_tuile = f"{chemin_image.stem}_decoupe_{indice}"
chemin_img_tuile = self.images_sortie / f"{nom_tuile}.jpg"
tuile_recadree.save(chemin_img_tuile, quality=95)
# Traitement des boîtes d'annotation pour la tuile
boites_tuile = []
for id_cls, box_orig_x1, box_orig_y1, box_orig_x2, box_orig_y2 in boites_originales:
# Calcul de l'intersection
inter_x1 = max(box_orig_x1, x1_tuile)
inter_y1 = max(box_orig_y1, y1_tuile)
inter_x2 = min(box_orig_x2, x2_tuile)
inter_y2 = min(box_orig_y2, y2_tuile)
if inter_x2 > inter_x1 and inter_y2 > inter_y1:
# Coordonnées par rapport à la tuile
nouv_x1 = inter_x1 - x1_tuile
nouv_y1 = inter_y1 - y1_tuile
nouv_x2 = inter_x2 - x1_tuile
nouv_y2 = inter_y2 - y1_tuile
# Conversion au format YOLO (normalisé)
largeur_tuile = x2_tuile - x1_tuile
hauteur_tuile = y2_tuile - y1_tuile
nouv_cx = (nouv_x1 + nouv_x2) / 2 / largeur_tuile
nouv_cy = (nouv_y1 + nouv_y2) / 2 / hauteur_tuile
nouv_l = (nouv_x2 - nouv_x1) / largeur_tuile
nouv_h = (nouv_y2 - nouv_y1) / hauteur_tuile
# Filtrage des boîtes trop petites après découpage
if nouv_l > 0.01 and nouv_h > 0.01:
boites_tuile.append(f"{id_cls} {nouv_cx:.6f} {nouv_cy:.6f} {nouv_l:.6f} {nouv_h:.6f}\n")
# Sauvegarde des annotations de la tuile
if boites_tuile:
chemin_label_tuile = self.labels_sortie / f"{nom_tuile}.txt"
with open(chemin_label_tuile, 'w') as f_lbl_tuile:
f_lbl_tuile.writelines(boites_tuile)
def traiter_section_dataset(self, nom_section):
"""Traite une section complète (train/val/test) du jeu de données."""
repertoire_images_section = self.repertoire_img_src / nom_section
repertoire_labels_section = self.repertoire_lbl_src / nom_section
if not repertoire_images_section.exists():
print(f"Avertissement: Le répertoire {repertoire_images_section} n'existe pas.")
return
fichiers_img = list(repertoire_images_section.glob("*.jpg"))
print(f"Traitement de la section '{nom_section}': {len(fichiers_img)} images à découper.")
for chemin_img in tqdm(fichiers_img, desc=f"Découpage des images pour {nom_section}"):
chemin_label = repertoire_labels_section / f"{chemin_img.stem}.txt"
self.decouper_image_unique(chemin_img, chemin_label)
def execution_principale_decoupage():
"""Point d'entrée principal pour le découpage des images VisDrone."""
# Chemins vers les données VisDrone originales
base_repertoire = Path("./datasets/VisDrone")
# Chemin de sortie pour les données découpées
repertoire_decoupe = Path("./datasets/VisDrone_Decoupe")
for section in ['train', 'val', 'test']:
gestionnaire_decoupe = DecoupageVisDrone(
repertoire_img_src=base_repertoire / "images",
repertoire_lbl_src=base_repertoire / "labels",
repertoire_dest=repertoire_decoupe / section,
taille_tuile=640, # Taille d'entrée recommandée pour YOLOv26
taux_chevauchement=0.2 # Chevauchement de 20% pour éviter la césure d'objets
)
gestionnaire_decoupe.traiter_section_dataset(section)
print("Processus de découpage des images terminé ! Astuce : il est recommandé d'utiliser un mélange d'images originales et découpées pour l'entraînement.")
if __name__ == "__main__":
execution_principale_decoupage()
1.3 Fichier de Configuration des Données
# data/visdrone.yaml
path: ./datasets/VisDrone # Répertoire racine du jeu de données (peut être modifié pour VisDrone_Decoupe si utilisé)
train: images/train # Répertoire des images d'entraînement
val: images/val # Répertoire des images de validation
test: images/test # Répertoire des images de test
# 10 catégories du jeu de données VisDrone2019-DET
names:
0: piéton
1: personne (groupe/assis/cycliste)
2: vélo
3: voiture
4: camionnette
5: camion
6: tricycle
7: tricycle-bâche
8: bus
9: moto
# Paramètres d'optimisation pour la détection de petits objets
nc: 10 # Nombre de classes
# Configuration de l'augmentation des données (optimisée pour petits objets)
augment:
# Améliorations spatiales
hsv_h: 0.015 # Amélioration de la teinte HSV (utile pour variations de lumière des drones)
hsv_s: 0.7 # Amélioration de la saturation HSV
hsv_v: 0.4 # Amélioration de la luminosité HSV
degrees: 15 # Angle de rotation (pour différentes perspectives de drone)
translate: 0.2 # Translation
scale: 0.5 # Mise à l'échelle (simule différentes altitudes)
shear: 2 # Cisaillement
perspective: 0.001 # Transformation en perspective
flipud: 0.5 # Retournement vertical
fliplr: 0.5 # Retournement horizontal
mosaic: 1.0 # Augmentation Mosaic (efficace pour scènes denses)
mixup: 0.2 # Augmentation Mixup
copy_paste: 0.1 # Copier-Coller (pour dupliquer de petits objets)
# Traitement spécifique aux petits objets
small_object_augment: true # Activation de l'augmentation par copie pour petits objets
small_object_threshold: 0.01 # Seuil (en % de la surface de l'image) pour définir un petit objet
copy_paste_prob: 0.3 # Probabilité d'appliquer l'augmentation Copy-Paste
Phase 2 : Configuration et Modification du Modèle (Optimisation des Petits Objets)
2.1 Ajustement de la Configuration YOLOv26 pour les Petits Objets
YOLOv26 intègre nativement des optimisations comme ProgLoss et STAL pour la détection de petits objets. Cependant, des ajustements spécifiques à la tête de détection peuvent être bénéfiques.
# models/yolo26-visdrone.yaml
# Basé sur YOLOv26 Nano (équilibre vitesse/précision). Utiliser Small ou Medium pour une plus grande précision.
model:
c1: 10 # Nombre de classes VisDrone
# Ancrages optimisés pour VisDrone (résultats du clustering K-means)
anchors:
# P3/8 (Couche pour petits objets) - Ajustée pour les objets <16x16 pixels de VisDrone
- [5,6, 7,9, 11,10] # Cibles minuscules comme les piétons, motos
# P4/16 (Couche pour objets de taille moyenne)
- [15,18, 23,25, 34,38] # Vélos, voitures
# P5/32 (Couche pour grands objets)
- [58,65, 102,108, 162,180] # Bus, camions
backbone:
# Backbone YOLOv26 (avec optimisation ProgLoss)
type: YOLOv26Backbone
pretrained: yolo26n.pt # Poids pré-entraînés
progloss: true # Activation de la perte progressive (bénéfique pour petits objets)
head:
type: YOLOv26Head
# Optimisation de la tête de détection pour les petits objets
small_object_head: true
# Utilisation de cartes de caractéristiques à plus haute résolution
p3_channels: 256 # Renforcement de la couche P3 (cruciale pour les petits objets)
p4_channels: 512
p5_channels: 1024
# STAL: Spatio-Temporal Anchor Learning (pour les scènes avec occlusion)
stal:
enabled: true
anchor_matching: "small_focus" # Stratégie de correspondance d'ancres axée sur les petits objets
# Tête découplée (améliore les performances dans les scènes denses)
decoupled_head: true
2.2 Module Personnalisé de Tête de Détection pour Petits Objets (Avancé, Optionnel)
Si une modification plus profonde du code source est souhaitée, voici un exemple de module pour la tête de détection spécialisée dans les petits objets.
# models/tete_detection_mini.py
import torch
import torch.nn as nn
import torch.nn.functional as F
class TeteDetectionMiniature(nn.Module):
"""
Tête de détection optimisée pour les objets extrêmement petits de VisDrone.
Caractéristiques : fusion de caractéristiques plus profondes + mécanismes d'attention.
"""
def __init__(self, canaux_entree, nombre_classes, nombre_ancres=3):
super().__init__()
self.nombre_classes = nombre_classes
self.nombre_ancres = nombre_ancres
# Couche d'upsampling (pour récupérer plus de détails)
self.upsample_det = nn.Upsample(scale_factor=2, mode='nearest')
# Module d'amélioration des caractéristiques pour petits objets
self.module_amelioration_mini = nn.Sequential(
# Convolution séparable en profondeur pour réduire la complexité
nn.Conv2d(canaux_entree, canaux_entree, kernel_size=3, padding=1, groups=canaux_entree),
nn.BatchNorm2d(canaux_entree),
nn.SiLU(),
nn.Conv2d(canaux_entree, canaux_entree // 2, kernel_size=1),
nn.BatchNorm2d(canaux_entree // 2),
nn.SiLU(),
# Mécanisme d'attention sur les canaux
AttentionCanal(canaux_entree // 2),
# Mécanisme d'attention spatiale (utile pour scènes denses)
AttentionSpatiale()
)
# Couche de détection
self.couche_detection = nn.Sequential(
nn.Conv2d(canaux_entree // 2 + canaux_entree // 4, 256, kernel_size=3, padding=1),
nn.BatchNorm2d(256),
nn.SiLU(),
nn.Dropout(0.1) # Prévention du surapprentissage
)
# Couche de prédiction: x, y, w, h, obj_conf, classes
self.pred_finale = nn.Conv2d(256, nombre_ancres * (5 + nombre_classes), kernel_size=1)
def forward(self, x_input, high_level_feat=None):
# Caractéristiques initiales traitées par le module d'amélioration
feat_initial = self.module_amelioration_mini(x_input)
# Fusion avec des caractéristiques de haut niveau si disponibles
if high_level_feat is not None:
feat_high_res = F.interpolate(high_level_feat, size=feat_initial.shape[2:], mode='nearest')
feat_initial = torch.cat([feat_initial, feat_high_res[:, :feat_initial.size(1) // 2]], dim=1)
resultat_det = self.couche_detection(feat_initial)
sortie = self.pred_finale(resultat_det)
# Réarrangement de la sortie: [B, A*(5+C), H, W] -> [B, A, H, W, 5+C]
batch_size, _, height, width = sortie.shape
sortie = sortie.view(batch_size, self.nombre_ancres, 5 + self.nombre_classes, height, width).permute(0, 1, 3, 4, 2).contiguous()
return sortie
class AttentionCanal(nn.Module):
"""Module d'attention sur les canaux."""
def __init__(self, canaux_input, reduction_ratio=16):
super().__init__()
self.pool_avg = nn.AdaptiveAvgPool2d(1)
self.pool_max = nn.AdaptiveMaxPool2d(1)
self.reseau_fc = nn.Sequential(
nn.Conv2d(canaux_input, canaux_input // reduction_ratio, kernel_size=1, bias=False),
nn.SiLU(),
nn.Conv2d(canaux_input // reduction_ratio, canaux_input, kernel_size=1, bias=False)
)
self.activation_sigmoid = nn.Sigmoid()
def forward(self, x_input):
avg_out = self.reseau_fc(self.pool_avg(x_input))
max_out = self.reseau_fc(self.pool_max(x_input))
return x_input * self.activation_sigmoid(avg_out + max_out)
class AttentionSpatiale(nn.Module):
"""Module d'attention spatiale (efficace pour les scènes avec occlusion)."""
def __init__(self):
super().__init__()
self.convolution_att = nn.Conv2d(2, 1, kernel_size=7, padding=3, bias=False)
self.activation_sigmoid = nn.Sigmoid()
def forward(self, x_input):
avg_output = torch.mean(x_input, dim=1, keepdim=True)
max_output, _ = torch.max(x_input, dim=1, keepdim=True)
concatenation = torch.cat([avg_output, max_output], dim=1)
return x_input * self.activation_sigmoid(self.convolution_att(concatenation))
def substituer_tete_yolo_mini(modele_yolo, nb_classes=10):
"""
Remplace la tête standard de YOLOv26 par une tête optimisée pour petits objets.
Exemple d'utilisation: modele_modifie = substituer_tete_yolo_mini(modele_yolo_original).
"""
# Cette implémentation suppose une structure de tête YOLOv26.
# Il est crucial d'adapter en fonction de la structure réelle du modèle Ultralytics.
if hasattr(modele_yolo, 'model') and hasattr(modele_yolo.model, 'model'):
for i, m in enumerate(modele_yolo.model.model):
# Ultralytics structure typically has a 'C2f' or 'Detect' module at the end
# This is a placeholder and needs to match the actual head module
if isinstance(m, nn.Module) and hasattr(m, 'nl'): # Assuming 'nl' is a common attribute for detection layers
# Replace the last detection head, or specifically P3
# This part is highly dependent on the internal structure of YOLOv26 (ultralytics)
# A more robust solution would be to inspect model.model.model[-1] and its submodules
print(f"Tentative de remplacement de la tête de détection à l'index {i}...")
# Assuming 'm' is a detection layer like Detect()
in_channels_head = m.cv3[0].in_channels # Example for a common head structure
modele_yolo.model.model[i] = TeteDetectionMiniature(in_channels_head, nb_classes, m.na) # m.na is num_anchors
print(f"Tête de détection à l'index {i} remplacée par TeteDetectionMiniature.")
break # Assuming only one head needs replacement, or specific logic for multiple
return modele_yolo
Phase 3 : Script d'Entraînement
3.1 Script d'Entraînement Principal (avec Stratégies d'Optimisation pour Petits Objets)
# entrainer.py
import os
import torch
import yaml
from pathlib import Path
from ultralytics import YOLO # Utilisation de la bibliothèque Ultralytics pour YOLOv26
# Gestionnaire d'entraînement personnalisé pour VisDrone
class EntraineurVisDrone:
def __init__(self, chemin_configuration):
with open(chemin_configuration, 'r') as f_config:
self.configuration = yaml.safe_load(f_config)
self.appareil = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Appareil utilisé pour l'entraînement: {self.appareil}")
def preparer_modele(self):
"""Initialise le modèle YOLOv26."""
# Chargement de la variante de modèle YOLOv26 (Nano pour la vitesse, Small pour plus de précision)
variante_modele = self.configuration.get('variante_modele', 'yolo26n')
# Téléchargement automatique des poids pré-entraînés si non trouvés localement
chemin_modele = f"{variante_modele}.pt"
print(f"Chargement du modèle: {chemin_modele}")
modele = YOLO(chemin_modele)
# Application des optimisations pour petits objets si activé
if self.configuration.get('utiliser_tete_detection_mini', False):
from models.tete_detection_mini import substituer_tete_yolo_mini
modele = substituer_tete_yolo_mini(modele, nb_classes=10)
print("Tête de détection optimisée pour petits objets activée.")
return modele
def lancer_entrainement(self):
"""Exécute la procédure d'entraînement."""
modele = self.preparer_modele()
# Hyperparamètres optimisés pour VisDrone
arguments_entrainement = {
# Configuration des données
'data': self.configuration['fichier_yaml_donnees'],
'epochs': self.configuration.get('epochs', 200), # VisDrone requiert un entraînement plus long
'imgsz': self.configuration.get('taille_image', 640), # 640 pour équilibre, 1280 pour précision
'batch': self.configuration.get('taille_batch', 16), # Ajuster selon la VRAM du GPU
'workers': 8,
# Optimiseur (YOLOv26 utilise MuSGD)
'optimizer': 'MuSGD', # Optimiseur natif de YOLOv26, convergence plus rapide
'lr0': 0.01, # Taux d'apprentissage initial
'lrf': 0.01, # Taux d'apprentissage final
'momentum': 0.937,
'weight_decay': 0.0005,
'warmup_epochs': 3, # Époques de préchauffage (nécessaire pour scènes VisDrone denses)
# Augmentation des données (spécifique à VisDrone)
'hsv_h': 0.015, # Teinte HSV (pour variations d'éclairage des drones)
'hsv_s': 0.7, # Saturation HSV
'hsv_v': 0.4, # Luminosité HSV
'degrees': 15, # Rotation (simule différentes perspectives)
'translate': 0.2, # Translation
'scale': 0.5, # Mise à l'échelle (simule différentes altitudes)
'shear': 2, # Cisaillement
'perspective': 0.001, # Perspective
'flipud': 0.5, # Retournement vertical
'fliplr': 0.5, # Retournement horizontal
'mosaic': 1.0, # Mosaic (très efficace pour petits objets denses)
'mixup': 0.2, # Mixup
'copy_paste': 0.3, # Copy-Paste (clé pour l'augmentation des petits objets)
# Traitement spécifique pour petits objets
'box': 7.5, # Gain pour la perte de boîte
'cls': 0.5, # Gain pour la perte de classe
'dfl': 1.5, # Perte de focalisation de distribution (caractéristique de YOLOv26)
# Stratégie d'entraînement
'patience': 50, # Patience pour l'arrêt précoce
'save_period': 10, # Sauvegarde tous les 10 epochs
'device': self.appareil,
'project': 'visdrone-yolo26',
'name': f"{self.configuration.get('nom_experience', 'exp')}",
'exist_ok': True,
'pretrained': True,
'resume': self.configuration.get('reprendre_entrainement', False),
# Validation
'val': True,
'iou': 0.6, # Seuil IoU pour NMS (valeur plus faible pour scènes denses)
'conf': 0.001, # Seuil de confiance (plus faible pour petits objets)
# Autres
'verbose': True,
'seed': 42,
'deterministic': False, # Accélère l'entraînement non déterministe
}
print("Début de l'entraînement...")
resultats = modele.train(**arguments_entrainement)
# Chemin du modèle final sauvegardé
chemin_final = Path(arguments_entrainement['project']) / arguments_entrainement['name'] / 'weights' / 'best.pt'
print(f"Entraînement terminé ! Le meilleur modèle est enregistré à : {chemin_final}")
return resultats
def main_entrainement():
"""Fonction principale pour lancer l'entraînement."""
config_entrainement = {
'fichier_yaml_donnees': './data/visdrone.yaml',
'variante_modele': 'yolo26n', # Options : yolo26n (nano), yolo26s (small), yolo26m (medium)
'epochs': 150, # VisDrone recommande 150-300 epochs
'taille_batch': 16, # Ajuster selon la VRAM (32 possible avec RTX 4090)
'taille_image': 640, # 1280 pour une plus grande précision (mais plus lent)
'utiliser_tete_detection_mini': False, # Activer si la tête personnalisée est implémentée
'nom_experience': 'yolo26n-visdrone-base',
'reprendre_entrainement': False, # True pour reprendre depuis le dernier checkpoint
}
# Sauvegarde de la configuration
with open('config_entrainement.yaml', 'w') as f_cfg:
yaml.dump(config_entrainement, f_cfg, sort_keys=False) # sort_keys=False pour conserver l'ordre
entraineur = EntraineurVisDrone('config_entrainement.yaml')
entraineur.lancer_entrainement()
if __name__ == "__main__":
# Configuration des variables d'environnement pour l'optimisation
os.environ['OMP_NUM_THREADS'] = '1'
os.environ['MKL_NUM_THREADS'] = '1'
main_entrainement()
3.2 Script d'Entraînement Multi-Échelles (Avancé)
Pour mieux gérer les variations d'échelle d'objets sur VisDrone, une stratégie d'entraînement multi-échelles peut être utilisée.
# entrainer_multi_echelles.py
import random
from ultralytics import YOLO
def entrainement_multi_echelles():
"""Stratégie d'entraînement multi-échelles (taille d'entrée aléatoire par époque)."""
modele = YOLO('yolo26n.pt')
# Paramètres de base pour l'entraînement
parametres_base = {
'data': './data/visdrone.yaml',
'epochs': 150,
'batch': 8, # Réduire la taille de batch pour les modes multi-échelles (plus gourmands en VRAM)
'optimizer': 'MuSGD',
'mosaic': 1.0,
'copy_paste': 0.3,
'project': 'visdrone-yolo26',
'name': 'experience-multi-echelles',
}
# Boucle d'entraînement multi-échelles
for epoch_courante in range(150):
# Sélection aléatoire de la taille d'image (entre 480 et 1280)
taille_image = random.choice([480, 512, 640, 768, 896, 1024, 1280])
parametres_base['imgsz'] = taille_image
# Les premières époques peuvent utiliser une taille fixe pour la phase de "warm-up"
if epoch_courante < 10:
parametres_base['imgsz'] = 640
print(f"Époque {epoch_courante+1}/150, Taille d'entraînement: {parametres_base['imgsz']}")
# Lancement de l'entraînement pour une seule époque
if epoch_courante == 0:
parametres_base['epochs'] = 1
modele.train(**parametres_base)
else:
# Reprise de l'entraînement pour les époques suivantes
modele.train(resume=True, **parametres_base)
if __name__ == "__main__":
entrainement_multi_echelles()
Phase 4 : Validation et Évaluation
4.1 Script de Validation (incluant les Métriques VisDrone)
# valider.py
import torch
import json
from pathlib import Path
from ultralytics import YOLO
import numpy as np
from collections import defaultdict
class EvaluateurVisDrone:
"""Gère l'évaluation d'un modèle YOLOv26 sur le jeu de données VisDrone."""
def __init__(self, chemin_modele, fichier_config_data):
self.modele = YOLO(chemin_modele)
self.fichier_config_data = fichier_config_data
def executer_validation(self):
"""Procède à la validation standard du modèle."""
resultats_val = self.modele.val(
data=self.fichier_config_data,
batch=16,
imgsz=640,
conf=0.001, # Seuil de confiance bas pour VisDrone
iou=0.6, # Seuil IoU pour NMS dans des scènes denses
device='0',
save_json=True, # Sauvegarde au format JSON (COCO)
save_hybrid=True, # Sauvegarde des étiquettes hybrides pour une évaluation précise
project='evaluation-visdrone',
name='resultats_exp',
)
# Affichage des métriques clés
print("\n" + "="*50)
print("Résultats de validation VisDrone")
print("="*50)
print(f"mAP@0.5:0.95: {resultats_val.box.map:.4f}")
print(f"mAP@0.5: {resultats_val.box.map50:.4f}")
print(f"mAP@0.75: {resultats_val.box.map75:.4f}")
print(f"Rappel: {resultats_val.box.mar:.4f}")
print(f"Précision: {resultats_val.box.mp:.4f}")
# AP par catégorie (important pour VisDrone)
noms_classes = ['piéton', 'personne', 'vélo', 'voiture', 'camionnette',
'camion', 'tricycle', 'tricycle-bâche', 'bus', 'moto']
print("\nAP@0.5 par catégorie:")
for i, nom_cls in enumerate(noms_classes):
if i < len(resultats_val.box.ap50):
print(f" {nom_cls:15s}: {resultats_val.box.ap50[i]:.4f}")
return resultats_val
def analyser_objets_minuscules(self, resultats_evaluation):
"""Effectue une analyse des performances de détection des petits objets."""
# Les résultats.box devraient contenir des informations sur la surface des objets.
# Cette partie est une adaptation; `results.box.areas` n'est pas standardisé dans Ultralytics pour les `val` outputs.
# Il faudrait parser le JSON des prédictions pour une analyse exacte de la taille des bbox.
print("\nL'analyse détaillée des petits objets nécessite des informations de boîte plus granulaires,")
print("généralement obtenues en traitant les fichiers de prédiction COCO JSON ou en modifiant")
print("le code d'évaluation pour collecter les aires des objets détectés.")
print("Cette fonction est un exemple conceptuel.")
return {'small_ap': 0, 'medium_ap': 0, 'large_ap': 0}
def main_validation():
"""Fonction principale pour la validation."""
evaluator_visdrone = EvaluateurVisDrone(
chemin_modele='./visdrone-yolo26/exp/weights/best.pt',
fichier_config_data='./data/visdrone.yaml'
)
resultats_final = evaluator_visdrone.executer_validation()
# Tentative d'analyse des petits objets
try:
# NOTE: Cette partie nécessite que 'resultats_final' contienne des informations sur les aires
# ou qu'une logique de traitement des prédictions par taille soit implémentée.
# Pour Ultralytics, il faudrait généralement charger les prédictions depuis le fichier JSON généré.
# Par souci de simplification et fidélité au format Ultralytics, nous laissons un placeholder.
print("\nAnalyse des petits objets (conceptuelle, nécessite implémentation détaillée des aires)")
# Exemple de parsing du JSON des prédictions si disponible:
# from ultralytics.utils.metrics import DetMetrics
# metrics = DetMetrics.from_results(path_to_results_json)
# small_object_ap = metrics.ap_per_size['small'] # Si Ultralytics expose cela
# print(f"mAP pour les petits objets : {small_object_ap:.4f}")
except Exception as e:
print(f"\nImpossible de réaliser l'analyse des petits objets : {e}")
print("Cette fonctionnalité requiert l'accès aux aires des bounding boxes prédites.")
if __name__ == "__main__":
main_validation()
4.2 Script de Visualisation des Résultats
# visualiser_resultats.py
import cv2
import numpy as np
from pathlib import Path
from ultralytics import YOLO
import random
def afficher_predictions_visuelles(chemin_modele, repertoire_images_input, repertoire_sortie_vis, nombre_echantillons=20):
"""
Visualise les résultats de prédiction sur un ensemble d'images
pour évaluer l'efficacité de la détection de petits objets.
"""
modele = YOLO(chemin_modele)
fichiers_images = list(Path(repertoire_images_input).glob("*.jpg"))
if len(fichiers_images) > nombre_echantillons:
fichiers_images = random.sample(fichiers_images, nombre_echantillons) # Sélectionne un sous-ensemble aléatoire
repertoire_sortie_vis = Path(repertoire_sortie_vis)
repertoire_sortie_vis.mkdir(parents=True, exist_ok=True)
# Génération de couleurs aléatoires pour chaque classe
palettes_couleurs = [(random.randint(0,255), random.randint(0,255), random.randint(0,255))
for _ in range(10)]
noms_classes = ['piéton', 'personne', 'vélo', 'voiture', 'camionnette',
'camion', 'tricycle', 'tricycle-bâche', 'bus', 'moto']
for chemin_img in fichiers_images:
# Lecture de l'image
image_a_dessiner = cv2.imread(str(chemin_img))
if image_a_dessiner is None:
print(f"Erreur: Impossible de lire l'image {chemin_img}. Ignoré.")
continue
hauteur_img, largeur_img = image_a_dessiner.shape[:2]
# Exécution de la prédiction
resultats_prediction = modele.predict(image_a_dessiner, conf=0.25, iou=0.45, verbose=False)
# Dessin des résultats sur l'image
for resultat_unique in resultats_prediction:
boites_detectees = resultat_unique.boxes
for boite in boites_detectees:
x1, y1, x2, y2 = map(int, boite.xyxy[0].cpu().numpy())
id_cls = int(boite.cls[0])
confiance = float(boite.conf[0])
couleur = palettes_couleurs[id_cls]
etiquette = f"{noms_classes[id_cls]} {confiance:.2f}"
# Dessin du rectangle englobant
cv2.rectangle(image_a_dessiner, (x1, y1), (x2, y2), couleur, 2)
# Dessin de l'étiquette (ajustement de la taille de police pour les petits objets)
aire_boite = (x2-x1)*(y2-y1)
echelle_police = 0.5 if aire_boite < 1000 else 0.6
taille_texte = cv2.getTextSize(etiquette, 0, echelle_police, 1)[0]
cv2.rectangle(image_a_dessiner, (x1, y1 - taille_texte[1] - 3), (x1 + taille_texte[0], y1), couleur, -1)
cv2.putText(image_a_dessiner, etiquette, (x1, y1 - 4), 0, echelle_police, (255,255,255), 1)
# Sauvegarde de l'image visualisée
chemin_sortie_img = repertoire_sortie_vis / f"vis_{chemin_img.name}"
cv2.imwrite(str(chemin_sortie_img), image_a_dessiner)
print(f"Image visualisée enregistrée: {chemin_sortie_img}")
if __name__ == "__main__":
afficher_predictions_visuelles(
chemin_modele='./visdrone-yolo26/exp/weights/best.pt',
repertoire_images_input='./datasets/VisDrone/images/val',
repertoire_sortie_vis='./visualisations',
nombre_echantillons=50
)
Phase 5 : Déploiement et Inférence
5.1 Script de Déploiement pour l'Inférence (avec Découpage SAHI pour Petits Objets)
# deployer.py
import cv2
import torch
import numpy as np
from pathlib import Path
from ultralytics import YOLO
import time
from typing import List, Tuple
class DeploiementVisDrone:
"""
Classe pour le déploiement et l'exécution de l'inférence YOLOv26
sur des images VisDrone, avec une option pour l'inférence SAHI.
"""
def __init__(self, chemin_modele, seuil_confiance=0.25, seuil_iou=0.45,
peripherique='0', utiliser_sahi=False, taille_decoupe=640):
self.modele_yolo = YOLO(chemin_modele)
self.seuil_confiance = seuil_confiance
self.seuil_iou = seuil_iou
self.peripherique = peripherique
self.utiliser_sahi = utiliser_sahi
self.taille_decoupe = taille_decoupe
# Initialisation SAHI si activé
if utiliser_sahi:
try:
from sahi import AutoDetectionModel
from sahi.predict import get_sliced_prediction
self.modele_sahi_auto = AutoDetectionModel.from_yolov8(chemin_modele) # Compatible YOLOv8/26
self.fonction_sahi_decoupe = get_sliced_prediction
except ImportError:
print("Avertissement: La bibliothèque SAHI n'est pas installée. Veuillez exécuter 'pip install sahi'.")
self.utiliser_sahi = False
def pretraiter_image(self, chemin_image):
"""Lit et pré-traite une image."""
img = cv2.imread(str(chemin_image))
if img is None:
raise ValueError(f"Erreur : Impossible de lire l'image depuis {chemin_image}")
return img
def executer_inference(self, chemin_image):
"""Exécute l'inférence sur une image unique, avec ou sans SAHI."""
if self.utiliser_sahi:
return self._inference_avec_sahi(chemin_image)
else:
return self._inference_standard(chemin_image)
def _inference_standard(self, chemin_image):
"""Exécute l'inférence standard."""
image_input = self.pretraiter_image(chemin_image)
hauteur, largeur = image_input.shape[:2]
debut_inference = time.time()
resultats_yolo = self.modele_yolo.predict(
image_input,
conf=self.seuil_confiance,
iou=self.seuil_iou,
device=self.peripherique,
verbose=False
)
temps_inference = time.time() - debut_inference
# Parsing des résultats de YOLO
detections_finales = []
for resultat in resultats_yolo:
boites_detectees = resultat.boxes
for boite in boites_detectees:
x1, y1, x2, y2 = boite.xyxy[0].cpu().numpy()
confiance = float(boite.conf[0])
id_classe = int(boite.cls[0])
detections_finales.append({
'bbox': [float(x1), float(y1), float(x2), float(y2)],
'confiance': confiance,
'id_classe': id_classe,
'aire': (x2-x1)*(y2-y1)
})
return {
'taille_image': (largeur, hauteur),
'nombre_detections': len(detections_finales),
'temps_inference_ms': temps_inference * 1000,
'detections': detections_finales,
'sahi_active': False
}
def _inference_avec_sahi(self, chemin_image):
"""Exécute l'inférence assistée par SAHI (découpage d'image)."""
debut_inference = time.time()
resultat_sahi = self.fonction_sahi_decoupe(
source=str(chemin_image),
detection_model=self.modele_sahi_auto,
slice_height=self.taille_decoupe,
slice_width=self.taille_decoupe,
overlap_height_ratio=0.2,
overlap_width_ratio=0.2,
postprocess_type="NMS",
postprocess_match_metric="IOU",
postprocess_match_threshold=self.seuil_iou,
postprocess_class_agnostic=True,
)
temps_inference = time.time() - debut_inference
# Parsing des résultats SAHI
detections_finales = []
for pred_obj in resultat_sahi.object_prediction_list:
bbox_sahi = pred_obj.bbox
detections_finales.append({
'bbox': [bbox_sahi.minx, bbox_sahi.miny, bbox_sahi.maxx, bbox_sahi.maxy],
'confiance': pred_obj.score.value,
'id_classe': pred_obj.category.id,
'aire': (bbox_sahi.maxx-bbox_sahi.minx)*(bbox_sahi.maxy-bbox_sahi.miny)
})
image_input = cv2.imread(str(chemin_image))
hauteur, largeur = image_input.shape[:2]
return {
'taille_image': (largeur, hauteur),
'nombre_detections': len(detections_finales),
'temps_inference_ms': temps_inference * 1000,
'detections': detections_finales,
'sahi_active': True
}
def executer_inference_en_lot(self, repertoire_images_input, fichier_json_sortie=None):
"""Exécute l'inférence sur un répertoire d'images."""
fichiers_images_a_traiter = list(Path(repertoire_images_input).glob("*.jpg"))
tous_les_resultats = []
print(f"Début du traitement de {len(fichiers_images_a_traiter)} images...")
for i, chemin_img_lot in enumerate(fichiers_images_a_traiter):
resultat_img = self.executer_inference(chemin_img_lot)
resultat_img['nom_image'] = chemin_img_lot.name
tous_les_resultats.append(resultat_img)
if (i+1) % 100 == 0:
print(f"Images traitées: {i+1}/{len(fichiers_images_a_traiter)}")
# Statistiques récapitulatives
total_detections = sum(res['nombre_detections'] for res in tous_les_resultats)
temps_inference_moyen = np.mean([res['temps_inference_ms'] for res in tous_les_resultats])
print(f"\nInférence par lot terminée:")
print(f" Nombre total d'images: {len(fichiers_images_a_traiter)}")
print(f" Nombre total de détections: {total_detections}")
print(f" Détections moyennes par image: {total_detections/len(fichiers_images_a_traiter):.2f}")
print(f" Temps d'inférence moyen: {temps_inference_moyen:.2f} ms par image")
# Statistique des petits objets
compte_petits_objets = sum(
1 for res in tous_les_resultats for det in res['detections']
if det['aire'] < 32*32
)
print(f" Détections de petits objets (<32x32px): {compte_petits_objets} ({100*compte_petits_objets/total_detections:.1f}%)")
# Sauvegarde au format JSON
if fichier_json_sortie:
import json
with open(fichier_json_sortie, 'w') as f_json:
json.dump(tous_les_resultats, f_json, indent=2)
print(f"Les résultats ont été sauvegardés dans: {fichier_json_sortie}")
return tous_les_resultats
def main_deploiement():
"""Fonction principale pour l'exécution du déploiement et de l'inférence."""
# Instanciation du déployeur
gestionnaire_deploiement = DeploiementVisDrone(
chemin_modele='./visdrone-yolo26/exp/weights/best.pt',
seuil_confiance=0.25, # Pour VisDrone et petits objets, 0.2-0.25 est recommandé
seuil_iou=0.45, # Pour scènes denses, 0.4-0.5 est suggéré
peripherique='0', # Utilisation du GPU 0
utiliser_sahi=True, # Activation de l'inférence par découpage SAHI
taille_decoupe=640 # Taille des tuiles pour SAHI
)
# Lancement de l'inférence sur le jeu de validation
resultats_deploiement = gestionnaire_deploiement.executer_inference_en_lot(
repertoire_images_input='./datasets/VisDrone/images/val',
fichier_json_sortie='./resultats_deploiement.json'
)
if __name__ == "__main__":
main_deploiement()
5.2 Exportation du Modèle (Accélération TensorRT)
# exporter.py
from ultralytics import YOLO
import time
from pathlib import Path
import numpy as np
def exporter_modele_pour_deploiement():
"""Exporte le modèle entraîné dans divers formats pour le déploiement."""
modele_entraine = YOLO('./visdrone-yolo26/exp/weights/best.pt')
# Exportation ONNX (compatibilité large)
print("Exportation au format ONNX en cours...")
modele_entraine.export(format='onnx', imgsz=640, half=True, simplify=True)
# Exportation TensorRT (optimisation NVIDIA GPU pour la rapidité)
print("Exportation au format TensorRT (.engine) en cours...")
modele_entraine.export(format='engine', imgsz=640, half=True, workspace=4,
dynamic=True, batch=1) # 'dynamic' permet un batch size variable
# Exportation OpenVINO (optimisation CPU Intel)
print("Exportation au format OpenVINO en cours...")
modele_entraine.export(format='openvino', imgsz=640, half=True)
print("Processus d'exportation terminé !")
def comparer_performances_formats():
"""Compare la vitesse d'inférence entre différents formats exportés."""
formats_a_tester = {
'pytorch': 'visdrone-yolo26/exp/weights/best.pt',
'onnx': 'visdrone-yolo26/exp/weights/best.onnx',
'engine': 'visdrone-yolo26/exp/weights/best.engine',
}
# Création d'une image factice pour le test
entree_factice = np.random.randint(0, 255, (640, 640, 3), dtype=np.uint8)
for nom_format, chemin_modele in formats_a_tester.items():
if not Path(chemin_modele).exists():
print(f"Avertissement: Le fichier {chemin_modele} n'existe pas. Ignoré.")
continue
modele_charge = YOLO(chemin_modele)
# Phase de préchauffage
print(f"Préchauffage du modèle {nom_format}...")
for _ in range(10):
modele_charge.predict(entree_factice, verbose=False)
# Mesure de la vitesse
temps_mesures = []
print(f"Mesure de la vitesse d'inférence pour {nom_format} (100 exécutions)...")
for _ in range(100):
debut_mesure = time.time()
modele_charge.predict(entree_factice, verbose=False)
temps_mesures.append(time.time() - debut_mesure)
temps_moyen_ms = np.mean(temps_mesures) * 1000
print(f"{nom_format:10s}: {temps_moyen_ms:.2f} ms")
if __name__ == "__main__":
exporter_modele_pour_deploiement()
# Décommenter la ligne suivante pour exécuter la comparaison des performances
# comparer_performances_formats()
Phase 6 : Techniques d'Optimisation Avancées
6.1 Calcul Adaptatif des Boîtes d'Ancrage (Re-clustering pour VisDrone)
# calculer_ancres.py
import numpy as np
from pathlib import Path
import yaml
from tqdm import tqdm
def generer_ancres_kmeans(fichier_yaml_dataset, k_ancres=9, taille_image_ref=640):
"""
Calcule des boîtes d'ancrage optimisées pour VisDrone en utilisant l'algorithme K-means
sur les boîtes d'annotation du jeu d'entraînement.
"""
with open(fichier_yaml_dataset, 'r') as f_yaml:
config_data = yaml.safe_load(f_yaml)
repertoire_labels_train = Path(config_data['path']) / 'labels' / 'train'
# Collecte de toutes les dimensions des boîtes d'annotation
toutes_les_boites = []
for fichier_label in tqdm(list(repertoire_labels_train.glob("*.txt")), desc="Collecte des boîtes"):
with open(fichier_label, 'r') as f_lbl:
for ligne in f_lbl:
parties = ligne.strip().split()
if len(parties) == 5:
largeur_norm, hauteur_norm = float(parties[3]), float(parties[4])
toutes_les_boites.append([largeur_norm, hauteur_norm])
toutes_les_boites_np = np.array(toutes_les_boites)
# Implémentation de K-means
def calculer_iou(boite, grappes):
"""Calcule l'Intersection Over Union (IoU) entre une boîte et des grappes."""
x_inter = np.minimum(grappes[:, 0], boite[0])
y_inter = np.minimum(grappes[:, 1], boite[1])
intersection = x_inter * y_inter
aire_boite = boite[0] * boite[1]
aire_grappes = grappes[:, 0] * grappes[:, 1]
iou_val = intersection / (aire_boite + aire_grappes - intersection)
return iou_val
def algorithme_kmeans(boites, k_clusters):
"""Algorithme K-means pour le regroupement de boîtes."""
nombre_boites = boites.shape[0]
derniers_clusters = np.zeros((nombre_boites,), dtype=int)
# Initialisation aléatoire des centres de grappe
indices_initiaux = np.random.choice(nombre_boites, k_clusters, replace=False)
centres_grappes = boites[indices_initiaux]
while True:
# Calcul des distances (1 - IoU)
distances = 1 - calculer_iou(boites[:, None], centres_grappes[None, :])
# Assignation de chaque boîte à la grappe la plus proche
grappes_proches = np.argmin(distances, axis=1)
# Vérification de la convergence
if (derniers_clusters == grappes_proches).all():
break
# Mise à jour des centres de grappes
for idx_cluster in range(k_clusters):
boites_dans_cluster = boites[grappes_proches == idx_cluster]
if len(boites_dans_cluster) > 0:
centres_grappes[idx_cluster] = np.median(boites_dans_cluster, axis=0) # Utilise la médiane
derniers_clusters = grappes_proches
return centres_grappes
clusters_generes = algorithme_kmeans(toutes_les_boites_np, k_ancres)
# Tri des clusters par aire
clusters_tries = clusters_generes[np.argsort(clusters_generes[:, 0] * clusters_generes[:, 1])]
print(f"Boîtes d'ancrage optimisées (par rapport à une image de {taille_image_ref}x{taille_image_ref} pixels):")
liste_ancres_formatees = []
for i in range(0, k_ancres, 3): # Regroupement par 3 pour les 3 couches de détection (P3, P4, P5)
groupe_ancres_pixels = (clusters_tries[i:i+3] * taille_image_ref).flatten().tolist()
# Conversion en entiers pour le fichier de configuration
liste_ancres_formatees.append([int(round(val)) for val in groupe_ancres_pixels])
print(f" P{3+i//3}: {liste_ancres_formatees[-1]}")
return liste_ancres_formatees
if __name__ == "__main__":
ancres_optimisees = generer_ancres_kmeans('./data/visdrone.yaml')
# Sauvegarde des ancres générées dans un fichier YAML
with open('ancres_visdrone_calculees.yaml', 'w') as f_ancres:
yaml.dump({'anchors': ancres_optimisees}, f_ancres, sort_keys=False)
6.2 Script de Surveillance de l'Entraînement
# surveiller_entrainement.py
import wandb
from ultralytics import YOLO
def entrainer_avec_wandb():
"""Intègre Weights & Biases pour la surveillance de l'entraînement."""
# Initialisation de la session Weights & Biases
wandb.init(project="visdrone-yolo26", name="experience-surveillee")
modele_yolo = YOLO('yolo26n.pt')
# Lancement de l'entraînement avec les configurations de base
resultats_entrainement = modele_yolo.train(
data='./data/visdrone.yaml',
epochs=150,
imgsz=640,
batch=16,
project='visdrone-yolo26',
name='rapport_wandb',
)
# Fin de la session Weights & Biases
wandb.finish()
if __name__ == "__main__":
entrainer_avec_wandb()
Liste de Contrôle : Démarrage Rapide
Étape 1 : Préparation de l'Environnement
# Installation des dépendances essentielles
pip install ultralytics>=8.3.0 # Pour la compatibilité YOLOv26
pip install opencv-python pillow pyyaml tqdm requests
pip install sahi # Optionnel, pour l'inférence par découpage
pip install wandb # Optionnel, pour la surveillance de l'entraînement
Étape 2 : Préparation des Données
# 1. Téléchargement et conversion du jeu de données VisDrone
python data/telecharger_visdrone.py
# 2. (Recommandé) Traitement de découpage pour les petits objets
python data/decouper_images.py
# Puis, modifiez le fichier visdrone.yaml : changez 'path' à 'VisDrone_Decoupe/'
Étape 3 : Entraînement
# Entraînement de base (commencez par cette étape pour établir une référence)
python entrainer.py
# Entraînement multi-échelles (avancé)
python entrainer_multi_echelles.py
Étape 4 : Validation
# Validation standard du modèle
python valider.py
# Visualisation des résultats de détection
python visualiser_resultats.py
Étape 5 : Déploiement
# Exportation du modèle pour l'accélération
python exporter.py
# Test d'inférence
python deployer.py
Récapitulatif des Optimisations Clés pour VisDrone
| Point d'Optimisation | Mesure Spécifique | Amélioration Attendue |
|---|---|---|
| Niveau Données | Découpage d'images (640x640, 20% chevauchement) | AP pour petits objets +5~8% |
| Ancrages | Re-clustering K-means (ancrages plus petits) | Rappel pour petits objets +3~5% |
| Augmentation des Données | Copy-Paste 0.3, Mosaic 1.0 | Performance en scènes denses +4~6% |
| Stratégie d'Entraînement | Optimiseur MuSGD, 150-300 époques | Vitesse de convergence +30% |
| Optimisation de l'Inférence | Inférence par découpage SAHI | Détection de petits objets +8~12% |
| Ajustement NMS | Seuil IoU 0.4-0.5 (réduction pour scènes denses) | Réduction des omissions de détection |