Tutorial: Erkennen von Objekten mithilfe von ONNX in ML.NET

Erfahren Sie, wie Sie ein vortrainiertes ONNX-Modell in ML.NET verwenden, um Objekte in Bildern zu erkennen.

Das von Grund auf neue Trainieren eines Objekterkennungsmodells erfordert das Festlegen von Millionen von Parametern, zahlreiche bezeichnete Trainingsdaten und eine große Menge an Computeressourcen (Hunderte von GPU-Stunden). Die Verwendung eines vortrainierten Modells ermöglicht es Ihnen, den Trainingsprozess zu verkürzen.

In diesem Tutorial lernen Sie Folgendes:

  • Das Problem verstehen
  • Erfahren Sie, was ONNX ist und wie ONNX mit ML.NET funktioniert.
  • Verstehen des Modells
  • Wiederverwenden des vortrainierten Modells
  • Erkennen von Objekten mit einem geladenen Modell

Voraussetzungen

ONNX-Objekterkennungsbeispiel: Übersicht

Dieses Beispiel erstellt eine .NET Core-Konsolenanwendung, die Objekte in einem Bild mithilfe eines vortrainierten Deep Learning ONNX-Modells erkennt. Den Code für dieses Beispiel finden Sie im dotnet/machinelearning-Beispielrepository auf GitHub.

Was ist Objekterkennung?

Objekterkennung ist ein Problem des maschinellen Sehens. Obwohl die Objekterkennung eng mit der Bildklassifizierung verwandt ist, führt sie die Bildklassifizierung auf einer detaillierteren Ebene durch. Die Objekterkennung ermittelt und kategorisiert Entitäten in Bildern. Objekterkennungsmodelle werden häufig mit Deep Learning und neuronalen Netzwerken trainiert. Weitere Informationen finden Sie unter Deep Learning im Vergleich zu maschinellem Lernen.

Verwenden Sie die Objekterkennung, wenn Bilder mehrere Objekte verschiedener Typen enthalten.

Screenshots showing Image Classification versus Object Classification.

Einige Anwendungsfälle für die Objekterkennung sind:

  • Autonomes Fahren
  • Robotik
  • Gesichtserkennung
  • Arbeitsplatzsicherheit
  • Objektzählung
  • Aktivitätserkennung

Auswählen eines Deep Learning-Modells

Deep Learning ist eine Teilmenge von Machine Learning. Zum Trainieren von Deep Learning-Modellen sind große Mengen von Daten erforderlich. Muster in den Daten werden durch eine Reihe von Schichten dargestellt. Die Beziehungen in den Daten werden als Verbindungen zwischen den Schichten mit Gewichtungen codiert. Je höher die Gewichtung, desto stärker die Beziehung. Zusammen werden diese Schichten und Verbindungen als künstliche neuronale Netze bezeichnet. Je mehr Schichten in einem Netzwerk vorhanden sind, desto „tiefer“ ist es, was es zu einem Deep Neural Network macht.

Es gibt verschiedene Arten von neuronalen Netzen, wobei die häufigsten MLP (Multi-Layered Perceptron), CNN (Convolutional Neural Network) und RNN (Recurrent Neural Network) sind. Das einfachste neuronale Netz ist MLP, das eine Reihe von Eingaben einem Satz von Ausgaben zuordnet. Dieses neuronale Netz eignet sich gut, wenn die Daten nicht über eine räumliche oder zeitliche Komponente verfügen. Das CNN nutzt Faltungsschichten, um die in den Daten enthaltenen räumlichen Informationen zu verarbeiten. Ein guter Anwendungsfall für CNNs ist die Bildverarbeitung, um das Vorhandensein eines Merkmals in einer Region eines Bilds zu erkennen (Beispiel: befindet sich eine Nase in der Mitte eines Bilds?). RNNs schließlich ermöglichen die Verwendung von Persistenz von Zustand oder Speicher als Eingabe. RNNs werden für die Zeitreihenanalyse verwendet, bei der die sequenzielle Reihenfolge und der Kontext der Ereignisse wichtig sind.

Verstehen des Modells

Objekterkennung ist eine Bildverarbeitungsaufgabe. Daher sind die meisten Deep Learning-Modelle, die zur Lösung dieses Problems trainiert werden, CNNs. Das in diesem Tutorial verwendete Modell ist das Tiny YOLOv2-Modell, eine kompaktere Version des YOLOv2-Modells, das im folgenden Dokument beschrieben wird: „YOLO9000: Better, Faster, Stronger“ von Redmon und Farhadi. Tiny YOLOv2 wird mit dem Pascal VOC-Dataset trainiert und besteht aus 15 Schichten, die 20 verschiedene Klassen von Objekten vorhersagen können. Da Tiny YOLOv2 eine komprimierte Version des ursprünglichen YOLOv2-Modells ist, wird ein Kompromiss zwischen Geschwindigkeit und Genauigkeit erzielt. Die verschiedenen Schichten, die das Modell bilden, können mithilfe von Tools wie etwa Netron visualisiert werden. Die Untersuchung des Modells würde eine Zuordnung der Verbindungen zwischen allen Schichten ergeben, aus denen sich das neuronale Netz zusammensetzt, wobei jede Schicht den Namen der Schicht zusammen mit den Dimensionen der jeweiligen Ein- bzw. Ausgabe enthalten würde. Die Datenstrukturen, die zum Beschreiben der Eingaben und Ausgaben des Modells verwendet werden, werden als Tensoren bezeichnet. Tensoren können als Container betrachtet werden, in denen Daten in N Dimensionen gespeichert werden. Im Fall von Tiny YOLOv2 ist der Name der Eingabeschicht image, und es wird ein Tensor mit den Dimensionen 3 x 416 x 416 erwartet. Der Name der Ausgabeschicht ist grid, und es wird ein Tensor mit den Dimensionen 125 x 13 x 13 generiert.

Input layer being split into hidden layers, then output layer

Das Yolo-Modell verwendet ein Bild mit 3(RGB) x 416px x 416px. Das Modell nimmt diese Eingabe an und übergibt sie durch die verschiedenen Schichten, um eine Ausgabe zu generieren. Die Ausgabe teilt das Eingabebild in ein 13 x 13-Raster auf, wobei jede Zelle im Raster aus 125 Werten besteht.

Was ist ein ONNX-Modell?

ONNX (Open Neural Network Exchange) ist ein Open-Source-Format für KI-Modelle. ONNX unterstützt die Interoperabilität zwischen Frameworks. Das bedeutet, dass Sie ein Modell in einem der vielen gängigen Machine Learning-Frameworks wie PyTorch trainieren, es in das ONNX-Format konvertieren und das ONNX-Modell in einem anderen Framework wie ML.NET verwenden können. Weitere Informationen finden Sie auf der ONNX-Website.

Diagram of ONNX supported formats being used.

Das vortrainierte Tiny YOLOv2-Modell wird im ONNX-Format gespeichert, einer serialisierten Darstellung der Schichten und erlernten Muster dieser Schichten. In ML.NET wird die Interoperabilität mit ONNX mit den NuGet-Paketen ImageAnalytics und OnnxTransformer erreicht. Das Paket ImageAnalytics enthält eine Reihe von Transformationen, die ein Bild annehmen und in numerische Werte codieren, die als Eingabe in eine Vorhersage- oder Trainingspipeline verwendet werden können. Das Paket OnnxTransformer nutzt die ONNX Runtime, um ein ONNX-Modell zu laden und damit Vorhersagen basierend auf bereitgestellten Eingaben zu treffen.

