Руководство. Обнаружение объектов с помощью ONNX в ML.NET

Узнайте, как использовать предварительно обученную модель ONNX в ML.NET для обнаружения объектов в изображениях.

Обучение модели для обнаружения объектов с нуля предусматривает настройку нескольких миллионов параметров, а также использование больших объемов помеченных данных обучения и вычислительных ресурсов (сотни часов работы GPU). Использование предварительно обученной модели позволяет упростить процесс обучения.

В этом руководстве описано следующее:

  • Определение проблемы
  • Узнайте, что такое ONNX и как он работает с ML.NET
  • Общие сведения о модели
  • Повторное использование предварительно обученной модели
  • Обнаружение объектов в загруженной модели

Предварительные требования

Обзор примера обнаружения объектов в ONNX

В этом примере создается консольное приложение .NET Core, которое обнаруживает объекты на изображении с помощью предварительно обученной модели ONNX для глубокого обучения. Код для этого примера можно найти в репозитории dotnet/machinelearning-samples на сайте GitHub.

Что такое обнаружение объектов?

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

Используйте обнаружение объектов, если изображения содержат несколько объектов разных типов.

Screenshots showing Image Classification versus Object Classification.

Некоторые варианты применения обнаружения объектов:

  • Автомобили с автономным управлением
  • Робототехника
  • Обнаружение лиц
  • Техника безопасности
  • Подсчет объектов
  • Распознавание активности

Выбор модели глубокого обучения

Глубокое обучение — это подраздел машинного обучения. Для обучения моделей глубокого обучения требуются большие объемы данных. Закономерности в данных представлены рядом слоев. Связи в данных кодируются как соединения между слоями, имеющие веса. Чем выше вес, тем сильнее связь. В совокупности этот ряд уровней и соединений называют искусственными нейронными сетями. Чем больше уровней в сети, тем глубже нейронная сеть.

Существуют различные типы нейронных сетей; наиболее распространенные — многоуровневый перцептрон (MLP), сверточная нейронная сеть (CNN) и рекуррентная нейронная сеть (RNN). Самым простым вариантом является MLP, который сопоставляет набор входных данных с набором выходов. Эта нейронная сеть хорошо подходит, когда в данных отсутствует пространственный или временный компонент. CNN использует слои свертки для обработки пространственных данных, содержащихся в данных. Хорошим вариантом использования CNN является обработка изображений, позволяющая обнаружить присутствие некой черты в определенном регионе изображения (например, есть ли нос в центре изображения). Наконец, RNN позволяет использовать сохраняемость состояния или памяти, используемых в качестве входных данных. RNN используются для анализа временных рядов, где важны упорядочение и контекст событий.

Общие сведения о модели

Обнаружение объектов — это задача обработки изображений. Поэтому большинство моделей глубокого обучения, предназначенных для решения этой проблемы, относятся к CNN. Модель, используемая в этом руководстве, — это модель Tiny YOLOv2, более компактная версия модели YOLOv2, описанная в документе: "YOLO9000: Лучше, быстрее, сильнее" Redmon и Farhadi. Модель Tiny YOLOv2 обучена на наборе данных Pascal ВОК и состоит из 15 уровней, которые могут прогнозировать 20 различных классов объектов. Так как Tiny YOLOv2 является сжатой версией исходной модели YOLOv2, между скоростью и точностью есть компромисс. Различные слои, составляющие модель, можно использовать для визуализации с помощью таких средств, как Netron. Изучение модели приведет к сопоставлению соединений между всеми слоями, которые составляют нейронную сеть, где каждый слой будет содержать имя слоя вместе с размерами соответствующих входных и выходных данных. Структуры данных, используемые для описания входных и выходных данных модели, называются тензорами. Их можно рассматривать как контейнеры, в которых данные хранятся в 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?

Open Neural Network Exchange (ONNX) — это формат с открытым исходным кодом для моделей ИИ. ONNX поддерживает взаимодействие между разными платформами. Это означает, что модель можно обучить в одной из многих популярных платформ машинного обучения, таких как PyTorch, преобразовать ее в формат ONNX и использовать модель ONNX в другой инфраструктуре, такой как ML.NET. Дополнительные сведения см. на веб-сайте ONNX.

Diagram of ONNX supported formats being used.

