Compartilhar via


Introdução aos modelos do ONNX no aplicativo do WinUI com o ONNX Runtime

Este artigo orienta a criação de um aplicativo do WinUI 3 que usa um modelo do ONNX para classificar objetos em uma imagem e exibir a INT.CONFIANÇA de cada classificação. Para obter mais informações sobre como usar modelos de IA e machine learning no aplicativo do Windows, confira Get started using AI and Machine Learning models in your Windows app.

O que é o runtime do ONNX

O ONNX Runtime é um acelerador de modelo de machine learning multiplataforma, com uma interface flexível para integrar bibliotecas específicas de hardware. O ONNX Runtime pode ser usado com modelos de PyTorch, Tensorflow/Keras, TFLite scikit-learn e outras estruturas. Para obter mais informações, confira o site do ONNX Runtime e https://onnxruntime.ai/docs/.

Este exemplo usa o DirectML Execution Provider, que abstrai e é executado nas diferentes opções de hardware em dispositivos Windows e oferece suporte à execução em aceleradores locais, como GPU e NPU.

Pré-requisitos

  • O dispositivo precisa ter o modo de desenvolvedor habilitado. Para obter mais informações, confira Habilitar o dispositivo para desenvolvimento.
  • Visual Studio 2022 ou posterior com a carga de trabalho de desenvolvimento do .NET para área de trabalho.

Criar um aplicativo do WinUI em C#

No Visual Studio, crie um novo projeto. Na caixa de diálogo Criar um projeto, defina o filtro de linguagem como "C#" e o filtro do tipo de projeto como "winui"; em seguida, selecione o modelo Aplicativo em branco, Empacotado (WinUI3 na Área de Trabalho). Nomeie o novo projeto como "ONNXWinUIExample".

Adicione referências aos pacotes NuGet

No Gerenciador de Soluções, clique com o botão direito do mouse em Dependências e selecione Gerenciar pacotes NuGet.... No gerenciador de pacotes NuGet, selecione a guia Navegar. Procure os pacotes a seguir e, para cada um, selecione a versão estável mais recente na lista suspensa Versão e clique em Instalar.

Pacote Descrição
Microsoft.ML.OnnxRuntime.DirectML Fornece APIs para executar modelos do ONNX na GPU.
SixLabors.ImageSharp Fornece utilitários de imagem direcionados ao processamento de imagens para entrada de modelo.
SharpDX.DXGI Fornece APIs para acessar o dispositivo DirectX no C#.

Adicione as diretivas using a seguir à parte superior de MainWindows.xaml.cs para acessar as APIs nessas bibliotecas.

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

Adicionar o modelo ao projeto

No Gerenciador de Soluções, clique com o botão direito do mouse no projeto e selecione Adicionar->Nova Pasta. Nomeie a nova pasta como "model". Neste exemplo, usaremos o modelo resnet50-v2-7.onnx de https://github.com/onnx/models. Vá para a exibição de repositório do modelo em https://github.com/onnx/models/blob/main/validated/vision/classification/resnet/model/resnet50-v2-7.onnx. Clique no botão *Baixar arquivo RAW. Copie esse arquivo para o diretório "model" que acabou de criar.

No Gerenciador de Soluções, clique no arquivo do modelo e defina Copiar para Diretório de Saída como "Copiar se for mais recente".

Criar uma interface do usuário simples

Neste exemplo, criaremos uma interface do usuário simples que inclui um Botão para permitir que o usuário selecione uma imagem a fim de avaliar com o modelo, um controle de Imagem para exibir a imagem selecionada e um TextBlock para listar os objetos que o modelo detectou na imagem e a INT.CONFIANÇA de cada classificação de objeto.

No arquivo MainWindow.xaml, substitua o elemento padrão StackPanel pelo código XAML a seguir.

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

Inicializar o modelo

No arquivo MainWindow.xaml.cs, dentro da classe MainWindow, crie um método auxiliar chamado InitModel que inicializará o modelo. Esse método usa APIs da biblioteca SharpDX.DXGI para selecionar o primeiro adaptador disponível. O adaptador selecionado é definido no objeto SessionOptions para o provedor de execução DirectML nessa sessão. Por fim, uma nova InferenceSession é inicializada, passando o caminho ao arquivo de modelo e às opções de sessão.

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

}

Carregar e analisar uma imagem

Para simplificar, neste exemplo, todas as etapas para carregar e formatar a imagem, invocar o modelo e exibir os resultados serão colocadas no manipulador de clique no botão. Observe que adicionamos a palavra-chave assíncrono ao manipulador de clique no botão incluído no modelo padrão para que possamos executar operações assíncronas no manipulador.

// MainWindow.xaml.cs

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

Use um FileOpenPicker para permitir que o usuário selecione uma imagem de seu computador para analisá-la e exibi-la na interface do usuário.

    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;

Em seguida, precisamos processar a entrada para obtê-la em um formato que tenha suporte do modelo. A biblioteca SixLabors.ImageSharp é usada para carregar a imagem no formato RGB de 24 bits e redimensioná-la para 224x224 pixels. Em seguida, os valores de pixel são normalizados com uma média de 255*[0,485, 0,456, 0,406] e desvio padrão de 255*[0,229, 0,224, 0,225]. Os detalhes do formato esperado pelo modelo podem ser encontrados na página do github, o modelo 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];
            }
        }
    });

Em seguida, configuramos as entradas criando um OrtValue do tipo Tensor sobre a matriz de dados de imagem gerenciada.

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

Em seguida, se a sessão de inferência ainda não tiver sido inicializada, chame o método auxiliar InitModel. Em seguida, chame o método Executar para executar o modelo e recuperar os resultados.

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

O modelo gera os resultados como um buffer de tensor nativo. O código a seguir converte a saída em uma matriz de flutuações. Uma função softmax é aplicada para que os valores fiquem no intervalo [0,1] e somem 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);

O índice de cada valor na matriz de saída é mapeado para um rótulo no qual o modelo foi treinado, e o valor nesse índice é a INT.CONFIANÇA do modelo de que o rótulo representa um objeto detectado na imagem de entrada. Escolhemos os 10 resultados com maior valor de INT.CONFIANÇA. Esse código usa alguns objetos auxiliares que definiremos na próxima etapa.

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

Declarar objetos auxiliares

A classe de Previsão fornece apenas uma maneira simples de associar um rótulo de objeto a um valor de INT.CONFIANÇA. Em MainPage.xaml.cs, adicione essa classe ao bloco de namespace ONNXWinUIExample , mas fora da definição de classe MainWindow.

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

Em seguida, adicione a classe auxiliar LabelMap que lista todos os rótulos de objeto nos quais o modelo foi treinado, em uma ordem específica, para que os rótulos sejam mapeados aos índices dos resultados retornados pelo modelo. A lista de rótulos é longa demais para ser apresentada na íntegra aqui. Você pode copiar a classe LabelMap completa de um arquivo de código de exemplo no repositório github ONNXRuntime e colá-la no bloco de namespace ONNXWinUIExample.

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

Executar o exemplo

Compile e execute o projeto. Clique no botão Selecionar foto e escolha um arquivo de imagem para analisar. Você pode examinar a definição da classe auxiliar LabelMap para ver o que o modelo consiga reconhecer e escolher uma imagem que possa ter resultados interessantes. Depois que o modelo for inicializado, na primeira vez que ele for executado e depois que o processamento dele for concluído, você verá uma lista de objetos que foram detectados na imagem e o valor de INT.CONFIANÇA de cada previsão.

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

Confira também