Comece com os modelos ONNX na sua aplicação WinUI com ONNX Runtime
Este artigo orienta você na criação de um aplicativo WinUI 3 que usa um modelo ONNX para classificar objetos em uma imagem e exibir a confiança de cada classificação. Para obter mais informações sobre como usar modelos de IA e aprendizado de máquina em seu aplicativo do Windows, consulte Introdução ao uso de modelos de IA e Machine Learning em seu aplicativo do Windows.
Ao utilizar recursos de IA, recomendamos que você revise: Desenvolvendo aplicativos e recursos de IA generativa responsável no Windows.
O que é o tempo de execução do ONNX
ONNX Runtime é um acelerador de modelo de aprendizado de máquina multiplataforma, com uma interface flexível para integrar bibliotecas específicas de hardware. ONNX Runtime pode ser usado com modelos de PyTorch, Tensorflow/Keras, TFLite, scikit-learne outros frameworks. Para obter mais informações, consulte o site da ONNX Runtime em https://onnxruntime.ai/docs/.
Este exemplo usa o DirectML Execution Provider que abstrai e executa as diferentes opções de hardware em dispositivos Windows e oferece suporte à execução em aceleradores locais, como GPU e NPU.
Pré-requisitos
- Seu dispositivo deve ter o modo de desenvolvedor ativado. Para obter mais informações, consulte Habilitar seu dispositivo para desenvolvimento.
- Visual Studio 2022 ou posterior com a carga de trabalho de desenvolvimento de desktop .NET.
Criar um novo aplicativo WinUI em C#
No Visual Studio, crie um novo projeto. Na caixa de diálogo Criar um novo projeto, defina o filtro de idioma como "C#" e o filtro de tipo de projeto como "winui", depois selecione o modelo Aplicação em Branco, Empacotada (WinUI3 para Desktop). Nomeie o novo projeto como "ONNXWinUIExample".
Adicionar referências a pacotes Nuget
No Explorador de Soluções, clique com o botão direito em Dependências e selecione Gerir pacotes NuGet.... No gestor de pacotes NuGet, selecione o separador Procurar. Pesquise os seguintes pacotes e, para cada um, selecione a versão estável mais recente na lista suspensa de Versão e clique em Instalar.
Embalagem | Descrição |
---|---|
Microsoft.ML.OnnxRuntime.DirectML | Fornece APIs para executar modelos ONNX na GPU. |
SixLabors.ImageSharp | Fornece utilitários de imagem para o processamento de imagens como entrada para modelos. |
SharpDX.DXGI | Fornece APIs para acessar o dispositivo DirectX a partir de C#. |
Adicione os seguintes usando diretivas no topo de MainWindows.xaml.cs
para poder aceder às APIs destas 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 seu projeto
No Gerenciador de Soluções, clique com o botão direito do mouse em seu projeto e selecione Adicionar->Nova Pasta. Nomeie a nova pasta como "modelo". Para este exemplo, usaremos o modelo resnet50-v2-7.onnx da https://github.com/onnx/models. Vá para o 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 *Transferir arquivo bruto. Copie este arquivo para o diretório "modelo" que você acabou de criar.
No Gerenciador de Soluções, clique no arquivo de modelo e defina Copiar para o Diretório de Saída como "Copiar se for mais recente".
Criar uma interface do usuário simples
Para este exemplo, criaremos uma interface do utilizador simples que inclui um Button para permitir que o utilizador selecione uma imagem para avaliar com o modelo, um controlo Image para exibir a imagem selecionada e um TextBlock para listar os objetos que o modelo detetou na imagem e a confiança associada a cada classificação de objeto.
No arquivo
<!--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
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 dentro do manipulador de clique de botão. Observe que adicionamos a palavra-chave async ao manipulador de clique de 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 colocá-la em um formato que seja suportado pelo modelo. A biblioteca
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 InitModel método auxiliar. Em seguida, chame o método Run 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 produz os resultados como um buffer tensor nativo. O código a seguir converte a saída em uma matriz de floats. É aplicada uma função softmax de modo a que os valores se situem 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 mapeia para um rótulo no qual o modelo foi treinado, e o valor nesse índice é a confiança do modelo de que o rótulo representa um objeto detetado na imagem de entrada. Escolhemos os 10 resultados com o maior valor de confiança. Este 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 Prediction fornece uma maneira simples de associar um rótulo de objeto a um valor de confiança. No
internal class Prediction
{
public object Label { get; set; }
public float Confidence { get; set; }
}
Em seguida, adicione o LabelMap classe auxiliar 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 para os índices dos resultados retornados pelo modelo. A lista de rótulos é demasiado longa para ser apresentada na íntegra aqui. Você pode copiar a classe completa LabelMap de um arquivo de código de exemplo no repositório GitHub ONNXRuntime e colá-la no bloco de espaço de nomes ONNXWinUIExample.
public class LabelMap
{
public static readonly string[] Labels = new[] {
"tench",
"goldfish",
"great white shark",
...
"hen-of-the-woods",
"bolete",
"ear",
"toilet paper"};
Execute o exemplo
Crie e execute o projeto. Clique no botão Selecionar foto e escolha um arquivo de imagem para analisar. Você pode olhar para a definição da classe auxiliar LabelMap para ver as coisas que o modelo pode 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 do modelo for concluído, você verá uma lista de objetos que foram detetados na imagem e o valor de 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