Предварительно обученная модель Tiny YOLOv2 хранится в формате ONNX: сериализованном представлении слоев и полученных закономерностях этих слоев. В ML.NET взаимодействие с ONNX достигается с помощью пакетов NuGet ImageAnalytics и OnnxTransformer. Пакет ImageAnalytics содержит ряд преобразований, которые принимают изображение и кодируют его в числовые значения, которые можно использовать в качестве входных данных в конвейере прогнозирования или обучения. Пакет OnnxTransformer использует среду выполнения ONNX для загрузки модели ONNX и использует ее для создания прогнозов на основе предоставленных входных данных.

Data flow of ONNX file into the ONNX Runtime.

Настройка консольного проекта .NET

Теперь, когда у вас есть общее представление о том, что такое ONNX и как работает Tiny YOLOv2, пришло время создать приложение.

Создание консольного приложение

  1. Создайте консольное приложение C# с именем ObjectDetection. Нажмите кнопку Далее.

  2. Выберите .NET 6 в качестве используемой платформы. Нажмите кнопку Создать.

  3. Установите пакет NuGet для Microsoft.ML:

    Примечание.

    В этом примере используется последняя стабильная версия пакетов NuGet, упомянутых выше, если не указано иное.

    • В обозревателе решений щелкните проект правой кнопкой мыши и выберите Управление пакетами NuGet.
    • Выберите nuget.org в качестве источника пакета, откройте вкладку "Обзор" и выполните поиск Microsoft.ML.
    • Нажмите кнопку Установить.
    • Нажмите кнопку ОК в диалоговом окне Предварительный просмотр изменений, а затем нажмите кнопку Принимаю в диалоговом окне Принятие условий лицензионного соглашения, если вы согласны с указанными условиями лицензионного соглашения для выбранных пакетов.
    • Повторите эти действия для Microsoft.Windows.Compatibility, Microsoft.ML.ImageAnalytics, Microsoft.ML.OnnxTransformer и Microsoft.ML.OnnxRuntime.

Подготовка данных и предварительно обученной модели

  1. Скачайте ZIP-файл каталога с ресурсами проекта и распакуйте его.

  2. Скопируйте каталог assets в каталог проекта ObjectDetection. Этот каталог и его подкаталоги содержат файлы изображений (за исключением модели Tiny YOLOv2, которую вы скачаете и добавите на следующем шаге), необходимые для работы с этим руководством.

  3. Скачайте модель Tiny YOLOv2 из репозитория ONNX Model Zoo.

  4. Скопируйте каталог model.onnx в каталог assets\Model проекта ObjectDetection и присвойте имя TinyYolo2_model.onnx. Этот каталог содержит модель, необходимую для работы с этим руководством.

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

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

Добавьте следующие новые операторы using в начало файла Program.cs:

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

Затем определите пути к различным ресурсам.

  1. Сначала создайте метод GetAbsolutePath в конце файла Program.cs.

    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 содержит метод ReadFromFile, который загружает несколько файлов изображений, хранящихся по указанном пути imageFolder, и возвращает их в виде коллекции объектов ImageNetData.

Создайте класс прогноза в каталоге структур структуры 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, которая может использоваться всеми объектами в рамках процесса создания модели. По существу он аналогичен классу DBContext в Entity Framework.

Инициализируйте переменную mlContext с помощью нового экземпляра MLContext, добавив под полем outputFolder следующую строку.

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 классов, прогнозируемых моделью.

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

Выходные данные, формируемые предварительно обученной моделью ONNX, представляют собой массив длины 21125, представляющий тензорные элементы с 125 x 13 x 13 измерениями. Чтобы преобразовать прогнозы, созданные моделью, в тензоры, необходимо выполнить некоторые действия для постобработки. Для этого создайте набор классов для упрощения анализа выходных данных.

Добавьте в проект новый каталог для упорядочения набора классов средства анализа.

  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;
    

    Непосредственно над существующим определением класса добавьте новое определение класса с именем BoundingBoxDimensions, который наследует от класса DimensionsBase, чтобы вместить размеры соответствующего ограничивающего прямоугольника.

    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. Добавьте следующие using инструкции в начало YoloOutputParser.cs:

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

    В существующем определении класса YoloOutputParser добавьте вложенный класс, который содержит размеры каждой ячейки в изображении. Добавьте следующий код для класса CellDimensions, который наследуется от класса DimensionsBase, в верхней части определения класса YoloOutputParser.

    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. В каждой ячейке есть пять ограничивающих прямоугольников, каждый из которых содержит пять компонентов (X, Y, ширина, высота, достоверность). Кроме того, каждый ограничивающий прямоугольник содержит вероятность каждого из классов, которых в данном случае насчитывается 20. Таким образом, каждая ячейка содержит 125 элементов данных (пять функций + 20 вероятностей классов).