Data flow of ONNX file into the ONNX Runtime.

Einrichten des .NET-Konsolenprojekts

Da Sie nun ein allgemeines Verständnis davon haben, was ONNX ist und wie Tiny YOLOv2 funktioniert, ist es an der Zeit, die Anwendung zu erstellen.

Erstellen einer Konsolenanwendung

  1. Erstellen Sie eine C#-Konsolenanwendung mit dem Namen „ObjectDetection“. Klicken Sie auf die Schaltfläche Weiter.

  2. Wählen Sie .NET 6 als zu verwendendes Framework aus. Klicken Sie auf die Schaltfläche Erstellen .

  3. Installieren des Microsoft.ML NuGet-Pakets:

    Hinweis

    In diesem Beispiel wird, sofern nicht anders angegeben, die neueste stabile Version der genannten NuGet-Pakete verwendet.

    • Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf das Projekt, und wählen Sie NuGet-Pakete verwalten aus.
    • Wählen Sie „nuget.org“ als Paketquelle aus, wählen Sie die Registerkarte „Durchsuchen“ aus, und suchen Sie nach Microsoft.ML.
    • Wählen Sie die Schaltfläche Installieren aus.
    • Wählen Sie die Schaltfläche OK im Dialogfeld Vorschau der Änderungen und dann die Schaltfläche Ich stimme zu im Dialogfeld Zustimmung zur Lizenz aus, wenn Sie den Lizenzbedingungen für die aufgelisteten Pakete zustimmen.
    • Wiederholen Sie diese Schritte für Microsoft.Windows.Compatibility, Microsoft.ML.ImageAnalytics, Microsoft.ML.OnnxTransformer und Microsoft.ML.OnnxRuntime.

Vorbereiten der Daten und des vortrainierten Modells

  1. Laden Sie die ZIP-Datei aus dem Projektressourcenverzeichnis herunter, und entzippen Sie sie.

  2. Kopieren Sie das Verzeichnis assets in Ihr ObjectDetection-Projektverzeichnis. Dieses Verzeichnis und seine Unterverzeichnisse enthalten die für dieses Tutorial erforderlichen Bilddateien (mit Ausnahme des Tiny YOLOv2-Modells, das Sie im nächsten Schritt herunterladen und hinzufügen).

  3. Laden Sie das Tiny YOLOv2-Modell aus dem ONNX Model Zoo herunter.

  4. Kopieren Sie die Datei model.onnx in Ihr ObjectDetection-Projektverzeichnis assets\Model und benennen Sie sie in TinyYolo2_model.onnx um. Dieses Verzeichnis enthält das für dieses Tutorial erforderliche Modell.

  5. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf alle Dateien im Assetverzeichnis und den Unterverzeichnissen, und wählen Sie Eigenschaften aus. Ändern Sie unter Erweitert den Wert von In Ausgabeverzeichnis kopieren in Kopieren, wenn neuer.

Erstellen von Klassen und Definieren von Pfaden

Öffnen Sie die Datei Program.cs, und fügen Sie am Anfang der Datei folgende zusätzliche using-Anweisungen hinzu:

using System.Drawing;
using System.Drawing.Drawing2D;
using ObjectDetection.YoloParser;
using ObjectDetection.DataStructures;
using ObjectDetection;
using Microsoft.ML;

Definieren Sie als nächstes die Pfade der verschiedenen Ressourcen.

  1. Erstellen Sie zunächst die GetAbsolutePath-Methode am Ende der Datei Program.cs.

    string GetAbsolutePath(string relativePath)
    {
        FileInfo _dataRoot = new FileInfo(typeof(Program).Assembly.Location);
        string assemblyFolderPath = _dataRoot.Directory.FullName;
    
        string fullPath = Path.Combine(assemblyFolderPath, relativePath);
    
        return fullPath;
    }
    
  2. Erstellen Sie dann unterhalb der using-Anweisungen Felder, um den Speicherort Ihrer Ressourcen zu speichern.

    var assetsRelativePath = @"../../../assets";
    string assetsPath = GetAbsolutePath(assetsRelativePath);
    var modelFilePath = Path.Combine(assetsPath, "Model", "TinyYolo2_model.onnx");
    var imagesFolder = Path.Combine(assetsPath, "images");
    var outputFolder = Path.Combine(assetsPath, "images", "output");
    

Fügen Sie Ihrem Projekt ein neues Verzeichnis hinzu, um die Eingabedaten und Vorhersageklassen zu speichern.

Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf das Projekt, und wählen Sie dann Hinzufügen>Neuer Ordner aus. Wenn der neue Ordner im Projektmappen-Explorer angezeigt wird, nennen Sie ihn „DataStructures“.

Erstellen Sie die Eingabedatenklasse im neu erstellten Verzeichnis DataStructures.

  1. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf das Verzeichnis DataStructures, und wählen Sie dann Hinzufügen>Neues Element aus.

  2. Wählen Sie im Dialogfeld Neues Element hinzufügen die Option Klasse aus, und ändern Sie das Feld Name in ImageNetData.cs. Wählen Sie dann die Schaltfläche Hinzufügen aus.

    Die Datei ImageNetData.cs wird im Code-Editor geöffnet. Fügen Sie die folgende using-Anweisung am Anfang der Datei ImageNetData.cs hinzu:

    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using Microsoft.ML.Data;
    

    Entfernen Sie die vorhandene Klassendefinition, und fügen Sie der Datei ImageNetData.cs den folgenden Code für die ImageNetData-Klasse hinzu:

    public class ImageNetData
    {
        [LoadColumn(0)]
        public string ImagePath;
    
        [LoadColumn(1)]
        public string Label;
    
        public static IEnumerable<ImageNetData> ReadFromFile(string imageFolder)
        {
            return Directory
                .GetFiles(imageFolder)
                .Where(filePath => Path.GetExtension(filePath) != ".md")
                .Select(filePath => new ImageNetData { ImagePath = filePath, Label = Path.GetFileName(filePath) });
        }
    }
    

    ImageNetData ist die Eingabebilddaten-Klasse mit den folgenden String-Feldern:

    • ImagePath enthält den Pfad, in dem das Bild gespeichert ist.
    • Label enthält den Namen der Datei.

    Darüber hinaus enthält ImageNetData eine Methode ReadFromFile, die mehrere Bilddateien lädt, die im angegebenen Pfad imageFolder gespeichert sind, und sie als eine Sammlung von ImageNetData-Objekten zurückgibt.

Erstellen Sie die Vorhersageklasse im Verzeichnis DataStructures.

  1. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf das Verzeichnis DataStructures, und wählen Sie dann Hinzufügen>Neues Element aus.

  2. Wählen Sie im Dialogfeld Neues Element hinzufügen die Option Klasse aus, und ändern Sie das Feld Name in ImageNetPrediction.cs. Wählen Sie dann die Schaltfläche Hinzufügen aus.

    Die Datei ImageNetPrediction.cs wird im Code-Editor geöffnet. Fügen Sie die folgende using-Anweisung am Anfang der Datei ImageNetPrediction.cs hinzu:

    using Microsoft.ML.Data;
    

    Entfernen Sie die vorhandene Klassendefinition, und fügen Sie der Datei ImageNetPrediction.cs den folgenden Code für die ImageNetPrediction-Klasse hinzu:

    public class ImageNetPrediction
    {
        [ColumnName("grid")]
        public float[] PredictedLabels;
    }
    

    ImageNetPrediction ist die Vorhersagedatenklasse und weist das folgende float[]-Feld auf:

    • PredictedLabels enthält die Dimensionen, die Objektbewertung und die Klassenwahrscheinlichkeiten für jeden der Begrenzungsrahmen, der in einem Bild erkannt wird.

