Aller au contenu
Vuk Cvetković
Projets

Système comparatif de détection d’objets

Une même image soumise à deux écosystèmes d’apprentissage automatique, avec une réponse au même format.

Année
2026
Cadre
Mémoire de master, Faculté de génie électronique de Niš
Domaine
Équipements de protection individuelle sur chantier

Le parcours d’une requête

  1. Entrée: Frontend React - React
  2. Image, multipart/form-data
  3. Orchestration: Passerelle NestJS - NestJS
  4. En parallèle
    • Service FastAPI - Ultralytics YOLOv8m - Python, FastAPI
    • Service ASP.NET Core - Modèle ML.NET - C#, ASP.NET Core
  5. Un seul format JSON : cadres, classes, confiance

Démo

Aperçu

Le mémoire étudie ce qui change quand une même tâche de détection est réalisée deux fois : une fois en Python, l’écosystème privilégié par la recherche, et une fois en .NET, celui sur lequel tourne déjà le backend. Le système entraîne un modèle YOLOv8m et un modèle ML.NET sur les mêmes images annotées, place les deux derrière une seule API, et fait passer une image par l’un, l’autre ou les deux.

Le domaine est l’équipement de protection individuelle sur chantier, en six classes : casque, gilet et gants, chacun présent ou absent. Tout l’intérêt est dans les classes négatives. Un tel système ne sert que s’il peut dire que quelqu’un ne porte pas de casque, et pas seulement qu’un casque se trouve quelque part dans l’image.

Technologies

Frontend
React
Passerelle
NestJS
Service Python
Python
FastAPI
Ultralytics
PyTorch
OpenCV
NumPy
Service .NET
ASP.NET Core
C#
ML.NET
Jeu de données
Roboflow

Architecture

  1. Le frontend envoie l’image

    React envoie le fichier en multipart/form-data, avec le modèle choisi dans un second champ. Si vous demandez les deux, il lance les deux requêtes en parallèle : la comparaison porte donc sur une même image au même instant.

  2. La passerelle l’aiguille

    La passerelle NestJS est la seule adresse que connaît le frontend. Elle reçoit le fichier et le transmet au service FastAPI ou au service ASP.NET Core, ce qui laisse les deux services ML évoluer indépendamment du client.

  3. Le service exécute l’inférence

    Python charge l’image en RGB et la passe à YOLOv8m, en gardant les poids en mémoire pour ne pas relire le fichier .pt à chaque requête. Le côté .NET convertit l’image en MLImage et sélectionne le modèle ML.NET par son identifiant.

  4. Les deux répondent au même format

    Classe, score et cadre sous la forme x1, y1, x2, y2, avec l’identifiant du modèle, le jeu d’annotations sur lequel il a été entraîné et les dimensions d’origine de l’image. Le côté .NET borne d’abord ses coordonnées à ces dimensions, pour qu’un cadre ne puisse jamais sortir de l’image.

  5. Le frontend affiche le résultat

    Puisque la réponse est identique dans les deux cas, il n’y a ni branche YOLO ni branche ML.NET dans l’interface. Elle ramène les coordonnées de la taille d’origine à la taille affichée et trace les cadres, les classes et les scores de confiance sur l’image.

Fonctionnalités

  • Un modèle ou les deux

    Un service seul, ou les deux à la fois dans deux panneaux sur la même image, avec les quatre modèles entraînés accessibles par leur nom.

  • Un seuil de confiance réglable

    Un curseur masque tout ce qui passe sous le seuil, comme en production, où l’on n’agit que sur les prédictions au-dessus d’une limite et non sur tout ce que renvoie le modèle.

  • Toutes les détections, en liste

    Classe et score pour chaque détection. En sélectionner une l’isole dans l’image, ce qui permet de vérifier une scène où huit cadres se chevauchent.

  • Contours, remplissage, zoom et déplacement

    Des cadres en contour ou remplis, une seule détection ou toutes, avec zoom et déplacement pour vérifier un cadre au pixel près.

  • Les deux réponses côte à côte

    Durée de la requête, nombre de détections, confiance moyenne, meilleure détection, et le service qui a répondu le premier.

  • État des services

    La passerelle, l’API Python et l’API .NET indiquent chacune leur état et leur temps de réponse, car trois processus peuvent tomber indépendamment, et un panneau vide doit dire lequel est en panne.

