Kurz: Detekce objektů pomocí ONNX v ML.NET

Naučte se používat předem natrénovaný model ONNX v ML.NET k detekci objektů v obrázcích.

Trénování modelu detekce objektů od začátku vyžaduje nastavení milionů parametrů, velké množství trénovacích dat s popiskem a obrovské množství výpočetních prostředků (stovky hodin GPU). Použití předem natrénovaného modelu umožňuje zástupce procesu trénování.

V tomto kurzu se naučíte:

  • Pochopení problému
  • Zjistěte, co je ONNX a jak funguje s ML.NET
  • Vysvětlení modelu
  • Opětovné použití předem natrénovaného modelu
  • Detekce objektů s načteným modelem

Požadavky

Přehled ukázky detekce objektů ONNX

Tato ukázka vytvoří konzolovou aplikaci .NET Core, která pomocí předem natrénovaného modelu ONNX rozpozná objekty v rámci image. Kód pro tuto ukázku najdete v úložišti dotnet/machinelearning-samples na GitHubu.

Co je detekce objektů?

Rozpoznávání objektů je problém počítačového zpracování obrazu. Zatímco úzce souvisí s klasifikací obrázků, detekce objektů provádí klasifikaci obrázků v podrobnějším měřítku. Rozpoznávání objektů vyhledá i kategorizuje entity v obrázcích. Modely rozpoznávání objektů se běžně trénují pomocí hlubokého učení a neurálních sítí. Další informace najdete v tématu Hluboké učení a strojové učení .

Rozpoznávání objektů použijte, když obrázky obsahují více objektů různých typů.

Screenshots showing Image Classification versus Object Classification.

Mezi případy použití detekce objektů patří:

  • Autojezdové vozy
  • Robotika
  • Detekce obličeje
  • Pracovní Sejf ty
  • Počítání objektů
  • Rozpoznávání aktivit

Výběr modelu hlubokého učení

Hluboké učení je podmnožinou strojového učení. K trénování modelů hlubokého učení se vyžaduje velké množství dat. Vzory v datech jsou reprezentovány řadou vrstev. Relace v datech jsou zakódovány jako propojení mezi vrstvami obsahujícími váhy. Čím vyšší je váha, tím silnější je vztah. Souhrnně se tato řada vrstev a spojení označuje jako umělé neurální sítě. Čím více vrstev v síti, tím "hlubší" je, že se jedná o hlubokou neurální síť.

Existují různé typy neurálních sítí, nejběžnější je vícevrstvý perceptron (MLP), konvoluční neurální síť (CNN) a rekurentní neurální síť (RNN). Nejzásadnější je MLP, který mapuje sadu vstupů na sadu výstupů. Tato neurální síť je dobrá, pokud data nemají prostorovou nebo časovou komponentu. Síť CNN využívá konvoluční vrstvy ke zpracování prostorových informací obsažených v datech. Dobrým případem použití sítí CNN je zpracování obrázků, aby bylo možné zjistit přítomnost funkce v oblasti obrázku (například je uprostřed obrázku nos?). Sítě RNN nakonec umožňují používat jako vstup trvalost stavu nebo paměti. Sítě RNN se používají pro analýzu časových řad, kde je důležité sekvenční řazení a kontext událostí.

Vysvětlení modelu

Rozpoznávání objektů je úloha zpracování obrázků. Proto většina modelů hlubokého učení natrénovaných k vyřešení tohoto problému jsou sítě CNN. Model použitý v tomto kurzu je model Tiny YOLOv2, kompaktnější verze modelu YOLOv2 popsané v dokumentu: "YOLO9000: Better, Faster, Stronger" od Redmon a Farhadi. Tiny YOLOv2 je trénován na datové sadě Pascal VOC a skládá se z 15 vrstev, které mohou předpovědět 20 různých tříd objektů. Vzhledem k tomu, že Tiny YOLOv2 je zhuštěná verze původního modelu YOLOv2, je kompromis mezi rychlostí a přesností. Různé vrstvy, které tvoří model, je možné vizualizovat pomocí nástrojů, jako je Netron. Při kontrole modelu by se zobrazilo mapování propojení mezi všemi vrstvami, které tvoří neurální síť, kde by každá vrstva obsahovala název vrstvy spolu s dimenzemi příslušného vstupu a výstupu. Datové struktury používané k popisu vstupů a výstupů modelu se označují jako tensory. Tensors lze považovat za kontejnery, které ukládají data v N dimenzích. V případě Tiny YOLOv2 je název vstupní vrstvy image a očekává tensor dimenzí 3 x 416 x 416. Název výstupní vrstvy je grid a generuje výstupní tensor dimenzí 125 x 13 x 13.