Initialisieren der Variablen

Die MLContext-Klasse ist der Startpunkt für alle ML.NET-Vorgänge. Durch das Initialisieren von mlContext wird eine neue ML.NET-Umgebung erstellt, die für mehrere Objekte des Modellerstellungsworkflows verwendet werden kann. Die Klasse ähnelt dem Konzept von DBContext in Entity Framework.

Initialisieren Sie die mlContext-Variable mit einer neuen Instanz von MLContext, indem Sie die folgende Zeile unter dem Feld outputFolder hinzufügen.

MLContext mlContext = new MLContext();

Erstellen eines Parsers für die Nachverarbeitung von Modellausgaben

Das Modell segmentiert ein Bild in ein 13 x 13-Raster, in dem jede Rasterzelle 32px x 32px groß ist. Jede Rasterzelle enthält fünf potenzielle Objektbegrenzungsrahmen. Ein Begrenzungsrahmen enthält 25 Elemente:

Grid sample on the left, and Bounding Box sample on the right

  • x: die x-Position der Mitte des Begrenzungsrahmens relativ zu der Rasterzelle, der er zugeordnet ist.
  • y: die y-Position der Mitte des Begrenzungsrahmens relativ zu der Rasterzelle, der er zugeordnet ist.
  • w: Die Breite des Begrenzungsrahmens.
  • h: Die Höhe des Begrenzungsrahmens.
  • o: Der Konfidenzwert, mit dem ein Objekt im Begrenzungsrahmen vorhanden ist (auch als Objectness Score bezeichnet).
  • p1-p20: Klassenwahrscheinlichkeiten für jede der 20 vom Modell vorhergesagten Klassen.

Insgesamt bilden die 25 Elemente, die jeden der 5 Begrenzungsrahmen beschreiben, die 125 Elemente, die in jeder Rasterzelle enthalten sind.

Die Ausgabe, die vom vortrainierten ONNX-Modell generiert wird, ist ein float-Array der Länge 21125, das die Elemente eines Tensors mit Dimensionen 125 x 13 x 13 darstellt. Um die vom Modell generierten Vorhersagen in einen Tensor zu transformieren, sind einige Nachverarbeitungsaufgaben erforderlich. Erstellen Sie zu diesem Zweck eine Reihe von Klassen, um die Analyse der Ausgabe zu unterstützen.

Fügen Sie Ihrem Projekt ein neues Verzeichnis hinzu, um den Satz von Parserklassen zu organisieren.

  1. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf das Projekt, und wählen Sie dann Hinzufügen>Neuer Ordner aus. Wenn der neue Ordner im Projektmappen-Explorer angezeigt wird, nennen Sie ihn „YoloParser“.

Erstellen von Begrenzungsrahmen und Dimensionen

Die Daten, die vom Modell ausgegeben werden, enthalten Koordinaten und Dimensionen der Begrenzungsrahmen von Objekten im Bild. Erstellen Sie eine Basisklasse für Dimensionen.

  1. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf das Verzeichnis YoloParser, und wählen Sie dann Hinzufügen>Neues Element aus.

  2. Wählen Sie im Dialogfeld Neues Element hinzufügen die Option Klasse aus, und ändern Sie das Feld Name in DimensionsBase.cs. Wählen Sie dann die Schaltfläche Hinzufügen aus.

    Die Datei DimensionsBase.cs wird im Code-Editor geöffnet. Entfernen Sie alle using-Anweisungen und die vorhandene Klassendefinition.

    Fügen Sie der Datei DimensionsBase.cs den folgenden Code für die DimensionsBase-Klasse hinzu:

    public class DimensionsBase
    {
        public float X { get; set; }
        public float Y { get; set; }
        public float Height { get; set; }
        public float Width { get; set; }
    }
    

    DimensionsBase weist die folgenden float-Eigenschaften auf:

    • X enthält die Position des Objekts auf der X-Achse.
    • Y enthält die Position des Objekts auf der Y-Achse.
    • Height enthält die Höhe des Objekts.
    • Width enthält die Breite des Objekts.

Erstellen Sie nun eine Klasse für Ihre Begrenzungsrahmen.

  1. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf das Verzeichnis YoloParser, und wählen Sie dann Hinzufügen>Neues Element aus.

  2. Wählen Sie im Dialogfeld Neues Element hinzufügen die Option Klasse aus, und ändern Sie das Feld Name in YoloBoundingBox.cs. Wählen Sie dann die Schaltfläche Hinzufügen aus.

    Die Datei YoloBoundingBox.cs wird im Code-Editor geöffnet. Fügen Sie die folgende using-Anweisung am Anfang der Datei YoloBoundingBox.cs hinzu:

    using System.Drawing;
    

    Fügen Sie direkt oberhalb der vorhandenen Klassendefinition eine neue Klassendefinition namens BoundingBoxDimensions hinzu, die von der DimensionsBase-Klasse erbt, um die Dimensionen des entsprechenden Begrenzungsrahmens zu enthalten.

    public class BoundingBoxDimensions : DimensionsBase { }
    

    Entfernen Sie die vorhandene YoloBoundingBox-Klassendefinition, und fügen Sie der Datei YoloBoundingBox.cs den folgenden Code für die YoloBoundingBox-Klasse hinzu:

    public class YoloBoundingBox
    {
        public BoundingBoxDimensions Dimensions { get; set; }
    
        public string Label { get; set; }
    
        public float Confidence { get; set; }
    
        public RectangleF Rect
        {
            get { return new RectangleF(Dimensions.X, Dimensions.Y, Dimensions.Width, Dimensions.Height); }
        }
    
        public Color BoxColor { get; set; }
    }
    

    YoloBoundingBox weist die folgenden Eigenschaften auf:

    • Dimensions enthält Dimensionen des Begrenzungsrahmens.
    • Label enthält die Klasse des Objekts, das innerhalb des Begrenzungsrahmens erkannt wurde.
    • Confidence enthält die Konfidenz der Klasse.
    • Rect enthält die Rechteckdarstellung der Abmessungen des Begrenzungsrahmens.
    • BoxColor enthält die Farbe, die der jeweiligen Klasse zugeordnet ist, die zum Zeichnen des Bilds verwendet wird.

Erstellen des Parsers

