Compartir a través de


Introducción a los modelos ONNX en su aplicación WinUI con ONNX Runtime

En este artículo se explica cómo crear una aplicación WinUI 3 que use un modelo ONNX para clasificar objetos en una imagen y mostrar la confianza de cada clasificación. Para más información sobre el uso de modelos de IA y Machine Learning en la aplicación de Windows, consulte Introducción al uso de modelos de IA y Machine Learning en la aplicación de Windows.

¿Qué es ONNX Runtime?

ONNX Runtime es un acelerador de modelos de Machine Learning multiplataforma, con una interfaz flexible para integrar librerías específicas de hardware. ONNX Runtime puede usarse con modelos de PyTorch, Tensorflow/Keras, TFLite, scikit-learn y otros marcos. Para obtener más información, consulta el sitio web de ONNX Runtime en https://onnxruntime.ai/docs/.

Esta muestra usa el DirectML Execution Provider, que abstrae y ejecuta a través de las diferentes opciones de hardware en dispositivos Windows y soporta la ejecución a través de aceleradores locales, como la GPU y la NPU.

Requisitos previos

  • El dispositivo debe tener habilitado el modo de desarrollador. Para obtener más información, vea Habilitar el dispositivo para el desarrollo.
  • Visual Studio 2022 o posterior con la carga de trabajo de desarrollo de escritorio .NET.

Crear una nueva aplicación C# WinUI

En Visual Studio, cree un nuevo proyecto. En el cuadro de diálogo Crear un nuevo proyecto, establezca el filtro de idioma en "C#" y el filtro de tipo de proyecto en "winui" y, a continuación, seleccione la plantilla Aplicación vacía, empaquetada (WinUI3 en escritorio). Asigne al nuevo proyecto el nombre "ONNXWinUIExample".

Agregue referencias a paquetes Nuget

En Explorador de soluciones, haga clic con el botón derecho en Dependencias y seleccione Administrar paquetes NuGet.... En el gestor de paquetes NuGet, seleccione la pestaña Examinar. Busque los siguientes paquetes y, para cada uno de ellos, seleccione la última versión estable en el menú desplegable Versión y, a continuación, haga clic en Instalar.

Paquete Descripción
Microsoft.ML.OnnxRuntime.DirectML Proporciona API para ejecutar modelos ONNX en la GPU.
SixLabors.ImageSharp Proporciona utilidades de imagen para el procesamiento de imágenes para la entrada de modelos.
SharpDX.DXGI Proporciona API para acceder al dispositivo DirectX desde C#.

Agregue las siguientes directivas de uso a la parte superior de MainWindows.xaml.cs para acceder a las API de estas 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;

Agregue el modelo a su proyecto

En Explorador de soluciones, haga clic con el botón derecho en su proyecto y seleccione Agregar->Nueva carpeta. Nombre la nueva carpeta "modelo". Para este ejemplo, usaremos el modelo resnet50-v2-7.onnx de https://github.com/onnx/models. Vaya a la vista del repositorio del modelo en https://github.com/onnx/models/blob/main/validated/vision/classification/resnet/model/resnet50-v2-7.onnx. Haga clic en el botón *Descargar archivo raw. Copie este archivo en el directorio "model" que acaba de crear.

En el Explorador de soluciones, haga clic en el archivo del modelo y establezca Copiar en el directorio de salida en "Copiar si es nuevo".

Crear una interfaz de usuario sencilla

Para este ejemplo, vamos a crear una interfaz de usuario simple que incluye un Botón para permitir al usuario seleccionar una imagen para evaluar con el modelo, un Imagen de control para mostrar la imagen seleccionada, y un TextBlock para enumerar los objetos que el modelo detectado en la imagen y la confianza de cada clasificación de objetos.

En el archivo MainWindow.xaml , sustituya el elemento predeterminado StackPanel por el siguiente código 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>

Inicializar el modelo

En el archivo MainWindow.xaml.cs , dentro de la clase MainWindow, cree un método de ayuda llamado InitModel que inicializará el modelo. Este método usa las API de la librería SharpDX.DXGI para seleccionar el primer adaptador disponible. El adaptador seleccionado se establece en el objeto SessionOptions para el proveedor de ejecución DirectML en esta sesión. Por último , se inicializa una nueva InferenceSession, a la que se le pasa la ruta de acceso al archivo del modelo y las opciones de la sesión.

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

}

Cargar y analizar una imagen

Para simplificar, en este ejemplo todos los pasos para cargar y formatear la imagen, invocar el modelo y mostrar los resultados se colocarán dentro del controlador de clic del botón. Observe que agregamos la palabra clave async al manejador de clic de botón incluido en la plantilla predeterminada para que podamos ejecutar operaciones asíncronas en el controlador.

// MainWindow.xaml.cs

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

Usaremos un FileOpenPicker para permitir al usuario seleccionar una imagen de su ordenador para analizarla y mostrarla en la interfaz de usuario.

    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 continuación, tenemos que procesar la entrada para conseguir que en un formato que es compatible con el modelo. La biblioteca SixLabors.ImageSharp se usa para cargar la imagen en formato RGB de 24 bits y redimensionarla a 224x224 píxeles. A continuación, se normalizan los valores de los píxeles con una media de 255*[0,485, 0,456, 0,406] y una desviación estándar de 255*[0,229, 0,224, 0,225]. Los detalles del formato que espera el modelo se pueden encontrar en la página de github resnet model.

    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];
            }
        }
    });

A continuación, configuramos las entradas mediante la creación de un OrtValue de tipo Tensor en la parte superior de la matriz de datos de imagen administrada.

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

A continuación, si la sesión de inferencia no se inicializó todavía, llama al método de ayuda InitModel. A continuación, llame al método Run para ejecutar el modelo y obtener los resultados.

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

El modelo muestra los resultados como un búfer tensor nativo. El código siguiente convierte la salida en una matriz de valores flotantes. Se aplica una función softmax para que los valores se encuentren en el rango [0,1] y sumen 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);

El índice de cada valor de la matriz de salida corresponde a una etiqueta con la que se entrenó el modelo, y el valor de ese índice es la confianza del modelo en que la etiqueta representa un objeto detectado en la imagen de entrada. Seleccionamos los 10 resultados con el valor de confianza más alto. Este código usa algunos objetos ayudantes que definiremos en el siguiente paso.

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

La clase Predicción proporciona una forma sencilla de asociar una etiqueta de objeto con un valor de confianza. En MainPage.xaml.cs, agregue esta clase dentro del bloque de espacio de nombres ONNXWinUIExample, pero fuera de la definición de la clase MainWindow.

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

A continuación, agregue la clase de ayuda LabelMap que enumera todas las etiquetas de objetos con las que se entrenó el modelo, en un orden específico para que las etiquetas se correspondan con los índices de los resultados devueltos por el modelo. La lista de etiquetas es demasiado larga para presentarla completa aquí. Puede copiar la clase LabelMap completa de un archivo de código de ejemplo en el repositorio de github ONNXRuntime y pegarla en el bloque de espacio de nombres ONNXWinUIExample.

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

Ejecutar el ejemplo

Crear y ejecutar el proyecto. Haga clic en el botón Seleccionar foto y elija un archivo de imagen para analizar. Puede consultar la definición de la clase de ayuda LabelMap para ver las cosas que el modelo puede reconocer y elegir una imagen que pueda tener resultados interesantes. Después de que el modelo se inicialice, la primera vez que se ejecute, y después de que el procesamiento del modelo se haya completado , debería ver una lista de objetos que fueron detectados en la imagen, y el valor de confianza de cada predicción.

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

Consulte también