다음을 통해 공유


자습서: ML.NET ONNX를 사용하여 개체 검색

ML.NET에서 미리 학습된 ONNX 모델을 사용하여 이미지에서 개체를 검색하는 방법을 알아봅니다.

개체 검색 모델을 처음부터 학습시키려면 수백만 개의 매개 변수, 다량의 레이블 지정 학습 데이터 및 많은 양의 컴퓨팅 리소스(수백 시간의 GPU 시간)를 설정해야 합니다. 미리 학습된 모델을 사용하면 학습 프로세스를 바로 수행할 수 있습니다.

이 자습서에서는 다음을 하는 방법을 알아볼 수 있습니다.

  • 문제 이해
  • ONNX의 개념과 ML.NET에서 작동하는 방식에 대해 알아보기
  • 모델 이해
  • 미리 학습된 모델 다시 사용
  • 로드된 모델을 사용하여 개체 검색

필수 구성 요소

ONNX 개체 검색 샘플 개요

이 샘플에서는 미리 학습된 딥 러닝 ONNX 모델을 사용하여 이미지 내에서 개체를 검색하는 .NET core 콘솔 애플리케이션을 만듭니다. 이 샘플의 코드는 GitHub의 dotnet/machinelearning-samples repository에서 찾을 수 있습니다.

개체 검색의 개념

개체 검색은 컴퓨터 비전 문제입니다. 개체 검색은 이미지 분류와 밀접하게 관련된 반면, 보다 세부적인 규모에서 이미지 분류를 수행합니다. 개체 검색에서는 이미지에서 엔터티 찾기 분류를 둘 다 수행합니다. 개체 감지 모델은 일반적으로 딥 러닝 및 신경망을 사용하여 학습됩니다. 자세한 내용은 딥 러닝 대 기계 학습을 참조하세요.

이미지에 서로 다른 유형의 개체가 여러 개 포함되어 있는 경우 개체 검색을 사용합니다.

Screenshots showing Image Classification versus Object Classification.

개체 검색의 몇 가지 사용 사례는 다음과 같습니다.

  • 자동 주행 자동차
  • Robotics
  • 얼굴 감지
  • 작업 영역 안전성
  • 개체 수 계산
  • 활동 인식

딥 러닝 모델 선택

딥 러닝은 기계 학습의 하위 집합입니다. 딥 러닝 모델을 학습시키려면 많은 양의 데이터가 필요합니다. 데이터의 패턴은 일련의 계층으로 표시됩니다. 데이터의 관계는 가중치를 포함하는 계층 간의 연결로 인코딩됩니다. 가중치가 높을수록 관계가 강해집니다. 이러한 일련의 계층 및 연결을 통칭하여 인공신경망이라고 합니다. 네트워크에 있는 계층이 많을수록 “심층 강화”되어, 심층 신경망으로 만듭니다.

신경망의 유형은 여러 가지가 있습니다. 가장 일반적으로 MLP(Multi-Layered Perceptron), CNN(Convolutional Neural Network) 및 RNN(Recurrent Neural Network)이 있습니다. 가장 기본적인 유형은 일련의 입력을 출력 집합에 매핑하는 MLP입니다. 이 신경망은 데이터에 공간 또는 시간 구성 요소가 없는 경우에 유용합니다. CNN은 나선형 계층을 사용하여 데이터에 포함된 공간 정보를 처리합니다. CNN의 좋은 사용 사례는 이미지의 영역에 기능이 있는지 검색하는 이미지 처리입니다(예: 이미지의 중심에 코가 있나요?). 마지막으로 RNN은 상태 또는 메모리의 지속성을 입력으로 사용할 수 있도록 허용합니다. RNN은 순차 순서와 이벤트 컨텍스트가 중요한 시계열 분석에 사용됩니다.

모델 이해

개체 검색은 이미지 처리 작업입니다. 따라서 이 문제를 해결하도록 학습된 대부분의 딥 러닝 모델은 CNN입니다. 이 자습서에서 사용된 모델은 Redmon과 Farhadi의 "YOLO9000: 더 나은, 더 빠름, 더 강해"라는 논문에 설명된 YOLOv2 모델의 더 컴팩트한 버전인 Tiny YOLOv2 모델입니다. Tiny YOLOv2는 Pascal VOC 데이터 세트에서 학습되며 20개의 서로 다른 개체 클래스를 예측할 수 있는 15개의 계층으로 구성됩니다. Tiny YOLOv2는 소스 YOLOv2 모델의 축소된 버전이며, 속도와 정확도가 서로 절충됩니다. Netron과 같은 도구를 사용하여 모델을 구성하는 다양한 계층을 시각화할 수 있습니다. 모델을 검사하면 신경망을 구성하는 모든 계층 간에 연결 매핑이 일시 중단됩니다. 여기서 각 계층에는 각 입력/출력의 차원과 함께 계층 이름이 포함됩니다. 모델의 입력 및 출력을 설명하는 데 사용하는 데이터 구조를 텐서(tensor)라고 합니다. 텐서는 N 차원에 데이터를 저장하는 컨테이너로 간주할 수 있습니다. Tiny YOLOv2의 경우 입력 계층의 이름은 image이고 3 x 416 x 416 차원의 텐서가 있어야 합니다. 출력 계층의 이름은 grid이고 125 x 13 x 13 차원의 출력 텐서를 생성합니다.

Input layer being split into hidden layers, then output layer

YOLO 모델에서는 3(RGB) x 416px x 416px 이미지를 사용합니다. 이 모델에서는 출력을 생성하기 위해 이 입력을 받아 다양한 계층에 전달합니다. 출력은 입력 이미지를 13 x 13 그리드로 나누며, 그리드의 각 셀은 125 값으로 구성됩니다.

ONNX 모델의 개념

ONNX(Open Neural Network Exchange)는 AI 모델의 오픈 소스 형식입니다. ONNX에서는 프레임워크 간의 상호 운용성을 지원합니다. 즉, PyTorch와 같이 널리 사용되는 여러 기계 학습 프레임워크 중 하나에서 모델을 학습시키고, ONNX 형식으로 변환하며, ML.NET과 같은 다른 프레임워크에서 ONNX 모델을 사용할 수 있습니다. 자세히 알아보려면 ONNX 웹 사이트를 방문하세요.

Diagram of ONNX supported formats being used.

미리 학습된 Tiny YOLOv2 모델은 계층과 해당 계층의 학습된 패턴을 직렬화하여 표시하는 ONNX 형식으로 저장됩니다. ML.NET에서 ONNX와의 상호 운용성은 ImageAnalyticsOnnxTransformer NuGet 패키지를 통해 달성합니다. ImageAnalytics 패키지에는 이미지를 받아 예측이나 학습 파이프라인의 입력으로 사용할 수 있는 숫자 값으로 인코딩하는 일련의 변환이 포함되어 있습니다. OnnxTransformer 패키지에서는 ONNX Runtime을 사용하여 ONNX 모델을 로드하고 제공된 입력을 기반으로 예측하는 데 사용합니다.

