Bien démarrer avec les modèles ONNX dans votre application WinUI avec ONNX Runtime
Cet article vous guide tout au long de la création d’une application WinUI 3 qui utilise un modèle ONNX pour classifier des objets dans une image et afficher la confiance de chaque classification. Pour plus d’informations sur l’utilisation des modèles IA et Machine Learning dans votre application Windows, consultez Commencer avec les modèles IA et Machine Learning dans votre application Windows.
Qu’est-ce que le runtime ONNX ?
ONNX Runtime est un accélérateur de modèle Machine Learning multiplateforme avec une interface flexible pour intégrer des bibliothèques spécifiques au matériel. ONNX Runtime peut être utilisé avec des modèles à partir de PyTorch, Tensorflow/Keras, TFLite, scikit-learn et d’autres frameworks. Pour plus d’informations, consultez le site web ONNX Runtime à l’adresse suivante : https://onnxruntime.ai/docs/.
Cet exemple utilise DirectML Execution Provider qui fait abstraction et s’exécute sur les différentes options matérielles des appareils Windows et prend en charge l’exécution sur les accélérateurs locaux, tels que le GPU et le NPU.
Prérequis
- Le mode développeur doit être activé sur votre appareil. Pour plus d’informations, consultez Activer votre appareil pour le développement.
- Visual Studio 2022 ou version ultérieure avec la charge de travail de développement de bureau .NET.
Créer une application C# WinUI
Dans Visual Studio, créez un projet. Dans la boîte de dialogue Créer un projet, définissez le filtre de langage sur « C# » et le filtre de type de projet sur « winui », puis sélectionnez le modèle Blank app, Packaged (WinUI3 in Desktop). Nommez le nouveau projet « ONNXWinUIExample ».
Ajouter des références aux packages NuGet
Dans Explorateur de solutions, cliquez avec le bouton droit sur Dépendances et sélectionnez Gérer les packages NuGet.... Dans le gestionnaire de package NuGet, sélectionnez l’onglet Parcourir. Recherchez les packages suivants et, pour chacun d’eux, sélectionnez la dernière version stable dans la liste déroulante de Version, puis cliquez sur Installer.
Package | Description |
---|---|
Microsoft.ML.OnnxRuntime.DirectML | Fournit des API pour l’exécution de modèles ONNX sur le GPU. |
SixLabors.ImageSharp | Fournit des utilitaires d’image pour le traitement des images pour l’entrée de modèle. |
SharpDX.DXGI | Fournit des API pour l’accès à l’appareil DirectX à partir de C#. |
Ajoutez les directives using suivantes en haut de MainWindows.xaml.cs
pour accéder aux API à partir de ces bibliothèques.
// MainWindow.xaml.cs
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using SharpDX.DXGI;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
Ajouter le modèle à votre projet
Dans l’Explorateur de solutions, cliquez avec le bouton droit sur le projet, puis sélectionnez Ajouter->Nouveau dossier. Nommez ce nouveau dossier « modèle ». Pour cet exemple, nous allons utiliser le modèle resnet50-v2-7.onnx de https://github.com/onnx/models. Accédez à l’affichage dépôt du modèle à l’adresse https://github.com/onnx/models/blob/main/validated/vision/classification/resnet/model/resnet50-v2-7.onnx. Cliquez sur le bouton *Télécharger le fichier RAW. Copiez ce fichier dans le répertoire « modèle » que vous venez de créer.
Dans Explorateur de solutions, cliquez sur le fichier de modèle et définissez Copier dans le répertoire de sortie sur « Copier si plus récent ».
Créer une interface utilisateur simple
Pour cet exemple, nous allons créer une interface utilisateur simple qui inclut un bouton pour permettre à l’utilisateur de sélectionner une image à évaluer avec le modèle, un contrôle Image pour afficher l’image sélectionnée et un TextBlock pour répertorier les objets détectés dans l’image et la confiance de chaque classification d’objets.
Dans le fichier MainWindow.xaml
, remplacez l’élément StackPanel par défaut par le code XAML suivant.
<!--MainWindow.xaml-->
<Grid Padding="25" >
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Button x:Name="myButton" Click="myButton_Click" Grid.Column="0" VerticalAlignment="Top">Select photo</Button>
<Image x:Name="myImage" MaxWidth="300" Grid.Column="1" VerticalAlignment="Top"/>
<TextBlock x:Name="featuresTextBlock" Grid.Column="2" VerticalAlignment="Top"/>
</Grid>
Initialiser le modèle
Dans le fichier MainWindow.xaml.cs
, à l’intérieur de la classe MainWindow, créez une méthode d’assistance appelée InitModel qui initialisera le modèle. Cette méthode utilise des API de la bibliothèque SharpDX.DXGI pour sélectionner le premier adaptateur disponible. L’adaptateur sélectionné est défini dans l’objet SessionOptions pour le fournisseur d’exécution DirectML dans cette session. Enfin, une nouvelle inferenceSession est initialisée, en passant le chemin d’accès au fichier de modèle et aux options de session.
// MainWindow.xaml.cs
private InferenceSession _inferenceSession;
private string modelDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "model");
private void InitModel()
{
if (_inferenceSession != null)
{
return;
}
// Select a graphics device
var factory1 = new Factory1();
int deviceId = 0;
Adapter1 selectedAdapter = factory1.GetAdapter1(0);
// Create the inference session
var sessionOptions = new SessionOptions
{
LogSeverityLevel = OrtLoggingLevel.ORT_LOGGING_LEVEL_INFO
};
sessionOptions.AppendExecutionProvider_DML(deviceId);
_inferenceSession = new InferenceSession($@"{modelDir}\resnet50-v2-7.onnx", sessionOptions);
}
Charger et analyser une image
Par souci de simplicité, pour cet exemple, toutes les étapes de chargement et de mise en forme de l’image, l’appel du modèle et l’affichage des résultats seront placés dans le gestionnaire de clics du bouton. Notez que nous ajoutons le mot clé asynchrone au gestionnaire de clics de bouton inclus dans le modèle par défaut, afin que nous puissions exécuter des opérations asynchrones dans le gestionnaire.
// MainWindow.xaml.cs
private async void myButton_Click(object sender, RoutedEventArgs e)
{
...
}
Utilisez un FileOpenPicker pour permettre à l’utilisateur de sélectionner une image à partir de son ordinateur pour l’analyser et l’afficher dans l’interface utilisateur.
FileOpenPicker fileOpenPicker = new()
{
ViewMode = PickerViewMode.Thumbnail,
FileTypeFilter = { ".jpg", ".jpeg", ".png", ".gif" },
};
InitializeWithWindow.Initialize(fileOpenPicker, WinRT.Interop.WindowNative.GetWindowHandle(this));
StorageFile file = await fileOpenPicker.PickSingleFileAsync();
if (file == null)
{
return;
}
// Display the image in the UI
var bitmap = new BitmapImage();
bitmap.SetSource(await file.OpenAsync(Windows.Storage.FileAccessMode.Read));
myImage.Source = bitmap;
Ensuite, nous devons traiter l’entrée pour l’obtenir dans un format pris en charge par le modèle. La bibliothèque SixLabors.ImageSharp est utilisée pour charger l’image au format RVB 24 bits et redimensionner l’image en 224 x 224 pixels. Ensuite, les valeurs de pixels sont normalisées avec une moyenne de 255*[0,485, 0,456, 0,406] et un écart type de 255*[0,229, 0,224, 0,225]. Vous trouverez les détails du format attendu par le modèle sur la page github du modèle resnet.
using var fileStream = await file.OpenStreamForReadAsync();
IImageFormat format = SixLabors.ImageSharp.Image.DetectFormat(fileStream);
using Image<Rgb24> image = SixLabors.ImageSharp.Image.Load<Rgb24>(fileStream);
// Resize image
using Stream imageStream = new MemoryStream();
image.Mutate(x =>
{
x.Resize(new ResizeOptions
{
Size = new SixLabors.ImageSharp.Size(224, 224),
Mode = ResizeMode.Crop
});
});
image.Save(imageStream, format);
// Preprocess image
// We use DenseTensor for multi-dimensional access to populate the image data
var mean = new[] { 0.485f, 0.456f, 0.406f };
var stddev = new[] { 0.229f, 0.224f, 0.225f };
DenseTensor<float> processedImage = new(new[] { 1, 3, 224, 224 });
image.ProcessPixelRows(accessor =>
{
for (int y = 0; y < accessor.Height; y++)
{
Span<Rgb24> pixelSpan = accessor.GetRowSpan(y);
for (int x = 0; x < accessor.Width; x++)
{
processedImage[0, 0, y, x] = ((pixelSpan[x].R / 255f) - mean[0]) / stddev[0];
processedImage[0, 1, y, x] = ((pixelSpan[x].G / 255f) - mean[1]) / stddev[1];
processedImage[0, 2, y, x] = ((pixelSpan[x].B / 255f) - mean[2]) / stddev[2];
}
}
});
Nous configuons ensuite les entrées en créant une ortValue du type Tensor au-dessus du tableau de données d’image managée.
// Setup inputs
// Pin tensor buffer and create a OrtValue with native tensor that makes use of
// DenseTensor buffer directly. This avoids extra data copy within OnnxRuntime.
// It will be unpinned on ortValue disposal
using var inputOrtValue = OrtValue.CreateTensorValueFromMemory(OrtMemoryInfo.DefaultInstance,
processedImage.Buffer, new long[] { 1, 3, 224, 224 });
var inputs = new Dictionary<string, OrtValue>
{
{ "data", inputOrtValue }
};
Après, si la session d’inférence n’a pas encore été initialisée, appelez la méthode d’assistance InitModel. Appelez ensuite la méthode Run pour exécuter le modèle et récupérer les résultats.
// Run inference
if (_inferenceSession == null)
{
InitModel();
}
using var runOptions = new RunOptions();
using IDisposableReadOnlyCollection<OrtValue> results = _inferenceSession.Run(runOptions, inputs, _inferenceSession.OutputNames);
Le modèle génère les résultats sous la forme d’une mémoire tampon de tenseur native. Le code suivant convertit la sortie en tableau de floats. Une fonction softmax est appliquée afin que les valeurs se trouvent dans la plage [0,1] et qu’elles correspondent à 1.
// Postprocess output
// We copy results to array only to apply algorithms, otherwise data can be accessed directly
// from the native buffer via ReadOnlySpan<T> or Span<T>
var output = results[0].GetTensorDataAsSpan<float>().ToArray();
float sum = output.Sum(x => (float)Math.Exp(x));
IEnumerable<float> softmax = output.Select(x => (float)Math.Exp(x) / sum);
L’index de chaque valeur du tableau de sortie est mappé à une étiquette sur laquelle le modèle a été entraîné, et la valeur à cet index est la confiance du modèle que l’étiquette représente un objet détecté dans l’image d’entrée. Nous choisissons les 10 résultats avec la valeur de confiance la plus élevée. Ce code utilise des objets d’assistance que nous allons définir à l’étape suivante.
// Extract top 10
IEnumerable<Prediction> top10 = softmax.Select((x, i) => new Prediction { Label = LabelMap.Labels[i], Confidence = x })
.OrderByDescending(x => x.Confidence)
.Take(10);
// Print results
featuresTextBlock.Text = "Top 10 predictions for ResNet50 v2...\n";
featuresTextBlock.Text += "-------------------------------------\n";
foreach (var t in top10)
{
featuresTextBlock.Text += $"Label: {t.Label}, Confidence: {t.Confidence}\n";
}
} // End of myButton_Click
Déclarer des objets d’assistance
La classe Prediction fournit simplement un moyen simple d’associer une étiquette d’objet à une valeur de confiance. Dans MainPage.xaml.cs
, ajoutez cette classe à l’intérieur du bloc d’espace de noms ONNXWinUIExample, mais en dehors de la définition de classe MainWindow.
internal class Prediction
{
public object Label { get; set; }
public float Confidence { get; set; }
}
Ensuite, ajoutez la classe d’assistance LabelMap qui répertorie toutes les étiquettes d’objet sur laquelle le modèle a été entraîné, dans un ordre spécifique afin que les étiquettes correspondent aux index des résultats retournés par le modèle. La liste des étiquettes est trop longue pour être présentée entièrement ici. Vous pouvez copier la classe complète LabelMap à partir d’un fichier d’exemple de code dans le dépôt github ONNXRuntime et la coller dans le bloc d’espace de noms ONNXWinUIExample.
public class LabelMap
{
public static readonly string[] Labels = new[] {
"tench",
"goldfish",
"great white shark",
...
"hen-of-the-woods",
"bolete",
"ear",
"toilet paper"};
Exécuter l’exemple
Générez et exécutez le projet. Cliquez sur le bouton Sélectionner une photo, puis choisissez un fichier image à analyser. Vous pouvez examiner la définition de classe d’assistance LabelMap pour voir les éléments que le modèle peut reconnaître et choisir une image susceptible d’avoir des résultats intéressants. Après l’initialisation du modèle, sa première exécution et l’achèvement de son traitement, vous devez voir une liste d’objets détectés dans l’image et la valeur de confiance de chaque prédiction.
Top 10 predictions for ResNet50 v2...
-------------------------------------
Label: lakeshore, Confidence: 0.91674984
Label: seashore, Confidence: 0.033412453
Label: promontory, Confidence: 0.008877817
Label: shoal, Confidence: 0.0046836217
Label: container ship, Confidence: 0.001940886
Label: Lakeland Terrier, Confidence: 0.0016400366
Label: maze, Confidence: 0.0012478716
Label: breakwater, Confidence: 0.0012336193
Label: ocean liner, Confidence: 0.0011933135
Label: pier, Confidence: 0.0011284945