Compartir vía


Tutorial: Detección de objetos mediante ONNX en ML.NET

Obtenga información sobre cómo usar un modelo ONNX previamente entrenado en ML.NET para detectar objetos en imágenes.

El entrenamiento de un modelo de detección de objetos desde cero requiere establecer millones de parámetros, una gran cantidad de datos de entrenamiento etiquetados y una gran cantidad de recursos de proceso (cientos de horas de GPU). Usar un modelo preentrenado permite acortar el proceso de entrenamiento.

En este tutorial, aprenderá a:

  • Comprender el problema
  • Obtenga información sobre qué es ONNX y cómo funciona con ML.NET
  • Descripción del modelo
  • Reutiliza el modelo entrenado previamente
  • Detección de objetos con un modelo cargado

Prerrequisitos

Introducción al ejemplo de detección de objetos ONNX

En este ejemplo se crea una aplicación de consola de .NET Core que detecta objetos dentro de una imagen mediante un modelo ONNX de aprendizaje profundo previamente entrenado. El código de este ejemplo se puede encontrar en el repositorio dotnet/machinelearning-samples en GitHub.

¿Qué es la detección de objetos?

La detección de objetos es un problema de computer vision. Aunque está estrechamente relacionado con la clasificación de imágenes, la detección de objetos realiza la clasificación de imágenes a una escala más granular. La detección de objetos localiza y clasifica las entidades dentro de las imágenes. Los modelos de detección de objetos suelen entrenarse mediante redes neuronales y aprendizaje profundo. Consulte Aprendizaje profundo frente al aprendizaje automático para obtener más información.

Use la detección de objetos cuando las imágenes contengan varios objetos de tipos diferentes.

Capturas de pantalla que muestran la clasificación de imágenes frente a la clasificación de objetos.

Algunos casos de uso para la detección de objetos incluyen:

  • Coches Autónomos
  • Robótica
  • Detección de caras
  • Seguridad en el lugar de trabajo
  • Recuento de objetos
  • Reconocimiento de actividad

Selección de un modelo de aprendizaje profundo

El aprendizaje profundo es un subconjunto del aprendizaje automático. Para entrenar modelos de aprendizaje profundo, se requieren grandes cantidades de datos. Los patrones de los datos se representan mediante una serie de capas. Las relaciones de los datos se codifican como conexiones entre las capas que contienen pesos. Cuanto mayor sea el peso, más fuerte será la relación. En conjunto, esta serie de capas y conexiones se conocen como redes neuronales artificiales. Cuantos más capas de una red, más profunda es, lo que lo convierte en una red neuronal profunda.

Hay diferentes tipos de redes neuronales, la más común es perceptrón multicapa (MLP), red neuronal convolucional (CNN) y red neuronal recurrente (RNN). El más básico es el MLP, que asigna un conjunto de entradas a un conjunto de salidas. Esta red neuronal es buena cuando los datos no tienen un componente espacial o de tiempo. La CNN usa capas convolucionales para procesar información espacial contenida en los datos. Un buen caso de uso para los CNN es el procesamiento de imágenes para detectar la presencia de una característica en una región de una imagen (por ejemplo, ¿hay una nariz en el centro de una imagen?). Por último, los RNN permiten que la persistencia del estado o la memoria puedan usarse como entrada. Los RNN se usan para el análisis de series temporales, donde es importante el orden secuencial y el contexto de los eventos.

Descripción del modelo

La detección de objetos es una tarea de procesamiento de imágenes. Por consiguiente, la mayoría de los modelos de aprendizaje profundo entrenados para resolver este problema son redes neuronales convolucionales (CNN). El modelo usado en este tutorial es el modelo Tiny YOLOv2, una versión más compacta del modelo YOLOv2 descrito en el documento: "YOLO9000: Better, Faster, Stronger" de Redmon y Farhadi. Tiny YOLOv2 se entrena en el conjunto de datos Pascal VOC y se compone de 15 capas que pueden predecir 20 clases diferentes de objetos. Dado que Tiny YOLOv2 es una versión condensada del modelo YOLOv2 original, se produce un equilibrio entre la velocidad y la precisión. Las distintas capas que componen el modelo se pueden visualizar mediante herramientas como Netron. La inspección del modelo produciría una asignación de las conexiones entre todas las capas que componen la red neuronal, donde cada capa contendrá el nombre de la capa junto con las dimensiones de la entrada y salida correspondientes. Las estructuras de datos usadas para describir las entradas y salidas del modelo se conocen como tensores. Los tensores se pueden considerar como contenedores que almacenan datos en N dimensiones. En el caso de Tiny YOLOv2, el nombre de la capa de entrada es image y espera un tensor de dimensiones 3 x 416 x 416. El nombre de la capa de salida es grid y genera un tensor de salida de dimensiones 125 x 13 x 13.

