Zum Inhalt springen
Vuk Cvetković
Projekte

Vergleichendes System zur Objekterkennung

Ein Bild, zwei Machine-Learning-Ökosysteme, eine Antwort im selben Format.

Jahr
2026
Rahmen
Masterarbeit, Fakultät für Elektronik in Niš
Domäne
Persönliche Schutzausrüstung auf der Baustelle

Der Weg einer Anfrage

  1. Eingang: React-Frontend - React
  2. Bild, multipart/form-data
  3. Orchestrierung: NestJS-Gateway - NestJS
  4. Parallel
    • FastAPI-Service - Ultralytics YOLOv8m - Python, FastAPI
    • ASP.NET-Core-Service - ML.NET-Modell - C#, ASP.NET Core
  5. Ein JSON-Format: Rahmen, Klassen, Konfidenz

Demo

Überblick

Die Masterarbeit geht der Frage nach, was sich ändert, wenn dieselbe Erkennungsaufgabe zweimal umgesetzt wird: einmal in Python, wie in der Forschung üblich, und einmal in .NET, auf dem das Backend ohnehin läuft. Das System trainiert ein YOLOv8m-Modell und ein ML.NET-Modell auf denselben annotierten Bildern, stellt beide hinter eine gemeinsame API und schickt ein Bild durch eines der beiden oder durch beide.

Es geht um persönliche Schutzausrüstung auf der Baustelle, in sechs Klassen: Helm, Weste und Handschuhe, jeweils vorhanden oder fehlend. Entscheidend sind die negativen Klassen. So ein System nützt nur, wenn es erkennt, dass jemand keinen Helm trägt, und nicht bloß, dass irgendwo im Bild ein Helm zu sehen ist.

Technologien

Frontend
React
Gateway
NestJS
Python-Service
Python
FastAPI
Ultralytics
PyTorch
OpenCV
NumPy
.NET-Service
ASP.NET Core
C#
ML.NET
Datensatz
Roboflow

Architektur

  1. Das Frontend sendet das Bild

    React schickt die Datei als multipart/form-data, das gewählte Modell steht in einem zweiten Feld. Sind beide Modelle gewählt, gehen die zwei Anfragen parallel hinaus, und verglichen wird dasselbe Bild im selben Moment.

  2. Das Gateway leitet es weiter

    NestJS ist die einzige Adresse, die das Frontend kennt. Das Gateway nimmt den Upload an und gibt ihn an den FastAPI-Service oder den ASP.NET-Core-Service weiter. So können sich die beiden ML-Services unabhängig vom Client ändern.

  3. Der Service führt die Inferenz aus

    Python liest das Bild als RGB ein und übergibt es an YOLOv8m. Die geladenen Gewichte bleiben im Speicher, damit die .pt-Datei nicht bei jeder Anfrage neu gelesen wird. Die .NET-Seite konvertiert das Bild in ein MLImage und wählt das ML.NET-Modell anhand seiner ID.

  4. Beide antworten in derselben Form

    Klasse, Score und ein Rahmen als x1, y1, x2, y2, dazu die Modell-ID, der Annotationssatz, auf dem trainiert wurde, und die ursprünglichen Bildmaße. Die .NET-Seite begrenzt ihre Koordinaten zuerst auf diese Maße, damit ein Rahmen das Bild nie verlassen kann.

  5. Das Frontend zeichnet es

    Die Antwort ist in beiden Fällen gleich aufgebaut, deshalb gibt es in der Oberfläche keinen YOLO-Zweig und keinen ML.NET-Zweig. Das Frontend rechnet die Koordinaten von der Originalgröße auf die angezeigte Größe um und zeichnet Rahmen, Klassen und Konfidenzwerte über das Bild.

Was es kann

  • Ein Modell oder beide

    Ein Service allein oder beide gleichzeitig, in zwei Panels mit demselben Bild. Alle vier trainierten Modelle lassen sich namentlich auswählen.

  • Eine Konfidenzschwelle, die Sie einstellen

    Ein Schieberegler blendet alles unterhalb der Schwelle aus, so wie ein System im echten Einsatz nur auf Vorhersagen oberhalb eines Grenzwerts reagiert und nicht auf alles, was das Modell liefert.

  • Alle Erkennungen als Liste

    Klasse und Score je Erkennung. Eine ausgewählte Erkennung wird im Bild freigestellt, und so lässt sich auch eine Szene mit acht überlappenden Rahmen prüfen.

  • Umriss, Füllung, Zoom und Pan

    Rahmen als Umriss oder als gefüllte Fläche, eine Erkennung oder alle, dazu Zoom und Pan, um einen Rahmen mit den Pixeln darunter abzugleichen.

  • Die beiden Antworten nebeneinander

    Dauer der Anfrage, Zahl der Erkennungen, mittlere Konfidenz, stärkste Erkennung und welcher Service zuerst geantwortet hat.

  • Health-Checks

    Gateway, Python-API und .NET-API melden jeweils Status und Antwortzeit, denn drei Prozesse können unabhängig voneinander ausfallen, und bei einem leeren Panel soll erkennbar sein, welcher es ist.