Da nun die Klassen für Dimensionen und Begrenzungsrahmen erstellt wurden, ist es an der Zeit, den Parser zu erstellen.

  1. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf das Verzeichnis YoloParser, und wählen Sie dann Hinzufügen>Neues Element aus.

  2. Wählen Sie im Dialogfeld Neues Element hinzufügen die Option Klasse aus, und ändern Sie das Feld Name in YoloOutputParser.cs. Wählen Sie dann die Schaltfläche Hinzufügen aus.

    Die Datei YoloOutputParser.cs wird im Code-Editor geöffnet. Fügen Sie die folgenden using-Anweisungen am Anfang der Datei YoloOutputParser.cs hinzu:

    using System;
    using System.Collections.Generic;
    using System.Drawing;
    using System.Linq;
    

    Fügen Sie innerhalb der vorhandenen YoloOutputParser-Klassendefinition eine geschachtelte Klasse hinzu, die die Dimensionen der einzelnen Zellen im Bild enthält. Fügen Sie den folgenden Code am Anfang der YoloOutputParser-Klassendefinition für die CellDimensions-Klasse hinzu, die von der DimensionsBase-Klasse erbt.

    class CellDimensions : DimensionsBase { }
    
  3. Fügen Sie in der YoloOutputParser-Klassendefinition die folgenden Konstanten und Felder hinzu.

    public const int ROW_COUNT = 13;
    public const int COL_COUNT = 13;
    public const int CHANNEL_COUNT = 125;
    public const int BOXES_PER_CELL = 5;
    public const int BOX_INFO_FEATURE_COUNT = 5;
    public const int CLASS_COUNT = 20;
    public const float CELL_WIDTH = 32;
    public const float CELL_HEIGHT = 32;
    
    private int channelStride = ROW_COUNT * COL_COUNT;
    
    • ROW_COUNT ist die Anzahl der Zeilen im Raster, in die das Bild aufgeteilt wird.
    • COL_COUNT ist die Anzahl der Spalten im Raster, in die das Bild aufgeteilt wird.
    • CHANNEL_COUNT ist die Gesamtzahl der Werte, die in einer Zelle des Rasters enthalten sind.
    • BOXES_PER_CELL ist die Anzahl der Begrenzungsrahmen in einer Zelle.
    • BOX_INFO_FEATURE_COUNT ist die Anzahl der in einem Feld enthaltenen Merkmale (x, y, Höhe, Breite, Konfidenz).
    • CLASS_COUNT ist die Anzahl der Klassenvorhersagen, die in jedem Begrenzungsrahmen enthalten sind.
    • CELL_WIDTH ist die Breite einer Zelle im Bildraster.
    • CELL_HEIGHT ist die Höhe einer Zelle im Bildraster.
    • channelStride ist die Anfangsposition der aktuellen Zelle im Raster.

    Wenn das Modell eine Vorhersage erstellt (auch als Bewertung bezeichnet), unterteilt es das Eingabebild 416px x 416px in ein Raster von Zellen mit einer Größe von 13 x 13. Jede enthaltene Zelle ist 32px x 32px groß. In jeder Zelle sind fünf Begrenzungsrahmen enthalten, die jeweils fünf Merkmale enthalten (x, y, Breite, Höhe, Konfidenz). Außerdem enthält jeder Begrenzungsrahmen die Wahrscheinlichkeit der einzelnen Klassen (in diesem Fall ist sie 20). Daher enthält jede Zelle 125 Informationen (5 Merkmale und 20 Klassenwahrscheinlichkeiten).

Erstellen Sie eine Liste der Anker unter channelStride für alle 5 Begrenzungsrahmen:

private float[] anchors = new float[]
{
    1.08F, 1.19F, 3.42F, 4.41F, 6.63F, 11.38F, 9.42F, 5.11F, 16.62F, 10.52F
};

Anker sind vordefinierte Höhen- und Breitenverhältnisse für Begrenzungsrahmen. Die meisten von einem Modell erkannten Objekte oder Klassen weisen ähnliche Verhältnisse auf. Dies ist nützlich, wenn es um das Erstellen von Begrenzungsrahmen geht. Anstatt die Begrenzungsrahmen vorherzusagen, wird der Offset zu den vordefinierten Abmessungen berechnet, wodurch die für die Vorhersage des Begrenzungsrahmens erforderliche Berechnung verringert wird. Normalerweise werden diese Ankerverhältnisse basierend auf dem verwendeten Dataset berechnet. In diesem Fall können die Anker hartcodiert werden, da das Dataset bekannt ist und die Werte vorberechnet wurden.

Definieren Sie nun die Bezeichnungen oder Klassen, die vom Modell vorhergesagt werden. Dieses Modell sagt 20 Klassen voraus, was eine Teilmenge der Gesamtzahl der vom ursprünglichen YOLOv2-Modell vorhergesagten Klassen ist.

Fügen Sie die Liste der Bezeichnungen unterhalb von anchors hinzu.

private string[] labels = new string[]
{
    "aeroplane", "bicycle", "bird", "boat", "bottle",
    "bus", "car", "cat", "chair", "cow",
    "diningtable", "dog", "horse", "motorbike", "person",
    "pottedplant", "sheep", "sofa", "train", "tvmonitor"
};

Jeder der Klassen sind Farben zugeordnet. Weisen Sie Ihre Klassenfarben unter labels zu:

private static Color[] classColors = new Color[]
{
    Color.Khaki,
    Color.Fuchsia,
    Color.Silver,
    Color.RoyalBlue,
    Color.Green,
    Color.DarkOrange,
    Color.Purple,
    Color.Gold,
    Color.Red,
    Color.Aquamarine,
    Color.Lime,
    Color.AliceBlue,
    Color.Sienna,
    Color.Orchid,
    Color.Tan,
    Color.LightPink,
    Color.Yellow,
    Color.HotPink,
    Color.OliveDrab,
    Color.SandyBrown,
    Color.DarkTurquoise
};

Erstellen von Hilfsfunktionen

In der Nachverarbeitungsphase sind mehrere Schritte erforderlich. Zur Unterstützung können mehrere Hilfsmethoden eingesetzt werden.

Die folgenden Hilfsmethoden werden vom Parser verwendet:

  • Sigmoid wendet die Sigmoidfunktion an, die eine Zahl zwischen 0 und 1 ausgibt.
  • Softmax normalisiert einen Eingabevektor in eine Wahrscheinlichkeitsverteilung.
  • GetOffset ordnet Elemente in der eindimensionalen Modellausgabe der entsprechenden Position in einem 125 x 13 x 13-Tensor zu.
  • ExtractBoundingBoxes extrahiert die Abmessungen des Begrenzungsrahmens mithilfe der GetOffset-Methode aus der Modellausgabe.
  • GetConfidence extrahiert den Konfidenzwert, der angibt, wie sicher sich das Modell ist, dass ein Objekt erkannt wurde, und verwendet die Sigmoid-Funktion, um diesen Wert in eine Prozentangabe umzuwandeln.
  • MapBoundingBoxToCell verwendet die Abmessungen des Begrenzungsrahmens und ordnet diese der entsprechenden Zelle im Bild zu.
  • ExtractClasses extrahiert die Klassenvorhersagen für den Begrenzungsrahmen aus der Modellausgabe unter Verwendung der GetOffset-Methode und wandelt sie mithilfe der Softmax-Methode in eine Wahrscheinlichkeitsverteilung um.
  • GetTopResult wählt die Klasse aus der Liste der vorhergesagten Klassen mit der höchsten Wahrscheinlichkeit aus.
  • IntersectionOverUnion filtert sich überlappende Begrenzungsrahmen mit geringeren Wahrscheinlichkeiten.

Fügen Sie den Code für alle Hilfsmethoden unterhalb der Liste der classColors hinzu.