Data flow of ONNX file into the ONNX Runtime.

.NET 콘솔 프로젝트 설정

이제 ONNX의 개념과 Tiny YOLOv2의 작동 원리를 대략적으로 파악했으므로 이제 애플리케이션을 빌드하겠습니다.

콘솔 애플리케이션 만들기

  1. “ObjectDetection”이라는 C# 콘솔 애플리케이션을 만듭니다. 다음 단추를 클릭합니다.

  2. 사용할 프레임워크로 .NET 6을 선택합니다. 만들기 단추를 클릭합니다.

  3. Microsoft.ML NuGet 패키지를 설치합니다.

    참고 항목

    이 샘플에서는 별도로 지정하지 않은 한 언급된 NuGet 패키지의 최신 안정화 버전을 사용합니다.

    • 솔루션 탐색기에서 프로젝트를 마우스 오른쪽 단추로 클릭하고 NuGet 패키지 관리를 선택합니다.
    • 패키지 소스로 “nuget.org”를 선택하고, [찾아보기] 탭을 선택하고, Microsoft.ML을 검색합니다.
    • 설치 단추를 선택합니다.
    • 변경 내용 미리 보기 대화 상자에서 확인 단추를 선택한 다음, 나열된 패키지의 사용 조건에 동의하는 경우 라이선스 승인 대화 상자에서 동의함 단추를 선택합니다.
    • Microsoft.Windows.Compatibility, Microsoft.ML.ImageAnalytics, Microsoft.ML.OnnxTransformerMicrosoft.ML.OnnxRuntime에 대해 다음 단계를 반복합니다.

데이터 및 미리 학습된 모델 준비

  1. 프로젝트 자산 디렉터리 zip 파일을 다운로드하고 압축을 풉니다.

  2. assets 디렉터리를 ObjectDetection 프로젝트 디렉터리에 복사합니다. 이 디렉터리 및 해당 하위 디렉터리에는 이 자습서에 필요한 이미지 파일이 포함되어 있습니다(다음 단계에서 다운로드 및 추가하는 Tiny YOLOv2 모델 제외).

  3. ONNX Model Zoo에서 Tiny YOLOv2 모델을 다운로드합니다.

  4. model.onnx 파일을 ObjectDetection 프로젝트 assets\Model 디렉터리에 복사하고 이름을 TinyYolo2_model.onnx로 바꿉니다. 이 디렉터리에는 이 자습서에 필요한 모델이 포함되어 있습니다.

  5. 솔루션 탐색기에서 자산 디렉터리 및 하위 디렉터리의 각 파일을 마우스 오른쪽 단추로 클릭하고 속성을 선택합니다. 고급 아래에서 출력 디렉터리에 복사 값을 변경된 내용만 복사로 변경합니다.

클래스 만들기 및 경로 정의

Program.cs 파일을 열고 다음 추가 using 문을 파일의 맨 위에 추가합니다.

using System.Drawing;
using System.Drawing.Drawing2D;
using ObjectDetection.YoloParser;
using ObjectDetection.DataStructures;
using ObjectDetection;
using Microsoft.ML;

다음으로 다양한 자산의 경로를 정의합니다.

  1. 먼저 Program.cs 파일의 맨 아래에 GetAbsolutePath 메서드를 만듭니다.

    string GetAbsolutePath(string relativePath)
    {
        FileInfo _dataRoot = new FileInfo(typeof(Program).Assembly.Location);
        string assemblyFolderPath = _dataRoot.Directory.FullName;
    
        string fullPath = Path.Combine(assemblyFolderPath, relativePath);
    
        return fullPath;
    }
    
  2. 그런 다음 using 문 아래에 자산의 위치를 저장할 필드를 만듭니다.

    var assetsRelativePath = @"../../../assets";
    string assetsPath = GetAbsolutePath(assetsRelativePath);
    var modelFilePath = Path.Combine(assetsPath, "Model", "TinyYolo2_model.onnx");
    var imagesFolder = Path.Combine(assetsPath, "images");
    var outputFolder = Path.Combine(assetsPath, "images", "output");
    

프로젝트에 새 디렉터리를 추가하여 입력 데이터 및 예측 클래스를 저장합니다.

솔루션 탐색기에서 프로젝트를 마우스 오른쪽 단추로 클릭하고 추가>새 폴더를 선택합니다. 새 폴더가 솔루션 탐색기에 표시되면 이름을 “DataStructures”로 표시합니다.

새로 만든 DataStructures 디렉터리에 입력 데이터 클래스를 만듭니다.

  1. 솔루션 탐색기에서 DataStructures 디렉터리를 마우스 오른쪽 단추로 클릭한 다음 추가>새 항목을 선택합니다.

  2. 새 항목 추가 대화 상자에서 클래스를 선택하고 이름 필드를 ImageNetData.cs로 변경합니다. 그런 다음, 추가 단추를 선택합니다.

    ImageNetData.cs 파일이 코드 편집기에서 열립니다. 다음 using 문을 ImageNetData.cs의 맨 위에 추가합니다.

    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using Microsoft.ML.Data;
    

    기존 클래스 정의를 제거하고 ImageNetData 클래스에 대한 다음 코드를 ImageNetData.cs 파일에 추가합니다.

    public class ImageNetData
    {
        [LoadColumn(0)]
        public string ImagePath;
    
        [LoadColumn(1)]
        public string Label;
    
        public static IEnumerable<ImageNetData> ReadFromFile(string imageFolder)
        {
            return Directory
                .GetFiles(imageFolder)
                .Where(filePath => Path.GetExtension(filePath) != ".md")
                .Select(filePath => new ImageNetData { ImagePath = filePath, Label = Path.GetFileName(filePath) });
        }
    }
    

    ImageNetData는 입력 이미지 데이터 클래스이며 다음 String 필드를 포함합니다.

    • ImagePath에는 이미지가 저장되는 경로가 포함되어 있습니다.
    • Label에는 파일 이름이 포함되어 있습니다.

    또한 ImageNetData에는 지정된 imageFolder 경로에 저장된 여러 이미지 파일을 로드하여 ImageNetData 개체의 컬렉션으로 반환하는 ReadFromFile 메서드가 포함되어 있습니다.

