다음을 통해 공유


ONNX Runtime을 사용하여 당신의 WinUI 앱에서 ONNX 모델을 시작하세요.

이 문서에서는 ONNX 모델을 사용하여 이미지의 개체를 분류하고 각 분류의 신뢰도를 표시하는 WinUI 3 앱을 만드는 방법을 안내합니다. AI 및 기계 학습 모델을 Windows 앱에서 사용하는 방법에 대한 자세한 내용은 Windows 앱에서 AI 및 기계 학습 모델 사용을 시작하기를 참조하세요.

AI 기능을 활용할 때는 Windows에서 '책임 있는 생성 AI 애플리케이션 및 기능 개발 '을 검토하는 것이 좋습니다.

ONNX 런타임이란?

ONNX Runtime 하드웨어 관련 라이브러리를 통합하는 유연한 인터페이스를 갖춘 플랫폼 간 기계 학습 모델 가속기입니다. ONNX Runtime PyTorch, Tensorflow/Keras, TFLite, scikit-learn및 기타 프레임워크의 모델과 함께 사용할 수 있습니다. 자세한 내용은 ONNX Runtime 웹 사이트 https://onnxruntime.ai/docs/을 참조하세요.

이 샘플에서는 Windows 디바이스의 다양한 하드웨어 옵션을 추상화하고 실행하는 DirectML Execution Provider 사용하며 GPU 및 NPU와 같은 로컬 가속기에서 실행을 지원합니다.

필수 구성 요소

  • 디바이스에 개발자 모드가 설정되어 있어야 합니다. 자세한 내용은 개발 디바이스사용을 참조하세요.
  • .NET 데스크톱 개발 워크로드가 있는 Visual Studio 2022 이상.

새 C# WinUI 앱 만들기

Visual Studio에서 새 프로젝트를 만듭니다. 새 프로젝트 만들기 대화 상자에서 언어 필터를 "C#"으로 설정하고 프로젝트 형식 필터를 "winui"로 설정한 다음, 비어 있는 앱인 Packaged(데스크톱의 WinUI3) 템플릿을 선택합니다. 새 프로젝트의 이름을 "ONNXWinUIExample"로 지정합니다.

Nuget 패키지에 대한 참조 추가

솔루션 탐색기종속성 마우스 오른쪽 단추로 클릭하고 NuGet 패키지 관리를 선택합니다.. NuGet 패키지 관리자에서 찾아보기 탭을 선택합니다. 다음 패키지를 검색하고 각각에 대해 버전 드롭다운에서 안정적인 최신 버전을 선택한 다음 설치클릭합니다.

패키지 설명
Microsoft.ML.OnnxRuntime.DirectML GPU에서 ONNX 모델을 실행하기 위한 API를 제공합니다.
SixLabors.ImageSharp 모델 입력을 위해 이미지를 처리하기 위한 이미지 유틸리티를 제공합니다.
SharpDX.DXGI C#에서 DirectX 디바이스에 액세스하기 위한 API를 제공합니다.

지시문을 사용하여 맨 위에 다음 추가하여 이러한 라이브러리에서 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;

프로젝트에 모델 추가

솔루션 탐색기에서 프로젝트를 마우스 오른쪽 버튼으로 클릭하고 추가 ->새 폴더를 선택합니다. 새 폴더 이름을 "model"으로 지정합니다. 이 예제에서는 https://github.com/onnx/models 모델을 사용합니다. https://github.com/onnx/models/blob/main/validated/vision/classification/resnet/model/resnet50-v2-7.onnx모델의 리포지토리 보기로 이동합니다. *원시 파일 다운로드 단추를 클릭합니다. 이 파일을 방금 만든 "모델" 디렉터리에 복사합니다.

솔루션 탐색기에서 모델 파일을 클릭하고 출력 디렉터리 복사를 "최신인 경우 복사"로 설정합니다.

간단한 UI 만들기

이 예제에서는 사용자가 모델을 사용하여 평가할 이미지를 선택할 수 있도록 단추 포함하는 간단한 UI를 만들고, 선택한 이미지를 표시하는 이미지 컨트롤, 이미지에서 검색된 개체와 각 개체 분류의 신뢰도를 나열하는 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 형식으로 로드하고 이미지 크기를 224x224 픽셀로 조정하는 데 사용됩니다. 그런 다음 픽셀 값이 정규화되고 평균이 255*[0.485, 0.456, 0.406]이고 표준 편차가 255*[0.229, 0.224, 0.225]로 정규화됩니다. 모델이 예상하는 형식의 세부 정보는 github 페이지에서 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];
            }
        }
    });

다음으로, 관리되는 이미지 데이터 배열을 기반으로 한 Tensor 유형의 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);

모델은 결과를 네이티브 텐서 버퍼로 출력합니다. 다음 코드는 출력을 부동 소수 자릿수 배열로 변환합니다. 값이 [0,1] 범위에 있고 합계가 1이 되도록 softmax 함수가 적용됩니다.

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

예제 실행

프로젝트를 빌드하고 실행합니다. 사진 선택 단추를 클릭하고 분석할 이미지 파일을 선택합니다. 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

참고 항목