Input layer being split into hidden layers, then output layer

Model YOLO vezme obrázek 3(RGB) x 416px x 416px. Model vezme tento vstup a předá ho různými vrstvami, aby vytvořil výstup. Výstup rozdělí vstupní obrázek do 13 x 13 mřížky, přičemž každá buňka v mřížce se skládá z 125 hodnot.

Co je model ONNX?

Open Neural Network Exchange (ONNX) je open source formát pro modely AI. ONNX podporuje interoperabilitu mezi architekturami. To znamená, že model můžete trénovat v některé z mnoha oblíbených architektur strojového učení, jako je PyTorch, převést ho na formát ONNX a využívat model ONNX v jiné rozhraní, jako je ML.NET. Další informace najdete na webu ONNX.

Diagram of ONNX supported formats being used.

Předem natrénovaný model Tiny YOLOv2 je uložený ve formátu ONNX, serializované znázornění vrstev a naučených vzorů těchto vrstev. V ML.NET se interoperabilita s ONNX dosahuje s ImageAnalytics balíčky NuGet.OnnxTransformer Balíček ImageAnalytics obsahuje řadu transformací, které převezmou obrázek a zakódují ho do číselných hodnot, které lze použít jako vstup do prediktivního nebo trénovacího kanálu. Balíček OnnxTransformer využívá modul runtime ONNX k načtení modelu ONNX a jeho použití k vytváření předpovědí na základě zadaného vstupu.

Data flow of ONNX file into the ONNX Runtime.

Nastavení projektu konzoly .NET

Teď, když máte obecný přehled o tom, co je ONNX a jak Tiny YOLOv2 funguje, je čas sestavit aplikaci.

Vytvoření konzolové aplikace

  1. Vytvořte konzolovou aplikaci jazyka C# s názvem ObjectDetection. Klikněte na tlačítko Next.

  2. Jako architekturu, která se má použít, zvolte .NET 6. Klikněte na tlačítko Vytvořit.

  3. Nainstalujte balíček NuGet Microsoft.ML:

    Poznámka:

    Tato ukázka používá nejnovější stabilní verzi uvedených balíčků NuGet, pokud není uvedeno jinak.

    • V Průzkumník řešení klikněte pravým tlačítkem na projekt a vyberte Spravovat balíčky NuGet.
    • Jako zdroj balíčku zvolte "nuget.org", vyberte kartu Procházet a vyhledejte Microsoft.ML.
    • Vyberte tlačítko Instalovat.
    • V dialogovém okně Náhled změn vyberte tlačítko OK a pak v dialogovém okně Přijetí licence vyberte tlačítko Přijmout, pokud souhlasíte s licenčními podmínkami pro uvedené balíčky.
    • Opakujte tyto kroky pro Microsoft.Windows.Compatibility, Microsoft.ML.ImageAnalytics, Microsoft.ML.OnnxTransformer a Microsoft.ML.OnnxRuntime.

Příprava dat a předem natrénovaného modelu

  1. Stáhněte soubor ZIP adresáře prostředků projektu a rozbalte ho.

  2. assets Zkopírujte adresář do adresáře projektu ObjectDetection. Tento adresář a jeho podadresáře obsahují soubory obrázků (s výjimkou modelu Tiny YOLOv2, který si stáhnete a přidáte v dalším kroku) potřebný pro tento kurz.

  3. Stáhněte si model Tiny YOLOv2 z modelové zoo ONNX.

  4. model.onnx Zkopírujte soubor do adresáře projektu assets\Model ObjectDetection a přejmenujte ho na TinyYolo2_model.onnx. Tento adresář obsahuje model potřebný pro tento kurz.

  5. V Průzkumník řešení klikněte pravým tlačítkem na všechny soubory v adresáři prostředků a podadresářích a vyberte Vlastnosti. V části Upřesnit změňte hodnotu kopírovat do výstupního adresáře , pokud je novější.