private float Sigmoid(float value)
{
    var k = (float)Math.Exp(value);
    return k / (1.0f + k);
}

private float[] Softmax(float[] values)
{
    var maxVal = values.Max();
    var exp = values.Select(v => Math.Exp(v - maxVal));
    var sumExp = exp.Sum();

    return exp.Select(v => (float)(v / sumExp)).ToArray();
}

private int GetOffset(int x, int y, int channel)
{
    // YOLO outputs a tensor that has a shape of 125x13x13, which 
    // WinML flattens into a 1D array.  To access a specific channel 
    // for a given (x,y) cell position, we need to calculate an offset
    // into the array
    return (channel * this.channelStride) + (y * COL_COUNT) + x;
}

private BoundingBoxDimensions ExtractBoundingBoxDimensions(float[] modelOutput, int x, int y, int channel)
{
    return new BoundingBoxDimensions
    {
        X = modelOutput[GetOffset(x, y, channel)],
        Y = modelOutput[GetOffset(x, y, channel + 1)],
        Width = modelOutput[GetOffset(x, y, channel + 2)],
        Height = modelOutput[GetOffset(x, y, channel + 3)]
    };
}

private float GetConfidence(float[] modelOutput, int x, int y, int channel)
{
    return Sigmoid(modelOutput[GetOffset(x, y, channel + 4)]);
}

private CellDimensions MapBoundingBoxToCell(int x, int y, int box, BoundingBoxDimensions boxDimensions)
{
    return new CellDimensions
    {
        X = ((float)x + Sigmoid(boxDimensions.X)) * CELL_WIDTH,
        Y = ((float)y + Sigmoid(boxDimensions.Y)) * CELL_HEIGHT,
        Width = (float)Math.Exp(boxDimensions.Width) * CELL_WIDTH * anchors[box * 2],
        Height = (float)Math.Exp(boxDimensions.Height) * CELL_HEIGHT * anchors[box * 2 + 1],
    };
}

public float[] ExtractClasses(float[] modelOutput, int x, int y, int channel)
{
    float[] predictedClasses = new float[CLASS_COUNT];
    int predictedClassOffset = channel + BOX_INFO_FEATURE_COUNT;
    for (int predictedClass = 0; predictedClass < CLASS_COUNT; predictedClass++)
    {
        predictedClasses[predictedClass] = modelOutput[GetOffset(x, y, predictedClass + predictedClassOffset)];
    }
    return Softmax(predictedClasses);
}

private ValueTuple<int, float> GetTopResult(float[] predictedClasses)
{
    return predictedClasses
        .Select((predictedClass, index) => (Index: index, Value: predictedClass))
        .OrderByDescending(result => result.Value)
        .First();
}

private float IntersectionOverUnion(RectangleF boundingBoxA, RectangleF boundingBoxB)
{
    var areaA = boundingBoxA.Width * boundingBoxA.Height;

    if (areaA <= 0)
        return 0;

    var areaB = boundingBoxB.Width * boundingBoxB.Height;

    if (areaB <= 0)
        return 0;

    var minX = Math.Max(boundingBoxA.Left, boundingBoxB.Left);
    var minY = Math.Max(boundingBoxA.Top, boundingBoxB.Top);
    var maxX = Math.Min(boundingBoxA.Right, boundingBoxB.Right);
    var maxY = Math.Min(boundingBoxA.Bottom, boundingBoxB.Bottom);

    var intersectionArea = Math.Max(maxY - minY, 0) * Math.Max(maxX - minX, 0);

    return intersectionArea / (areaA + areaB - intersectionArea);
}

Nachdem Sie alle Hilfsmethoden definiert haben, ist es an der Zeit, Sie zum Verarbeiten der Modellausgabe zu verwenden.

Erstellen Sie unter der IntersectionOverUnion-Methode die ParseOutputs-Methode, um die vom Modell generierte Ausgabe zu verarbeiten.

public IList<YoloBoundingBox> ParseOutputs(float[] yoloModelOutputs, float threshold = .3F)
{

}

Erstellen Sie eine Liste, um die Begrenzungsrahmen zu speichern, und definieren Sie Variablen in der ParseOutputs-Methode.

var boxes = new List<YoloBoundingBox>();

Jedes Bild wird in ein Raster von 13 x 13-Zellen aufgeteilt. Jede Zelle enthält fünf Begrenzungsrahmen. Fügen Sie unterhalb der Variablen boxes Code hinzu, um alle Rahmen jeder Zelle zu verarbeiten.

for (int row = 0; row < ROW_COUNT; row++)
{
    for (int column = 0; column < COL_COUNT; column++)
    {
        for (int box = 0; box < BOXES_PER_CELL; box++)
        {

        }
    }
}

Berechnen Sie in der innersten Schleife die Anfangsposition des aktuellen Rahmens innerhalb der eindimensionalen Modellausgabe.

var channel = (box * (CLASS_COUNT + BOX_INFO_FEATURE_COUNT));

Verwenden Sie direkt darunter die ExtractBoundingBoxDimensions-Methode, um die Abmessungen des aktuellen Begrenzungsrahmens abzurufen.

BoundingBoxDimensions boundingBoxDimensions = ExtractBoundingBoxDimensions(yoloModelOutputs, row, column, channel);

Verwenden Sie dann die GetConfidence-Methode, um die Konfidenz für den aktuellen Begrenzungsrahmen abzurufen.

float confidence = GetConfidence(yoloModelOutputs, row, column, channel);

Verwenden Sie anschließend die MapBoundingBoxToCell-Methode, um der aktuellen Zelle, die verarbeitet wird, den aktuellen Begrenzungsrahmen zuzuordnen.

CellDimensions mappedBoundingBox = MapBoundingBoxToCell(row, column, box, boundingBoxDimensions);

Überprüfen Sie vor der weiteren Verarbeitung, ob Ihr Konfidenzwert größer als der angegebene Schwellenwert ist. Wenn dies nicht der Fall ist, verarbeiten Sie den nächsten Begrenzungsrahmen.

if (confidence < threshold)
    continue;

Setzen Sie andernfalls die Verarbeitung der Ausgabe fort. Der nächste Schritt besteht darin, die Wahrscheinlichkeitsverteilung der vorhergesagten Klassen für den aktuellen Begrenzungsrahmen mithilfe der ExtractClasses-Methode abzurufen.

float[] predictedClasses = ExtractClasses(yoloModelOutputs, row, column, channel);

Verwenden Sie dann die GetTopResult-Methode, um den Wert und den Index der Klasse mit der höchsten Wahrscheinlichkeit für den aktuellen Rahmen abzurufen, und berechnen Sie die Bewertung.

var (topResultIndex, topResultScore) = GetTopResult(predictedClasses);
var topScore = topResultScore * confidence;

Verwenden Sie topScore, um erneut nur die Begrenzungsrahmen beizubehalten, die über dem angegebenen Schwellenwert liegen.

if (topScore < threshold)
    continue;

Wenn der aktuelle Begrenzungsrahmen den Schwellenwert überschreitet, erstellen Sie ein neues BoundingBox-Objekt, und fügen Sie es der boxes-Liste hinzu.