Capa de entrada que se divide en capas ocultas y, a continuación, capa de salida

El modelo YOLO toma una imagen 3(RGB) x 416px x 416px. El modelo toma esta entrada y la pasa a través de las distintas capas para generar una salida. La salida divide la imagen de entrada en una 13 x 13 cuadrícula, con cada celda de la cuadrícula compuesta por 125 valores.

¿Qué es un modelo ONNX?

Open Neural Network Exchange (ONNX) es un formato de código abierto para los modelos de IA. ONNX admite la interoperabilidad entre marcos. Esto significa que puede entrenar un modelo en uno de los muchos marcos de aprendizaje automático populares, como PyTorch, convertirlo en formato ONNX y consumir el modelo ONNX en un marco diferente, como ML.NET. Para obtener más información, visite el sitio web de ONNX.

Diagrama de los formatos admitidos de ONNX que se usan.

El modelo tiny YOLOv2 previamente entrenado se almacena en formato ONNX, una representación serializada de las capas y patrones aprendidos de esas capas. En ML.NET, la interoperabilidad con ONNX se logra con los paquetes ImageAnalytics NuGetOnnxTransformer. El ImageAnalytics paquete contiene una serie de transformaciones que toman una imagen y la codifican en valores numéricos que se pueden usar como entrada en una canalización de predicción o entrenamiento. El OnnxTransformer paquete aprovecha el entorno de ejecución de ONNX para cargar un modelo ONNX y usarlo para realizar predicciones basadas en la entrada proporcionada.

Flujo de datos del archivo ONNX en el entorno de ejecución de ONNX.

Configuración del proyecto de consola de .NET

Ahora que tiene un conocimiento general de lo que es ONNX y cómo funciona Tiny YOLOv2, es el momento de compilar la aplicación.

Creación de una aplicación de consola

  1. Cree una aplicación de consola de C# denominada "ObjectDetection". Haga clic en el botón Siguiente .

  2. Elija .NET 8 como marco de trabajo que se va a usar. Haga clic en el botón Crear.

  3. Instale el paquete NuGet de Microsoft.ML:

    Nota:

    En este ejemplo se usa la versión estable más reciente de los paquetes NuGet mencionados a menos que se indique lo contrario.

    • En el Explorador de soluciones, haga clic con el botón derecho en el proyecto y seleccione Administrar paquetes NuGet.
    • Elija "nuget.org" como origen del paquete, seleccione la pestaña Examinar y busque Microsoft.ML.
    • Seleccione el botón Instalar .
    • Seleccione el botón Aceptar en el cuadro de diálogo Vista previa de cambios y, a continuación, seleccione el botón Acepto en el cuadro de diálogo Aceptación de licencia si está de acuerdo con los términos de licencia de los paquetes enumerados.
    • Repita estos pasos para Microsoft.Windows.Compatibility, Microsoft.ML.ImageAnalytics, Microsoft.ML.OnnxTransformer y Microsoft.ML.OnnxRuntime.

Preparación de los datos y el modelo entrenado previamente

  1. Descargue el archivo ZIP del directorio de recursos del proyecto y descomprima.

  2. Copie el directorio assets en su directorio del proyecto ObjectDetection. Este directorio y sus subdirectorios contienen los archivos de imagen (excepto el modelo Tiny YOLOv2, que descargará y agregará en el paso siguiente) necesario para este tutorial.

  3. Descarga el modelo Tiny YOLOv2 del ONNX Model Zoo.

  4. Copie el archivo en el model.onnx directorio del proyecto assets\Model y cámbielo por TinyYolo2_model.onnx. Este directorio contiene el modelo necesario para este tutorial.

  5. En el Explorador de soluciones, haga clic con el botón derecho en cada uno de los archivos del directorio de recursos y subdirectorios y seleccione Propiedades. En Avanzado, cambie el valor de Copiar al directorio de salida a Copiar si es más reciente.

