ONNX Runtime を使用した WinUI アプリでの ONNX モデルの概要

この記事では、ONNX モデルを使用して画像内のオブジェクトを分類し、分類ごとの信頼度を表示する WinUI 3 アプリの作成手順について説明します。 Windows アプリで AI および機械学習モデルを使用する方法の詳細については、「Get started using AI and Machine Learning models in your Windows app (Windows アプリで AI および機械学習モデルの使用を開始する)」を参照してください。

ONNX Runtime とは

ONNX Runtime は、ハードウェア固有のライブラリを統合するための柔軟なインターフェイスを備えたクロスプラットフォームの機械学習モデル アクセラレータです。 ONNX Runtime は、PyTorch、Tensorflow/Keras、TFLite、scikit-learn、およびその他のフレームワークのモデルで使用できます。 詳細については、「https://onnxruntime.ai/docs/ での ONNX Runtime Web サイトを参照してください。

このサンプルでは、Windows デバイスのさまざまなハードウェア オプションを抽象化して実行し、GPU や NPU などのローカル アクセラレータ間での実行をサポートする DirectML Execution Provider を使用します。

前提条件

  • デバイスで開発者モードが有効になっている必要があります。 詳しくは、「デバイスを開発用に有効にする」をご覧ください。
  • .NET デスクトップ開発ワークロードを利用する Visual Studio 2022 以降

新しい C# WinUI アプリを作成する

Visual Studio で、新しいプロジェクトを作成します。 [新しいプロジェクトの作成] ダイアログで、言語フィルターを "C#" に設定し、プロジェクト タイプ フィルターを "winui" に設定してから、[Blank app, Packaged (WinUI3 in Desktop)] (空のアプリ、パッケージ (デスクトップの WinUI 3)) テンプレートを選択します。 新しいプロジェクトに "ONNXWinUIExample" という名前を付けます。

NuGet パッケージの参照を追加する

ソリューション エクスプローラーで、[依存関係] を右クリックし、[NuGet パッケージの管理] を選択します。NuGet パッケージ マネージャーで、[参照] タブを選択します。次のパッケージを検索し、それぞれに対して、[バージョン] ドロップダウンで最新の安定バージョンを選択し、[インストール] をクリックします。

Package 説明
Microsoft.ML.OnnxRuntime.DirectML GPU で ONNX モデルを実行するための API を提供します。
SixLabors.ImageSharp モデル入力の画像を処理するための画像ユーティリティを提供します。
SharpDX.DXGI C# から DirectX デバイスにアクセスするための API を提供します。

これらのライブラリから API にアクセスするには、次の using ディレクティブを MainWindows.xaml.cs の先頭に追加します。

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

モデルをプロジェクトに追加する

ソリューション エクスプローラーで、プロジェクトを右クリックして [追加]>[新しいフォルダー] を選択します。 新しいフォルダーに "model" という名前を付けます。 この例では、https://github.com/onnx/modelsresnet50-v2-7.onnx モデルを使用します。 https://github.com/onnx/models/blob/main/validated/vision/classification/resnet/model/resnet50-v2-7.onnx でモデルのリポジトリ ビューに移動します。 *[raw ファイルのダウンロード] ボタンをクリックします。 先ほど作成した "model" ディレクトリにこのファイルをコピーします。

ソリューション エクスプローラーで、モデル ファイルをクリックし、[出力ディレクトリにコピー] を [新しい場合はコピーする] に設定します。

簡単な UI を作成する

この例では、単純な UI を作成します。これには、ユーザーがモデルで評価する画像を選択できるようにする Button、選択した画像を表示する Image コントロール、画像でモデルにより検出されたオブジェクトと各オブジェクト分類の信頼度を一覧表示する 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 というヘルパー メソッドを作成します。 このメソッドでは、SharpDX.DXGI ライブラリの API を使用して、使用可能な最初のアダプターを選択します。 選択されたアダプターは、このセッションの DirectML 実行プロバイダーの SessionOptions オブジェクトに設定されます。 最後に、新しい 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 を使用して、ユーザーがコンピューターから画像を選択して分析し、UI に表示できるようにします。

    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 形式で画像を読み込み、画像のサイズを 224 x 224 ピクセルに変更します。 その後、ピクセル値を平均 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 を作成して、入力を設定します。

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

モデルは、ネイティブ テンソル バッファーとして結果を出力します。 次のコードでは、出力を float の配列に変換します。 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

ヘルパー オブジェクトを宣言する

Prediction クラスは、オブジェクト ラベルを信頼度値に関連付ける簡単な方法を提供するだけです。 MainPage.xaml.cs で、このクラスを ONNXWinUIExample 名前空間ブロック内、MainWindow クラス定義の外部に追加します。

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

次に、モデルがトレーニングされたすべてのオブジェクト ラベルを特定の順序で一覧表示する LabelMap ヘルパー クラスを追加し、ラベルがモデルによって返された結果のインデックスにマップされるようにします。 ラベルの一覧は長すぎるので、ここにすべては表示しません。 ONNXRuntime GitHub リポジトリのサンプル コード ファイルから完全な LabelMap クラスをコピーし、ONNXWinUIExample 名前空間ブロックに貼り付けることができます。

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

例を実行する

プロジェクトをビルドして実行します。 [Select photo] (写真の選択) ボタンをクリックし、分析する画像ファイルを選択します。 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

関連項目