boxes.Add(new YoloBoundingBox()
{
    Dimensions = new BoundingBoxDimensions
    {
        X = (mappedBoundingBox.X - mappedBoundingBox.Width / 2),
        Y = (mappedBoundingBox.Y - mappedBoundingBox.Height / 2),
        Width = mappedBoundingBox.Width,
        Height = mappedBoundingBox.Height,
    },
    Confidence = topScore,
    Label = labels[topResultIndex],
    BoxColor = classColors[topResultIndex]
});

Nachdem alle Zellen im Bild verarbeitet wurden, geben Sie die boxes-Liste zurück. Fügen Sie die folgende Rückgabeanweisung unterhalb der äußersten for-Schleife in der ParseOutputs-Methode hinzu.

return boxes;

Filtern sich überlappender Rahmen

Nachdem nun alle Begrenzungsrahmen mit einem hohen Konfidenzwert aus der Modellausgabe extrahiert wurden, muss eine zusätzliche Filterung durchgeführt werden, um sich überlappende Bilder zu entfernen. Fügen Sie unter der ParseOutputs-Methode eine Methode namens FilterBoundingBoxes hinzu:

public IList<YoloBoundingBox> FilterBoundingBoxes(IList<YoloBoundingBox> boxes, int limit, float threshold)
{

}

Erstellen Sie innerhalb der FilterBoundingBoxes-Methode zunächst ein Array in der Größe der erkannten Rahmen, und markieren Sie alle Slots als aktiv oder bereit zur Verarbeitung.

var activeCount = boxes.Count;
var isActiveBoxes = new bool[boxes.Count];

for (int i = 0; i < isActiveBoxes.Length; i++)
    isActiveBoxes[i] = true;

Sortieren Sie die Liste mit den Begrenzungsrahmen dann in absteigender Reihenfolge basierend auf dem Konfidenzwert.

var sortedBoxes = boxes.Select((b, i) => new { Box = b, Index = i })
                    .OrderByDescending(b => b.Box.Confidence)
                    .ToList();

Erstellen Sie anschließend eine Liste, in der die gefilterten Ergebnisse gespeichert werden.

var results = new List<YoloBoundingBox>();

Beginnen Sie mit der Verarbeitung der einzelnen Begrenzungsrahmen, indem Sie die einzelnen Begrenzungsrahmen durchlaufen.

for (int i = 0; i < boxes.Count; i++)
{

}

Überprüfen Sie in dieser for-Schleife, ob der aktuelle Begrenzungsrahmen verarbeitet werden kann.

if (isActiveBoxes[i])
{

}

Wenn dies der Fall ist, fügen Sie den Begrenzungsrahmen der Ergebnisliste hinzu. Wenn die Ergebnisse den angegebenen Grenzwert für die zu extrahierenden Rahmen überschreiten, unterbrechen Sie die Schleife. Fügen Sie den folgenden Code in der if-Anweisung hinzu.

var boxA = sortedBoxes[i].Box;
results.Add(boxA);

if (results.Count >= limit)
    break;

Untersuchen Sie andernfalls die angrenzenden Begrenzungsrahmen. Fügen Sie unter der Überprüfung der Rahmengrenze den folgenden Code hinzu.

for (var j = i + 1; j < boxes.Count; j++)
{

}

Wenn der benachbarte Rahmen aktiv oder bereit zur Verarbeitung ist, verwenden Sie wie beim ersten Rahmen die IntersectionOverUnion-Methode, um zu überprüfen, ob der erste Rahmen und der zweite Rahmen den angegebenen Schwellenwert überschreiten. Fügen Sie der innersten for-Schleife den folgenden Code hinzu.

if (isActiveBoxes[j])
{
    var boxB = sortedBoxes[j].Box;

    if (IntersectionOverUnion(boxA.Rect, boxB.Rect) > threshold)
    {
        isActiveBoxes[j] = false;
        activeCount--;

        if (activeCount <= 0)
            break;
    }
}

Überprüfen Sie außerhalb der inneren for-Schleife, die angrenzende Begrenzungsrahmen überprüft, ob noch verbleibende Begrenzungsrahmen verarbeitet werden müssen. Wenn dies nicht der Fall ist, unterbrechen Sie die äußere for-Schleife.

if (activeCount <= 0)
    break;

Geben Sie schließlich außerhalb der anfänglichen for-Schleife der FilterBoundingBoxes-Methode die Ergebnisse zurück:

return results;

Großartig! Nun ist es an der Zeit, diesen Code zusammen mit dem Modell für die Bewertung zu verwenden.

Verwenden des Modells für die Bewertung

Genau wie bei der Nachverarbeitung gibt sind einige Bewertungsschritte erforderlich. Fügen Sie dem Projekt zur Unterstützung dieser Aufgaben eine Klasse hinzu, die die Bewertungslogik enthält.

  1. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf das Projekt, und wählen Sie dann Hinzufügen>Neues Element aus.

  2. Wählen Sie im Dialogfeld Neues Element hinzufügen die Option Klasse aus, und ändern Sie das Feld Name in OnnxModelScorer.cs. Wählen Sie dann die Schaltfläche Hinzufügen aus.

    Die Datei OnnxModelScorer.cs wird im Code-Editor geöffnet. Fügen Sie die folgenden using-Anweisungen am Anfang der Datei OnnxModelScorer.cs hinzu:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.ML;
    using Microsoft.ML.Data;
    using ObjectDetection.DataStructures;
    using ObjectDetection.YoloParser;
    

    Fügen Sie innerhalb der OnnxModelScorer-Klassendefinition die folgenden Variablen hinzu.

    private readonly string imagesFolder;
    private readonly string modelLocation;
    private readonly MLContext mlContext;
    
    private IList<YoloBoundingBox> _boundingBoxes = new List<YoloBoundingBox>();
    

    Erstellen Sie direkt darunter einen Konstruktor für die OnnxModelScorer-Klasse, die die zuvor definierten Variablen initialisiert.

    public OnnxModelScorer(string imagesFolder, string modelLocation, MLContext mlContext)
    {
        this.imagesFolder = imagesFolder;
        this.modelLocation = modelLocation;
        this.mlContext = mlContext;
    }
    

    Nachdem Sie den Konstruktor erstellt haben, definieren Sie einige Strukturen, die Variablen enthalten, die sich auf das Bild und die Modelleinstellungen beziehen. Erstellen Sie eine Struktur namens ImageNetSettings, die die Höhe und Breite enthält, die als Eingabe für das Modell erwartet wird.

    public struct ImageNetSettings
    {
        public const int imageHeight = 416;
        public const int imageWidth = 416;
    }
    

    Erstellen Sie anschließend eine weitere Struktur namens TinyYoloModelSettings, die die Namen der Eingabe- und Ausgabeschichten des Modells enthält. Um den Namen der Eingabe- und Ausgabeschichten des Modells zu visualisieren, können Sie ein Tool wie Netron verwenden.

    public struct TinyYoloModelSettings
    {
        // for checking Tiny yolo2 Model input and  output  parameter names,
        //you can use tools like Netron, 
        // which is installed by Visual Studio AI Tools
    
        // input tensor name
        public const string ModelInput = "image";
    
        // output tensor name
        public const string ModelOutput = "grid";
    }
    

    Erstellen Sie nun den ersten Satz von Methoden, die für die Bewertung verwendet werden. Erstellen Sie die LoadModel-Methode in der OnnxModelScorer-Klasse.

    private ITransformer LoadModel(string modelLocation)
    {
    
    }
    

    Fügen Sie in der LoadModel-Methode den folgenden Code für die Protokollierung hinzu.

    Console.WriteLine("Read model");
    Console.WriteLine($"Model location: {modelLocation}");
    Console.WriteLine($"Default parameters: image size=({ImageNetSettings.imageWidth},{ImageNetSettings.imageHeight})");
    

    ML.NET-Pipelines müssen das Datenschema kennen, das verwendet werden soll, wenn die Methode Fit aufgerufen wird. In diesem Fall wird ein Prozess ähnlich dem Training verwendet. Da jedoch kein tatsächliches Training stattfindet, ist es akzeptabel, eine leere IDataView zu verwenden. Erstellen Sie eine neue IDataView für die Pipeline aus einer leeren Liste.

    var data = mlContext.Data.LoadFromEnumerable(new List<ImageNetData>());
    

    Definieren Sie darunter die Pipeline. Die Pipeline besteht aus vier Transformationen.

    • LoadImages lädt das Bild als Bitmap.
    • ResizeImages wandelt das Bild um die angegebene Größe um (in diesem Fall 416 x 416).
    • ExtractPixels ändert die Pixeldarstellung des Bilds aus einer Bitmap in einen numerischen Vektor.
    • ApplyOnnxModel lädt das ONNX-Modell und verwendet es, um die bereitgestellten Daten zu bewerten.

    Definieren Sie die Pipeline in der LoadModel-Methode unter der data-Variablen.

    var pipeline = mlContext.Transforms.LoadImages(outputColumnName: "image", imageFolder: "", inputColumnName: nameof(ImageNetData.ImagePath))
                    .Append(mlContext.Transforms.ResizeImages(outputColumnName: "image", imageWidth: ImageNetSettings.imageWidth, imageHeight: ImageNetSettings.imageHeight, inputColumnName: "image"))
                    .Append(mlContext.Transforms.ExtractPixels(outputColumnName: "image"))
                    .Append(mlContext.Transforms.ApplyOnnxModel(modelFile: modelLocation, outputColumnNames: new[] { TinyYoloModelSettings.ModelOutput }, inputColumnNames: new[] { TinyYoloModelSettings.ModelInput }));
    

    Jetzt ist es an der Zeit, das Modell für die Bewertung zu instanziieren. Rufen Sie die Fit-Methode für die Pipeline auf, und geben Sie sie zur weiteren Verarbeitung zurück.

    var model = pipeline.Fit(data);
    
    return model;
    