Creación de clases y definición de rutas de acceso

Abra el archivo Program.cs y agregue las siguientes directivas adicionales using a la parte superior del archivo:

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

A continuación, defina las rutas de acceso de los distintos recursos.

  1. En primer lugar, cree el GetAbsolutePath método en la parte inferior del archivo 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. A continuación, debajo de las using directivas, cree campos para almacenar la ubicación de los recursos.

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

Agregue un nuevo directorio al proyecto para almacenar los datos de entrada y las clases de predicción.

En el Explorador de soluciones, haga clic con el botón derecho en el proyecto y seleccione Agregar>nueva carpeta. Cuando aparezca la nueva carpeta en el Explorador de soluciones, asígnela el nombre "DataStructures".

Cree la clase de datos de entrada en el directorio DataStructures recién creado.

  1. En el Explorador de soluciones, haga clic con el botón derecho en el directorio DataStructures y seleccione Agregar>nuevo elemento.

  2. En el cuadro de diálogo Agregar nuevo elemento , seleccione Clase y cambie el campo Nombre a ImageNetData.cs. A continuación, seleccione Agregar.

    El archivo ImageNetData.cs se abre en el editor de código. Agregue la siguiente using directiva a la parte superior de ImageNetData.cs:

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

    Quite la definición de clase existente y agregue el código siguiente para la ImageNetData clase al archivo 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 es la clase de datos de imagen de entrada y tiene los siguientes String campos:

    • ImagePath contiene la ruta de acceso donde se almacena la imagen.
    • Label contiene el nombre del archivo.

    Además, ImageNetData contiene un método ReadFromFile que carga varios archivos de imagen almacenados en la imageFolder ruta de acceso especificada y los devuelve como una colección de ImageNetData objetos.

Cree la clase de predicción en el directorio DataStructures .

  1. En el Explorador de soluciones, haga clic con el botón derecho en el directorio DataStructures y seleccione Agregar>nuevo elemento.

  2. En el cuadro de diálogo Agregar nuevo elemento , seleccione Clase y cambie el campo Nombre a ImageNetPrediction.cs. A continuación, seleccione Agregar.

    El archivo ImageNetPrediction.cs se abre en el editor de código. Agregue la siguiente using directiva a la parte superior de ImageNetPrediction.cs:

    using Microsoft.ML.Data;
    

    Quite la definición de clase existente y agregue el código siguiente para la ImageNetPrediction clase al archivo ImageNetPrediction.cs :

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

    ImageNetPrediction es la clase de datos de predicción y tiene el siguiente float[] campo:

    • PredictedLabels contiene las dimensiones, la puntuación de existencia del objeto y las probabilidades de las clases para cada una de las cajas delimitadoras detectadas en una imagen.

Inicialización de variables

La clase MLContext es un punto de partida para todas las operaciones de ML.NET e inicializar mlContext crea un nuevo entorno de ML.NET que se puede compartir entre los objetos de flujo de trabajo de creación de modelos. Es similar, conceptualmente, a DBContext en Entity Framework.

Inicialice la mlContext variable con una nueva instancia de MLContext agregando la siguiente línea debajo del outputFolder campo.

MLContext mlContext = new MLContext();

Creación de un analizador para salidas del modelo posterior al proceso

El modelo segmenta una imagen en una 13 x 13 cuadrícula, donde cada celda de cuadrícula es 32px x 32px. Cada celda de cuadrícula contiene 5 posibles cajas delimitadoras de objetos. Una caja delimitadora tiene 25 elementos.