DataStructures 디렉터리에 예측 클래스를 만듭니다.

  1. 솔루션 탐색기에서 DataStructures 디렉터리를 마우스 오른쪽 단추로 클릭한 다음 추가>새 항목을 선택합니다.

  2. 새 항목 추가 대화 상자에서 클래스를 선택하고 이름 필드를 ImageNetPrediction.cs로 변경합니다. 그런 다음, 추가 단추를 선택합니다.

    ImageNetPrediction.cs 파일이 코드 편집기에서 열립니다. 다음 using 문을 ImageNetPrediction.cs의 맨 위에 추가합니다.

    using Microsoft.ML.Data;
    

    기존 클래스 정의를 제거하고 ImageNetPrediction 클래스에 대한 다음 코드를 ImageNetPrediction.cs 파일에 추가합니다.

    public class ImageNetPrediction
    {
        [ColumnName("grid")]
        public float[] PredictedLabels;
    }
    

    ImageNetPrediction은 예측 데이터 클래스이며 다음 float[] 필드를 포함합니다.

    • PredictedLabels에는 이미지에서 검색된 각 경계 상자의 크기, 개체 점수 및 클래스 확률이 포함되어 있습니다.

변수 초기화

MLContext 클래스는 모든 ML.NET 작업의 시작점이며, mlContext를 초기화하면 모델 생성 워크플로 개체 간에 공유할 수 있는 새 ML.NET 환경이 생성됩니다. 개념적으로 Entity Framework의 DBContext와 유사합니다.

outputFolder 필드 아래에 다음 줄을 추가하여 mlContext 변수를 새로운 MLContext 인스턴스로 초기화합니다.

MLContext mlContext = new MLContext();

파서를 만들어 모델 출력 후처리

모델은 이미지를 13 x 13 그리드로 나눕니다. 여기서 각 그리드 셀은 32px x 32px입니다. 각 그리드 셀에는 5개의 잠재적 개체 경계 상자가 포함되어 있습니다. 경계 상자에는 25개의 요소가 있습니다.

Grid sample on the left, and Bounding Box sample on the right

  • x 연결된 그리드 셀을 기준으로 하는 경계 상자 중심의 x 위치입니다.
  • y 연결된 그리드 셀을 기준으로 하는 경계 상자 중심의 y 위치입니다.
  • w 경계 상자의 너비입니다.
  • h 경계 상자의 높이입니다.
  • o 경계 상자 내에 개체가 있는 신뢰도 값입니다(개체성 점수라고도 함).
  • p1-p20 모델에서 예측하는 20개 클래스 각각의 클래스 확률입니다.

각각 5개의 경계 상자를 설명하는 총 25개의 요소는 각 그리드 셀에 포함된 125개의 요소를 구성합니다.

미리 학습된 ONNX 모델에서 생성한 출력은 125 x 13 x 13 크기의 텐서 요소를 나타내는 21125 길이의 부동 소수점 배열입니다. 모델에서 생성한 예측을 텐서로 변환하려면 몇 가지 후처리 작업이 필요합니다. 이렇게 하려면 출력을 구문 분석하는 데 도움이 되는 클래스 집합을 만드세요.

프로젝트에 새 디렉터리를 추가하여 파서 클래스 집합을 구성합니다.

  1. 솔루션 탐색기에서 프로젝트를 마우스 오른쪽 단추로 클릭하고 추가>새 폴더를 선택합니다. 새 폴더가 솔루션 탐색기에 표시되면 이름을 “YoloParser”로 표시합니다.

경계 상자 및 차원 만들기

모델의 데이터 출력에는 이미지 내 개체의 경계 상자에 대한 좌표와 크기가 포함됩니다. 차원의 기본 클래스를 만듭니다.

  1. 솔루션 탐색기에서 YoloParser 디렉터리를 마우스 오른쪽 단추로 클릭한 다음 추가>새 항목을 선택합니다.

  2. 새 항목 추가 대화 상자에서 클래스를 선택하고 이름 필드를 DimensionsBase.cs로 변경합니다. 그런 다음, 추가 단추를 선택합니다.

    DimensionsBase.cs 파일이 코드 편집기에서 열립니다. 모든 using 문과 기존 클래스 정의를 제거합니다.

    DimensionsBase 클래스의 다음 코드를 DimensionsBase.cs 파일에 추가합니다.

    public class DimensionsBase
    {
        public float X { get; set; }
        public float Y { get; set; }
        public float Height { get; set; }
        public float Width { get; set; }
    }
    

    DimensionsBase에는 다음과 같은 float 속성이 있습니다.

    • X에는 x축의 개체 위치가 포함되어 있습니다.
    • Y에는 y축의 개체 위치가 포함되어 있습니다.
    • Height에는 개체의 높이가 포함되어 있습니다.
    • Width에는 개체의 너비가 포함되어 있습니다.

다음으로 경계 상자의 클래스를 만듭니다.

  1. 솔루션 탐색기에서 YoloParser 디렉터리를 마우스 오른쪽 단추로 클릭한 다음 추가>새 항목을 선택합니다.

  2. 새 항목 추가 대화 상자에서 클래스를 선택하고 이름 필드를 YoloBoundingBox.cs로 변경합니다. 그런 다음, 추가 단추를 선택합니다.

    YoloBoundingBox.cs 파일이 코드 편집기에서 열립니다. 다음 using 문을 YoloBoundingBox.cs의 맨 위에 추가합니다.

    using System.Drawing;
    

    각 경계 상자의 크기를 포함하도록 기존 클래스 정의 바로 위에 DimensionsBase 클래스에서 상속하는 BoundingBoxDimensions라는 새 클래스 정의를 추가합니다.

    public class BoundingBoxDimensions : DimensionsBase { }
    

    기존 YoloBoundingBox 클래스 정의를 제거하고 YoloBoundingBox 클래스의 다음 코드를 YoloBoundingBox.cs 파일에 추가합니다.

    public class YoloBoundingBox
    {
        public BoundingBoxDimensions Dimensions { get; set; }
    
        public string Label { get; set; }
    
        public float Confidence { get; set; }
    
        public RectangleF Rect
        {
            get { return new RectangleF(Dimensions.X, Dimensions.Y, Dimensions.Width, Dimensions.Height); }
        }
    
        public Color BoxColor { get; set; }
    }
    

    YoloBoundingBox에는 다음 속성이 있습니다.

    • Dimensions에는 경계 상자의 크기가 포함되어 있습니다.
    • Label에는 경계 상자 내에서 검색된 개체의 클래스가 포함되어 있습니다.
    • Confidence에는 클래스의 신뢰도가 포함되어 있습니다.
    • Rect에는 경계 상자 크기의 사각형 표현이 포함되어 있습니다.
    • BoxColor에는 이미지를 그리는 데 사용되는 각 클래스와 관련된 색이 포함되어 있습니다.

파서 만들기