Vytváření tříd a definování cest

Otevřete soubor Program.cs a na začátek souboru přidejte následující další using příkazy:

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

Dále definujte cesty různých prostředků.

  1. Nejprve vytvořte metodu GetAbsolutePath v dolní části souboru 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. Potom pod příkazy using vytvořte pole pro uložení umístění vašich prostředků.

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

Přidejte do projektu nový adresář pro ukládání vstupních dat a tříd predikcí.

V Průzkumník řešení klikněte pravým tlačítkem myši na projekt a pak vyberte Přidat>novou složku. Jakmile se nová složka zobrazí v Průzkumník řešení, pojmenujte ji "Datové struktury".

Vytvořte vstupní třídu dat v nově vytvořeném adresáři DataStructures .

  1. V Průzkumník řešení klikněte pravým tlačítkem myši na adresář Datové struktury a pak vyberte Přidat>novou položku.

  2. V dialogovém okně Přidat novou položku vyberte Třídu a změňte pole Název na ImageNetData.cs. Pak vyberte tlačítko Přidat .

    Soubor ImageNetData.cs se otevře v editoru kódu. Na začátek souboru ImageNetData.cs přidejte následující using příkaz:

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

    Odeberte existující definici třídy a přidejte následující kód pro ImageNetData třídu do souboru 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 je vstupní třída dat obrázku a má následující String pole:

    • ImagePath obsahuje cestu, kde je obrázek uložen.
    • Label obsahuje název souboru.

    Kromě toho obsahuje metoduReadFromFile, ImageNetData která načte více souborů obrázků uložených imageFolder v zadané cestě a vrací je jako kolekci ImageNetData objektů.

Vytvořte prediktivní třídu v adresáři DataStructures .

  1. V Průzkumník řešení klikněte pravým tlačítkem myši na adresář Datové struktury a pak vyberte Přidat>novou položku.

  2. V dialogovém okně Přidat novou položku vyberte Třídu a změňte pole Název na ImageNetPrediction.cs. Pak vyberte tlačítko Přidat .

    Soubor ImageNetPrediction.cs se otevře v editoru kódu. Na začátek imageNetPrediction.cs přidejte následující using příkaz:

    using Microsoft.ML.Data;
    

    Odeberte existující definici třídy a do souboru ImageNetPrediction.cs přidejte následující kód třídy:ImageNetPrediction

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

    ImageNetPrediction je třída dat předpovědi a má následující float[] pole:

    • PredictedLabels obsahuje rozměry, skóre objektu a pravděpodobnosti tříd pro každou ohraničující rámečky rozpoznané na obrázku.

Inicializace proměnných

MLContext třída je výchozím bodem pro všechny operace ML.NET a inicializace mlContext vytvoří nové ML.NET prostředí, které lze sdílet napříč objekty pracovního postupu vytváření modelu. Je to podobné, koncepčně, jako DBContext v Entity Frameworku.

Inicializace mlContext proměnné s novou instancí MLContext přidáním následujícího řádku pod outputFolder pole.

MLContext mlContext = new MLContext();

Vytvoření analyzátoru pro výstupy modelu po zpracování

Model segmentuje obrázek do 13 x 13 mřížky, kde je 32px x 32pxkaždá buňka mřížky . Každá buňka mřížky obsahuje 5 potenciálních ohraničujících rámečků objektů. Ohraničující rámeček má 25 prvků:

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

  • x pozice x ohraničujícího rámečku vzhledem k buňce mřížky, ke které je přidružená.
  • y umístění ohraničujícího rámečku vzhledem k buňce mřížky, ke které je přidružená.
  • w šířka ohraničujícího rámečku.
  • h výška ohraničujícího rámečku.
  • o hodnota spolehlivosti, kterou objekt existuje v ohraničujícím rámečku, označované také jako skóre objektu.
  • p1-p20 pravděpodobnosti třídy pro každou z 20 tříd predikovaných modelem.