Ejemplo de cuadrícula a la izquierda y Ejemplo de rectángulo delimitador a la derecha

  • x la posición x del centro del rectángulo delimitador en relación con la celda de cuadrícula a la que está asociada.
  • y la posición y del centro del cuadro delimitador con respecto a la celda de cuadrícula con la que está asociada.
  • w ancho de la caja delimitadora.
  • h la altura de la caja delimitadora.
  • o el valor de confianza de que un objeto existe dentro del cuadro delimitador, también conocido como puntuación de objetividad.
  • p1-p20 probabilidades de clase para cada una de las 20 clases previstas por el modelo.

En total, los 25 elementos que describen cada una de las 5 cajas delimitadoras componen los 125 elementos contenidos en cada celda de la cuadrícula.

La salida generada por el modelo ONNX previamente entrenado es una matriz flotante de longitud 21125, que representa los elementos de un tensor con dimensiones 125 x 13 x 13. Para transformar las predicciones generadas por el modelo en un tensor, se requiere un trabajo posterior al procesamiento. Para ello, cree un conjunto de clases para ayudar a analizar la salida.

Agregue un nuevo directorio al proyecto para organizar el conjunto de clases del analizador.

  1. En el Explorador de soluciones, haga clic con el botón derecho en el proyecto y seleccione Agregar>nueva carpeta. Cuando aparezca la nueva carpeta en el Explorador de soluciones, asígnelo el nombre "YoloParser".

Crear cuadros delimitadores y dimensiones

La salida de datos del modelo contiene coordenadas y dimensiones de las cajas delimitadoras de los objetos dentro de la imagen. Cree una clase base para dimensiones.

  1. En el Explorador de soluciones, haga clic con el botón derecho en el directorio YoloParser y seleccione Agregar>nuevo elemento.

  2. En el cuadro de diálogo Agregar nuevo elemento , seleccione Clase y cambie el campo Nombre a DimensionsBase.cs. A continuación, seleccione Agregar.

    El archivo DimensionsBase.cs se abre en el editor de código. Quite todas las using directivas y la definición de clase existente.

    Agregue el código siguiente para la DimensionsBase clase al archivo 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 tiene las siguientes float propiedades:

    • X contiene la posición del objeto a lo largo del eje X.
    • Y contiene la posición del objeto a lo largo del eje Y.
    • Height contiene el alto del objeto .
    • Width contiene el ancho del objeto .

A continuación, cree una clase para las cajas delimitadoras.

  1. En el Explorador de soluciones, haga clic con el botón derecho en el directorio YoloParser y seleccione Agregar>nuevo elemento.

  2. En el cuadro de diálogo Agregar nuevo elemento , seleccione Clase y cambie el campo Nombre a YoloBoundingBox.cs. A continuación, seleccione Agregar.

    El archivo YoloBoundingBox.cs se abre en el editor de código. Agregue la siguiente using directiva a la parte superior de YoloBoundingBox.cs:

    using System.Drawing;
    

    Justo encima de la definición de clase existente, agregue una nueva definición de clase denominada BoundingBoxDimensions que herede de la DimensionsBase clase para contener las dimensiones del cuadro de límite correspondiente.

    public class BoundingBoxDimensions : DimensionsBase { }
    

    Quite la definición de clase existente YoloBoundingBox y agregue el código siguiente para la YoloBoundingBox clase al archivo 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 tiene las siguientes propiedades:

    • Dimensions contiene dimensiones de la caja delimitadora.
    • Label contiene la clase de objeto detectado dentro del cuadro de límite.
    • Confidence contiene la confianza de la clase .
    • Rect contiene la representación del rectángulo de las dimensiones del cuadro delimitador.
    • BoxColor contiene el color asociado a la clase correspondiente que se usa para dibujar en la imagen.

Creación del analizador

