Поделиться через


Начало работы с моделями ONNX в приложении WinUI с помощью ONNX Runtime

В этой статье описывается создание приложения WinUI 3, использующего модель ONNX для классификации объектов на изображении и отображения достоверности каждой классификации. Дополнительные сведения об использовании моделей ИИ и машинного обучения в приложении Windows см. в статье Начало работы с моделями ИИ и машинного обучения в приложении Windows.

При использовании функций ИИ мы рекомендуем вам ознакомиться с материалом: Разработка ответственных генеративных приложений и функций ИИ в Windows.

Что такое среда выполнения ONNX

ONNX Runtime — это кроссплатформенный акселератор модели машинного обучения с гибким интерфейсом для интеграции библиотек, относящихся к оборудованию. ONNX Runtime можно использовать с моделями из PyTorch, Tensorflow/Keras, TFLite, scikit-learnи других платформ. Дополнительные сведения см. на веб-сайте ONNX Runtime по адресу https://onnxruntime.ai/docs/.

В этом примере используется DirectML Execution Provider, которое абстрагируется и работает на различных аппаратных вариантах устройств Windows и поддерживает выполнение на локальных ускорителях, таких как GPU и NPU.

Необходимые условия

  • Устройство должно быть включено в режиме разработчика. Для получения дополнительной информации см. Включение устройства для разработки.
  • Visual Studio 2022 или более поздней версии с компонентом разработки настольных приложений .NET.

Создание нового приложения WinUI C#

В Visual Studio создайте проект. В диалоговом окне Создание нового проекта задайте для фильтра языка значение C#, а для фильтра типа проекта — winui, после чего выберите шаблон Пустое приложение, Упаковка (WinUI3 для рабочего стола). Назовите новый проект ONNXWinUIExample.

Добавление ссылок на пакеты Nuget

В обозревателе решенийщелкните правой кнопкой мыши зависимости и выберите Управление пакетами NuGet.... В диспетчере пакетов NuGet выберите вкладку Обзор. Найдите следующие пакеты и для каждого из них выберите последнюю стабильную версию в раскрывающемся списке Версия, а затем нажмите Установить.

Пакет Описание
Microsoft.ML.OnnxRuntime.DirectML Предоставляет API для запуска моделей ONNX на GPU.
SixLabors.ImageSharp Предоставляет служебные программы изображений для обработки изображений для ввода модели.
SharpDX.DXGI Предоставляет API-интерфейсы для доступа к устройству DirectX из C#.

Добавьте следующие с помощью директив в верхнюю часть MainWindows.xaml.cs для доступа к API из этих библиотек.

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

Добавление модели в проект

В обозревателе решенийщелкните проект правой кнопкой мыши и выберите Add->New Folder. Назовите новую папку "model". В этом примере мы будем использовать модель resnet50-v2-7.onnx из https://github.com/onnx/models. Перейдите в представление репозитория для модели в https://github.com/onnx/models/blob/main/validated/vision/classification/resnet/model/resnet50-v2-7.onnx. Нажмите кнопку *Скачать необработанный файл. Скопируйте этот файл в только что созданный каталог model.

В обозревателе решений нажмите на файл модели и установите для Копировать в выходной каталог значение "Копировать при изменении".

Создание простого пользовательского интерфейса

В этом примере мы создадим простой пользовательский интерфейс, содержащий кнопку , чтобы пользователь мог выбрать изображение для оценки с помощью модели, элемент управления изображением для отображения выбранного изображения, а также TextBlock для перечисления объектов, обнаруженных в изображении, и уверенности в классификации каждого объекта.

В файле MainWindow.xaml замените элемент StackPanel по умолчанию следующим кодом 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>

Инициализация модели

В файле MainWindow.xaml.cs в классе MainWindow создайте вспомогательный метод под названием InitModel, который будет инициализировать модель. Этот метод использует API из библиотеки SharpDX.DXGI для выбора первого доступного адаптера. Выбранный адаптер задан в объекте SessionOptions для поставщика выполнения DirectML в этом сеансе. Наконец, новый InferenceSession инициализируется, передав путь к файлу модели и параметрам сеанса.

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

}

Загрузка и анализ образа

Для простоты в этом примере все шаги по загрузке и форматированию изображения, вызову модели и отображению результатов будут помещены в обработчик нажатия кнопки. Обратите внимание, что мы добавляем ключевое слово async в обработчик нажатия кнопки, включенный в шаблон по умолчанию, чтобы можно было выполнять асинхронные операции в обработчике.

// MainWindow.xaml.cs

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

Используйте FileOpenPicker, чтобы разрешить пользователю выбрать изображение на компьютере для анализа и отображения его в пользовательском интерфейсе.

    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;

Затем необходимо обработать входные данные, чтобы получить его в формате, поддерживаемом моделью. Библиотека SixLabors.ImageSharp используется для загрузки изображения в 24-разрядном формате RGB и изменения размера изображения до 224x224 пикселей. Затем значения пикселей нормализуются со средним значением 255*[0.485, 0.456, 0.406] и стандартным отклонением 255*[0.229, 0.224, 0.225]. Сведения о формате, который ожидает модель resnet, можно найти на странице GitHub .

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

Затем мы настроим входные данные, создав OrtValue типа Tensor на основе массива управляемых данных изображений.

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

Затем, если сеанс вывода еще не инициализирован, вызовите вспомогательный метод InitModel. Затем вызовите метод Run, чтобы запустить модель и получить результаты.

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

Модель выводит результаты в виде собственного буфера тензора. Следующий код преобразует выходные данные в массив с плавающей запятой. Функция softmax применяется таким образом, чтобы значения лежали в диапазоне [0,1] и в сумме равнялись 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);

Индекс каждого значения в выходном массиве сопоставляется с меткой, на которую была обучена модель, и значение по указанному индексу является уверенностью модели в том, что метка представляет объект, обнаруженный на входном изображении. Мы выбрали 10 результатов с самым высоким значением достоверности. Этот код использует некоторые вспомогательные объекты, которые мы определим на следующем шаге.

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

Объявление вспомогательных объектов

Класс прогнозирования предоставляет лёгкий способ связывания метки объекта со значением достоверности. В MainPage.xaml.csдобавьте этот класс в блок пространства имен ONNXWinUIExample, но вне определения класса MainWindow.

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

Затем добавьте вспомогательный класс LabelMap, в который перечислены все метки объектов, на которые была обучена модель, в определенном порядке, чтобы метки сопоставлялись с индексами результатов, возвращаемых моделью. Список меток слишком длинный, чтобы представить в полном объеме здесь. Вы можете скопировать полный класс LabelMap из примера файла кода в репозитории github ONNXRuntime и вставить его в блок пространства имен ONNXWinUIExample.

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

Запустите пример

Соберите проект и запустите его. Нажмите кнопку Выбрать фотографию и выберите файл изображения для анализа. Вы можете посмотреть на определение вспомогательного класса LabelMap, чтобы увидеть, что модель может распознавать и выбирать изображение, которое может иметь интересные результаты. После инициализации модели при первом запуске и после завершения обработки модели вы увидите список объектов, обнаруженных на изображении, и значение достоверности каждого прогноза.

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

См. также