Données et entraînement

Le jeu de données a été assemblé dans Roboflow à partir de deux ensembles publics, puis relu et fusionné en un seul : 2 911 images réparties en 2 374 pour l’entraînement, 290 pour la validation et 247 pour le test, réorientées automatiquement et redimensionnées en 640x640. Le côté Python l’utilise au format YOLO et le côté .NET au format COCO, puisque c’est ce que Model Builder attend pour la détection : chaque outil reçoit son format, sur des images et des classes identiques.

Vient ensuite ce que je n’avais pas prévu. La première version comptait 8 813 annotations, et une reprise complète a porté ce total à 17 942 : plus de la moitié des objets n’étaient donc pas étiquetés. Un objet présent dans l’image mais absent des étiquettes apprend au modèle, pendant l’entraînement, qu’il fait partie du fond, puis lui est compté comme une erreur à l’évaluation quand le modèle le détecte malgré tout.

Les deux modèles ont été entraînés pendant 50 epochs en 640x640 à partir de poids pré-entraînés, avec des paramètres identiques entre les deux versions du jeu de données, pour qu’un écart de résultats reflète la qualité des annotations et non la configuration. Model Builder s’arrête au modèle entraîné, il a donc fallu écrire un service d’évaluation pour le côté .NET : il charge les annotations COCO, prédit sur le jeu de validation et calcule precision, recall, F1 et mAP@0.5 avec les matrices de confusion et les courbes precision-recall, ce qui a mis ML.NET à égalité avec ce qu’Ultralytics fournit d’office.

Résultats

Système comparatif de détection d’objets - Résultats
ModèleAnnotationsPrecisionRecallF1mAP@0.5mAP@0.5:0.95
YOLOv8m8 8130,7380,800-0,7900,455
YOLOv8m17 9420,8940,867-0,9160,552
ML.NET8 8130,6690,6450,6570,580-
ML.NET17 9420,8620,7090,7780,748-

La reprise des annotations a fait évoluer toutes les métriques des deux modèles. YOLOv8m est passé de 0,790 à 0,916 en mAP@0.5 et ML.NET de 0,580 à 0,748 : en relatif, c’est le modèle le plus faible qui gagne le plus, 29 pour cent contre 16. C’est lui que la première version pénalisait le plus.

YOLOv8m l’emporte sur les chiffres, et l’écart qui compte est celui du recall : 0,867 contre 0,709 sur le jeu corrigé, à precision presque égale. Pour des équipements de protection, tout se joue sur cette asymétrie : une détection manquée, c’est une personne que le système déclare conforme sans rien signaler, et la precision seule ne permet pas de le voir.

La comparaison est équitable parce que les conditions étaient les mêmes des deux côtés : un seul domaine d’images, les mêmes six classes, et des paramètres comparables plutôt qu’un réglage séparé pour chaque modèle.

Ce que j’en retiens

Le résultat dépendait plus des données que du choix du framework. Reprendre les annotations des mêmes 2 911 images a fait progresser les deux modèles plus que l’écart entre les deux écosystèmes ne pénalisait ML.NET, et ce n’est pas la conclusion que je pensais écrire.

Côté ingénierie, les enseignements sont plus pratiques. Python m’a laissé de la place pour expérimenter et m’a fourni les éléments d’évaluation sans effort, .NET m’a donné un modèle qui s’intègre à un service ASP.NET Core sans couche intermédiaire, et c’est uniquement grâce à la réponse standardisée qu’un seul frontend peut les traiter comme interchangeables.

Tous les projets

Esc