Ahora que se crean las clases para dimensiones y cuadros de límite, es el momento de crear el analizador.

  1. En el Explorador de soluciones, haga clic con el botón derecho en el directorio YoloParser y seleccione Agregar>nuevo elemento.

  2. En el cuadro de diálogo Agregar nuevo elemento , seleccione Clase y cambie el campo Nombre a YoloOutputParser.cs. A continuación, seleccione Agregar.

    El archivo YoloOutputParser.cs se abre en el editor de código. Agregue las siguientes using directivas a la parte superior de YoloOutputParser.cs:

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

    Dentro de la definición de clase existente YoloOutputParser , agregue una clase anidada que contenga las dimensiones de cada una de las celdas de la imagen. Agregue el siguiente código para la clase CellDimensions que hereda de la clase DimensionsBase en la parte superior de la definición de la clase YoloOutputParser.

    class CellDimensions : DimensionsBase { }
    
  3. Dentro de la definición de clase YoloOutputParser , agregue las siguientes constantes y campos.

    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 es el número de filas de la cuadrícula en la que se divide la imagen.
    • COL_COUNT es el número de columnas de la cuadrícula en la que se divide la imagen.
    • CHANNEL_COUNT es el número total de valores contenidos en una celda de la cuadrícula.
    • BOXES_PER_CELL es el número de cajas delimitadoras en una celda,
    • BOX_INFO_FEATURE_COUNT es la cantidad de características contenidas en un cuadro (x,y,altura,anchura,confianza).
    • CLASS_COUNT es el número de predicciones de clase contenidas en cada cuadro delimitador.
    • CELL_WIDTH es el ancho de una celda de la cuadrícula de imágenes.
    • CELL_HEIGHT es la altura de una celda en la cuadrícula de imágenes.
    • channelStride es la posición inicial de la celda actual de la cuadrícula.

    Cuando el modelo realiza una predicción, también conocida como puntuación, divide la 416px x 416px imagen de entrada en una cuadrícula de celdas el tamaño de 13 x 13. Cada celda contiene es 32px x 32px. Dentro de cada celda, hay 5 cajas delimitadoras, cada una contiene 5 características (x, y, ancho, alto, confianza). Además, cada cuadro de límite contiene la probabilidad asociada a cada una de las clases, que en este caso son 20. Por lo tanto, cada celda contiene 125 fragmentos de información (5 características + 20 probabilidades de clase).

Cree una lista de delimitadores a continuación channelStride para los 5 cuadros de límite:

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

Las anclas son proporciones predefinidas de alto y ancho de cajas delimitadoras. La mayoría de los objetos o clases detectados por un modelo tienen proporciones similares. Esto es valioso cuando se trata de crear cajas delimitadoras. En lugar de predecir los cuadros de límite, se calcula el desplazamiento de las dimensiones predefinidas, lo que reduce el cálculo necesario para predecir el cuadro de límite. Normalmente, estas relaciones de anclaje se calculan en función del conjunto de datos usado. En este caso, dado que el conjunto de datos se conoce y los valores se han precomputizado, los delimitadores se pueden codificar de forma rígida.

A continuación, defina las etiquetas o clases que predecirá el modelo. Este modelo predice 20 clases, que es un subconjunto del número total de clases previstas por el modelo YOLOv2 original.

Agregue la lista de etiquetas debajo de .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"
};

Hay colores asociados a cada una de las clases. Asigne los colores de clase debajo de 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
};

Creación de funciones auxiliares

Hay una serie de pasos implicados en la fase posterior al procesamiento. Para ayudar con eso, se pueden emplear varios métodos auxiliares.

Los métodos auxiliares utilizados en por el analizador son:

  • Sigmoid aplica la función sigmoid que genera un número entre 0 y 1.
  • Softmax normaliza un vector de entrada en una distribución de probabilidad.
  • GetOffset asigna los elementos de la salida del modelo unidimensional a la posición correspondiente en un 125 x 13 x 13 tensor.
  • ExtractBoundingBoxes extrae las dimensiones de la caja delimitadora de la salida del modelo mediante el método GetOffset.
  • GetConfidence extrae el valor de confianza que indica la certeza de que el modelo es que ha detectado un objeto y usa la Sigmoid función para convertirlo en un porcentaje.
  • MapBoundingBoxToCell utiliza las dimensiones de la caja delimitadora y las asigna a la celda correspondiente dentro de la imagen.
  • ExtractClasses extrae las predicciones de clase para el cuadro delimitador de la salida del modelo usando el método GetOffset y las convierte en una distribución de probabilidad usando el método Softmax.
  • GetTopResult selecciona la clase de la lista de clases predichas con la probabilidad más alta.
  • IntersectionOverUnion filtra los cuadros de límite superpuestos con probabilidades más bajas.