이제 차원 및 경계 상자의 클래스가 생성되므로, 파서를 만들 때입니다.

  1. 솔루션 탐색기에서 YoloParser 디렉터리를 마우스 오른쪽 단추로 클릭한 다음 추가>새 항목을 선택합니다.

  2. 새 항목 추가 대화 상자에서 클래스를 선택하고 이름 필드를 YoloOutputParser.cs로 변경합니다. 그런 다음, 추가 단추를 선택합니다.

    YoloOutputParser.cs 파일이 코드 편집기에서 열립니다. YoloOutputParser.cs의 맨 위에 다음 using 문을 추가합니다.

    using System;
    using System.Collections.Generic;
    using System.Drawing;
    using System.Linq;
    

    기존 YoloOutputParser 클래스 정의 내에서 이미지에 있는 각 셀의 크기를 포함하는 중첩된 클래스를 추가합니다. YoloOutputParser 클래스 정의의 맨 위에 있는 DimensionsBase 클래스에서 상속하는 CellDimensions 클래스의 다음 코드를 추가합니다.

    class CellDimensions : DimensionsBase { }
    
  3. YoloOutputParser 클래스 정의 내에 다음 상수 및 필드를 추가합니다.

    public const int ROW_COUNT = 13;
    public const int COL_COUNT = 13;
    public const int CHANNEL_COUNT = 125;
    public const int BOXES_PER_CELL = 5;
    public const int BOX_INFO_FEATURE_COUNT = 5;
    public const int CLASS_COUNT = 20;
    public const float CELL_WIDTH = 32;
    public const float CELL_HEIGHT = 32;
    
    private int channelStride = ROW_COUNT * COL_COUNT;
    
    • ROW_COUNT는 그리드에서 이미지가 나뉘는 행의 수입니다.
    • COL_COUNT는 그리드에서 이미지가 나뉘는 열의 수입니다.
    • CHANNEL_COUNT는 그리드의 한 셀에 포함된 총 값입니다.
    • BOXES_PER_CELL은 셀의 경계 상자 수이며,
    • BOX_INFO_FEATURE_COUNT는 상자에 포함된 기능 수입니다(x, y, 높이, 너비, 신뢰도).
    • CLASS_COUNT는 각 경계 상자에 포함된 클래스 예측 수입니다.
    • CELL_WIDTH는 이미지 그리드에 있는 한 셀의 너비입니다.
    • CELL_HEIGHT는 이미지 그리드에 있는 한 셀의 높이입니다.
    • channelStride는 그리드에서 현재 셀의 시작 위치입니다.

    모델은 점수라고도 하는 예측을 생성할 때 416px x 416px 입력 이미지를 13 x 13 크기의 셀 그리드로 나눕니다. 포함된 각 셀은 32px x 32px입니다. 각 셀에는 각각 5개의 기능(x, y, 너비, 높이, 신뢰도)을 포함하는 5개의 경계 상자가 있습니다. 또한 각 경계 상자에는 각 클래스의 확률이 포함되며, 이 경우에는 20입니다. 따라서 각 셀에는 125개의 정보(5개의 기능 + 20개의 클래스 확률)가 포함됩니다.

channelStride 아래에 5개의 경계 상자 모두에 대한 고정 목록을 만듭니다.

private float[] anchors = new float[]
{
    1.08F, 1.19F, 3.42F, 4.41F, 6.63F, 11.38F, 9.42F, 5.11F, 16.62F, 10.52F
};

앵커는 경계 상자의 미리 정의된 높이 및 너비 비율입니다. 모델에서 검색한 대부분의 개체 또는 클래스는 비율이 서로 비슷합니다. 따라서 경계 상자를 만들 때 유용합니다. 경계 상자를 예측하는 대신 미리 정의된 차원의 오프셋을 계산하므로 경계 상자를 예측하는 데 필요한 계산이 줄어듭니다. 일반적으로 이러한 고정 비율은 사용된 데이터 세트를 기반으로 계산합니다. 이 경우 데이터 세트가 알려져 있고 값이 미리 컴퓨팅되었기 때문에 앵커는 하드 코딩할 수 있습니다.

그런 다음 모델에서 예측할 레이블 또는 클래스를 정의합니다. 이 모델은 원래 YOLOv2 모델에서 예측한 총 클래스 수의 하위 집합인 20개의 클래스를 예측합니다.

anchors 아래에 레이블 목록을 추가합니다.

private string[] labels = new string[]
{
    "aeroplane", "bicycle", "bird", "boat", "bottle",
    "bus", "car", "cat", "chair", "cow",
    "diningtable", "dog", "horse", "motorbike", "person",
    "pottedplant", "sheep", "sofa", "train", "tvmonitor"
};

각 클래스와 관련된 색이 있습니다. labels 아래에 클래스 색을 할당합니다.

private static Color[] classColors = new Color[]
{
    Color.Khaki,
    Color.Fuchsia,
    Color.Silver,
    Color.RoyalBlue,
    Color.Green,
    Color.DarkOrange,
    Color.Purple,
    Color.Gold,
    Color.Red,
    Color.Aquamarine,
    Color.Lime,
    Color.AliceBlue,
    Color.Sienna,
    Color.Orchid,
    Color.Tan,
    Color.LightPink,
    Color.Yellow,
    Color.HotPink,
    Color.OliveDrab,
    Color.SandyBrown,
    Color.DarkTurquoise
};

도우미 함수 만들기

후처리 단계와 관련된 일련의 단계가 있습니다. 이를 지원하려면 몇 가지 도우미 메서드를 사용할 수 있습니다.

파서에서 사용하는 도우미 메서드는 다음과 같습니다.

  • Sigmoid는 0에서 1 사이의 숫자를 출력하는 sigmoid 함수를 적용합니다.
  • Softmax는 입력 벡터를 확률 분포로 정규화합니다.
  • GetOffset은 1차원 모델 출력의 요소를 125 x 13 x 13 텐서의 해당 위치에 매핑합니다.
  • ExtractBoundingBoxes는 모델 출력의 GetOffset 메서드를 사용하여 경계 상자 차원을 추출합니다.
  • GetConfidence는 모델에서 개체를 발견한 확률을 나타내는 신뢰도 값을 추출하고 Sigmoid 함수를 사용하여 백분율로 전환합니다.
  • MapBoundingBoxToCell은 경계 상자 차원을 사용하여 이미지 내의 해당 셀에 매핑합니다.
  • ExtractClassesGetOffset 메서드를 사용하여 모델 출력에서 경계 상자의 클래스 예측을 추출한 다음 Softmax 메서드를 사용하여 확률 분포로 전환합니다.
  • GetTopResult는 확률이 가장 높은 예측 클래스 목록에서 클래스를 선택합니다.
  • IntersectionOverUnion은 낮은 확률의 겹치는 경계 상자를 필터링합니다.