Celkem 25 prvků popisujících každou z 5 ohraničujících polí tvoří 125 prvků obsažených v každé buňce mřížky.

Výstup vygenerovaný předem natrénovaným modelem ONNX je pole s plovoucí délkou 21125představující prvky tensoru s rozměry 125 x 13 x 13. Aby bylo možné transformovat předpovědi vygenerované modelem na tensor, je vyžadována nějaká práce po zpracování. Uděláte to tak, že vytvoříte sadu tříd, které vám pomůžou analyzovat výstup.

Přidejte do projektu nový adresář pro uspořádání sady tříd analyzátoru.

  1. V Průzkumník řešení klikněte pravým tlačítkem myši na projekt a pak vyberte Přidat>novou složku. Když se nová složka zobrazí v Průzkumník řešení, pojmenujte ji YoloParser.

Vytvoření ohraničujících polí a dimenzí

Výstup dat modelu obsahuje souřadnice a rozměry ohraničujících polí objektů v rámci obrázku. Vytvořte základní třídu pro dimenze.

  1. V Průzkumník řešení klikněte pravým tlačítkem myši na adresář YoloParser a pak vyberte Přidat>novou položku.

  2. V dialogovém okně Přidat novou položku vyberte Třídu a změňte pole Název na DimensionsBase.cs. Pak vyberte tlačítko Přidat .

    Soubor DimensionsBase.cs se otevře v editoru kódu. Odeberte všechny using příkazy a existující definici třídy.

    Do souboru DimensionsBase.cs přidejte následující kód třídy:DimensionsBase

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

    DimensionsBase má následující float vlastnosti:

    • X obsahuje pozici objektu podél osy x.
    • Y obsahuje pozici objektu podél osy y.
    • Height obsahuje výšku objektu.
    • Width obsahuje šířku objektu.

Dále vytvořte třídu pro ohraničující pole.

  1. V Průzkumník řešení klikněte pravým tlačítkem myši na adresář YoloParser a pak vyberte Přidat>novou položku.

  2. V dialogovém okně Přidat novou položku vyberte Třídu a změňte pole Název na YoloBoundingBox.cs. Pak vyberte tlačítko Přidat .

    Soubor YoloBoundingBox.cs se otevře v editoru kódu. Na začátek souboru YoloBoundingBox.cs přidejte následující using příkaz:

    using System.Drawing;
    

    Přímo nad existující definici třídy přidejte novou definici třídy, BoundingBoxDimensions která dědí z DimensionsBase třídy tak, aby obsahovala rozměry příslušného ohraničujícího rámečku.

    public class BoundingBoxDimensions : DimensionsBase { }
    

    Odeberte existující YoloBoundingBox definici třídy a přidejte následující kód pro YoloBoundingBox třídu do souboru 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 má následující vlastnosti:

    • Dimensions obsahuje rozměry ohraničujícího rámečku.
    • Label obsahuje třídu objektu rozpoznaného v ohraničujícím rámečku.
    • Confidence obsahuje jistotu třídy.
    • Rect obsahuje obdélníkové znázornění rozměrů ohraničujícího rámečku.
    • BoxColor obsahuje barvu přidruženou k příslušné třídě použité k kreslení na obrázku.

Vytvoření analyzátoru