Создайте список привязок ниже channelStride для всех пяти ограничивающих прямоугольников:

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

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

Затем определите метки или классы, которые будет прогнозировать модель. Эта модель прогнозирует 20 классов, которые являются подмножеством общего числа классов, прогнозируемых исходной моделью YOLOv2.

Добавьте список меток под 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.
  • Softmax нормализует входной вектор в распределение вероятности.
  • GetOffset сопоставляет элементы в выходных данных одномерной модели с соответствующей позицией в тензоре 125 x 13 x 13.
  • ExtractBoundingBoxes извлекает измерения ограничивающего прямоугольника с помощью метода GetOffset из выходных данных модели.
  • GetConfidence извлекает значение достоверности того, что модель обнаружила объект, и использует функцию Sigmoid, чтобы преобразовать ее в процент.
  • MapBoundingBoxToCell использует измерения ограничивающего прямоугольника и сопоставляет их с соответствующей ячейкой на изображении.
  • ExtractClasses извлекает прогнозы класса для ограничивающего прямоугольника из выходных данных модели с помощью метода GetOffset и превращает их в распределение вероятности с помощью метода 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. Каждая ячейка содержит пять ограничивающих прямоугольников. Под переменной 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++)
        {

        }
    }
}

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

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. Добавьте следующий оператор return под внешним циклом for в методе ParseOutputs.

return boxes;

Фильтрация перекрывающихся полей

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

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;

Наконец, за пределами начального цикла for метода FilterBoundingBoxes следует вернуть результаты:

return results;

Отлично! Теперь пришло время использовать этот код вместе с моделью для оценки.

Использование модели для оценки

Как и в случае с постобработкой, оценка проводится в несколько шагов. Чтобы помочь в этом, добавьте класс, который будет содержать логику оценки для проекта.

  1. В обозревателе решений щелкните правой кнопкой мыши проект, а затем выберите Добавить>Новый элемент.

  2. В диалоговом окне Добавление нового элемента выберите Класс и измените значение поля Имя на OnnxModelScorer.cs. Теперь нажмите кнопку Добавить.

    Файл OnnxModelScorer.cs откроется в редакторе кода. Добавьте следующие using инструкции в начало OnnxModelScorer.cs:

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

    Затем создайте первый набор методов, используемый для оценки. Создайте метод LoadModel внутри класса OnnxModelScorer.

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

    Ниже определите конвейер. Конвейер будет состоять из четырех преобразований.

    • LoadImages загружает изображение в виде точечного рисунка.
    • ResizeImages изменяет масштаб изображения до указанного размера (в данном случае 416 x 416).
    • ExtractPixels изменяет пиксельное представление изображения с точечного рисунка на числовой вектор.
    • ApplyOnnxModel загружает модель ONNX и использует ее для оценки предоставленных данных.

    Определите конвейер в методе LoadModel под переменной data.

    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;
    

После загрузки модели ее можно использовать для создания прогнозов. Чтобы упростить этот процесс, создайте метод PredictDataUsingModel под методом LoadModel.

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;

Теперь, когда оба шага настроены, объедините их в один метод. Добавьте новый метод Score под методом PredictDataUsingModel:

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 под методом GetAbsolutePath в Program.cs.

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

}

Сначала загрузите изображение и получите измерения Height и Width в методе 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);

Затем нарисуйте текст и ограничивающий прямоугольник на изображении с помощью методов DrawString и DrawRectangle.

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 под методом DrawBoundingBox в файле Program.cs для вывода обнаруженных объектов на консоль.

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

Поздравляем! Вы успешно создали модель машинного обучения для обнаружения объектов на основе предварительно обученной модели ONNX в ML.NET.

Исходный код для этого руководства можно найти в репозитории dotnet/machinelearning-samples.

Из этого руководства вы узнали, как:

  • Определение проблемы
  • Узнайте, что такое ONNX и как он работает с ML.NET
  • Общие сведения о модели
  • Повторное использование предварительно обученной модели
  • Обнаружение объектов в загруженной модели

Ознакомьтесь с примерами машинного обучения в репозитории GitHub, чтобы подробнее изучить расширенный пример обнаружения объектов.