모든 도우미 메서드 코드를 classColors 목록 아래 추가합니다.

private float Sigmoid(float value)
{
    var k = (float)Math.Exp(value);
    return k / (1.0f + k);
}

private float[] Softmax(float[] values)
{
    var maxVal = values.Max();
    var exp = values.Select(v => Math.Exp(v - maxVal));
    var sumExp = exp.Sum();

    return exp.Select(v => (float)(v / sumExp)).ToArray();
}

private int GetOffset(int x, int y, int channel)
{
    // YOLO outputs a tensor that has a shape of 125x13x13, which 
    // WinML flattens into a 1D array.  To access a specific channel 
    // for a given (x,y) cell position, we need to calculate an offset
    // into the array
    return (channel * this.channelStride) + (y * COL_COUNT) + x;
}

private BoundingBoxDimensions ExtractBoundingBoxDimensions(float[] modelOutput, int x, int y, int channel)
{
    return new BoundingBoxDimensions
    {
        X = modelOutput[GetOffset(x, y, channel)],
        Y = modelOutput[GetOffset(x, y, channel + 1)],
        Width = modelOutput[GetOffset(x, y, channel + 2)],
        Height = modelOutput[GetOffset(x, y, channel + 3)]
    };
}

private float GetConfidence(float[] modelOutput, int x, int y, int channel)
{
    return Sigmoid(modelOutput[GetOffset(x, y, channel + 4)]);
}

private CellDimensions MapBoundingBoxToCell(int x, int y, int box, BoundingBoxDimensions boxDimensions)
{
    return new CellDimensions
    {
        X = ((float)x + Sigmoid(boxDimensions.X)) * CELL_WIDTH,
        Y = ((float)y + Sigmoid(boxDimensions.Y)) * CELL_HEIGHT,
        Width = (float)Math.Exp(boxDimensions.Width) * CELL_WIDTH * anchors[box * 2],
        Height = (float)Math.Exp(boxDimensions.Height) * CELL_HEIGHT * anchors[box * 2 + 1],
    };
}

public float[] ExtractClasses(float[] modelOutput, int x, int y, int channel)
{
    float[] predictedClasses = new float[CLASS_COUNT];
    int predictedClassOffset = channel + BOX_INFO_FEATURE_COUNT;
    for (int predictedClass = 0; predictedClass < CLASS_COUNT; predictedClass++)
    {
        predictedClasses[predictedClass] = modelOutput[GetOffset(x, y, predictedClass + predictedClassOffset)];
    }
    return Softmax(predictedClasses);
}

private ValueTuple<int, float> GetTopResult(float[] predictedClasses)
{
    return predictedClasses
        .Select((predictedClass, index) => (Index: index, Value: predictedClass))
        .OrderByDescending(result => result.Value)
        .First();
}

private float IntersectionOverUnion(RectangleF boundingBoxA, RectangleF boundingBoxB)
{
    var areaA = boundingBoxA.Width * boundingBoxA.Height;

    if (areaA <= 0)
        return 0;

    var areaB = boundingBoxB.Width * boundingBoxB.Height;

    if (areaB <= 0)
        return 0;

    var minX = Math.Max(boundingBoxA.Left, boundingBoxB.Left);
    var minY = Math.Max(boundingBoxA.Top, boundingBoxB.Top);
    var maxX = Math.Min(boundingBoxA.Right, boundingBoxB.Right);
    var maxY = Math.Min(boundingBoxA.Bottom, boundingBoxB.Bottom);

    var intersectionArea = Math.Max(maxY - minY, 0) * Math.Max(maxX - minX, 0);

    return intersectionArea / (areaA + areaB - intersectionArea);
}

모든 도우미 메서드를 정의하고 나면 이제 이 메서드를 사용하여 모델 출력을 처리해야 합니다.

IntersectionOverUnion 메서드 아래에서 ParseOutputs 메서드를 만들어 모델에서 생성한 출력을 처리합니다.

public IList<YoloBoundingBox> ParseOutputs(float[] yoloModelOutputs, float threshold = .3F)
{

}

목록을 만들어 경계 상자를 저장하고 ParseOutputs 메서드 내에 변수를 정의합니다.

var boxes = new List<YoloBoundingBox>();

각 이미지는 13 x 13 셀의 그리드로 나뉩니다. 각 셀에는 5개의 경계 상자가 포함되어 있습니다. boxes 변수 아래에 각 셀의 모든 상자를 처리하는 코드를 추가합니다.

for (int row = 0; row < ROW_COUNT; row++)
{
    for (int column = 0; column < COL_COUNT; column++)
    {
        for (int box = 0; box < BOXES_PER_CELL; box++)
        {

        }
    }
}

1차원 모델 출력에서 현재 상자의 시작 위치를 가장 안쪽의 루프에서 계산합니다.

var channel = (box * (CLASS_COUNT + BOX_INFO_FEATURE_COUNT));

바로 아래에서 ExtractBoundingBoxDimensions 메서드를 사용하여 현재 경계 상자의 크기를 가져옵니다.

BoundingBoxDimensions boundingBoxDimensions = ExtractBoundingBoxDimensions(yoloModelOutputs, row, column, channel);

그런 다음 GetConfidence 메서드를 사용하여 현재 경계 상자의 신뢰도를 가져옵니다.

float confidence = GetConfidence(yoloModelOutputs, row, column, channel);

그런 다음 MapBoundingBoxToCell 메서드를 사용하여 현재 경계 상자를 처리 중인 현재 셀에 매핑합니다.

CellDimensions mappedBoundingBox = MapBoundingBoxToCell(row, column, box, boundingBoxDimensions);

추가 처리를 수행하기 전에 신뢰도 값이 제공된 임계값보다 큰지 확인합니다. 그렇지 않은 경우 다음 경계 상자를 처리합니다.

if (confidence < threshold)
    continue;

그렇지 않으면 출력을 계속 처리합니다. 다음 단계에서는 ExtractClasses 메서드를 사용하여 현재 경계 상자에 대해 예측된 클래스의 확률 분포를 얻습니다.

float[] predictedClasses = ExtractClasses(yoloModelOutputs, row, column, channel);

그런 다음 GetTopResult 메서드를 사용하여 현재 상자의 확률이 가장 높은 클래스 값과 색인을 가져오고 해당 점수를 계산합니다.

var (topResultIndex, topResultScore) = GetTopResult(predictedClasses);
var topScore = topResultScore * confidence;

topScore를 사용하여 지정된 임계값을 초과하는 경계 상자만 유지합니다.

if (topScore < threshold)
    continue;