Teď, když jsou vytvořeny třídy pro dimenze a ohraničující rámečky, je čas vytvořit analyzátor.

  1. V Průzkumník řešení klikněte pravým tlačítkem myši na adresář YoloParser a pak vyberte Přidat>novou položku.

  2. V dialogovém okně Přidat novou položku vyberte Třídu a změňte pole Název na YoloOutputParser.cs. Pak vyberte tlačítko Přidat .

    Soubor YoloOutputParser.cs se otevře v editoru kódu. Na začátek souboru YoloOutputParser.cs přidejte následující using příkazy:

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

    Uvnitř existující YoloOutputParser definice třídy přidejte vnořenou třídu, která obsahuje rozměry jednotlivých buněk na obrázku. Přidejte následující kód pro CellDimensions třídu, která dědí z DimensionsBase třídy v horní části YoloOutputParser definice třídy.

    class CellDimensions : DimensionsBase { }
    
  3. YoloOutputParser Do definice třídy přidejte následující konstanty a pole.

    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 je počet řádků v mřížce, do které je obrázek rozdělený.
    • COL_COUNT je počet sloupců v mřížce, do kterých je obrázek rozdělen.
    • CHANNEL_COUNT je celkový počet hodnot obsažených v jedné buňce mřížky.
    • BOXES_PER_CELL je počet ohraničujících polí v buňce,
    • BOX_INFO_FEATURE_COUNT je počet funkcí obsažených v rámečku (x,y, výška, šířka, spolehlivost).
    • CLASS_COUNT je počet predikcí tříd obsažených v každém ohraničujícím rámečku.
    • CELL_WIDTH je šířka jedné buňky v mřížce obrázku.
    • CELL_HEIGHT je výška jedné buňky v mřížce obrázku.
    • channelStride je počáteční pozice aktuální buňky v mřížce.

    Když model vytvoří předpověď, označovanou také jako bodování, rozdělí 416px x 416px vstupní obrázek do mřížky buněk, ve které je 13 x 13velikost buňky . Každá buňka obsahuje .32px x 32px V každé buňce je 5 ohraničujících polí obsahujících 5 funkcí (x, y, šířka, výška, spolehlivost). Kromě toho každý ohraničující rámeček obsahuje pravděpodobnost každé třídy, která v tomto případě je 20. Proto každá buňka obsahuje 125 informací (5 funkcí + 20 pravděpodobností třídy).

Vytvořte seznam ukotvení níže channelStride pro 5 ohraničujících polí:

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

Kotvy jsou předem definované poměry výšky a šířky ohraničujících polí. Většina objektů nebo tříd zjištěných modelem má podobné poměry. To je cenné, pokud jde o vytváření ohraničující rámečky. Místo předpovídání ohraničujících polí se vypočítá posun od předdefinovaných dimenzí, což snižuje výpočet potřebný k predikci ohraničujícího rámečku. Obvykle se tyto poměry ukotvení počítají na základě použité datové sady. V tomto případě, protože datová sada je známá a hodnoty jsou předem vypočítané, lze ukotvení pevně zakódovat.

Dále definujte popisky nebo třídy, které model predikuje. Tento model předpovídá 20 tříd, což je podmnožina celkového počtu tříd předpovídaných původním modelem YOLOv2.

Přidejte seznam popisků pod .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"
};

K jednotlivým třídám jsou přidruženy barvy. Přiřaďte své barvy předmětu pod: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
};

Vytváření pomocných funkcí

Fáze následného zpracování zahrnuje řadu kroků. K tomu lze použít několik pomocných metod.

Pomocné metody používané analyzátorem jsou:

  • Sigmoid použije funkci sigmoid, která vypíše číslo od 0 do 1.
  • Softmax normalizuje vstupní vektor do rozdělení pravděpodobnosti.
  • GetOffset mapuje prvky ve výstupu jednorozměrného modelu na odpovídající pozici v tensoru 125 x 13 x 13 .
  • ExtractBoundingBoxes extrahuje ohraničující kóty pomocí GetOffset metody z výstupu modelu.
  • GetConfidence extrahuje hodnotu spolehlivosti, která uvádí, že model zjistil objekt a pomocí funkce ho Sigmoid převede na procento.
  • MapBoundingBoxToCell používá ohraničující rozměry rámečku a mapuje je na příslušnou buňku v obrázku.
  • ExtractClasses extrahuje predikce třídy pro ohraničující rámeček z výstupu modelu pomocí GetOffset metody a pomocí metody je převede na rozdělení Softmax pravděpodobnosti.
  • GetTopResult vybere třídu ze seznamu predikovaných tříd s nejvyšší pravděpodobností.
  • IntersectionOverUnion filtry překrývající se ohraničující rámečky s nižší pravděpodobností.

Přidejte kód pro všechny pomocné metody pod seznamem 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);
}

Jakmile definujete všechny pomocné metody, je čas je použít ke zpracování výstupu modelu.

Pod metodou IntersectionOverUnion vytvořte metodu ParseOutputs pro zpracování výstupu vygenerovaného modelem.

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

}

Vytvořte seznam pro uložení ohraničujících polí a definování proměnných uvnitř ParseOutputs metody.