Nachdem das Modell geladen wurde, kann es verwendet werden, um Vorhersagen zu treffen. Um diesen Prozess zu ermöglichen, erstellen Sie eine Methode namens PredictDataUsingModel unter der LoadModel-Methode.

private IEnumerable<float[]> PredictDataUsingModel(IDataView testData, ITransformer model)
{

}

Fügen Sie in PredictDataUsingModel den folgenden Code für die Protokollierung hinzu.

Console.WriteLine($"Images location: {imagesFolder}");
Console.WriteLine("");
Console.WriteLine("=====Identify the objects in the images=====");
Console.WriteLine("");

Verwenden Sie dann die Transform-Methode, um die Daten zu bewerten.

IDataView scoredData = model.Transform(testData);

Extrahieren Sie die vorhergesagten Wahrscheinlichkeiten, und geben Sie sie zur weiteren Verarbeitung zurück.

IEnumerable<float[]> probabilities = scoredData.GetColumn<float[]>(TinyYoloModelSettings.ModelOutput);

return probabilities;

Nachdem Sie nun beide Schritte eingerichtet haben, kombinieren Sie sie in einer einzigen Methode. Fügen Sie unter der PredictDataUsingModel-Methode eine Methode namens Score hinzu.

public IEnumerable<float[]> Score(IDataView data)
{
    var model = LoadModel(modelLocation);

    return PredictDataUsingModel(data, model);
}

Fast geschafft! Jetzt ist es an der Zeit, alles zusammen einzusetzen.

Erkennen von Objekten

Nachdem das gesamte Setup nun vollständig ist, ist es an der Zeit, einige Objekte zu erkennen.

Bewerten und Analysieren von Modellausgaben

Fügen Sie unterhalb der Erstellung der mlContext-Variablen eine try-catch-Anweisung hinzu.

try
{

}
catch (Exception ex)
{
    Console.WriteLine(ex.ToString());
}

Beginnen Sie innerhalb des try-Blocks mit der Implementierung der Objekterkennungslogik. Laden Sie die Daten zunächst in eine IDataView.

IEnumerable<ImageNetData> images = ImageNetData.ReadFromFile(imagesFolder);
IDataView imageDataView = mlContext.Data.LoadFromEnumerable(images);

Erstellen Sie dann eine Instanz von OnnxModelScorer, und verwenden Sie diese zum Bewerten der geladenen Daten.

// Create instance of model scorer
var modelScorer = new OnnxModelScorer(imagesFolder, modelFilePath, mlContext);

// Use model to score data
IEnumerable<float[]> probabilities = modelScorer.Score(imageDataView);

Jetzt muss der Nachverarbeitungsschritt erfolgen. Erstellen Sie eine Instanz von YoloOutputParser, und verwenden Sie diese, um die Modellausgabe zu verarbeiten.

YoloOutputParser parser = new YoloOutputParser();

var boundingBoxes =
    probabilities
    .Select(probability => parser.ParseOutputs(probability))
    .Select(boxes => parser.FilterBoundingBoxes(boxes, 5, .5F));

Nachdem die Modellausgabe verarbeitet wurde, können die Begrenzungsrahmen auf den Bildern gezeichnet werden.

Visualisieren von Vorhersagen

Nachdem das Modell die Bilder bewertet hat und die Ausgaben verarbeitet wurden, müssen die Begrenzungsrahmen auf das Bild gezeichnet werden. Fügen Sie zu diesem Zweck eine Methode namens DrawBoundingBox unter der Methode GetAbsolutePath der Datei Program.cs hinzu.

void DrawBoundingBox(string inputImageLocation, string outputImageLocation, string imageName, IList<YoloBoundingBox> filteredBoundingBoxes)
{

}

Laden Sie zuerst das Bild, und rufen Sie die Abmessungen für die Höhe und Breite in der DrawBoundingBox-Methode ab.

Image image = Image.FromFile(Path.Combine(inputImageLocation, imageName));

var originalImageHeight = image.Height;
var originalImageWidth = image.Width;

Erstellen Sie dann eine for-each-Schleife, um über jeden der vom Modell erfassten Begrenzungsrahmen zu iterieren.

foreach (var box in filteredBoundingBoxes)
{

}

In der for-each-Schleife rufen Sie die Abmessungen des Begrenzungsrahmens ab.

var x = (uint)Math.Max(box.Dimensions.X, 0);
var y = (uint)Math.Max(box.Dimensions.Y, 0);
var width = (uint)Math.Min(originalImageWidth - x, box.Dimensions.Width);
var height = (uint)Math.Min(originalImageHeight - y, box.Dimensions.Height);

Da die Abmessungen des Begrenzungsrahmens der Modelleingabe von 416 x 416 entsprechen, skalieren Sie die Abmessungen des Begrenzungsrahmens so, dass sie der tatsächlichen Größe des Bilds entsprechen.