마지막으로 현재 경계 상자가 임계값을 초과하는 경우 새 BoundingBox 개체를 만들어 boxes 목록에 추가합니다.

boxes.Add(new YoloBoundingBox()
{
    Dimensions = new BoundingBoxDimensions
    {
        X = (mappedBoundingBox.X - mappedBoundingBox.Width / 2),
        Y = (mappedBoundingBox.Y - mappedBoundingBox.Height / 2),
        Width = mappedBoundingBox.Width,
        Height = mappedBoundingBox.Height,
    },
    Confidence = topScore,
    Label = labels[topResultIndex],
    BoxColor = classColors[topResultIndex]
});

이미지의 모든 셀을 처리한 후 boxes 목록을 반환합니다. ParseOutputs 메서드의 가장 바깥쪽 for 루프 아래에 다음 return 문을 추가합니다.

return boxes;

겹치는 상자 필터

모델 출력에서 신뢰도가 높은 모든 경계 상자를 추출했으므로 추가 필터링을 수행하여 겹치는 이미지를 제거해야 합니다. ParseOutputs 메서드 아래 FilterBoundingBoxes라는 메서드를 추가합니다.

public IList<YoloBoundingBox> FilterBoundingBoxes(IList<YoloBoundingBox> boxes, int limit, float threshold)
{

}

FilterBoundingBoxes 메서드 내에서 검색된 상자의 크기와 같은 배열을 만들고 모든 슬롯을 활성 또는 처리할 준비 완료로 표시하여 시작합니다.

var activeCount = boxes.Count;
var isActiveBoxes = new bool[boxes.Count];

for (int i = 0; i < isActiveBoxes.Length; i++)
    isActiveBoxes[i] = true;

그런 다음, 경계 상자를 포함하는 목록을 신뢰도에 따라 내림차순으로 정렬합니다.

var sortedBoxes = boxes.Select((b, i) => new { Box = b, Index = i })
                    .OrderByDescending(b => b.Box.Confidence)
                    .ToList();

그런 다음 필터링된 결과를 저장할 목록을 만듭니다.

var results = new List<YoloBoundingBox>();

각 경계 상자를 반복하여 각 경계 상자에 대한 처리를 시작합니다.

for (int i = 0; i < boxes.Count; i++)
{

}

이 for 루프 내에서 현재 경계 상자를 처리할 수 있는지 확인합니다.

if (isActiveBoxes[i])
{

}

그런 경우 경계 상자를 결과 목록에 추가합니다. 결과가 추출할 상자의 지정된 한도를 초과하면 루프를 중단합니다. If-문 내에 다음 코드를 추가합니다.

var boxA = sortedBoxes[i].Box;
results.Add(boxA);

if (results.Count >= limit)
    break;

처리할 수 없으면 인접한 경계 상자를 확인합니다. 상자 제한 검사 아래에 다음 코드를 추가합니다.

for (var j = i + 1; j < boxes.Count; j++)
{

}

첫 번째 상자와 같이 인접한 상자가 활성 상태이거나 처리할 준비가 된 경우 IntersectionOverUnion 메서드를 사용하여 첫 번째 상자와 두 번째 상자가 지정된 임계값을 초과하는지 확인합니다. 가장 안쪽에 있는 for 루프에 다음 코드를 추가합니다.

if (isActiveBoxes[j])
{
    var boxB = sortedBoxes[j].Box;

    if (IntersectionOverUnion(boxA.Rect, boxB.Rect) > threshold)
    {
        isActiveBoxes[j] = false;
        activeCount--;

        if (activeCount <= 0)
            break;
    }
}

인접 경계 상자를 확인하는 가장 안쪽의 for 루프 외부에서 처리할 나머지 경계 상자가 있는지 확인합니다. 그렇지 않은 경우 외부 for 루프를 중단합니다.

if (activeCount <= 0)
    break;

마지막으로 FilterBoundingBoxes 메서드의 초기 for 루프 외부에서 다음 결과를 반환합니다.

return results;

좋습니다! 이제 채점을 위해 모델과 함께 이 코드를 사용합니다.

모델을 사용하여 채점

후처리와 마찬가지로 채점 단계에는 몇 가지 단계가 있습니다. 이 작업을 지원하기 위해 채점 로직을 포함하는 클래스를 프로젝트에 추가합니다.

  1. 솔루션 탐색기에서 프로젝트를 마우스 오른쪽 단추로 클릭하고 추가>새 항목을 선택합니다.

  2. 새 항목 추가 대화 상자에서 클래스를 선택하고 이름 필드를 OnnxModelScorer.cs로 변경합니다. 그런 다음, 추가 단추를 선택합니다.

    OnnxModelScorer.cs 파일이 코드 편집기에서 열립니다. OnnxModelScorer.cs의 맨 위에 다음 using 문을 추가합니다.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.ML;
    using Microsoft.ML.Data;
    using ObjectDetection.DataStructures;
    using ObjectDetection.YoloParser;
    

    OnnxModelScorer 클래스 정의에 다음 변수를 추가합니다.

    private readonly string imagesFolder;
    private readonly string modelLocation;
    private readonly MLContext mlContext;
    
    private IList<YoloBoundingBox> _boundingBoxes = new List<YoloBoundingBox>();
    

    바로 아래에서 이전에 정의된 변수를 초기화할 OnnxModelScorer 클래스의 생성자를 만듭니다.

    public OnnxModelScorer(string imagesFolder, string modelLocation, MLContext mlContext)
    {
        this.imagesFolder = imagesFolder;
        this.modelLocation = modelLocation;
        this.mlContext = mlContext;
    }
    

    생성자를 만들었으면 이미지 및 모델 설정과 관련된 변수를 포함하는 두 개의 구조체를 정의합니다. ImageNetSettings라는 구조체를 만들어 예상 높이와 너비를 모델의 입력으로 포함시킵니다.

    public struct ImageNetSettings
    {
        public const int imageHeight = 416;
        public const int imageWidth = 416;
    }
    

    그런 다음 모델의 입력 및 출력 계층 이름을 포함하는 TinyYoloModelSettings라는 또 다른 구조체를 만듭니다. 모델의 입력 및 출력 계층 이름을 시각화하려면 Netron과 같은 도구를 사용할 수 있습니다.

    public struct TinyYoloModelSettings
    {
        // for checking Tiny yolo2 Model input and  output  parameter names,
        //you can use tools like Netron, 
        // which is installed by Visual Studio AI Tools
    
        // input tensor name
        public const string ModelInput = "image";
    
        // output tensor name
        public const string ModelOutput = "grid";
    }
    

    다음으로 채점에 사용되는 첫 번째 메서드 집합을 만듭니다. OnnxModelScorer 클래스 내부에 LoadModel 메서드를 만듭니다.

    private ITransformer LoadModel(string modelLocation)
    {
    
    }
    

    LoadModel 메서드 내에서 로깅을 위해 다음 코드를 추가합니다.

    Console.WriteLine("Read model");
    Console.WriteLine($"Model location: {modelLocation}");
    Console.WriteLine($"Default parameters: image size=({ImageNetSettings.imageWidth},{ImageNetSettings.imageHeight})");
    

    ML.NET 파이프라인은 Fit 메서드가 호출될 때 작동할 데이터 스키마를 알아야 합니다. 이 경우 학습과 비슷한 프로세스가 사용됩니다. 그러나 실제 학습이 발생하지 않기 때문에 빈 IDataView를 사용할 수 있습니다. 빈 목록에서 파이프라인의 새로운 IDataView를 만듭니다.

    var data = mlContext.Data.LoadFromEnumerable(new List<ImageNetData>());
    

    그런 다음 파이프라인을 정의합니다. 파이프라인은 4개의 변환으로 구성됩니다.

    • LoadImages에서는 이미지를 비트맵으로 로드합니다.
    • ResizeImages는 지정된 크기로 이미지의 크기를 조정합니다(이 경우 416 x 416).
    • ExtractPixels는 이미지의 픽셀 표현을 비트맵에서 숫자 벡터로 변경합니다.
    • ApplyOnnxModel는 ONNX 모델을 로드하고 제공된 데이터를 채점하는 데 사용합니다.

    data 변수 아래에 LoadModel 메서드의 파이프라인을 정의합니다.

    var pipeline = mlContext.Transforms.LoadImages(outputColumnName: "image", imageFolder: "", inputColumnName: nameof(ImageNetData.ImagePath))
                    .Append(mlContext.Transforms.ResizeImages(outputColumnName: "image", imageWidth: ImageNetSettings.imageWidth, imageHeight: ImageNetSettings.imageHeight, inputColumnName: "image"))
                    .Append(mlContext.Transforms.ExtractPixels(outputColumnName: "image"))
                    .Append(mlContext.Transforms.ApplyOnnxModel(modelFile: modelLocation, outputColumnNames: new[] { TinyYoloModelSettings.ModelOutput }, inputColumnNames: new[] { TinyYoloModelSettings.ModelInput }));
    

    이제 채점을 위해 모델을 인스턴스화해야 합니다. 파이프라인에서 Fit 메서드를 호출하고 추가 처리를 위해 반환합니다.

    var model = pipeline.Fit(data);
    
    return model;
    