var boxes = new List<YoloBoundingBox>();

Každý obrázek je rozdělený do mřížky 13 x 13 buněk. Každá buňka obsahuje pět ohraničujících polí. Pod proměnnou boxes přidejte kód pro zpracování všech polí v jednotlivých buňkách.

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++)
        {

        }
    }
}

Uvnitř vnitřní smyčky vypočítá počáteční pozici aktuálního rámečku ve výstupu jednorozměrného modelu.

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

Přímo pod tím použijte ExtractBoundingBoxDimensions metodu k získání dimenzí aktuálního ohraničujícího rámečku.

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

Pak pomocí GetConfidence metody získáte jistotu pro aktuální ohraničující rámeček.

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

Potom pomocí MapBoundingBoxToCell metody namapujte aktuální ohraničující pole na aktuální buňku, která se zpracovává.

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

Před dalším zpracováním zkontrolujte, jestli je hodnota spolehlivosti větší než zadaná prahová hodnota. Pokud ne, zpracujte další ohraničující rámeček.

if (confidence < threshold)
    continue;

V opačném případě pokračujte ve zpracování výstupu. Dalším krokem je získání rozdělení pravděpodobnosti predikovaných tříd pro aktuální ohraničující rámeček pomocí ExtractClasses metody.

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

Pak použijte GetTopResult metodu k získání hodnoty a indexu třídy s nejvyšší pravděpodobností pro aktuální pole a vypočítat jeho skóre.

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

topScore Znovu použijte možnost pro zachování pouze těch ohraničujících polí, která jsou nad zadanou prahovou hodnotou.

if (topScore < threshold)
    continue;

A konečně, pokud aktuální ohraničující rámeček překročí prahovou hodnotu, vytvořte nový BoundingBox objekt a přidejte ho boxes do seznamu.

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

Po zpracování všech buněk na obrázku vraťte boxes seznam. Přidejte následující návratový příkaz pod vnější-most for-loop v ParseOutputs metodě.

return boxes;

Filtrování překrývajících se polí

Teď, když byly ze výstupu modelu extrahovány všechny vysoce sebevědomé ohraničující rámečky, je potřeba provést další filtrování, aby se odstranily překrývající se obrázky. Přidejte metodu s názvem FilterBoundingBoxes pod metodu ParseOutputs :

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

}

FilterBoundingBoxes Uvnitř metody začněte vytvořením pole, které se rovná velikosti rozpoznaných polí a označení všech slotů jako aktivní nebo připravené ke zpracování.

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

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

Potom seřaďte seznam obsahující ohraničující pole v sestupném pořadí podle spolehlivosti.

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

Potom vytvořte seznam pro uložení filtrovaných výsledků.

var results = new List<YoloBoundingBox>();

Začněte zpracovávat jednotlivé ohraničující rámečky iterací nad jednotlivými ohraničujícími rámečky.

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

}

Uvnitř této smyčky for-loop zkontrolujte, zda je možné zpracovat aktuální ohraničující rámeček.

if (isActiveBoxes[i])
{

}

Pokud ano, přidejte ohraničující pole do seznamu výsledků. Pokud výsledky překročí zadaný limit polí, které se mají extrahovat, rozdělte smyčku. Do příkazu if přidejte následující kód.

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

if (results.Count >= limit)
    break;

Jinak se podívejte na sousední ohraničující rámečky. Pod zaškrtnutí limitu zaškrtávacího políčka přidejte následující kód.

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

}

Podobně jako u prvního pole, pokud je sousední pole aktivní nebo připravené ke zpracování, použijte IntersectionOverUnion metodu ke kontrole, zda první a druhé pole překročí zadanou prahovou hodnotu. Do nejvnitřnější smyčky for-loop přidejte následující kód.

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

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

        if (activeCount <= 0)
            break;
    }
}

Mimo vnitřní smyčku for-most, která kontroluje sousední ohraničující rámečky, zjistěte, zda existují nějaké zbývající ohraničující rámečky ke zpracování. Pokud ne, prolomte vnější smyčku for-loop.

if (activeCount <= 0)
    break;

Nakonec mimo počáteční smyčku pro metodu FilterBoundingBoxes vraťte výsledky:

return results;

Výborně! Teď je čas použít tento kód společně s modelem pro bodování.

Použití modelu k bodování

Stejně jako při následném zpracování je v bodovacím postupu několik kroků. Chcete-li s tím pomoct, přidejte do projektu třídu, která bude obsahovat logiku bodování.

  1. V Průzkumník řešení klikněte pravým tlačítkem myši na projekt a pak vyberte Přidat>novou položku.

  2. V dialogovém okně Přidat novou položku vyberte Třídu a změňte pole Název na OnnxModelScorer.cs. Pak vyberte tlačítko Přidat .

    Soubor OnnxModelScorer.cs se otevře v editoru kódu. Na začátek souboru OnnxModelScorer.cs přidejte následující using příkazy:

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

    OnnxModelScorer Do definice třídy přidejte následující proměnné.

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

    Přímo pod tím vytvořte konstruktor pro OnnxModelScorer třídu, která inicializuje dříve definované proměnné.

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

    Po vytvoření konstruktoru definujte několik struktur, které obsahují proměnné související s nastavením image a modelu. Vytvořte volanou ImageNetSettings strukturu, která bude obsahovat výšku a šířku očekávanou jako vstup pro model.

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

    Potom vytvořte další strukturu, TinyYoloModelSettings která obsahuje názvy vstupních a výstupních vrstev modelu. Pokud chcete vizualizovat název vstupní a výstupní vrstvy modelu, můžete použít nástroj, jako je 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";
    }
    

    Dále vytvořte první sadu metod, které se používají k bodování. Vytvořte metodu LoadModel uvnitř třídy OnnxModelScorer .

    private ITransformer LoadModel(string modelLocation)
    {
    
    }
    

    LoadModel Do metody přidejte následující kód pro protokolování.

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

    ML.NET kanály potřebují znát schéma dat, které se má pracovat při Fit zavolání metody. V tomto případě se použije proces podobný trénování. Vzhledem k tomu, že se neprobíhá žádné skutečné trénování, je přijatelné použít prázdný IDataView. Vytvořte nový IDataView kanál z prázdného seznamu.

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

    Pod tím definujte kanál. Kanál se skládá ze čtyř transformací.

    • LoadImages načte obrázek jako bitmapu.
    • ResizeImages rescales the image to the size specified (in this case, 416 x 416).
    • ExtractPixels změní reprezentaci obrázku z bitmapy na číselný vektor.
    • ApplyOnnxModel načte model ONNX a použije ho k určení skóre zadaných dat.

    Definujte svůj kanál v LoadModel metodě pod proměnnou 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 }));
    

    Teď je čas vytvořit instanci modelu pro bodování. Zavolejte metodu Fit kanálu a vraťte ji pro další zpracování.

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

Jakmile se model načte, můžete ho použít k předpovědím. Chcete-li tento proces usnadnit, vytvořte metodu volanou PredictDataUsingModel pod metodou LoadModel .

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

}

Do pole PredictDataUsingModel, přidejte následující kód pro protokolování.

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

Pak použijte metodu Transform k určení skóre dat.

IDataView scoredData = model.Transform(testData);

Extrahujte predikované pravděpodobnosti a vraťte je k dalšímu zpracování.

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

return probabilities;

Teď, když jsou oba kroky nastavené, je zkombinujte do jedné metody. Pod metodu PredictDataUsingModel přidejte novou metodu s názvem Score.

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

    return PredictDataUsingModel(data, model);
}

Už to bude! Teď je čas všechno použít.

Detekce objektů

Teď, když je všechna nastavení dokončená, je čas zjistit některé objekty.

Určení skóre a parsování výstupů modelu

Pod vytvoření mlContext proměnné přidejte příkaz try-catch.

try
{

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

try Uvnitř bloku začněte implementovat logiku detekce objektů. Nejprve načtěte data do objektu IDataView.

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

Pak vytvořte instanci OnnxModelScorer a použijte ji k určení skóre načtených dat.

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

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

Teď je čas na krok následného zpracování. Vytvořte instanci YoloOutputParser a použijte ji ke zpracování výstupu modelu.

YoloOutputParser parser = new YoloOutputParser();

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

Po zpracování výstupu modelu je čas nakreslit ohraničující rámečky na obrázcích.

Vizualizace předpovědí

Jakmile model vyhodnotí skóre obrázků a výstupy, musí být ohraničující rámečky na obrázku nakresleny. Uděláte to tak, že do souboru Program.cs přidáte metodu s názvem DrawBoundingBox pod metodu GetAbsolutePath.

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

}