x = (uint)originalImageWidth * x / OnnxModelScorer.ImageNetSettings.imageWidth;
y = (uint)originalImageHeight * y / OnnxModelScorer.ImageNetSettings.imageHeight;
width = (uint)originalImageWidth * width / OnnxModelScorer.ImageNetSettings.imageWidth;
height = (uint)originalImageHeight * height / OnnxModelScorer.ImageNetSettings.imageHeight;

Definieren Sie dann eine Vorlage für Text, der oberhalb jedes Begrenzungsrahmens angezeigt wird. Der Text enthält die Klasse des Objekts innerhalb des entsprechenden Begrenzungsrahmens sowie die Konfidenz.

string text = $"{box.Label} ({(box.Confidence * 100).ToString("0")}%)";

Um auf dem Bild zu zeichnen, konvertieren Sie es in ein Graphics-Objekt.

using (Graphics thumbnailGraphic = Graphics.FromImage(image))
{

}

Optimieren Sie innerhalb des using-Codeblocks die Graphics-Objekteinstellungen der Grafik.

thumbnailGraphic.CompositingQuality = CompositingQuality.HighQuality;
thumbnailGraphic.SmoothingMode = SmoothingMode.HighQuality;
thumbnailGraphic.InterpolationMode = InterpolationMode.HighQualityBicubic;

Legen Sie darunter die Schriftart- und Farboptionen für den Text und den Begrenzungsrahmen fest.

// Define Text Options
Font drawFont = new Font("Arial", 12, FontStyle.Bold);
SizeF size = thumbnailGraphic.MeasureString(text, drawFont);
SolidBrush fontBrush = new SolidBrush(Color.Black);
Point atPoint = new Point((int)x, (int)y - (int)size.Height - 1);

// Define BoundingBox options
Pen pen = new Pen(box.BoxColor, 3.2f);
SolidBrush colorBrush = new SolidBrush(box.BoxColor);

Erstellen und füllen Sie ein Rechteck oberhalb des Begrenzungsrahmens mithilfe der FillRectangle-Methode, damit es den Text enthält. Dies hilft dabei, den Text vom Hintergrund abzuheben und die Lesbarkeit zu verbessern.

thumbnailGraphic.FillRectangle(colorBrush, (int)x, (int)(y - size.Height - 1), (int)size.Width, (int)size.Height);

Zeichnen Sie dann den Text und den Begrenzungsrahmen auf dem Bild mithilfe der Methoden DrawString und DrawRectangle.

thumbnailGraphic.DrawString(text, drawFont, fontBrush, atPoint);

// Draw bounding box on image
thumbnailGraphic.DrawRectangle(pen, x, y, width, height);

Fügen Sie außerhalb der for-each-Schleife Code hinzu, um die Bilder im outputFolder zu speichern.

if (!Directory.Exists(outputImageLocation))
{
    Directory.CreateDirectory(outputImageLocation);
}

image.Save(Path.Combine(outputImageLocation, imageName));

Wenn Sie zusätzliches Feedback dazu erhalten möchten, ob die Anwendung Vorhersagen wie erwartet zur Laufzeit trifft, fügen Sie eine Methode namens LogDetectedObjects unterhalb der Methode DrawBoundingBox zu der Datei Program.cs hinzu, um die erkannten Objekte an die Konsole auszugeben.

void LogDetectedObjects(string imageName, IList<YoloBoundingBox> boundingBoxes)
{
    Console.WriteLine($".....The objects in the image {imageName} are detected as below....");

    foreach (var box in boundingBoxes)
    {
        Console.WriteLine($"{box.Label} and its Confidence score: {box.Confidence}");
    }

    Console.WriteLine("");
}

Nun, da Sie über Hilfsmethoden zum Erstellen von visuellem Feedback aus den Vorhersagen verfügen, sollten Sie eine For-Schleife hinzufügen, um die einzelnen bewerteten Bilder zu durchlaufen.

for (var i = 0; i < images.Count(); i++)
{

}

Rufen Sie in der for-Schleife den Namen der Bilddatei und die Begrenzungsrahmen ab, die ihr zugeordnet sind.

string imageFileName = images.ElementAt(i).Label;
IList<YoloBoundingBox> detectedObjects = boundingBoxes.ElementAt(i);

Verwenden Sie darunter die DrawBoundingBox-Methode, um die Begrenzungsrahmen auf dem Bild zu zeichnen.

DrawBoundingBox(imagesFolder, outputFolder, imageFileName, detectedObjects);

Verwenden Sie schließlich die Methode LogDetectedObjects, um Vorhersagen an die Konsole auszugeben.

LogDetectedObjects(imageFileName, detectedObjects);

Fügen Sie nach der try-catch-Anweisung zusätzliche Logik hinzu, um anzugeben, dass die Ausführung des Prozesses abgeschlossen wurde.

Console.WriteLine("========= End of Process..Hit any Key ========");

Das ist alles!

Ergebnisse

Nachdem Sie die vorherigen Schritte durchgeführt haben, führen Sie die Konsolen-App aus (STRG+F5). Ihre Ergebnisse sollten der folgenden Ausgabe ähneln. Möglicherweise werden Warnungen oder Verarbeitungsnachrichten angezeigt. Diese wurden jedoch aus Gründen der Übersichtlichkeit aus den folgenden Ergebnissen entfernt.

=====Identify the objects in the images=====

.....The objects in the image image1.jpg are detected as below....
car and its Confidence score: 0.9697262
car and its Confidence score: 0.6674225
person and its Confidence score: 0.5226039
car and its Confidence score: 0.5224892
car and its Confidence score: 0.4675332

.....The objects in the image image2.jpg are detected as below....
cat and its Confidence score: 0.6461141
cat and its Confidence score: 0.6400049

.....The objects in the image image3.jpg are detected as below....
chair and its Confidence score: 0.840578
chair and its Confidence score: 0.796363
diningtable and its Confidence score: 0.6056048
diningtable and its Confidence score: 0.3737402

.....The objects in the image image4.jpg are detected as below....
dog and its Confidence score: 0.7608147
person and its Confidence score: 0.6321323
dog and its Confidence score: 0.5967442
person and its Confidence score: 0.5730394
person and its Confidence score: 0.5551759

========= End of Process..Hit any Key ========

Navigieren Sie zum Verzeichnis assets/images/output/, um die Bilder mit Begrenzungsrahmen anzuzeigen. Unten sehen Sie ein Beispiel aus einem der verarbeiteten Bilder.

Sample processed image of a dining room

Herzlichen Glückwunsch! Sie haben nun durch Wiederverwenden eines vortrainierten ONNX-Modells in ML.NET erfolgreich ein Machine Learning-Modell für die Objekterkennung erstellt.

Sie finden den Quellcode für dieses Tutorial im Repository dotnet/machinelearning-samples.

In diesem Tutorial haben Sie Folgendes gelernt:

  • Das Problem verstehen
  • Erfahren Sie, was ONNX ist und wie ONNX mit ML.NET funktioniert.
  • Verstehen des Modells
  • Wiederverwenden des vortrainierten Modells
  • Erkennen von Objekten mit einem geladenen Modell

Sehen Sie sich im GitHub-Repository für Machine Learning-Beispiele nach einem Beispiel für erweiterte Objekterkennung um, damit Sie es untersuchen können.