Partager via


Commencez 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 de modèles IA et Machine Learning dans votre application Windows, consultez Prise en main de l’IA sur Windows.

Lorsque vous utilisez des fonctionnalités IA, nous vous recommandons de vous référer à l’article suivant : Développement d’applications et de fonctionnalités d’IA générative responsable sur Windows.

Qu’est-ce que le runtime ONNX ?

ONNX Runtime est un accélérateur de modèle Machine Learning multiplateforme disposant d’une interface flexible pour intégrer des bibliothèques spécifiques au matériel. ONNX Runtime peut être utilisé avec des modèles de PyTorch, Tensorflow/Keras, TFLite, scikit-learnet d'autres frameworks. Pour plus d’informations, consultez le site web ONNX Runtime à l'adresse https://onnxruntime.ai/docs/.

Cet exemple utilise le DirectML Execution Provider qui simplifie 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, comme 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 ultérieure avec la charge de travail de développement de bureau .NET.

Créer une nouvelle application WinUI C#

Dans Visual Studio, créez un projet. Dans la boîte de dialogue Créer un projet, définissez le filtre de langue sur « C# » et le filtre de type de projet sur « winui », puis sélectionnez le modèle Blank App, Packaged (WinUI 3 in Desktop). Nommez le nouveau projet « ONNXWinUIExample ».

Ajouter des références aux packages NuGet

Dans l’Explorateur de solutions, cliquez avec le bouton droit sur Dépendances et sélectionnez Gérer les packages NuGet.... Dans le gestionnaire de packages 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 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 traiter les images destinées à l’entrée du modèle.
SharpDX.DXGI Fournit des API pour accéder à l’appareil DirectX à partir de C#.

Ajoutez les éléments suivantes à l’aide de directives placées 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 de la souris sur votre projet et sélectionnez Ajouter> un nouveau dossier. Nommez le nouveau dossier « model ». Pour cet exemple, nous allons utiliser le modèle resnet50-v2-7.onnx à partir de https://github.com/onnx/models. Accédez à l’affichage référentiel du modèle en suivant ce lien : 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 brut. Copiez ce fichier dans le répertoire « model » que vous venez de créer.

Dans l’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 IU simple

Pour cet exemple, nous allons créer une IU 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 avec 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, dans 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 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 de bouton. Notez que nous ajoutons le mot clé async au gestionnaire de clics du 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 depuis son ordinateur, afin de l’analyser et de 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 224x224 pixels. Ensuite, les valeurs des 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];
            }
        }
    });

Ensuite, nous configurons les entrées en créant une OrtValue de type Tensor au-dessus du tableau de données d’image géré.

    // 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 }
    };

Ensuite, 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’un tampon tensoriel natif. 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 que leur somme soit égale à 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 correspond à une étiquette sur laquelle le modèle a été formé, et la valeur à cet index indique la confiance du modèle dans l’identification de l’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 offre un moyen simple d’associer une étiquette d’objet à une valeur de confiance. Dans MainPage.xaml.cs, ajoutez cette classe dans le 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; }
}

Ajoutez ensuite la classe d’assistance LabelMap qui répertorie toutes les étiquettes d’objet sur laquelle le modèle a été formé, 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 à présenter ici. Vous pouvez copier la classe LabelMap complète à partir d’un exemple de fichier de code dans le dépôt github ONNXRuntime et le 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 et sélectionnez 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. Une fois le modèle initialisé, la première fois qu’il est exécuté et une fois le traitement du modèle terminé, vous devriez voir une liste d’objets détectés dans l’image ainsi que 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

Voir aussi