Esercitazione: Rilevare gli oggetti usando ONNX in ML.NET
Informazioni su come usare un modello ONNX già sottoposto a training in ML.NET per rilevare gli oggetti nelle immagini.
Il training di un modello di rilevamento degli oggetti da zero richiede l'impostazione di milioni di parametri, numerosi dati di training con etichetta e una notevole quantità di risorse di calcolo (centinaia di ore di GPU). L'uso di un modello già sottoposto a training consente di abbreviare il processo di training.
In questa esercitazione apprenderai a:
- Informazioni sul problema
- Acquisire familiarità con ONNX e comprenderne il funzionamento con ML.NET
- Acquisire familiarità con il modello
- Riutilizzare il modello già sottoposto a training
- Rilevare oggetti con un modello caricato
Prerequisiti
- Visual Studio 2022.
- Pacchetto NuGet Microsoft.ML
- Pacchetto NuGet Microsoft.ML.ImageAnalytics
- Pacchetto NuGet Microsoft.ML.OnnxTransformer
- Modello Tiny YOLOv2 già sottoposto a training
- Netron (facoltativo)
Panoramica dell'esempio di rilevamento degli oggetti ONNX
Questo esempio crea un'applicazione console .NET Core che rileva gli oggetti all'interno di un'immagine usando un modello ONNX di Deep Learning già sottoposto a training. Il codice per questo esempio è disponibile nel repository dotnet/machinelearning-samples in GitHub.
Che cos'è il rilevamento degli oggetti?
Il rilevamento degli oggetti è una questione correlata alla visione artificiale. Sebbene sia un concetto strettamente correlato alla classificazione delle immagini, il rilevamento degli oggetti esegue l'operazione di classificazione delle immagini su scala più granulare. Il rilevamento degli oggetti individua e classifica le entità all'interno delle immagini. I modelli di rilevamento oggetti vengono comunemente sottoposti a training usando deep learning e reti neurali. Per altre informazioni, vedere Deep Learning e Machine Learning .
Usare il rilevamento degli oggetti quando le immagini contengono più oggetti di tipi diversi.
Ecco alcuni casi d'uso per il rilevamento degli oggetti:
- Auto senza guidatore
- Robotica
- Rilevamento viso
- Sicurezza nell'ambiente di lavoro
- Conteggio di oggetti
- Riconoscimento di attività
Selezionare un modello di Deep Learning
Deep Learning è un subset di Machine Learning. Per eseguire il training di modelli di Deep Learning, sono necessarie grandi quantità di dati. I modelli nei dati sono rappresentati da una serie di livelli. Le relazioni nei dati sono codificate come connessioni tra i livelli contenenti pesi. Maggiore è il peso, più forte è la relazione. Collettivamente, questa serie di livelli e connessioni è nota come rete neurale artificiale. Maggiore è il numero di livelli in una rete, più "profonda" è la rete, che diventa una rete neurale profonda.
Ci sono diversi tipi di reti neurali, tra cui i più comuni sono percettrone multistrato (MLP, Multi-Layered Perceptron), rete neurale convoluzionale (CNN, Convolutional Neural Network) e rete neurale ricorrente (RNN, Recurrent Neural Network). Il tipo più semplice è MLP, che esegue il mapping di un set di input a un set di output. Questa rete neurale è efficace quando i dati non hanno una componente spaziale o temporale. La rete CNN usa i livelli convoluzionali per elaborare le informazioni spaziali contenute nei dati. Un buon caso d'uso per le reti CNN è l'elaborazione di immagini per rilevare la presenza di una caratteristica in un'area di un'immagine (ad esempio, è presente un naso al centro di un'immagine?). Infine, le reti RNN consentono di usare come input la persistenza dello stato o della memoria. Le reti RNN vengono usate per l'analisi delle serie temporali, in cui l'ordinamento sequenziale e il contesto degli eventi sono importanti.
Acquisire familiarità con il modello
Il rilevamento degli oggetti è un'attività di elaborazione delle immagini. Per questo motivo, i modelli di Deep Learning sottoposti a training per risolvere questo problema sono prevalentemente di tipo CNN. Il modello usato in questa esercitazione è il modello Tiny YOLOv2, una versione più compatta del modello YOLOv2 descritta nel documento: "YOLO9000: Better, Faster, Stronger" di Redmon e Farhadi. Il training di Tiny YOLOv2 viene eseguito sul set di dati Pascal VOC ed è costituito da 15 livelli in grado di eseguire stime per 20 diverse classi di oggetti. Poiché il modello Tiny YOLOv2 è una versione ridotta del modello YOLOv2 originale, rappresenta un compromesso tra velocità e accuratezza. I diversi livelli che compongono il modello possono essere visualizzati usando strumenti come Netron. L'esame del modello restituirebbe un mapping delle connessioni tra tutti i livelli che compongono la rete neurale, in cui ogni livello contiene il nome del livello insieme alle dimensioni del rispettivo input/output. Le strutture di dati usate per descrivere gli input e gli output del modello sono note come tensori. I tensori possono essere considerati contenitori che archiviano i dati in N dimensioni. Nel caso di Tiny YOLOv2, il nome del livello di input è image
e prevede un tensore con dimensioni 3 x 416 x 416
. Il nome del livello di output è grid
e genera un tensore di output con dimensioni 125 x 13 x 13
.
Il modello YOLO accetta un'immagine 3(RGB) x 416px x 416px
. Il modello accetta questo input e lo passa attraverso i diversi livelli per produrre un output. L'output divide l'immagine di input in una griglia 13 x 13
, con ogni cella della griglia costituita da 125
valori.
Che cos'è un modello ONNX?
Open Neural Network Exchange (ONNX) è un formato open source per i modelli di intelligenza artificiale. ONNX supporta l'interoperabilità tra framework. Ciò significa che è possibile eseguire il training di un modello in uno dei numerosi framework di apprendimento automatico diffusi, ad esempio PyTorch, eseguire la conversione in formato ONNX e utilizzare il modello ONNX in un framework diverso, come ML.NET. Per altre informazioni, vedere il sito Web ONNX.
Il modello Tiny YOLOv2 già sottoposto a training è archiviato in formato ONNX, una rappresentazione serializzata dei livelli e dei modelli appresi di tali livelli. In ML.NET, l'interoperabilità con ONNX viene raggiunta con i pacchetti NuGet ImageAnalytics
e OnnxTransformer
. Il pacchetto ImageAnalytics
contiene una serie di trasformazioni che accettano un'immagine e la codificano in valori numerici che possono essere usati come input in una pipeline di stima o di training. Il pacchetto OnnxTransformer
sfrutta il runtime ONNX per caricare un modello ONNX e usarlo per eseguire stime basate sull'input fornito.
Configurare il progetto console .NET
Ora che sono state apprese le nozioni generali su ONNX e sul funzionamento di Tiny YOLOv2, è possibile creare l'applicazione.
Creare un'applicazione console
Creare un'applicazione console C# denominata "ObjectDetection". Fare clic sul pulsante Avanti.
Scegliere .NET 6 come framework da usare. Fare clic sul pulsante Crea.
Installare il pacchetto NuGet Microsoft.ML:
Nota
In questo esempio viene usata la versione stabile più recente dei pacchetti NuGet menzionati, a meno che non diversamente specificato.
- In Esplora soluzioni fare clic con il pulsante destro del mouse sul progetto e selezionare Gestisci pacchetti NuGet.
- Scegliere "nuget.org" come origine del pacchetto, selezionare la scheda Sfoglia e cercare Microsoft.ML.
- Selezionare il pulsante Installa.
- Selezionare il pulsante OK nella finestra di dialogo Anteprima modifiche e quindi selezionare il pulsante Accetto nella finestra di dialogo Accettazione della licenza se si accettano le condizioni di licenza per i pacchetti elencati.
- Ripetere questi passaggi per Microsoft.Windows.Compatibility, Microsoft.ML.ImageAnalytics, Microsoft.ML.OnnxTransformer e Microsoft.ML.OnnxRuntime.
Preparare i dati e il modello già sottoposto a training
Scaricare il file ZIP della directory assets del progetto e decomprimerlo.
Copiare la directory
assets
nella directory del progetto ObjectDetection. Questa directory e le relative sottodirectory contengono i file di immagine (ad eccezione del modello Tiny YOLOv2, che verrà scaricato e aggiunto nel passaggio successivo) richiesti per questa esercitazione.Scaricare il modello Tiny YOLOv2 dallo zoo del modello ONNX.
Copiare il file nella directory del
model.onnx
progettoassets\Model
ObjectDetection e rinominarlo inTinyYolo2_model.onnx
. Questa directory contiene il modello necessario per questa esercitazione.In Esplora soluzioni fare clic con il pulsante destro del mouse su ognuno dei file nella directory assets e nelle relative sottodirectory e selezionare Proprietà. In Avanzate impostare il valore di Copia nella directory di output su Copia se più recente.
Creare le classi e definire i percorsi
Aprire il file Program.cs e aggiungere le istruzioni using
aggiuntive seguenti all'inizio del file:
using System.Drawing;
using System.Drawing.Drawing2D;
using ObjectDetection.YoloParser;
using ObjectDetection.DataStructures;
using ObjectDetection;
using Microsoft.ML;
Definire quindi i percorsi dei diversi asset.
Creare prima di tutto il
GetAbsolutePath
metodo nella parte inferiore del file 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; }
Quindi, sotto le istruzioni using, creare campi per archiviare la posizione degli asset.
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");
Aggiungere una nuova directory al progetto per archiviare i dati di input e le classi di stima.
In Esplora soluzioni fare clic con il pulsante destro del mouse sul progetto e scegliere Aggiungi>Nuova cartella. Quando la nuova cartella viene visualizzata in Esplora soluzioni, assegnarle il nome "DataStructures".
Creare la classe di dati di input nella directory DataStructures appena creata.
In Esplora soluzioni fare clic con il pulsante destro del mouse sulla directory DataStructures e quindi scegliere Aggiungi>Nuovo elemento.
Nella finestra di dialogo Aggiungi nuovo elemento selezionare Classe e modificare il campo Nome in ImageNetData.cs. Selezionare quindi il pulsante Aggiungi.
Il file ImageNetData.cs viene aperto nell'editor del codice. Aggiungere l'istruzione
using
seguente all'inizio di ImageNetData.cs:using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.ML.Data;
Rimuovere la definizione di classe esistente e aggiungere il codice seguente per la classe
ImageNetData
al file ImageNetData.cs: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
è la classe dei dati di immagine di input e ha i campi String seguenti:ImagePath
contiene il percorso in cui è archiviata l'immagine.Label
contiene il nome del file.
Contiene inoltre
ImageNetData
un metodoReadFromFile
che carica più file di immagine archiviati nelimageFolder
percorso specificato e li restituisce come raccolta diImageNetData
oggetti.
Creare la classe di stima nella directory DataStructures.
In Esplora soluzioni fare clic con il pulsante destro del mouse sulla directory DataStructures e quindi scegliere Aggiungi>Nuovo elemento.
Nella finestra di dialogo Aggiungi nuovo elemento selezionare Classe e modificare il campo Nome in ImageNetPrediction.cs. Selezionare quindi il pulsante Aggiungi.
Il file ImageNetPrediction.cs viene aperto nell'editor del codice. Aggiungere l'istruzione
using
seguente all'inizio di ImageNetPrediction.cs:using Microsoft.ML.Data;
Rimuovere la definizione di classe esistente e aggiungere il codice seguente per la classe
ImageNetPrediction
al file ImageNetPrediction.cs:public class ImageNetPrediction { [ColumnName("grid")] public float[] PredictedLabels; }
ImageNetPrediction
è la classe di dati di stima e ha il campofloat[]
seguente:PredictedLabels
contiene le dimensioni, il punteggio di oggetti e le probabilità di classe per ognuna delle caselle di delimitazione rilevate in un'immagine.
Inizializzare le variabili
La classe MLContext è un punto di partenza per tutte le operazioni ML.NET e l'inizializzazione di mlContext
crea un nuovo ambiente ML.NET che può essere condiviso tra gli oggetti del flusso di lavoro della creazione del modello. Dal punto di vista concettuale è simile a DBContext
in Entity Framework.
Inizializzare la mlContext
variabile con una nuova istanza di MLContext
aggiungendo la riga seguente sotto il outputFolder
campo .
MLContext mlContext = new MLContext();
Creare un parser per la post-elaborazione degli output del modello
Il modello segmenta un'immagine in una griglia 13 x 13
, in cui ogni cella è 32px x 32px
. Ogni cella della griglia contiene 5 rettangoli di selezione di oggetti potenziali. Un rettangolo di selezione contiene 25 elementi:
x
la posizione x del centro del rettangolo di selezione rispetto alla cella della griglia a cui è associato.y
la posizione y del centro del rettangolo di selezione rispetto alla cella della griglia a cui è associato.w
la larghezza del rettangolo di selezione.h
l'altezza del rettangolo di selezione.o
il valore di confidenza che un oggetto esiste nel rettangolo di selezione, noto anche come punteggio di riconoscimento degli oggetti.p1-p20
le probabilità delle classi per ognuna delle 20 classi stimate dal modello.
In totale, i 25 elementi che descrivono ognuno dei 5 rettangoli di selezione costituiscono i 125 elementi contenuti in ogni cella della griglia.
L'output generato dal modello ONNX già sottoposto a training è una matrice mobile di lunghezza 21125
che rappresenta gli elementi di un tensore con dimensioni 125 x 13 x 13
. Per trasformare le stime generate dal modello in un tensore, è necessario eseguire alcune operazioni di post-elaborazione. A tale scopo, creare un set di classi per l'analisi dell'output.
Aggiungere una nuova directory al progetto per organizzare il set di classi parser.
- In Esplora soluzioni fare clic con il pulsante destro del mouse sul progetto e scegliere Aggiungi>Nuova cartella. Quando la nuova cartella viene visualizzata in Esplora soluzioni, assegnarle il nome "YoloParser".
Creare rettangoli di selezione e dimensioni
I dati restituiti dal modello contengono le coordinate e le dimensioni dei rettangoli di selezione degli oggetti all'interno dell'immagine. Creare una classe di base per le dimensioni.
In Esplora soluzioni fare clic con il pulsante destro del mouse sulla directory YoloParser e quindi scegliere Aggiungi>Nuovo elemento.
Nella finestra di dialogo Aggiungi nuovo elemento selezionare Classe e modificare il campo Nome in DimensionsBase.cs. Selezionare quindi il pulsante Aggiungi.
Il file DimensionsBase.cs viene aperto nell'editor del codice. Rimuovere tutte le istruzioni
using
e la definizione di classe esistente.Aggiungere il codice seguente per la classe
DimensionsBase
al file DimensionsBase.cs:public class DimensionsBase { public float X { get; set; } public float Y { get; set; } public float Height { get; set; } public float Width { get; set; } }
DimensionsBase
ha le proprietà seguentifloat
:X
contiene la posizione dell'oggetto lungo l'asse x.Y
contiene la posizione dell'oggetto lungo l'asse y.Height
contiene l'altezza dell'oggetto.Width
contiene la larghezza dell'oggetto.
Creare quindi una classe per i rettangoli di selezione.
In Esplora soluzioni fare clic con il pulsante destro del mouse sulla directory YoloParser e quindi scegliere Aggiungi>Nuovo elemento.
Nella finestra di dialogo Aggiungi nuovo elemento selezionare Classe e modificare il campo Nome in YoloBoundingBox.cs. Selezionare quindi il pulsante Aggiungi.
Il file YoloBoundingBox.cs viene aperto nell'editor del codice. Aggiungere l'istruzione
using
seguente all'inizio di YoloBoundingBox.cs:using System.Drawing;
Appena sopra la definizione di classe esistente, aggiungere una nuova definizione di classe denominata
BoundingBoxDimensions
che eredita dallaDimensionsBase
classe per contenere le dimensioni del rispettivo rettangolo di selezione.public class BoundingBoxDimensions : DimensionsBase { }
Rimuovere la definizione di classe
YoloBoundingBox
esistente e aggiungere il codice seguente per la classeYoloBoundingBox
al file YoloBoundingBox.cs: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
ha le proprietà seguenti:Dimensions
contiene le dimensioni del rettangolo di selezione.Label
contiene la classe dell'oggetto rilevato nel rettangolo di selezione.Confidence
contiene la confidenza della classe.Rect
contiene la rappresentazione del rettangolo delle dimensioni del rettangolo di selezione.BoxColor
contiene il colore associato alla rispettiva classe usata per disegnare l'immagine.
Creare il parser
Dopo aver creato le classi per le dimensioni e i rettangoli di selezione, è possibile creare il parser.
In Esplora soluzioni fare clic con il pulsante destro del mouse sulla directory YoloParser e quindi scegliere Aggiungi>Nuovo elemento.
Nella finestra di dialogo Aggiungi nuovo elemento selezionare Classe e modificare il campo Nome in YoloOutputParser.cs. Selezionare quindi il pulsante Aggiungi.
Il file YoloOutputParser.cs viene aperto nell'editor del codice. Aggiungere le istruzioni seguenti
using
all'inizio di YoloOutputParser.cs:using System; using System.Collections.Generic; using System.Drawing; using System.Linq;
All'interno della definizione di classe
YoloOutputParser
esistente aggiungere una classe annidata che contiene le dimensioni di ciascuna cella nell'immagine. Aggiungere il codice seguente per laCellDimensions
classe che eredita dallaDimensionsBase
classe all'inizio della definizione dellaYoloOutputParser
classe.class CellDimensions : DimensionsBase { }
All'interno della definizione della
YoloOutputParser
classe aggiungere le costanti e il campo seguenti.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
: numero di righe nella griglia in cui è divisa l'immagine.COL_COUNT
: numero di colonne nella griglia in cui è divisa l'immagine.CHANNEL_COUNT
: numero totale di valori contenuti in una cella della griglia.BOXES_PER_CELL
: numero di rettangoli di selezione in una cella.BOX_INFO_FEATURE_COUNT
: numero di funzionalità contenute all'interno di un rettangolo di selezione (x, y, altezza, larghezza, confidenza).CLASS_COUNT
numero di stime delle classi contenute in ogni rettangolo di selezione.CELL_WIDTH
: larghezza di una cella nella griglia dell'immagine.CELL_HEIGHT
: altezza di una cella nella griglia dell'immagine.channelStride
: posizione iniziale della cella corrente nella griglia.
Quando il modello esegue una stima, operazione nota anche come assegnazione di punteggi, divide l'immagine di input
416px x 416px
in una griglia di celle delle dimensioni di13 x 13
. Ogni cella contenuta è32px x 32px
. All'interno di ogni cella sono presenti 5 rettangoli di selezione, ognuno contenente 5 funzionalità (x, y, larghezza, altezza, confidenza). Inoltre, ogni rettangolo di selezione contiene la probabilità di ognuna delle classi, che in questo caso è 20. Di conseguenza, ogni cella contiene 125 informazioni (5 funzionalità + 20 probabilità delle classi).
Creare un elenco di ancoraggi sotto channelStride
per tutti i 5 rettangoli di selezione:
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
};
Gli ancoraggi sono rapporti di altezza e larghezza predefiniti per i rettangoli di selezione. La maggior parte degli oggetti o delle classi rilevate da un modello ha proporzioni simili. Questo è importante quando si tratta di creare rettangoli di selezione. Anziché stimare i rettangoli di selezione, viene calcolato l'offset dalle dimensioni predefinite, riducendo di conseguenza il calcolo necessario per stimare il rettangolo di selezione. In genere i rapporti di ancoraggio vengono calcolati in base al set di dati usato. In questo caso, poiché il set di dati è noto e i valori sono stati pre-calcolati, gli ancoraggi possono essere hardcoded.
Definire quindi le etichette o le classi che devono essere stimate dal modello. Questo modello stima 20 classi, ovvero un subset del numero totale di classi stimate dal modello YOLOv2 originale.
Aggiungere l'elenco di etichette sotto anchors
.
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"
};
A ognuna delle classi sono associati colori specifici. Assegnare i colori delle classi sotto labels
:
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
};
Creare funzioni di supporto
La fase di post-elaborazione prevede una serie di passaggi. Per semplificare questi passaggi, è possibile usare diversi metodi di supporto.
I metodi di supporto usati dal parser sono:
Sigmoid
: applica la funzione sigma che restituisce un numero compreso tra 0 e 1.Softmax
: normalizza un vettore di input in una distribuzione di probabilità.GetOffset
: esegue il mapping degli elementi nell'output di un modello unidimensionale alla posizione corrispondente in un tensore125 x 13 x 13
.ExtractBoundingBoxes
: estrae le dimensioni dei rettangoli di selezione usando il metodoGetOffset
dall'output del modello.GetConfidence
estrae il valore di attendibilità che indica in che modo il modello è che ha rilevato un oggetto e usa laSigmoid
funzione per trasformarla in una percentuale.MapBoundingBoxToCell
: usa le dimensioni del rettangolo di selezione e ne esegue il mapping alla rispettiva cella all'interno dell'immagine.ExtractClasses
: estrae le stime delle classi per il rettangolo di selezione dall'output del modello usando il metodoGetOffset
e le converte in una distribuzione di probabilità tramite il metodoSoftmax
.GetTopResult
: seleziona la classe dall'elenco delle classi stimate con la probabilità maggiore.IntersectionOverUnion
: filtra i rettangoli di selezione sovrapposti con probabilità inferiori.
Aggiungere il codice per tutti i metodi di supporto sotto l'elenco classColors
.
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);
}
Dopo aver definito tutti i metodi di supporto, è possibile usarli per elaborare l'output del modello.
Sotto il metodo IntersectionOverUnion
creare il metodo ParseOutputs
per elaborare l'output generato dal modello.
public IList<YoloBoundingBox> ParseOutputs(float[] yoloModelOutputs, float threshold = .3F)
{
}
Creare un elenco per archiviare i rettangoli di selezione e definire variabili all'interno del metodo ParseOutputs
.
var boxes = new List<YoloBoundingBox>();
Ogni immagine è divisa in una griglia di celle 13 x 13
. Ogni cella contiene cinque rettangoli di selezione. Sotto la variabile boxes
aggiungere il codice per elaborare tutti i rettangoli in ognuna delle celle.
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++)
{
}
}
}
All'interno del ciclo più interno calcolare la posizione iniziale del rettangolo corrente all'interno dell'output del modello unidimensionale.
var channel = (box * (CLASS_COUNT + BOX_INFO_FEATURE_COUNT));
Direttamente sotto usare il metodo ExtractBoundingBoxDimensions
per ottenere le dimensioni del rettangolo di selezione corrente.
BoundingBoxDimensions boundingBoxDimensions = ExtractBoundingBoxDimensions(yoloModelOutputs, row, column, channel);
Usare quindi il metodo GetConfidence
per ottenere la confidenza per il rettangolo di selezione corrente.
float confidence = GetConfidence(yoloModelOutputs, row, column, channel);
Usare ora il metodo MapBoundingBoxToCell
per eseguire il mapping del rettangolo di selezione corrente alla cella corrente in fase di elaborazione.
CellDimensions mappedBoundingBox = MapBoundingBoxToCell(row, column, box, boundingBoxDimensions);
Prima di eseguire altre operazioni di elaborazione, controllare se il valore di confidenza è maggiore della soglia specificata. In caso contrario, elaborare il rettangolo di selezione successivo.
if (confidence < threshold)
continue;
Altrimenti, proseguire con l'elaborazione dell'output. Il passaggio successivo consiste nell'ottenere la distribuzione di probabilità delle classi stimate per il rettangolo di selezione corrente usando il metodo ExtractClasses
.
float[] predictedClasses = ExtractClasses(yoloModelOutputs, row, column, channel);
Usare quindi il metodo GetTopResult
per ottenere il valore e l'indice della classe con la probabilità maggiore per il rettangolo corrente e calcolarne il punteggio.
var (topResultIndex, topResultScore) = GetTopResult(predictedClasses);
var topScore = topResultScore * confidence;
Usare topScore
ancora una volta per mantenere solo i rettangoli di selezione che superano la soglia specificata.
if (topScore < threshold)
continue;
Infine, se il rettangolo di selezione corrente supera la soglia, creare un nuovo oggetto BoundingBox
e aggiungerlo all'elenco boxes
.
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]
});
Dopo aver elaborato tutte le celle nell'immagine, restituire l'elenco boxes
. Aggiungere l'istruzione return seguente sotto il ciclo for più esterno nel metodo ParseOutputs
.
return boxes;
Filtrare i rettangoli sovrapposti
Ora che tutti i rettangoli di selezione con confidenza elevata sono stati estratti dall'output del modello, è necessario filtrarli ulteriormente per rimuovere le immagini sovrapposte. Aggiungere un metodo denominato FilterBoundingBoxes
sotto il metodo ParseOutputs
:
public IList<YoloBoundingBox> FilterBoundingBoxes(IList<YoloBoundingBox> boxes, int limit, float threshold)
{
}
All'interno del metodo FilterBoundingBoxes
iniziare creando una matrice uguale alle dimensioni dei rettangoli rilevati e contrassegnando tutti gli slot come attivi o pronti per l'elaborazione.
var activeCount = boxes.Count;
var isActiveBoxes = new bool[boxes.Count];
for (int i = 0; i < isActiveBoxes.Length; i++)
isActiveBoxes[i] = true;
Ordinare quindi l'elenco contenente i rettangoli di selezione in ordine decrescente in base alla confidenza.
var sortedBoxes = boxes.Select((b, i) => new { Box = b, Index = i })
.OrderByDescending(b => b.Box.Confidence)
.ToList();
Infine, creare un elenco che conterrà i risultati filtrati.
var results = new List<YoloBoundingBox>();
Iniziare a elaborare ogni rettangolo di selezione tramite l'iterazione.
for (int i = 0; i < boxes.Count; i++)
{
}
All'interno di questo ciclo for controllare se il rettangolo di selezione corrente può essere elaborato.
if (isActiveBoxes[i])
{
}
Se sì, aggiungere il rettangolo di selezione all'elenco dei risultati. Se i risultati superano il limite specificato di caselle da estrarre, interrompere il ciclo. Aggiungere il codice seguente all'interno dell'istruzione if.
var boxA = sortedBoxes[i].Box;
results.Add(boxA);
if (results.Count >= limit)
break;
In caso contrario, controllare i rettangoli di selezione adiacenti. Aggiungere il codice seguente sotto il controllo del limite di rettangoli.
for (var j = i + 1; j < boxes.Count; j++)
{
}
Come per il primo rettangolo, se il rettangolo adiacente è attivo o pronto per l'elaborazione, usare il metodo IntersectionOverUnion
per verificare se il primo e il secondo rettangolo superano la soglia specificata. Aggiungere il codice seguente al ciclo for più interno.
if (isActiveBoxes[j])
{
var boxB = sortedBoxes[j].Box;
if (IntersectionOverUnion(boxA.Rect, boxB.Rect) > threshold)
{
isActiveBoxes[j] = false;
activeCount--;
if (activeCount <= 0)
break;
}
}
All'esterno del ciclo for più interno che controlla i rettangoli di selezione adiacenti, verificare se sono presenti altri rettangoli di selezione da elaborare. In caso contrario, interrompere il ciclo for esterno.
if (activeCount <= 0)
break;
Infine, all'esterno del ciclo for iniziale del metodo FilterBoundingBoxes
restituire i risultati:
return results;
Ottimo. È ora possibile usare il codice insieme al modello per l'assegnazione dei punteggi.
Usare il modello per l'assegnazione dei punteggi
Analogamente alla post-elaborazione, la fase di assegnazione dei punteggi prevede alcuni passaggi. Per semplificare questi passaggi, aggiungere una classe che conterrà la logica di assegnazione dei punteggi al progetto.
In Esplora soluzioni fare clic con il pulsante destro del mouse sul progetto e quindi selezionare Aggiungi>Nuovo elemento.
Nella finestra di dialogo Aggiungi nuovo elemento selezionare Classe e modificare il campo Nome in OnnxModelScorer.cs. Selezionare quindi il pulsante Aggiungi.
Il file OnnxModelScorer.cs viene aperto nell'editor del codice. Aggiungere le istruzioni seguenti
using
all'inizio di OnnxModelScorer.cs:using System; using System.Collections.Generic; using System.Linq; using Microsoft.ML; using Microsoft.ML.Data; using ObjectDetection.DataStructures; using ObjectDetection.YoloParser;
Aggiungere le variabili seguenti all'interno della definizione di classe
OnnxModelScorer
.private readonly string imagesFolder; private readonly string modelLocation; private readonly MLContext mlContext; private IList<YoloBoundingBox> _boundingBoxes = new List<YoloBoundingBox>();
Direttamente sotto creare un costruttore per la classe
OnnxModelScorer
che Inizializzerà le variabili definite in precedenza.public OnnxModelScorer(string imagesFolder, string modelLocation, MLContext mlContext) { this.imagesFolder = imagesFolder; this.modelLocation = modelLocation; this.mlContext = mlContext; }
Dopo aver creato il costruttore, definire un paio di struct che contengono variabili correlate alle impostazioni dell'immagine e del modello. Creare uno struct denominato
ImageNetSettings
che conterrà l'altezza e la larghezza previste come input per il modello.public struct ImageNetSettings { public const int imageHeight = 416; public const int imageWidth = 416; }
Successivamente, creare un altro struct denominato
TinyYoloModelSettings
che contiene i nomi dei livelli di input e output del modello. Per visualizzare il nome dei livelli di input e output del modello, è possibile usare uno strumento come Netron.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"; }
Creare quindi il primo set di metodi da usare per l'assegnazione dei punteggi. Creare il metodo
LoadModel
all'interno della classeOnnxModelScorer
.private ITransformer LoadModel(string modelLocation) { }
All'interno del metodo
LoadModel
aggiungere il codice seguente per la registrazione.Console.WriteLine("Read model"); Console.WriteLine($"Model location: {modelLocation}"); Console.WriteLine($"Default parameters: image size=({ImageNetSettings.imageWidth},{ImageNetSettings.imageHeight})");
Le pipeline ML.NET devono conoscere lo schema dei dati su cui agire quando viene chiamato il metodo
Fit
. In questo caso, verrà usato un processo simile al training. Tuttavia, poiché di fatto non viene eseguito alcun training, è accettabile usare un'interfacciaIDataView
vuota. Creare una nuova interfacciaIDataView
per la pipeline da un elenco vuoto.var data = mlContext.Data.LoadFromEnumerable(new List<ImageNetData>());
Sotto questo codice definire la pipeline. La pipeline sarà costituita da quattro trasformazioni.
LoadImages
: carica l'immagine come bitmap.ResizeImages
: ridimensiona l'immagine in base alla dimensione specificata (in questo caso416 x 416
).ExtractPixels
: modifica la rappresentazione in pixel dell'immagine da bitmap a vettore numerico.ApplyOnnxModel
: carica il modello ONNX e lo usa per assegnare un punteggio ai dati forniti.
Definire la pipeline nel metodo
LoadModel
sotto la variabiledata
.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 }));
È ora possibile creare un'istanza del modello per l'assegnazione dei punteggi. Chiamare il metodo
Fit
nella pipeline e restituirlo per un'ulteriore elaborazione.var model = pipeline.Fit(data); return model;
Una volta caricato, il modello può essere usato per eseguire stime. Per semplificare il processo, creare un metodo denominato PredictDataUsingModel
sotto il metodo LoadModel
.
private IEnumerable<float[]> PredictDataUsingModel(IDataView testData, ITransformer model)
{
}
All'interno di PredictDataUsingModel
aggiungere il codice seguente per la registrazione.
Console.WriteLine($"Images location: {imagesFolder}");
Console.WriteLine("");
Console.WriteLine("=====Identify the objects in the images=====");
Console.WriteLine("");
Usare quindi il metodo Transform
per assegnare punteggi ai dati.
IDataView scoredData = model.Transform(testData);
Estrarre le probabilità stimate e restituirle per l'ulteriore elaborazione.
IEnumerable<float[]> probabilities = scoredData.GetColumn<float[]>(TinyYoloModelSettings.ModelOutput);
return probabilities;
Una volta configurati entrambi i passaggi, combinarli in un unico metodo. Sotto il metodo PredictDataUsingModel
aggiungere un nuovo metodo denominato Score
.
public IEnumerable<float[]> Score(IDataView data)
{
var model = LoadModel(modelLocation);
return PredictDataUsingModel(data, model);
}
La configurazione è stata completata. È ora possibile usarla per lo scopo di questa esercitazione.
Rilevare oggetti
Dopo aver completato la configurazione, è possibile iniziare a rilevare alcuni oggetti.
Assegnare punteggi agli output del modello e analizzarli
Sotto la creazione della mlContext
variabile aggiungere un'istruzione try-catch.
try
{
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
All'interno del blocco try
iniziare a implementare la logica di rilevamento degli oggetti. Prima di tutto, caricare i dati in un'interfaccia IDataView
.
IEnumerable<ImageNetData> images = ImageNetData.ReadFromFile(imagesFolder);
IDataView imageDataView = mlContext.Data.LoadFromEnumerable(images);
Creare quindi un'istanza di OnnxModelScorer
e usarla per assegnare punteggi ai dati caricati.
// Create instance of model scorer
var modelScorer = new OnnxModelScorer(imagesFolder, modelFilePath, mlContext);
// Use model to score data
IEnumerable<float[]> probabilities = modelScorer.Score(imageDataView);
È ora possibile passare alla fase di post-elaborazione. Creare un'istanza di YoloOutputParser
e usarla per elaborare l'output del modello.
YoloOutputParser parser = new YoloOutputParser();
var boundingBoxes =
probabilities
.Select(probability => parser.ParseOutputs(probability))
.Select(boxes => parser.FilterBoundingBoxes(boxes, 5, .5F));
Dopo aver elaborato l'output del modello, è possibile tracciare i rettangoli di selezione sulle immagini.
Visualizzare stime
Dopo che il modello ha assegnato un punteggio alle immagini e gli output sono stati elaborati, è necessario disegnare i rettangoli di selezione sull'immagine. A tale scopo, aggiungere un metodo denominato DrawBoundingBox
sotto il metodo GetAbsolutePath
all'interno di Program.cs.
void DrawBoundingBox(string inputImageLocation, string outputImageLocation, string imageName, IList<YoloBoundingBox> filteredBoundingBoxes)
{
}
Caricare prima di tutto l'immagine e ottenere le dimensioni di altezza e larghezza nel metodo DrawBoundingBox
.
Image image = Image.FromFile(Path.Combine(inputImageLocation, imageName));
var originalImageHeight = image.Height;
var originalImageWidth = image.Width;
Creare quindi un ciclo for-each per eseguire l'iterazione di ogni rettangolo di selezione rilevato dal modello.
foreach (var box in filteredBoundingBoxes)
{
}
All'interno del ciclo for-each, ottenere le dimensioni del rettangolo di selezione.
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);
Poiché le dimensioni del rettangolo di selezione corrispondono all'input del modello di 416 x 416
, ridimensionare il rettangolo di selezione in modo che le sue dimensioni corrispondano a quelle effettive dell'immagine.
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;
Definire quindi un modello per il testo che verrà visualizzato sopra ogni rettangolo di selezione. Il testo conterrà la classe dell'oggetto all'interno del rispettivo rettangolo di selezione e la confidenza.
string text = $"{box.Label} ({(box.Confidence * 100).ToString("0")}%)";
Per disegnare sull'immagine, convertirla in un oggetto Graphics
.
using (Graphics thumbnailGraphic = Graphics.FromImage(image))
{
}
All'interno del blocco di codice using
ottimizzare le impostazioni dell'oggetto Graphics
del grafico.
thumbnailGraphic.CompositingQuality = CompositingQuality.HighQuality;
thumbnailGraphic.SmoothingMode = SmoothingMode.HighQuality;
thumbnailGraphic.InterpolationMode = InterpolationMode.HighQualityBicubic;
Al di sotto, impostare le opzioni relative al tipo di carattere e al colore per il testo e il rettangolo di selezione.
// 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);
Creare e riempire un rettangolo sopra il rettangolo di selezione per contenere il testo usando il metodo FillRectangle
. Ciò consentirà di creare un contrasto per il testo e migliorare la leggibilità.
thumbnailGraphic.FillRectangle(colorBrush, (int)x, (int)(y - size.Height - 1), (int)size.Width, (int)size.Height);
Disegnare quindi il testo e il rettangolo di selezione nell'immagine usando i metodi DrawString
e DrawRectangle
.
thumbnailGraphic.DrawString(text, drawFont, fontBrush, atPoint);
// Draw bounding box on image
thumbnailGraphic.DrawRectangle(pen, x, y, width, height);
Al di fuori del ciclo for-each aggiungere il codice per salvare le immagini in outputFolder
.
if (!Directory.Exists(outputImageLocation))
{
Directory.CreateDirectory(outputImageLocation);
}
image.Save(Path.Combine(outputImageLocation, imageName));
Per ulteriori commenti e suggerimenti che l'applicazione sta effettuando stime come previsto in fase di esecuzione, aggiungere un metodo denominato LogDetectedObjects
sotto il DrawBoundingBox
metodo nel file Program.cs per restituire gli oggetti rilevati nella console.
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("");
}
Ora che sono disponibili metodi helper per creare un riscontro visivo dalle stime, aggiungere un ciclo for per scorrere ognuna delle immagini con punteggio.
for (var i = 0; i < images.Count(); i++)
{
}
All'interno del ciclo for ottenere il nome del file di immagine e dei rettangoli di selezione associati.
string imageFileName = images.ElementAt(i).Label;
IList<YoloBoundingBox> detectedObjects = boundingBoxes.ElementAt(i);
Sotto questo codice usare il metodo DrawBoundingBox
per tracciare i rettangoli di selezione sull'immagine.
DrawBoundingBox(imagesFolder, outputFolder, imageFileName, detectedObjects);
Infine, usare il metodo LogDetectedObjects
per restituire le stime alla console.
LogDetectedObjects(imageFileName, detectedObjects);
Dopo l'istruzione try-catch aggiungere logica aggiuntiva per indicare che l'esecuzione del processo è stata completata.
Console.WriteLine("========= End of Process..Hit any Key ========");
Ecco fatto!
Risultati
Dopo aver completato i passaggi precedenti, eseguire l'app console (CTRL+F5). I risultati saranno simili all'output seguente. È possibile che vengano visualizzati avvisi o messaggi di elaborazione che tuttavia, per chiarezza, sono stati rimossi dai risultati riportati di seguito.
=====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 ========
Per visualizzare le immagini con i rettangoli di selezione, passare alla directory assets/images/output/
. Di seguito viene fornito un esempio da una delle immagini elaborate.
Complimenti. È stato creato un modello di Machine Learning per il rilevamento di oggetti riutilizzando un modello ONNX
già sottoposto a training in ML.NET.
È possibile trovare il codice sorgente per questa esercitazione nel repository dotnet/machinelearning-samples .
Questa esercitazione ha descritto come:
- Informazioni sul problema
- Acquisire familiarità con ONNX e comprenderne il funzionamento con ML.NET
- Acquisire familiarità con il modello
- Riutilizzare il modello già sottoposto a training
- Rilevare oggetti con un modello caricato
Consultare il repository GitHub degli esempi di Machine Learning per esaminare un esempio di rilevamento di oggetti esteso.