Agregue el código para todos los métodos auxiliares debajo de la lista de 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);
}

Una vez que haya definido todos los métodos auxiliares, es el momento de usarlos para procesar la salida del modelo.

Debajo del IntersectionOverUnion método , cree el ParseOutputs método para procesar la salida generada por el modelo.

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

}

Cree una lista para almacenar sus cajas delimitadoras y defina variables dentro del ParseOutputs método.

var boxes = new List<YoloBoundingBox>();

Cada imagen se divide en una cuadrícula de 13 x 13 celdas. Cada celda contiene cinco cajas delimitadoras. Debajo de la boxes variable, agregue código para procesar todos los cuadros de cada una de las celdas.

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

        }
    }
}

Dentro del bucle más interno, calcule la posición inicial del cuadro actual dentro de la salida del modelo unidimensional.

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

Directamente debajo, use el ExtractBoundingBoxDimensions método para obtener las dimensiones del cuadro de límite actual.

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

A continuación, use el GetConfidence método para obtener la confianza del cuadro de límite actual.

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

Después, use el MapBoundingBoxToCell método para asignar el cuadro de límite actual a la celda actual que se está procesando.

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

Antes de realizar cualquier procesamiento adicional, compruebe si el valor de confianza es mayor que el umbral proporcionado. Si no es así, procesa el siguiente cuadro delimitador.

if (confidence < threshold)
    continue;

De lo contrario, continúe procesando la salida. El siguiente paso es obtener la distribución de probabilidad de las clases predichas para el cuadro de límite actual mediante el método ExtractClasses.

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

A continuación, use el GetTopResult método para obtener el valor y el índice de la clase con la mayor probabilidad del cuadro actual y calcular su puntuación.

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

Usa el topScore para mantener de nuevo solo los cuadros de límite que están por encima del umbral especificado.

if (topScore < threshold)
    continue;

Por último, si el cuadro de límite actual supera el umbral, cree un objeto BoundingBox nuevo y agréguelo a la lista 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]
});

Una vez procesadas todas las celdas de la imagen, devuelva la boxes lista. Agregue la siguiente instrucción return debajo del bucle for más externo en el método ParseOutputs.

return boxes;

Filtrar cuadros superpuestos

Ahora que se han extraído todos los cuadros delimitadores de alta confianza de la salida del modelo, es necesario realizar un filtrado adicional para eliminar las imágenes superpuestas. Agregue un método llamado FilterBoundingBoxes debajo del ParseOutputs método :

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

}

Dentro del FilterBoundingBoxes método, empiece creando una matriz igual al tamaño de los cuadros detectados y marcando todas las ranuras como activas o listas para su procesamiento.

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

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

A continuación, ordene la lista que contiene los cuadros de límite en orden descendente en función de la confianza.

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

Después, cree una lista para contener los resultados filtrados.

var results = new List<YoloBoundingBox>();

Comience a procesar cada cuadro delimitador iterando sobre cada uno de los cuadros delimitadores.

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

}

Dentro de este bucle for, verifique si se puede procesar la caja delimitadora actual.

if (isActiveBoxes[i])
{

}

Si es así, agregue el cuadro delimitador a la lista de resultados. Si los resultados superan el límite especificado de cuadros que se van a extraer, interrumpa el bucle. Agregue el código siguiente dentro de la instrucción if.

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

if (results.Count >= limit)
    break;

De lo contrario, examine las cajas delimitadoras adyacentes. Agregue el código siguiente debajo de la casilla límite.

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

}

Al igual que el primer cuadro, si el cuadro adyacente está activo o listo para procesarse, use el IntersectionOverUnion método para comprobar si la primera casilla y la segunda superan el umbral especificado. Añada el siguiente código al bucle for más interno.

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

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

        if (activeCount <= 0)
            break;
    }
}

Fuera del bucle for-most interno que comprueba los cuadros de límite adyacentes, vea si hay otros cuadros de límite que se van a procesar. Si no es así, salga del bucle for externo.

if (activeCount <= 0)
    break;

Por último, fuera del bucle for inicial del método FilterBoundingBoxes, devuelve los resultados:

return results;

¡Bien! Ahora es el momento de usar este código junto con el modelo para la puntuación.

Uso del modelo para la puntuación

Al igual que con el posprocesamiento, hay algunas etapas en el proceso de puntuación. Para ayudar con esto, agregue una clase que contendrá la lógica de puntuación al proyecto.

  1. En el Explorador de soluciones, haga clic con el botón derecho en el proyecto y seleccione Agregar>nuevo elemento.

  2. En el cuadro de diálogo Agregar nuevo elemento , seleccione Clase y cambie el campo Nombre a OnnxModelScorer.cs. A continuación, seleccione Agregar.

    El archivo OnnxModelScorer.cs se abre en el editor de código. Agregue las siguientes using directivas a la parte superior de OnnxModelScorer.cs:

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

    Dentro de la definición de clase OnnxModelScorer , agregue las siguientes variables.

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

    Directamente debajo, cree un constructor para la OnnxModelScorer clase que inicializará las variables definidas anteriormente.

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

    Una vez creado el constructor, defina un par de estructuras que contengan variables relacionadas con la imagen y la configuración del modelo. Cree una estructura denominada ImageNetSettings para contener el alto y el ancho esperados como entrada para el modelo.

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

    Después, cree otra estructura denominada TinyYoloModelSettings que contenga los nombres de las capas de entrada y salida del modelo. Para visualizar el nombre de las capas de entrada y salida del modelo, puede usar una herramienta como 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";
    }
    

    A continuación, cree el primer conjunto de métodos que se usan para la puntuación. Cree el LoadModel método dentro de la OnnxModelScorer clase .

    private ITransformer LoadModel(string modelLocation)
    {
    
    }
    

    Dentro del LoadModel método , agregue el código siguiente para el registro.

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

    Las canalizaciones de ML.NET deben conocer el esquema de datos en el que operar cuando se llama al método Fit. En este caso, se usará un proceso similar al entrenamiento. Sin embargo, dado que no se está realizando ningún entrenamiento real, es aceptable utilizar un IDataView vacío. Cree un nuevo IDataView para la canalización a partir de una lista vacía.

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

    Debajo, defina la canalización. La canalización constará de cuatro transformaciones.

    • LoadImages carga la imagen como un mapa de bits.
    • ResizeImages vuelve a escalar la imagen al tamaño especificado (en este caso, 416 x 416).
    • ExtractPixels cambia la representación de píxeles de la imagen de un mapa de bits a un vector numérico.
    • ApplyOnnxModel carga el modelo ONNX y lo usa para puntuar en los datos proporcionados.

    Defina la canalización en el LoadModel método debajo de la data variable .

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

    Ahora es el momento de crear una instancia del modelo para el cálculo de puntuaciones. Llame al Fit método en la canalización y vuelva a él para su posterior procesamiento.

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

Una vez cargado el modelo, se puede usar para realizar predicciones. Para facilitar ese proceso, cree un método denominado PredictDataUsingModel debajo del LoadModel método .

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

}

Dentro de PredictDataUsingModel, agregue el código siguiente para el registro.

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

A continuación, use el Transform método para puntuar los datos.

IDataView scoredData = model.Transform(testData);

Extraiga las probabilidades predichas y devuelvalas para su procesamiento adicional.

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

return probabilities;

Ahora que ambos pasos están configurados, combínelos en un único método. Debajo del PredictDataUsingModel método , agregue un nuevo método denominado Score.

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

    return PredictDataUsingModel(data, model);
}

¡Casi ahí! Ahora es el momento de ponerlo todo en uso.

Detección de objetos

Ahora que se ha completado toda la configuración, es el momento de detectar algunos objetos.

Puntuar y analizar las salidas del modelo

Debajo de la creación de la variable mlContext, agregue una sentencia try-catch.