Datensatz und Training

Der Datensatz entstand in Roboflow aus zwei öffentlichen Datensätzen, die geprüft und zusammengeführt wurden: 2.911 Bilder, davon 2.374 für das Training, 290 für die Validierung und 247 für den Test, automatisch ausgerichtet und auf 640x640 skaliert. Die Python-Seite bekommt ihn im YOLO-Format, die .NET-Seite im COCO-Format, weil Model Builder dieses Format für die Objekterkennung erwartet. So hat jedes Werkzeug sein Format, bei identischen Bildern und identischen Klassen.

Dann kam der Teil, den ich nicht eingeplant hatte. Nach dem ersten Durchgang gab es 8.813 Annotationen, eine zweite Durchsicht brachte die Zahl auf 17.942. Mehr als die Hälfte der Objekte war also nicht gelabelt. Ein Objekt, das im Bild vorhanden ist, in den Labels aber fehlt, bringt dem Modell im Training bei, dass es Hintergrund ist, und zählt in der Auswertung als Fehler, wenn das Modell es trotzdem erkennt.

Beide Modelle wurden ausgehend von vortrainierten Gewichten 50 Epochen lang bei 640x640 trainiert. Die Parameter blieben über beide Datensatzversionen gleich, damit sich ein Unterschied in den Ergebnissen auf die Annotationsqualität zurückführen lässt und nicht auf die Konfiguration. Model Builder hört beim trainierten Modell auf, deshalb brauchte die .NET-Seite einen eigenen Auswertungs-Service: Er lädt die COCO-Annotationen, erstellt Vorhersagen für den Validierungssatz und berechnet Precision, Recall, F1 und mAP@0.5 samt Konfusionsmatrizen und Precision-Recall-Kurven. Damit steht ML.NET auf derselben Grundlage wie die Ausgabe, die Ultralytics von sich aus liefert.

Ergebnisse

Vergleichendes System zur Objekterkennung - Ergebnisse
ModellAnnotationenPrecisionRecallF1mAP@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-

Die Neuannotation hat bei beiden Modellen jede Metrik verändert. YOLOv8m stieg von 0,790 auf 0,916 mAP@0.5, ML.NET von 0,580 auf 0,748. Relativ gesehen hat also das schwächere Modell am meisten gewonnen, 29 Prozent gegenüber 16. Der erste Durchgang hatte es am stärksten ausgebremst.

YOLOv8m liegt in den Zahlen vorn, und am wichtigsten ist der Abstand beim Recall: 0,867 gegenüber 0,709 auf dem korrigierten Datensatz, bei nahezu gleicher Precision. Bei Schutzausrüstung kommt es genau auf diese Asymmetrie an, denn eine verpasste Erkennung ist ein Mensch, den das System stillschweigend als in Ordnung meldet, und an der Precision allein sieht man nicht, dass das passiert ist.

Der Vergleich ist fair, weil der Aufbau auf beiden Seiten gleich war: eine Bilddomäne, dieselben sechs Klassen und vergleichbare Parameter statt einer getrennten Abstimmung für jedes Modell.

Was ich daraus mitnehme

Über das Ergebnis haben eher die Daten entschieden als die Wahl des Frameworks. Die Annotationen derselben 2.911 Bilder zu überarbeiten, hat beide Modelle weiter vorangebracht, als der Abstand zwischen den beiden Ökosystemen ML.NET zurückgeworfen hat, und mit diesem Fazit hatte ich nicht gerechnet.

Das technische Fazit ist praktischer. Python ließ mir Raum zum Experimentieren und lieferte das Auswertungsmaterial gleich mit, .NET lieferte ein Modell, das sich ohne Zwischenschicht in einen ASP.NET-Core-Service einfügt, und nur wegen der einheitlichen Antwort kann ein einziges Frontend die beiden als austauschbar behandeln.

Alle Projekte

Esc