모델을 로드한 후에는 모델을 사용하여 예측할 수 있습니다. 이 프로세스를 용이하게 하려면 LoadModel 메서드 아래 PredictDataUsingModel라는 메서드를 만듭니다.

private IEnumerable<float[]> PredictDataUsingModel(IDataView testData, ITransformer model)
{

}

PredictDataUsingModel내에서 로깅을 위해 다음 코드를 추가합니다.

Console.WriteLine($"Images location: {imagesFolder}");
Console.WriteLine("");
Console.WriteLine("=====Identify the objects in the images=====");
Console.WriteLine("");

그런 다음 Transform 메서드를 사용하여 데이터를 채점합니다.

IDataView scoredData = model.Transform(testData);

예측된 확률을 추출하고 추가 처리를 위해 반환합니다.

IEnumerable<float[]> probabilities = scoredData.GetColumn<float[]>(TinyYoloModelSettings.ModelOutput);

return probabilities;

이제 두 단계가 모두 설정되었으므로 단일 메서드로 결합합니다. PredictDataUsingModel 메서드 아래 Score라는 새 메서드를 추가합니다.

public IEnumerable<float[]> Score(IDataView data)
{
    var model = LoadModel(modelLocation);

    return PredictDataUsingModel(data, model);
}

거의 완료되었습니다! 이제 모두 사용할 수 있습니다.

개체 감지

이제 모든 설치가 완료되었으므로 일부 개체를 검색해 보겠습니다.

채점 및 구문 분석 모델 출력

mlContext 변수 생성 아래에 try-catch 문을 추가합니다.

try
{

}
catch (Exception ex)
{
    Console.WriteLine(ex.ToString());
}

try 블록에서 개체 검색 로직을 구현하기 시작합니다. 먼저 데이터를 IDataView에 로드합니다.

IEnumerable<ImageNetData> images = ImageNetData.ReadFromFile(imagesFolder);
IDataView imageDataView = mlContext.Data.LoadFromEnumerable(images);

그런 다음 OnnxModelScorer의 인스턴스를 만들고 이를 사용하여 로드된 데이터를 채점합니다.

// Create instance of model scorer
var modelScorer = new OnnxModelScorer(imagesFolder, modelFilePath, mlContext);

// Use model to score data
IEnumerable<float[]> probabilities = modelScorer.Score(imageDataView);

이제 후처리 단계가 진행됩니다. YoloOutputParser의 인스턴스를 만들고 이를 사용하여 모델 출력을 처리합니다.

YoloOutputParser parser = new YoloOutputParser();

var boundingBoxes =
    probabilities
    .Select(probability => parser.ParseOutputs(probability))
    .Select(boxes => parser.FilterBoundingBoxes(boxes, 5, .5F));

모델 출력을 처리하고 나면 이미지에 경계 상자를 그릴 차례입니다.

예측 시각화

모델에서 이미지를 채점하고 출력이 처리되고 나면 이미지에 경계 상자를 그려야 합니다. 그렇게 하려면 DrawBoundingBox 메서드를 Program.csGetAbsolutePath 메서드 아래 추가합니다.

void DrawBoundingBox(string inputImageLocation, string outputImageLocation, string imageName, IList<YoloBoundingBox> filteredBoundingBoxes)
{

}

먼저 이미지를 로드하고 DrawBoundingBox 메서드에서 높이 및 너비 차원을 가져옵니다.

Image image = Image.FromFile(Path.Combine(inputImageLocation, imageName));

var originalImageHeight = image.Height;
var originalImageWidth = image.Width;

그런 다음 모델에서 검색한 각 경계 상자를 반복하는 for-each 루프를 만듭니다.

foreach (var box in filteredBoundingBoxes)
{

}

for-each 루프 내에서 경계 상자의 크기를 가져옵니다.

var x = (uint)Math.Max(box.Dimensions.X, 0);
var y = (uint)Math.Max(box.Dimensions.Y, 0);
var width = (uint)Math.Min(originalImageWidth - x, box.Dimensions.Width);
var height = (uint)Math.Min(originalImageHeight - y, box.Dimensions.Height);

