Condividi tramite


Introduzione ai modelli ONNX nell'app WinUI con ONNX Runtime

Questo articolo illustra come creare un'app WinUI 3 che usa un modello ONNX per classificare gli oggetti in un'immagine e visualizzare l'attendibilità di ogni classificazione. Per altre informazioni sull'uso di modelli di intelligenza artificiale e Machine Learning nell'app windows, vedere Introduzione all'uso di modelli di intelligenza artificiale e Machine Learning nell'app di Windows.

Quando si usano le funzionalità di intelligenza artificiale, è consigliabile esaminare: Sviluppo di applicazioni e funzionalità di intelligenza artificiale responsabile in Windows.

Che cos'è il runtime ONNX

ONNX Runtime è un acceleratore di modelli di Machine Learning multipiattaforma, con un'interfaccia flessibile per integrare librerie specifiche dell'hardware. ONNX Runtime può essere usato con i modelli di PyTorch, Tensorflow/Keras, TFLite, scikit-learne altri framework. Per ulteriori informazioni, vedere il sito Web ONNX Runtime all'indirizzo https://onnxruntime.ai/docs/.

Questo esempio usa la DirectML Execution Provider che astrae ed esegue tra le diverse opzioni hardware nei dispositivi Windows e supporta l'esecuzione tra acceleratori locali, ad esempio gpu e NPU.

Prerequisiti

  • Il dispositivo deve avere la modalità sviluppatore abilitata. Per ulteriori informazioni, consultare Abilita il tuo dispositivo per lo sviluppo.
  • Visual Studio 2022 o versioni successive con il pacchetto di sviluppo per applicazioni desktop .NET.

Creare una nuova app WinUI C#

In Visual Studio creare un nuovo progetto. Nella finestra di dialogo Crea un nuovo progetto, imposta il filtro della lingua su "C#" e il filtro del tipo di progetto su "winui", quindi seleziona il modello app vuota, Packaged (WinUI3 in Desktop). Denominare il nuovo progetto "ONNXWinUIExample".

Aggiungere riferimenti ai pacchetti NuGet

In Esplora soluzioni, fare clic con il pulsante destro del mouse su Dipendenze e selezionare Gestisci pacchetti NuGet.... Nell'interfaccia di gestione pacchetti NuGet, selezionare la scheda Sfoglia. Cercare i pacchetti seguenti e per ognuno, selezionare la versione stabile più recente nell'elenco a discesa Versione e quindi fare clic su Installa.

Pacco Descrizione
Microsoft.ML.OnnxRuntime.DirectML Fornisce API per l'esecuzione di modelli ONNX nella GPU.
SixLabors.ImageSharp Fornisce utilità per l'elaborazione delle immagini destinate all'input del modello.
SharpDX.DXGI Fornisce API per l'accesso al dispositivo DirectX da C#.

Aggiungere le seguenti usando le direttive all'inizio di MainWindows.xaml.cs per accedere alle API da queste librerie.

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

Aggiungere il modello al progetto

In Esplora soluzioni , fare clic con il tasto destro del mouse sul progetto e selezionare Aggiungi->Nuova cartella. Assegnare alla nuova cartella il nome "model". Per questo esempio verrà usato il modello resnet50-v2-7.onnx da https://github.com/onnx/models. Passare alla visualizzazione del repository per il modello in https://github.com/onnx/models/blob/main/validated/vision/classification/resnet/model/resnet50-v2-7.onnx. Fare clic sul pulsante *Scarica file grezzo. Copiare questo file nella directory "model" appena creata.

In Esplora soluzioni fare clic sul file del modello e impostare Copia nella directory di output su "Copia se più recente".

Creare un'interfaccia utente semplice

Per questo esempio verrà creata una semplice interfaccia utente che include un Button per consentire all'utente di selezionare un'immagine da valutare con il modello, un controllo Image per visualizzare l'immagine selezionata e un TextBlock per elencare gli oggetti rilevati nell'immagine e la confidenza di ogni classificazione di oggetti.

Nel file MainWindow.xaml, sostituire l'elemento predefinito StackPanel con il seguente codice XAML.

<!--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>

Inizializzare il modello