Nejprve načtěte obrázek a získejte rozměry výšky a šířky v DrawBoundingBox metodě.

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

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

Pak vytvořte smyčku for-each, která bude iterovat přes každou ohraničující pole rozpoznaná modelem.

foreach (var box in filteredBoundingBoxes)
{

}

Uvnitř smyčky for-each získejte rozměry ohraničujícího rámečku.

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

Vzhledem k tomu, že rozměry ohraničujícího rámečku odpovídají vstupu 416 x 416modelu , škálujte rozměry ohraničujícího rámečku tak, aby odpovídaly skutečné velikosti obrázku.

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;

Pak definujte šablonu pro text, který se zobrazí nad každým ohraničujícím rámečkem. Text bude obsahovat třídu objektu uvnitř příslušného ohraničujícího pole a také spolehlivost.

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

Pokud chcete na obrázku kreslit, převeďte ho Graphics na objekt.

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

}

using Uvnitř bloku kódu vylaďte nastavení objektu grafikyGraphics.

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

Pod tím nastavte možnosti písma a barvy pro text a ohraničující pole.

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

Vytvořte a vyplňte obdélník nad ohraničujícím rámečkem, který bude obsahovat text pomocí FillRectangle metody. To pomůže s kontrastem textu a zlepšit čitelnost.

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

Potom pomocí a DrawRectangle metod nakreslete text a ohraničující pole na obrázkuDrawString.

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

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

Mimo smyčku for-each přidejte kód pro uložení obrázků v souboru outputFolder.

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

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

Pokud chcete získat další zpětnou vazbu, že aplikace provádí předpovědi podle očekávání v době běhu, přidejte metodu zvanou LogDetectedObjects pod DrawBoundingBox metodu v souboru Program.cs pro výstup rozpoznaných objektů do konzoly.

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

Teď, když máte pomocné metody pro vytvoření vizuální zpětné vazby z předpovědí, přidejte smyčku for-loop, která bude iterovat nad každou z vyhodnocených obrázků.

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

}

Uvnitř smyčky for-loop získejte název souboru obrázku a ohraničující rámečky přidružené k němu.

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

Pod tím pomocí DrawBoundingBox metody nakreslete ohraničující rámečky na obrázku.

DrawBoundingBox(imagesFolder, outputFolder, imageFileName, detectedObjects);

Nakonec použijte metodu LogDetectedObjects k výstupu předpovědí do konzoly.

LogDetectedObjects(imageFileName, detectedObjects);

Po příkazu try-catch přidejte další logiku, která indikuje, že proces je spuštěný.

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

A je to!

Výsledky

Po provedení předchozích kroků spusťte konzolovou aplikaci (Ctrl + F5). Výsledky by měly být podobné následujícímu výstupu. Může se zobrazit upozornění nebo zpracování zpráv, ale tyto zprávy byly z následujících výsledků odebrány, aby byly přehledné.

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

Pokud chcete zobrazit obrázky s ohraničujícími poli, přejděte do assets/images/output/ adresáře. Níže je ukázka z jedné z zpracovaných imagí.

Sample processed image of a dining room

Blahopřejeme! Nyní jste úspěšně vytvořili model strojového učení pro detekci objektů opětovným použitím předem natrénovaného ONNX modelu v ML.NET.

Zdrojový kód pro tento kurz najdete v úložišti dotnet/machinelearning-samples .

V tomto kurzu jste se naučili, jak:

  • Pochopení problému
  • Zjistěte, co je ONNX a jak funguje s ML.NET
  • Vysvětlení modelu
  • Opětovné použití předem natrénovaného modelu
  • Detekce objektů s načteným modelem

Podívejte se na úložiště GitHub s ukázkami Učení machine Učení a prozkoumejte rozbalenou ukázku detekce objektů.