경계 상자의 크기는 416 x 416의 모델 입력에 해당하므로 경계 상자 크기를 이미지의 실제 크기와 일치하도록 조정합니다.

x = (uint)originalImageWidth * x / OnnxModelScorer.ImageNetSettings.imageWidth;
y = (uint)originalImageHeight * y / OnnxModelScorer.ImageNetSettings.imageHeight;
width = (uint)originalImageWidth * width / OnnxModelScorer.ImageNetSettings.imageWidth;
height = (uint)originalImageHeight * height / OnnxModelScorer.ImageNetSettings.imageHeight;

그런 다음 각 경계 상자 위에 표시될 텍스트의 템플릿을 정의합니다. 텍스트에는 각 경계 상자 내의 개체 클래스와 신뢰도가 포함됩니다.

string text = $"{box.Label} ({(box.Confidence * 100).ToString("0")}%)";

이미지를 그리려면 Graphics 개체로 변환하세요.

using (Graphics thumbnailGraphic = Graphics.FromImage(image))
{

}

using 코드 블록에서 그래픽의 Graphics 개체 설정을 조정합니다.

thumbnailGraphic.CompositingQuality = CompositingQuality.HighQuality;
thumbnailGraphic.SmoothingMode = SmoothingMode.HighQuality;
thumbnailGraphic.InterpolationMode = InterpolationMode.HighQualityBicubic;

그런 다음 텍스트 및 경계 상자의 글꼴 및 색 옵션을 설정합니다.

// Define Text Options
Font drawFont = new Font("Arial", 12, FontStyle.Bold);
SizeF size = thumbnailGraphic.MeasureString(text, drawFont);
SolidBrush fontBrush = new SolidBrush(Color.Black);
Point atPoint = new Point((int)x, (int)y - (int)size.Height - 1);

// Define BoundingBox options
Pen pen = new Pen(box.BoxColor, 3.2f);
SolidBrush colorBrush = new SolidBrush(box.BoxColor);

FillRectangle 메서드를 사용하여 텍스트를 포함하도록 경계 상자 위에 직사각형을 만들어 채웁니다. 그러면 텍스트가 대비되어 가독성을 높이는 데 도움이 됩니다.

thumbnailGraphic.FillRectangle(colorBrush, (int)x, (int)(y - size.Height - 1), (int)size.Width, (int)size.Height);

그런 다음 DrawStringDrawRectangle 메서드를 사용하여 이미지에 텍스트와 경계 상자를 그립니다.

thumbnailGraphic.DrawString(text, drawFont, fontBrush, atPoint);

// Draw bounding box on image
thumbnailGraphic.DrawRectangle(pen, x, y, width, height);

for-each 루프 외부에서 outputFolder에 이미지를 저장하는 코드를 추가합니다.

if (!Directory.Exists(outputImageLocation))
{
    Directory.CreateDirectory(outputImageLocation);
}

image.Save(Path.Combine(outputImageLocation, imageName));

런타임에 애플리케이션이 예상대로 예측하는지에 대한 추가 피드백을 위해 LogDetectedObjects 메서드를 Program.cs 파일의 DrawBoundingBox 메서드 아래 추가하여 검색된 개체를 콘솔에 출력합니다.

void LogDetectedObjects(string imageName, IList<YoloBoundingBox> boundingBoxes)
{
    Console.WriteLine($".....The objects in the image {imageName} are detected as below....");

    foreach (var box in boundingBoxes)
    {
        Console.WriteLine($"{box.Label} and its Confidence score: {box.Confidence}");
    }

    Console.WriteLine("");
}

이제 예측에서 시각적 피드백을 만드는 도우미 메서드가 있으므로 채점된 각 이미지를 반복하는 for 루프를 추가합니다.

for (var i = 0; i < images.Count(); i++)
{

}

for 루프 내에서 이미지 파일의 이름과 연결된 경계 상자를 가져옵니다.

string imageFileName = images.ElementAt(i).Label;
IList<YoloBoundingBox> detectedObjects = boundingBoxes.ElementAt(i);

그런 다음 DrawBoundingBox 메서드를 사용하여 이미지에 경계 상자를 그립니다.

DrawBoundingBox(imagesFolder, outputFolder, imageFileName, detectedObjects);

마지막으로 LogDetectedObjects 메서드를 사용하여 예측을 콘솔에 출력합니다.

LogDetectedObjects(imageFileName, detectedObjects);

try-catch 문 다음에 프로세스 실행이 완료되었음을 나타내는 추가 로직을 추가합니다.

Console.WriteLine("========= End of Process..Hit any Key ========");

정말 간단하죠.

결과

이전 단계를 수행한 후 콘솔 앱을 실행합니다(Ctrl+F5). 다음 출력과 같은 결과가 나타나야 합니다. 경고 또는 처리 메시지가 표시될 수 있지만, 이해하기 쉽도록 이러한 메시지는 다음 결과에서 제거되었습니다.

=====Identify the objects in the images=====

.....The objects in the image image1.jpg are detected as below....
car and its Confidence score: 0.9697262
car and its Confidence score: 0.6674225
person and its Confidence score: 0.5226039
car and its Confidence score: 0.5224892
car and its Confidence score: 0.4675332

.....The objects in the image image2.jpg are detected as below....
cat and its Confidence score: 0.6461141
cat and its Confidence score: 0.6400049

.....The objects in the image image3.jpg are detected as below....
chair and its Confidence score: 0.840578
chair and its Confidence score: 0.796363
diningtable and its Confidence score: 0.6056048
diningtable and its Confidence score: 0.3737402

.....The objects in the image image4.jpg are detected as below....
dog and its Confidence score: 0.7608147
person and its Confidence score: 0.6321323
dog and its Confidence score: 0.5967442
person and its Confidence score: 0.5730394
person and its Confidence score: 0.5551759

========= End of Process..Hit any Key ========

경계 상자가 있는 이미지를 보려면 assets/images/output/ 디렉터리로 이동합니다. 다음은 처리된 이미지 중 하나의 샘플입니다.

Sample processed image of a dining room

축하합니다! 이제 ML.NET에서 미리 학습된 ONNX 모델을 다시 사용하여 개체 검색을 위한 기계 학습 모델을 성공적으로 빌드했습니다.

dotnet/samples 리포지토리에서 이 자습서의 소스 코드를 찾을 수 있습니다.

이 자습서에서는 다음 작업 방법을 알아보았습니다.

  • 문제 이해
  • ONNX의 개념과 ML.NET에서 작동하는 방식에 대해 알아보기
  • 모델 이해
  • 미리 학습된 모델 다시 사용
  • 로드된 모델을 사용하여 개체 검색

기계 학습 샘플 GitHub 리포지토리를 확인하여 확장된 개체 검색 샘플을 살펴보세요.