try
{

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

Dentro del try bloque, empiece a implementar la lógica de detección de objetos. En primer lugar, cargue los datos en un IDataView.

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

A continuación, cree una instancia de OnnxModelScorer y úsela para puntuar los datos cargados.

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

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

Ahora es el momento del paso posterior al procesamiento. Cree una instancia de YoloOutputParser y úsela para procesar la salida del modelo.

YoloOutputParser parser = new YoloOutputParser();

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

Una vez procesada la salida del modelo, es el momento de dibujar los cuadros de límite en las imágenes.

Visualización de predicciones

Una vez que el modelo haya puntuado las imágenes y se hayan procesado las salidas, se deben dibujar los rectángulos delimitadores en la imagen. Para ello, agregue un método denominado DrawBoundingBox debajo del GetAbsolutePath método dentro de Program.cs.

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

}

En primer lugar, cargue la imagen y obtenga las dimensiones de alto y ancho en el DrawBoundingBox método .

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

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

A continuación, cree un bucle for-each para recorrer en iteración cada uno de los cuadros de límite detectados por el modelo.

foreach (var box in filteredBoundingBoxes)
{

}

Dentro del bucle for-each, obtenga las dimensiones de la caja delimitadora.

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

Dado que las dimensiones del cuadro delimitador corresponden a la entrada del modelo de 416 x 416, ajuste las dimensiones del cuadro delimitador para que se correspondan con el tamaño real de la imagen.

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;

A continuación, defina una plantilla para el texto que aparecerá encima de cada caja delimitadora. El texto contendrá la clase del objeto dentro del cuadro delimitador correspondiente, así como el nivel de confianza.

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

Para dibujar en la imagen, conviértalo en un Graphics objeto .

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

}

Dentro del using bloque de código, ajuste la configuración del objeto del Graphics gráfico.

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

A continuación, establezca las opciones de fuente y color para el texto y el cuadro delimitador.

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

Cree y rellene un rectángulo encima de la caja delimitadora para contener el texto mediante el método FillRectangle. Esto ayudará a contrastar el texto y mejorar la legibilidad.

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

A continuación, dibuje el texto y el cuadro delimitador en la imagen mediante los métodos DrawString y DrawRectangle.

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

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

Fuera del bucle for-each, agregue código para guardar las imágenes en outputFolder.

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

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

Para obtener comentarios adicionales que la aplicación realiza predicciones según lo previsto en tiempo de ejecución, agregue un método denominado LogDetectedObjects debajo del DrawBoundingBox método en el archivo Program.cs para generar los objetos detectados en la consola.

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

Ahora que tiene métodos auxiliares para crear retroalimentación visual a partir de las predicciones, agregue un bucle for para recorrer cada una de las imágenes puntuadas.

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

}

Dentro del bucle for, obtenga el nombre del archivo de imagen y los cuadros de límite asociados.

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

A continuación, use el método DrawBoundingBox para dibujar las cajas de delimitación en la imagen.

DrawBoundingBox(imagesFolder, outputFolder, imageFileName, detectedObjects);

Por último, use el LogDetectedObjects método para generar predicciones en la consola.

LogDetectedObjects(imageFileName, detectedObjects);

Después de la declaración try-catch, agregue lógica adicional para indicar que el proceso ha terminado de ejecutarse.

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

Eso es todo.

Results

Después de seguir los pasos anteriores, ejecute la aplicación de consola (Ctrl + F5). Los resultados deben ser similares a los siguientes resultados. Es posible que vea advertencias o mensajes de procesamiento, pero estos mensajes se han quitado de los siguientes resultados para mayor claridad.

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

Para ver las imágenes con cuadros de límite, vaya al directorio assets/images/output/. A continuación se muestra un ejemplo de una de las imágenes procesadas.

Imagen procesada de ejemplo de un comedor

¡Felicidades! Ahora ha creado correctamente un modelo de aprendizaje automático para la detección de objetos mediante la reutilización de un modelo entrenado previamente ONNX en ML.NET.

Puede encontrar el código fuente de este tutorial en el repositorio dotnet/machinelearning-samples .

En este tutorial, ha aprendido a:

  • Comprender el problema
  • Obtenga información sobre qué es ONNX y cómo funciona con ML.NET
  • Descripción del modelo
  • Reutilización del modelo entrenado previamente
  • Detección de objetos con un modelo cargado

Consulte el repositorio de GitHub de ejemplos de Machine Learning para explorar un ejemplo de detección de objetos expandido.