Nel file , all'interno della classe MainWindow , creare un metodo helper denominato InitModel che inizializzerà il modello. Questo metodo utilizza le API della libreria SharpDX.DXGI per selezionare il primo adattatore disponibile. L'adattatore selezionato viene impostato nell'oggetto SessionOptions per il provider di esecuzione DirectML in questa sessione. Infine, viene inizializzato un nuovo InferenceSession, passando il percorso del file del modello e le opzioni di sessione.

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

}

Caricare e analizzare un'immagine

Per semplicità, per questo esempio tutti i passaggi per caricare e formattare l'immagine, richiamare il modello e visualizzare i risultati verranno inseriti all'interno del gestore di clic del pulsante. Si noti che aggiungiamo la parola chiave async al gestore del clic del pulsante, incluso nel modello predefinito, per eseguire operazioni asincrone nel gestore.

// MainWindow.xaml.cs

private async void myButton_Click(object sender, RoutedEventArgs e)
{
    ...
}

Usa un FileOpenPicker per consentire all'utente di selezionare un'immagine dal computer per analizzarla e visualizzarla nell'interfaccia utente.

    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;

A questo punto è necessario elaborare l'input per ottenerlo in un formato supportato dal modello. La libreria SixLabors.ImageSharp viene usata per caricare l'immagine in formato RGB a 24 bit e ridimensionare l'immagine a 224x224 pixel. I valori pixel vengono quindi normalizzati con una media di 255*[0,485, 0,456, 0,406] e deviazione standard di 255*[0,229, 0,224, 0,225]. I dettagli del formato previsto dal modello sono disponibili sulla pagina GitHub del modello 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];
            }
        }
    });

Successivamente, si configurano gli input creando un OrtValue di tipo Tensor sopra la matrice di dati dell'immagine gestita.

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

Successivamente, se la sessione di inferenza non è ancora stata inizializzata, chiamare il metodo helper InitModel. Quindi, chiamare il metodo Run per eseguire il modello e recuperare i risultati.

    // Run inference
    if (_inferenceSession == null)
    {
        InitModel();
    }
    using var runOptions = new RunOptions();
    using IDisposableReadOnlyCollection<OrtValue> results = _inferenceSession.Run(runOptions, inputs, _inferenceSession.OutputNames);

Il modello restituisce i risultati come un buffer nativo di tensore. Il codice seguente converte l'output in una matrice di valori float. Viene applicata una funzione softmax in modo che i valori si trovano nell'intervallo [0,1] e sommati a 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'indice di ogni valore nell'array di output corrisponde a un'etichetta su cui il modello è stato addestrato e il valore a quell'indice rappresenta la fiducia del modello che l'etichetta rappresenti un oggetto rilevato nell'immagine di input. Si selezionano i 10 risultati con il valore di attendibilità più alto. Questo codice usa alcuni oggetti helper che verranno definiti nel passaggio successivo.

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

Dichiarare gli oggetti ausiliari

La classe Prediction fornisce solo un modo semplice per associare un'etichetta di oggetto a un valore di attendibilità. In MainPage.xaml.cs, aggiungi questa classe all'interno del blocco dello spazio dei nomi ONNXWinUIExample, ma all'esterno della definizione della classe MainWindow.

internal class Prediction
{
    public object Label { get; set; }
    public float Confidence { get; set; }
}

Aggiungere quindi la classe helper LabelMap che elenca tutte le etichette degli oggetti su cui è stato eseguito il training del modello, in modo che corrispondano agli indici dei risultati restituiti dal modello. L'elenco delle etichette è troppo lungo da presentare qui. È possibile copiare la classe completa LabelMap da un file di codice di esempio nel repository ONNXRuntime su GitHub e incollarla nel blocco dello spazio dei nomi ONNXWinUIExample.

public class LabelMap
{
    public static readonly string[] Labels = new[] {
        "tench",
        "goldfish",
        "great white shark",
        ...
        "hen-of-the-woods",
        "bolete",
        "ear",
        "toilet paper"};

Eseguire l'esempio

Compilare ed eseguire il progetto. Fare clic sul pulsante Seleziona foto e selezionare un file di immagine da analizzare. È possibile esaminare la definizione della classe helper LabelMap per vedere gli elementi che il modello è in grado di riconoscere e scegliere un'immagine che potrebbe produrre risultati interessanti. Dopo l'inizializzazione del modello, la prima volta che viene eseguita e dopo il completamento dell'elaborazione del modello, verrà visualizzato un elenco di oggetti rilevati nell'immagine e il valore di confidenza di ogni stima.

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

Vedere anche