Note
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier les répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de changer de répertoire.
Découvrez comment utiliser un modèle ONNX préentraîné dans ML.NET pour détecter des objets dans des images.
L’entraînement d’un modèle de détection d’objet à partir de zéro nécessite la définition de millions de paramètres, une grande quantité de données d’entraînement étiquetées et une grande quantité de ressources de calcul (centaines d’heures GPU). L’utilisation d’un modèle préentraîné vous permet de raccourcir le processus d’entraînement.
Dans ce tutoriel, vous allez apprendre à :
- Comprendre le problème
- Découvrez ce qu’est ONNX et comment il fonctionne avec ML.NET
- Comprendre le modèle
- Réutiliser le modèle préentraîné
- Détecter des objets avec un modèle chargé
Prerequisites
- Visual Studio 2022 ou version ultérieure.
- Microsoft.ML NuGet Package
- Microsoft.ML.ImageAnalytics NuGet Package
- Microsoft.ML.OnnxTransformer NuGet Package
- Modèle préentraîné YOLOv2 minuscule
- Netron (facultatif)
Vue d’ensemble de l’exemple de détection d’objets ONNX
Cet exemple crée une application console .NET Core qui détecte des objets au sein d’une image à l’aide d’un modèle ONNX préentraîné. Le code de cet exemple se trouve dans le référentiel dotnet/machinelearning-samples sur GitHub.
Qu’est-ce que la détection d’objets ?
La détection d’objets est un problème de vision par ordinateur. Bien qu’elle soit étroitement liée à la classification d’images, la détection d’objets effectue une classification d’images à une échelle plus granulaire. La détection d’objets localise et catégorise les entités dans les images. Les modèles de détection d’objets sont couramment formés à l’aide d’apprentissage profond et de réseaux neuronaux. Pour plus d’informations, consultez Deep Learning et Machine Learning .
Utilisez la détection d’objets lorsque les images contiennent plusieurs objets de différents types.
Voici quelques cas d’usage pour la détection d’objets :
- Voitures Autonomes
- Robotique
- Détection des visages
- Sécurité en milieu de travail
- Comptage d’objets
- Reconnaissance de l’activité
Sélectionner un modèle d’apprentissage profond
Le Deep Learning est un sous-ensemble de Machine Learning. Pour entraîner des modèles d’apprentissage profond, de grandes quantités de données sont requises. Les modèles dans les données sont représentés par une série de couches. Les relations dans les données sont encodées en tant que connexions entre les couches contenant des pondérations. Plus le poids est élevé, plus la relation est forte. Collectivement, cette série de couches et de connexions est appelée réseaux neuronaux artificiels. Plus les couches d’un réseau sont « plus profondes », ce qui en fait un réseau neuronal profond.
Il existe différents types de réseaux neuronaux, le plus courant étant Perceptron multicouche (MLP), le réseau neuronal convolutionnel (CNN) et le réseau neuronal récurrent (RNN). Le plus simple est le MLP, qui mappe un ensemble d’entrées à un ensemble de sorties. Ce réseau neuronal est correct lorsque les données n’ont pas de composant spatial ou temporel. Le CNN utilise des couches convolutionnelles pour traiter les informations spatiales contenues dans les données. Un bon cas d’usage pour les cnns est le traitement d’images pour détecter la présence d’une fonctionnalité dans une région d’une image (par exemple, y a-t-il un nez au centre d’une image ?). Enfin, les RNN autorisent l’utilisation de la persistance de l’état ou de la mémoire comme entrée. Les RNN sont utilisés pour l’analyse de série chronologique, où l’ordre séquentiel et le contexte des événements sont importants.
Comprendre le modèle
La détection d’objets est une tâche de traitement d’images. Par conséquent, la plupart des modèles d’apprentissage profond formés pour résoudre ce problème sont des CNN. Le modèle utilisé dans ce tutoriel est le modèle Tiny YOLOv2, une version plus compacte du modèle YOLOv2 décrite dans le document : « YOLO9000 : Better, Faster, Stronger » de Redmon et Farhadi. Tiny YOLOv2 est formé sur le jeu de données Pascal VOC et se compose de 15 couches qui peuvent prédire 20 classes d’objets différentes. Étant donné que Tiny YOLOv2 est une version condensée du modèle YOLOv2 d’origine, un compromis est fait entre vitesse et précision. Les différentes couches qui composent le modèle peuvent être visualisées à l’aide d’outils comme Netron. L’inspection du modèle génère un mappage des connexions entre toutes les couches qui composent le réseau neuronal, où chaque couche contient le nom de la couche, ainsi que les dimensions de l’entrée/sortie respectives. Les structures de données utilisées pour décrire les entrées et sorties du modèle sont appelées tenseurs. Les tenseurs peuvent être considérés comme des conteneurs qui stockent des données dans des dimensions N. Dans le cas de Tiny YOLOv2, le nom de la couche d’entrée est image et il attend un tenseur de dimensions 3 x 416 x 416. Le nom de la couche de sortie est grid et génère un capteur de sortie de dimensions 125 x 13 x 13.
Le modèle YOLO prend une image 3(RGB) x 416px x 416px. Le modèle prend cette entrée et le transmet aux différentes couches pour produire une sortie. La sortie divise l’image d’entrée en grille 13 x 13 , avec chaque cellule de la grille composée de 125 valeurs.
Qu’est-ce qu’un modèle ONNX ?
Open Neural Network Exchange (ONNX) est un format open source pour les modèles IA. ONNX prend en charge l’interopérabilité entre les frameworks. Cela signifie que vous pouvez entraîner un modèle dans l’un des nombreux frameworks machine learning populaires comme PyTorch, le convertir au format ONNX et consommer le modèle ONNX dans un autre framework comme ML.NET. Pour en savoir plus, visitez le site web ONNX.
Le modèle Tiny YOLOv2 préentraîné est stocké au format ONNX, une représentation sérialisée des couches et des modèles appris de ces couches. Dans ML.NET, l’interopérabilité avec ONNX est obtenue avec les packages NuGet ImageAnalytics et OnnxTransformer. Le ImageAnalytics package contient une série de transformations qui prennent une image et l’encodent en valeurs numériques qui peuvent être utilisées comme entrée dans un pipeline de prédiction ou d’entraînement. Le OnnxTransformer package tire parti du runtime ONNX pour charger un modèle ONNX et l’utiliser pour effectuer des prédictions en fonction de l’entrée fournie.
Configurer le projet console .NET
Maintenant que vous avez une compréhension générale de ce qu’est ONNX et comment Tiny YOLOv2 fonctionne, il est temps de créer l’application.
Création d’une application console
Créez une application console C# appelée « ObjectDetection ». Cliquez sur le bouton Suivant .
Choisissez .NET 8 comme framework à utiliser. Cliquez sur le bouton Créer.
Installez le package NuGet Microsoft.ML :
Note
Cet exemple utilise la dernière version stable des packages NuGet mentionnés, sauf indication contraire.
- Dans l’Explorateur de solutions, cliquez avec le bouton droit sur votre projet, puis sélectionnez Gérer les packages NuGet.
- Choisissez « nuget.org » comme source du package, sélectionnez l’onglet Parcourir, recherchez Microsoft.ML.
- Sélectionnez le bouton Installer.
- Sélectionnez le bouton OK dans la boîte de dialogue Aperçu des modifications , puis sélectionnez le bouton J’acceptedans la boîte de dialogue Acceptation de licence si vous acceptez les termes du contrat de licence pour les packages répertoriés.
- Répétez ces étapes pour Microsoft.Windows.Compatibility, Microsoft.ML.ImageAnalytics, Microsoft.ML.OnnxTransformer et Microsoft.ML.OnnxRuntime.
Préparer vos données et votre modèle préentraîné
Téléchargez le fichier zip du répertoire des ressources du projet et décompressez-le.
Copiez le
assetsrépertoire dans votre répertoire de projet ObjectDetection . Ce répertoire et ses sous-répertoires contiennent les fichiers image (à l’exception du modèle Tiny YOLOv2, que vous allez télécharger et ajouter à l’étape suivante) nécessaires pour ce didacticiel.Téléchargez le modèle Tiny YOLOv2 à partir du Zoo de modèles ONNX.
Copiez le
model.onnxfichier dans votre répertoire de projetassets\Modelet renommez-leTinyYolo2_model.onnxen . Ce répertoire contient le modèle nécessaire pour ce didacticiel.Dans l’Explorateur de solutions, cliquez avec le bouton droit sur chacun des fichiers du répertoire de ressources et sous-répertoires, puis sélectionnez Propriétés. Sous Avancé, modifiez la valeur de Copie dans le répertoire de sortie pour copier si elle est plus récente.
Créer des classes et définir des chemins d’accès
Ouvrez le fichier Program.cs et ajoutez les directives supplémentaires using suivantes en haut du fichier :
using System.Drawing;
using System.Drawing.Drawing2D;
using ObjectDetection.YoloParser;
using ObjectDetection.DataStructures;
using ObjectDetection;
using Microsoft.ML;
Ensuite, définissez les chemins des différentes ressources.
Tout d’abord, créez la
GetAbsolutePathméthode en bas du fichier 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; }Ensuite, sous les
usingdirectives, créez des champs pour stocker l’emplacement de vos ressources.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");
Ajoutez un nouveau répertoire à votre projet pour stocker vos données d’entrée et vos classes de prédiction.
Dans l’Explorateur de solutions, cliquez avec le bouton droit sur le projet, puis sélectionnez Ajouter>un nouveau dossier. Lorsque le nouveau dossier apparaît dans l’Explorateur de solutions, nommez-le « DataStructures ».
Créez votre classe de données d’entrée dans le répertoire DataStructures nouvellement créé.
Dans l’Explorateur de solutions, cliquez avec le bouton droit sur le répertoire DataStructures , puis sélectionnez Ajouter>un nouvel élément.
Dans la boîte de dialogue Ajouter un nouvel élément , sélectionnez Classe et remplacez le champ Nom par ImageNetData.cs. Ensuite, sélectionnez Ajouter.
Le fichier ImageNetData.cs s’ouvre dans l’éditeur de code. Ajoutez la directive suivante
usingen haut de ImageNetData.cs :using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.ML.Data;Supprimez la définition de classe existante et ajoutez le code suivant pour la
ImageNetDataclasse au fichier 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) }); } }ImageNetDataest la classe de données d’image d’entrée et comporte les champs suivants String :-
ImagePathcontient le chemin d’accès où l’image est stockée. -
Labelcontient le nom du fichier.
En outre,
ImageNetDatacontient une méthodeReadFromFilequi charge plusieurs fichiers image stockés dans leimageFolderchemin d’accès spécifié et les retourne sous forme de collection d’objetsImageNetData.-
Créez votre classe de prédiction dans le répertoire DataStructures .
Dans l’Explorateur de solutions, cliquez avec le bouton droit sur le répertoire DataStructures , puis sélectionnez Ajouter>un nouvel élément.
Dans la boîte de dialogue Ajouter un nouvel élément , sélectionnez Classe et remplacez le champ Nom par ImageNetPrediction.cs. Ensuite, sélectionnez Ajouter.
Le fichier ImageNetPrediction.cs s’ouvre dans l’éditeur de code. Ajoutez la directive suivante
usingen haut de ImageNetPrediction.cs :using Microsoft.ML.Data;Supprimez la définition de classe existante et ajoutez le code suivant pour la
ImageNetPredictionclasse au fichier ImageNetPrediction.cs :public class ImageNetPrediction { [ColumnName("grid")] public float[] PredictedLabels; }ImageNetPredictionest la classe de données de prédiction et a le champ suivantfloat[]:-
PredictedLabelscontient les dimensions, le score d’objet et les probabilités de classe pour chacune des zones englobantes détectées dans une image.
-
Initialiser les variables
La classe MLContext est un point de départ pour toutes les opérations ML.NET, et l’initialisation mlContext crée un environnement ML.NET qui peut être partagé entre les objets de flux de travail de création de modèle. Il est similaire, conceptuellement, à DBContext entity Framework.
Initialisez la mlContext variable avec une nouvelle instance d’ajout de MLContext la ligne suivante sous le outputFolder champ.
MLContext mlContext = new MLContext();
Créer un analyseur pour publier des sorties de modèle post-processus
Le modèle segmente une image dans une 13 x 13 grille, où chaque cellule de grille est 32px x 32px. Chaque cellule de grille contient 5 zones de délimitation d’objets potentielles. Un cadre englobant comporte 25 éléments :
-
xposition x du centre de la boîte englobante par rapport à la cellule de grille associée. -
yla position y du centre de la boîte englobante par rapport à la cellule de grille associée. -
wlargeur de la boîte englobante. -
hhauteur du cadre englobant. -
ola valeur de confiance qu’un objet existe dans la boîte englobante, également appelée score d'existence. -
p1-p20probabilités de classe pour chacune des 20 classes prédites par le modèle.
Au total, les 25 éléments décrivant chacun des 5 rectangles englobants composent les 125 éléments contenus dans chaque cellule de grille.
La sortie générée par le modèle ONNX préentraîné est un tableau flottant de longueur 21125, représentant les éléments d’un tensoriel avec des dimensions 125 x 13 x 13. Pour transformer les prédictions générées par le modèle en capteur, certaines tâches de post-traitement sont requises. Pour ce faire, créez un ensemble de classes pour faciliter l’analyse de la sortie.
Ajoutez un nouveau répertoire à votre projet pour organiser l’ensemble de classes d’analyseur.
- Dans l’Explorateur de solutions, cliquez avec le bouton droit sur le projet, puis sélectionnez Ajouter>un nouveau dossier. Lorsque le nouveau dossier apparaît dans l’Explorateur de solutions, nommez-le « YoloParser ».
Créer des boîtes englobantes et des dimensions
La sortie de données par le modèle contient des coordonnées et des dimensions des zones englobantes d’objets dans l’image. Créez une classe de base pour les dimensions.
Dans l’Explorateur de solutions, cliquez avec le bouton droit sur le répertoire YoloParser , puis sélectionnez Ajouter>un nouvel élément.
Dans la boîte de dialogue Ajouter un nouvel élément , sélectionnez Classe et remplacez le champ Nom par DimensionsBase.cs. Ensuite, sélectionnez Ajouter.
Le fichier DimensionsBase.cs s’ouvre dans l’éditeur de code. Supprimez toutes les directives et la
usingdéfinition de classe existante.Ajoutez le code suivant pour la
DimensionsBaseclasse au fichier 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; } }DimensionsBasepossède les propriétés suivantesfloat:-
Xcontient la position de l’objet le long de l’axe x. -
Ycontient la position de l’objet le long de l’axe y. -
Heightcontient la hauteur de l’objet. -
Widthcontient la largeur de l’objet.
-
Ensuite, créez une classe pour vos boîtes englobantes.
Dans l’Explorateur de solutions, cliquez avec le bouton droit sur le répertoire YoloParser , puis sélectionnez Ajouter>un nouvel élément.
Dans la boîte de dialogue Ajouter un nouvel élément , sélectionnez Classe et remplacez le champ Nompar YoloBoundingBox.cs. Ensuite, sélectionnez Ajouter.
Le fichier YoloBoundingBox.cs s’ouvre dans l’éditeur de code. Ajoutez la directive suivante
usingen haut de YoloBoundingBox.cs :using System.Drawing;Juste au-dessus de la définition de classe existante, ajoutez une nouvelle définition de classe appelée
BoundingBoxDimensionsqui hérite de laDimensionsBaseclasse pour contenir les dimensions du cadre englobant respectif.public class BoundingBoxDimensions : DimensionsBase { }Supprimez la définition de classe existante
YoloBoundingBoxet ajoutez le code suivant pour laYoloBoundingBoxclasse au fichier 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; } }YoloBoundingBoxpossède les propriétés suivantes :-
Dimensionscontient les dimensions du cadre englobant. -
Labelcontient la classe d’objet détectée dans le cadre englobant. -
Confidencecontient la confiance de la classe. -
Rectcontient la représentation rectangulaire des dimensions de la boîte englobante. -
BoxColorcontient la couleur associée à la classe respective utilisée pour dessiner sur l’image.
-
Créer l’analyseur
Maintenant que les classes pour les dimensions et les zones englobantes sont créées, il est temps de créer l’analyseur.
Dans l’Explorateur de solutions, cliquez avec le bouton droit sur le répertoire YoloParser , puis sélectionnez Ajouter>un nouvel élément.
Dans la boîte de dialogue Ajouter un nouvel élément , sélectionnez Classe et remplacez le champ Nom par YoloOutputParser.cs. Ensuite, sélectionnez Ajouter.
Le fichier YoloOutputParser.cs s’ouvre dans l’éditeur de code. Ajoutez les directives suivantes
usingen haut de YoloOutputParser.cs :using System; using System.Collections.Generic; using System.Drawing; using System.Linq;À l’intérieur de la définition de classe existante
YoloOutputParser, ajoutez une classe imbriquée qui contient les dimensions de chacune des cellules de l’image. Ajoutez le code suivant pour laCellDimensionsclasse qui hérite de laDimensionsBaseclasse en haut de laYoloOutputParserdéfinition de classe.class CellDimensions : DimensionsBase { }Dans la définition de
YoloOutputParserclasse, ajoutez les constantes et le champ suivants.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_COUNTest le nombre de lignes dans la grille dans laquelle l’image est divisée. -
COL_COUNTest le nombre de colonnes dans la grille dans laquelle l’image est divisée. -
CHANNEL_COUNTest le nombre total de valeurs contenues dans une cellule de la grille. -
BOXES_PER_CELLest le nombre de boîtes de délimitation dans une cellule. -
BOX_INFO_FEATURE_COUNTest le nombre de caractéristiques contenues dans une boîte (x,y,height,width,confidence). -
CLASS_COUNTest le nombre de prédictions de classe contenues dans chaque boîte englobante. -
CELL_WIDTHest la largeur d’une cellule dans la grille d’image. -
CELL_HEIGHTest la hauteur d’une cellule dans la grille d’image. -
channelStrideest la position de départ de la cellule active dans la grille.
Lorsque le modèle effectue une prédiction, également appelée scoring, il divise l’image
416px x 416pxd’entrée en une grille de cellules de la taille de13 x 13. Chaque cellule contient .32px x 32pxDans chaque cellule, il existe 5 cadres englobants contenant chacune 5 caractéristiques (x, y, largeur, hauteur, confiance). En outre, chaque cadre englobant contient la probabilité de chacune des classes, qui, dans ce cas, est de 20. Par conséquent, chaque cellule contient 125 éléments d’information (5 caractéristiques + 20 probabilités de classe).-
Créez une liste d’ancrages ci-dessous channelStride pour les 5 zones englobantes :
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
};
Les ancres sont des ratios de hauteur et de largeur prédéfinis des boîtes englobantes. La plupart des objets ou classes détectés par un modèle ont des ratios similaires. Cela est utile quand il s’agit de créer des cadres englobants. Au lieu de prédire les boîtes englobantes, on calcule le décalage par rapport aux dimensions prédéfinies, ce qui réduit les calculs nécessaires pour prédire la boîte englobante. En règle générale, ces ratios d’ancrage sont calculés en fonction du jeu de données utilisé. Dans ce cas, étant donné que le jeu de données est connu et que les valeurs ont été précomputées, les ancres peuvent être codées en dur.
Ensuite, définissez les étiquettes ou les classes que le modèle prédira. Ce modèle prédit 20 classes, qui est un sous-ensemble du nombre total de classes prédites par le modèle YOLOv2 d’origine.
Ajoutez votre liste d’étiquettes sous le 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"
};
Il existe des couleurs associées à chacune des classes. Attribuez vos couleurs de classe sous votre 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
};
Créer des fonctions d’assistance
Il existe une série d’étapes impliquées dans la phase de post-traitement. Pour vous aider à cela, plusieurs méthodes d’assistance peuvent être utilisées.
Les méthodes d’assistance utilisées par l’analyseur sont les suivantes :
-
Sigmoidapplique la fonction sigmoid qui génère un nombre compris entre 0 et 1. -
Softmaxnormalise un vecteur d’entrée dans une distribution de probabilité. -
GetOffsetassocie les éléments de la sortie du modèle à une dimension à la position correspondante dans un125 x 13 x 13tenseur. -
ExtractBoundingBoxesextrait les dimensions de la boîte englobante à l’aide de la méthodeGetOffsetà partir de la sortie du modèle. -
GetConfidenceextrait la valeur de confiance qui indique comment le modèle est sûr qu’il a détecté un objet et utilise laSigmoidfonction pour la transformer en pourcentage. -
MapBoundingBoxToCellutilise les dimensions de cadre englobant et les mappe sur sa cellule respective dans l’image. -
ExtractClassesextrait les prédictions de classe pour la zone englobante à partir de la sortie du modèle à l’aide de laGetOffsetméthode et les transforme en une distribution de probabilité à l’aide de laSoftmaxméthode. -
GetTopResultsélectionne la classe dans la liste des classes prédites avec la probabilité la plus élevée. -
IntersectionOverUnionfiltre les zones englobantes qui se chevauchent avec des probabilités inférieures.
Ajoutez le code pour toutes les méthodes d’assistance sous votre liste .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);
}
Une fois que vous avez défini toutes les méthodes d’assistance, il est temps de les utiliser pour traiter la sortie du modèle.
Sous la IntersectionOverUnion méthode, créez la ParseOutputs méthode pour traiter la sortie générée par le modèle.
public IList<YoloBoundingBox> ParseOutputs(float[] yoloModelOutputs, float threshold = .3F)
{
}
Créez une liste pour stocker vos boîtes de délimitation et définissez des variables dans la méthode ParseOutputs.
var boxes = new List<YoloBoundingBox>();
Chaque image est divisée en une grille de 13 x 13 cellules. Chaque cellule contient cinq zones englobantes. Sous la variable, ajoutez du boxes code pour traiter toutes les zones de chacune des cellules.
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++)
{
}
}
}
À l’intérieur de la boucle la plus interne, calculez la position de départ de la boîte actuelle dans la sortie du modèle unidimensionnel.
var channel = (box * (CLASS_COUNT + BOX_INFO_FEATURE_COUNT));
Directement en dessous, utilisez la ExtractBoundingBoxDimensions méthode pour obtenir les dimensions du cadre englobant actuel.
BoundingBoxDimensions boundingBoxDimensions = ExtractBoundingBoxDimensions(yoloModelOutputs, row, column, channel);
Ensuite, utilisez la GetConfidence méthode pour obtenir la confiance pour le cadre englobant actuel.
float confidence = GetConfidence(yoloModelOutputs, row, column, channel);
Ensuite, utilisez la MapBoundingBoxToCell méthode pour mapper le cadre englobant actuel à la cellule active en cours de traitement.
CellDimensions mappedBoundingBox = MapBoundingBoxToCell(row, column, box, boundingBoxDimensions);
Avant d’effectuer un traitement supplémentaire, vérifiez si votre valeur de confiance est supérieure au seuil fourni. Si ce n’est pas le cas, traitez la boîte englobante suivante.
if (confidence < threshold)
continue;
Sinon, poursuivez le traitement de la sortie. L’étape suivante consiste à obtenir la distribution de probabilité des classes prédites pour la boîte englobante actuelle en utilisant la méthode ExtractClasses.
float[] predictedClasses = ExtractClasses(yoloModelOutputs, row, column, channel);
Ensuite, utilisez la GetTopResult méthode pour obtenir la valeur et l’index de la classe avec la probabilité la plus élevée pour la zone actuelle et calculer son score.
var (topResultIndex, topResultScore) = GetTopResult(predictedClasses);
var topScore = topResultScore * confidence;
Utilisez à nouveau la topScore valeur pour conserver uniquement les zones englobantes qui sont au-dessus du seuil spécifié.
if (topScore < threshold)
continue;
Enfin, si la boîte englobante actuelle dépasse le seuil, créez un objet BoundingBox et ajoutez-le à la liste 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]
});
Une fois que toutes les cellules de l’image ont été traitées, renvoyez la boxes liste. Ajoutez l’instruction de retour suivante sous la boucle for-most externe dans la ParseOutputs méthode.
return boxes;
Filtrer les boîtes qui se chevauchent
Maintenant que toutes les boîtes englobantes de haute confiance ont été extraites des résultats du modèle, un filtrage supplémentaire doit être effectué pour éliminer les images qui se chevauchent. Ajoutez une méthode appelée FilterBoundingBoxes sous la ParseOutputs méthode :
public IList<YoloBoundingBox> FilterBoundingBoxes(IList<YoloBoundingBox> boxes, int limit, float threshold)
{
}
À l’intérieur de la FilterBoundingBoxes méthode, commencez par créer un tableau égal à la taille des zones détectées et en marquant tous les emplacements comme actifs ou prêts à être traités.
var activeCount = boxes.Count;
var isActiveBoxes = new bool[boxes.Count];
for (int i = 0; i < isActiveBoxes.Length; i++)
isActiveBoxes[i] = true;
Ensuite, triez la liste contenant vos zones englobantes dans l'ordre décroissant en fonction du score de confiance.
var sortedBoxes = boxes.Select((b, i) => new { Box = b, Index = i })
.OrderByDescending(b => b.Box.Confidence)
.ToList();
Ensuite, créez une liste pour contenir les résultats filtrés.
var results = new List<YoloBoundingBox>();
Commencez à traiter chaque boîte englobante en itérant sur chacune des boîtes englobantes.
for (int i = 0; i < boxes.Count; i++)
{
}
À l’intérieur de cette boucle for-loop, vérifiez si la zone englobante actuelle peut être traitée.
if (isActiveBoxes[i])
{
}
Si c’est le cas, ajoutez la zone englobante à la liste des résultats. Si les résultats dépassent la limite spécifiée de zones à extraire, sortez de la boucle. Ajoutez le code suivant à l’intérieur de l’instruction if-statement.
var boxA = sortedBoxes[i].Box;
results.Add(boxA);
if (results.Count >= limit)
break;
Sinon, examinez les boîtes englobantes adjacentes. Ajoutez le code suivant sous la case limite de case.
for (var j = i + 1; j < boxes.Count; j++)
{
}
Comme la première case, si la zone adjacente est active ou prête à être traitée, utilisez la IntersectionOverUnion méthode pour vérifier si la première case et la deuxième case dépassent le seuil spécifié. Ajoutez le code suivant à votre boucle for-loop la plus interne.
if (isActiveBoxes[j])
{
var boxB = sortedBoxes[j].Box;
if (IntersectionOverUnion(boxA.Rect, boxB.Rect) > threshold)
{
isActiveBoxes[j] = false;
activeCount--;
if (activeCount <= 0)
break;
}
}
En dehors de la boucle for-most interne qui vérifie les zones englobantes adjacentes, vérifiez s’il existe des zones englobantes restantes à traiter. Si ce n’est pas le cas, sortez de la boucle for-loop externe.
if (activeCount <= 0)
break;
Enfin, retournez les résultats en dehors de la boucle for initiale de la méthode FilterBoundingBoxes.
return results;
Très bien ! Maintenant, il est temps d’utiliser ce code avec le modèle pour le scoring.
Utiliser le modèle pour le scoring
Tout comme avec le post-traitement, il existe quelques étapes dans les étapes de scoring. Pour vous aider à cela, ajoutez une classe qui contiendra la logique de scoring à votre projet.
Dans l’Explorateur de solutions, cliquez avec le bouton droit sur le projet, puis sélectionnez Ajouter>un nouvel élément.
Dans la boîte de dialogue Ajouter un nouvel élément , sélectionnez Classe et remplacez le champ Nom par OnnxModelScorer.cs. Ensuite, sélectionnez Ajouter.
Le fichier OnnxModelScorer.cs s’ouvre dans l’éditeur de code. Ajoutez les directives suivantes
usingen haut de OnnxModelScorer.cs :using System; using System.Collections.Generic; using System.Linq; using Microsoft.ML; using Microsoft.ML.Data; using ObjectDetection.DataStructures; using ObjectDetection.YoloParser;Dans la définition de
OnnxModelScorerclasse, ajoutez les variables suivantes.private readonly string imagesFolder; private readonly string modelLocation; private readonly MLContext mlContext; private IList<YoloBoundingBox> _boundingBoxes = new List<YoloBoundingBox>();Directement en dessous, créez un constructeur pour la
OnnxModelScorerclasse qui initialisera les variables précédemment définies.public OnnxModelScorer(string imagesFolder, string modelLocation, MLContext mlContext) { this.imagesFolder = imagesFolder; this.modelLocation = modelLocation; this.mlContext = mlContext; }Une fois que vous avez créé le constructeur, définissez quelques structs qui contiennent des variables liées aux paramètres d’image et de modèle. Créez un struct appelé
ImageNetSettingspour contenir la hauteur et la largeur attendues comme entrée pour le modèle.public struct ImageNetSettings { public const int imageHeight = 416; public const int imageWidth = 416; }Ensuite, créez un autre struct appelé
TinyYoloModelSettingsqui contient les noms des couches d’entrée et de sortie du modèle. Pour visualiser le nom des couches d’entrée et de sortie du modèle, vous pouvez utiliser un outil tel que 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"; }Ensuite, créez le premier ensemble de méthodes à utiliser pour le scoring. Créez la méthode à l’intérieur
LoadModelde votreOnnxModelScorerclasse.private ITransformer LoadModel(string modelLocation) { }Dans la
LoadModelméthode, ajoutez le code suivant pour la journalisation.Console.WriteLine("Read model"); Console.WriteLine($"Model location: {modelLocation}"); Console.WriteLine($"Default parameters: image size=({ImageNetSettings.imageWidth},{ImageNetSettings.imageHeight})");ML.NET pipelines doivent connaître le schéma de données à utiliser quand la
Fitméthode est appelée. Dans ce cas, un processus similaire à l’entraînement sera utilisé. Toutefois, étant donné qu’aucune formation réelle ne se produit, il est acceptable d’utiliser une instruction videIDataView. Créez un nouveauIDataViewpour le pipeline à partir d’une liste vide.var data = mlContext.Data.LoadFromEnumerable(new List<ImageNetData>());En dessous, définissez le pipeline. Le pipeline se compose de quatre transformations.
-
LoadImagescharge l’image en tant que bitmap. -
ResizeImagesrescale l’image à la taille spécifiée (dans ce cas,416 x 416). -
ExtractPixelsmodifie la représentation en pixels de l’image d’une bitmap en vecteur numérique. -
ApplyOnnxModelcharge le modèle ONNX et l’utilise pour noter les données fournies.
Définissez votre pipeline dans la
LoadModelméthode sous ladatavariable.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 }));Il est maintenant temps d’instancier le modèle pour le scoring. Appelez la
Fitméthode sur le pipeline et retournez-la pour un traitement ultérieur.var model = pipeline.Fit(data); return model;-
Une fois le modèle chargé, il peut ensuite être utilisé pour effectuer des prédictions. Pour faciliter ce processus, créez une méthode appelée PredictDataUsingModel sous la LoadModel méthode.
private IEnumerable<float[]> PredictDataUsingModel(IDataView testData, ITransformer model)
{
}
Dans le PredictDataUsingModelfichier , ajoutez le code suivant pour la journalisation.
Console.WriteLine($"Images location: {imagesFolder}");
Console.WriteLine("");
Console.WriteLine("=====Identify the objects in the images=====");
Console.WriteLine("");
Utilisez ensuite la Transform méthode pour noter les données.
IDataView scoredData = model.Transform(testData);
Extrayez les probabilités prédites et retournez-les pour un traitement supplémentaire.
IEnumerable<float[]> probabilities = scoredData.GetColumn<float[]>(TinyYoloModelSettings.ModelOutput);
return probabilities;
Maintenant que les deux étapes sont configurées, combinez-les en une seule méthode. En dessous de la PredictDataUsingModel méthode, ajoutez une nouvelle méthode appelée Score.
public IEnumerable<float[]> Score(IDataView data)
{
var model = LoadModel(modelLocation);
return PredictDataUsingModel(data, model);
}
On y est presque! Maintenant, il est temps de tout mettre en application.
Détecter des objets
Maintenant que toute la configuration est terminée, il est temps de détecter certains objets.
Noter et analyser les sorties du modèle
Sous la création de la mlContext variable, ajoutez une instruction try-catch.
try
{
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
À l’intérieur du try bloc, commencez à implémenter la logique de détection d’objet. Tout d’abord, chargez les données dans un IDataView.
IEnumerable<ImageNetData> images = ImageNetData.ReadFromFile(imagesFolder);
IDataView imageDataView = mlContext.Data.LoadFromEnumerable(images);
Ensuite, créez une instance de OnnxModelScorer données et utilisez-la pour noter les données chargées.
// Create instance of model scorer
var modelScorer = new OnnxModelScorer(imagesFolder, modelFilePath, mlContext);
// Use model to score data
IEnumerable<float[]> probabilities = modelScorer.Score(imageDataView);
Maintenant, il est temps pour l’étape de post-traitement. Créez une instance et YoloOutputParser utilisez-la pour traiter la sortie du modèle.
YoloOutputParser parser = new YoloOutputParser();
var boundingBoxes =
probabilities
.Select(probability => parser.ParseOutputs(probability))
.Select(boxes => parser.FilterBoundingBoxes(boxes, 5, .5F));
Une fois la sortie du modèle traitée, il est temps de tracer les zones englobantes sur les images.
Visualiser des prédictions
Une fois que le modèle a évalué les images et que les sorties ont été traitées, les boîtes de délimitation doivent être ajoutées à l’image. Pour ce faire, ajoutez une méthode appelée DrawBoundingBox sous la GetAbsolutePath méthode à l’intérieur de Program.cs.
void DrawBoundingBox(string inputImageLocation, string outputImageLocation, string imageName, IList<YoloBoundingBox> filteredBoundingBoxes)
{
}
Tout d’abord, chargez l’image et obtenez les dimensions de hauteur et de largeur dans la DrawBoundingBox méthode.
Image image = Image.FromFile(Path.Combine(inputImageLocation, imageName));
var originalImageHeight = image.Height;
var originalImageWidth = image.Width;
Ensuite, créez une boucle de type 'for-each' pour itérer sur chacune des zones englobantes détectées par le modèle.
foreach (var box in filteredBoundingBoxes)
{
}
À l’intérieur de la boucle for-each, récupérez les dimensions de la boîte englobante.
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);
Étant donné que les dimensions de la boîte englobante correspondent à l’entrée du modèle de 416 x 416, ajustez les dimensions de la boîte englobante pour qu’elles correspondent à la taille réelle de l’image.
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;
Ensuite, définissez un modèle pour le texte qui apparaîtra au-dessus de chaque boîte de délimitation. Le texte contiendra la classe de l'objet à l'intérieur du cadre de délimitation respectif, ainsi que le niveau de confiance.
string text = $"{box.Label} ({(box.Confidence * 100).ToString("0")}%)";
Pour dessiner sur l’image, convertissez-la en objet Graphics .
using (Graphics thumbnailGraphic = Graphics.FromImage(image))
{
}
À l’intérieur du bloc de using code, ajustez les paramètres d’objet du Graphics graphique.
thumbnailGraphic.CompositingQuality = CompositingQuality.HighQuality;
thumbnailGraphic.SmoothingMode = SmoothingMode.HighQuality;
thumbnailGraphic.InterpolationMode = InterpolationMode.HighQualityBicubic;
En dessous, définissez les options de police et de couleur pour le texte et la zone englobante.
// 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);
Créez et remplissez un rectangle au-dessus de la zone englobante pour contenir le texte à l’aide de la méthode FillRectangle. Cela permet de contraster le texte et d’améliorer la lisibilité.
thumbnailGraphic.FillRectangle(colorBrush, (int)x, (int)(y - size.Height - 1), (int)size.Width, (int)size.Height);
Ensuite, dessinez le texte et la boîte de délimitation sur l’image en utilisant les méthodes DrawString et DrawRectangle.
thumbnailGraphic.DrawString(text, drawFont, fontBrush, atPoint);
// Draw bounding box on image
thumbnailGraphic.DrawRectangle(pen, x, y, width, height);
En dehors de la boucle for-each, ajoutez du code pour enregistrer les images dans le outputFolder.
if (!Directory.Exists(outputImageLocation))
{
Directory.CreateDirectory(outputImageLocation);
}
image.Save(Path.Combine(outputImageLocation, imageName));
Pour confirmer que l'application effectue des prédictions comme prévu pendant l'exécution, ajoutez une méthode appelée LogDetectedObjects sous la méthode DrawBoundingBox dans le fichier Program.cs afin d'afficher les objets détectés dans la 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("");
}
Maintenant que vous avez des méthodes utilitaires pour créer des retours visuels à partir des prédictions, ajoutez une boucle for pour itérer sur chacune des images évaluées.
for (var i = 0; i < images.Count(); i++)
{
}
À l'intérieur de la boucle for, récupérez le nom du fichier image et les boîtes englobantes associées.
string imageFileName = images.ElementAt(i).Label;
IList<YoloBoundingBox> detectedObjects = boundingBoxes.ElementAt(i);
En dessous, utilisez la DrawBoundingBox méthode pour dessiner les zones englobantes sur l’image.
DrawBoundingBox(imagesFolder, outputFolder, imageFileName, detectedObjects);
Enfin, utilisez la LogDetectedObjects méthode pour générer des prédictions dans la console.
LogDetectedObjects(imageFileName, detectedObjects);
Après l’instruction try-catch, ajoutez une logique supplémentaire pour indiquer que le processus est en cours d’exécution.
Console.WriteLine("========= End of Process..Hit any Key ========");
C’est tout !
Results
Après avoir suivi les étapes précédentes, exécutez votre application console (Ctrl + F5). Vos résultats doivent être similaires à la sortie suivante. Vous pouvez recevoir des avertissements ou des messages de traitement, mais ces messages ont été supprimés des résultats suivants pour plus de clarté.
=====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 ========
Pour afficher les images avec des zones englobantes, accédez au répertoire assets/images/output/. Vous trouverez ci-dessous un exemple d’une des images traitées.
Félicitations! Vous avez maintenant créé un modèle Machine Learning pour la détection d’objets en réutilisant un modèle préentraîné ONNX dans ML.NET.
Vous trouverez le code source de ce didacticiel dans le référentiel dotnet/machinelearning-samples .
Dans ce didacticiel, vous avez appris à :
- Comprendre le problème
- Découvrez ce qu’est ONNX et comment il fonctionne avec ML.NET
- Comprendre le modèle
- Réutiliser le modèle préentraîné
- Détecter des objets avec un modèle chargé
Consultez le référentiel GitHub d’exemples Machine Learning pour explorer un exemple de détection d’objet développé.