Detección de Subimágenes en C#: Enfoques Personalizados y con AForge.NET

La habilidad de localizar una imagen más pequeña (subimagen) dentro de una imagen más grande es un requisito fundamental en diversas aplicaciones informáticas, como la automatización de procesos, el reconocimiento visual o los sistemas de segruidad. Este artículo explora dos metodologías principales para llevar a cabo esta tarea en C#: una implementación manual basada en la comparación de píxeles y una solución más optimizada que emplea la reconocida biblioteca AForge.NET.

Método 1: Búsqueda de Subimágenes con Lógica Personalizada

Un enfoque directo para encontrar una subimagen consiste en recorrer la imagen principal, probando cada posible posición de la subimagen y comparando los píxeles de esa región con los de la plantilla. Aunque este método ofrece un control total sobre el algoritmo, su rendimiento puede ser un factor limitante en escenarios con imágenes de gran tamaño o requisitos de velocidad elevados.

A continuación, se muestra cómo se podría invocar una función de búsqueda utilizando una clase de utilidad personalizada:


            var localizador = new BuscadorDeImagenes();
            var posicionesEncontradas = localizador.BuscarPlantilla(
                @"C:\ruta\a\mi\icono.png",      // Ruta de la imagen pequeña
                @"C:\ruta\a\mi\captura_pantalla.png", // Ruta de la imagen grande
                toleranciaColor: 10,            // Rango de ±10 para cada componente de color
                umbralCoincidencia: 0.90        // Requiere al menos un 90% de píxeles coincidentes
            );

            if (posicionesEncontradas.Any())
            {
                MessageBox.Show($"Plantilla encontrada en: X={posicionesEncontradas[0].X}, Y={posicionesEncontradas[0].Y}");
            }
            else
            {
                MessageBox.Show("Plantilla no detectada en la imagen principal.");
            }

La clase encargada de la búsqueda gestiona la carga de las imágenes y accede directamente a sus datos de píxeles para una comparación eficiente. El uso de Bitmap.LockBits es crucial para acelerar la lectura y escritura de píxeles, evitando la sobrecarga de los métodos GetPixel y SetPixel.


using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Linq;

public class BuscadorDeImagenes
{
    /// <summary>
    /// Realiza una búsqueda de una imagen plantilla dentro de una imagen base.
    /// </summary>
    /// <param name="rutaPlantilla"></param>Ruta completa a la imagen pequeña (plantilla) que se desea encontrar.
    /// <param name="rutaImagenBase"></param>Ruta completa a la imagen grande donde se realizará la búsqueda.
    /// <param name="toleranciaColor"></param>Margen de error (0-255) para la comparación de los componentes de color de los píxeles.
    /// <param name="areaDeBusqueda"></param>Un rectángulo que define la región de la imagen base donde se limitará la búsqueda. Si está vacío, se busca en toda la imagen.
    /// <param name="umbralCoincidencia"></param>El porcentaje mínimo (0.0 a 1.0) de píxeles que deben coincidir para considerar un hallazgo válido.
    /// <param name="encontrarTodasLasCoincidencias"></param>Indica si el método debe continuar buscando después de encontrar la primera coincidencia.
    /// <returns>Una lista de objetos Point, donde cada punto representa las coordenadas del centro de una plantilla encontrada.</returns>
    public List<point> BuscarPlantilla(
        string rutaPlantilla,
        string rutaImagenBase,
        byte toleranciaColor = 10,
        Rectangle areaDeBusqueda = default,
        double umbralCoincidencia = 0.90,
        bool encontrarTodasLasCoincidencias = false)
    {
        var resultados = new List<point>();

        using (var bmpPlantilla = new Bitmap(rutaPlantilla))
        using (var bmpBase = new Bitmap(rutaImagenBase))
        {
            if (areaDeBusqueda.IsEmpty)
            {
                areaDeBusqueda = new Rectangle(0, 0, bmpBase.Width, bmpBase.Height);
            }

            // Verificar que la plantilla no sea más grande que el área de búsqueda
            if (bmpPlantilla.Width > areaDeBusqueda.Width || bmpPlantilla.Height > areaDeBusqueda.Height)
            {
                Console.WriteLine("La imagen plantilla es más grande que el área de búsqueda definida.");
                return resultados;
            }

            // Bloquear los bits de las imágenes para un acceso rápido a la memoria de píxeles
            var datosPlantilla = bmpPlantilla.LockBits(
                new Rectangle(0, 0, bmpPlantilla.Width, bmpPlantilla.Height),
                ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
            var datosBase = bmpBase.LockBits(
                new Rectangle(0, 0, bmpBase.Width, bmpBase.Height),
                ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);

            // Copiar los datos de píxeles a arrays de bytes
            var bytesPlantilla = new byte[datosPlantilla.Stride * datosPlantilla.Height];
            var bytesBase = new byte[datosBase.Stride * datosBase.Height];
            Marshal.Copy(datosPlantilla.Scan0, bytesPlantilla, 0, bytesPlantilla.Length);
            Marshal.Copy(datosBase.Scan0, bytesBase, 0, bytesBase.Length);

            // Obtener el color del primer píxel de la plantilla para una comparación inicial rápida
            Color colorPrimerPixelPlantilla = Color.FromArgb(
                bytesPlantilla[3], bytesPlantilla[2], bytesPlantilla[1], bytesPlantilla[0]); // Formato ARGB (Little Endian)

            // Definir los límites de la búsqueda en la imagen base
            int limiteY = areaDeBusqueda.Y + areaDeBusqueda.Height - bmpPlantilla.Height;
            int limiteX = areaDeBusqueda.X + areaDeBusqueda.Width - bmpPlantilla.Width;

            // Iterar sobre cada posible posición de la esquina superior izquierda de la plantilla
            for (int yActual = areaDeBusqueda.Y; yActual < limiteY; yActual++)
            {
                for (int xActual = areaDeBusqueda.X; xActual < limiteX; xActual++)
                {
                    // Obtener el color del píxel de la imagen base en la posición actual
                    int indicePixelBase = yActual * datosBase.Stride + xActual * 4;
                    Color colorPixelBaseActual = Color.FromArgb(
                        bytesBase[indicePixelBase + 3], bytesBase[indicePixelBase + 2],
                        bytesBase[indicePixelBase + 1], bytesBase[indicePixelBase]);

                    // Si el primer píxel coincide (optimización)
                    if (CompararColoresConTolerancia(colorPixelBaseActual, colorPrimerPixelPlantilla, toleranciaColor))
                    {
                        int pixelesCoincidentes = 0;
                        int pixelesTotalesAComparar = 0;

                        // Comparar cada píxel de la plantilla con la región correspondiente de la imagen base
                        for (int subY = 0; subY < bmpPlantilla.Height; subY++)
                        {
                            for (int subX = 0; subX < bmpPlantilla.Width; subX++)
                            {
                                int indiceSub = subY * datosPlantilla.Stride + subX * 4;
                                Color colorSub = Color.FromArgb(
                                    bytesPlantilla[indiceSub + 3], bytesPlantilla[indiceSub + 2],
                                    bytesPlantilla[indiceSub + 1], bytesPlantilla[indiceSub]);

                                int yBaseComparar = yActual + subY;
                                int xBaseComparar = xActual + subX;

                                int indiceBaseComparar = yBaseComparar * datosBase.Stride + xBaseComparar * 4;
                                Color colorBaseComparar = Color.FromArgb(
                                    bytesBase[indiceBaseComparar + 3], bytesBase[indiceBaseComparar + 2],
                                    bytesBase[indiceBaseComparar + 1], bytesBase[indiceBaseComparar]);

                                pixelesTotalesAComparar++;
                                if (CompararColoresConTolerancia(colorBaseComparar, colorSub, toleranciaColor))
                                {
                                    pixelesCoincidentes++;
                                }
                            }
                        }

                        // Si el porcentaje de coincidencia supera el umbral
                        if ((double)pixelesCoincidentes / pixelesTotalesAComparar >= umbralCoincidencia)
                        {
                            Point centroEncontrado = new Point(
                                xActual + (bmpPlantilla.Width / 2),
                                yActual + (bmpPlantilla.Height / 2));

                            if (!ContienePuntoCercano(resultados, centroEncontrado, 10))
                            {
                                resultados.Add(centroEncontrado);
                            }

                            if (!encontrarTodasLasCoincidencias)
                            {
                                // Salir de los bucles una vez encontrada la primera coincidencia
                                yActual = limiteY; 
                                xActual = limiteX;
                            }
                        }
                    }
                }
            }
            
            // Desbloquear los bits de las imágenes
            bmpPlantilla.UnlockBits(datosPlantilla);
            bmpBase.UnlockBits(datosBase);
        } // Los bloques 'using' garantizan la liberación de recursos (Dispose)

        GC.Collect(); // Ayuda a la gestión de memoria, aunque 'using' ya libera recursos gráficos.
        return resultados;
    }

    /// <summary>
    /// Determina si dos colores son similares dentro de un rango de tolerancia especificado.
    /// </summary>
    private bool CompararColoresConTolerancia(Color colorA, Color colorB, byte tolerancia)
    {
        return Math.Abs(colorA.A - colorB.A) <= tolerancia &&
               Math.Abs(colorA.R - colorB.R) <= tolerancia &&
               Math.Abs(colorA.G - colorB.G) <= tolerancia &&
               Math.Abs(colorA.B - colorB.B) <= tolerancia;
    }

    /// <summary>
    /// Verifica si una lista de puntos ya contiene un punto que esté lo suficientemente cerca.
    /// Esto es útil para evitar múltiples detecciones de la misma área.
    /// </summary>
    private bool ContienePuntoCercano(List<point> listaPuntos, Point nuevoPunto, double radioError)
    {
        foreach (var pExistente in listaPuntos)
        {
            if (Math.Abs(pExistente.X - nuevoPunto.X) <= radioError &&
                Math.Abs(pExistente.Y - nuevoPunto.Y) <= radioError)
            {
                return true;
            }
        }
        return false;
    }
}
</point></point></point>

Esta implementación personalizada, aunque funcional, puede presentar limitaciones de rendimiento considerables al trabajar con imágenes de alta resolución o al requerir búsquedas muy rápidas, debido a la naturaleza iterativa de la comparación píxel a píxel.

Método 2: Búsqueda de Subimágenes con AForge.NET

Para aplicaciones que demandan mayor velocidad y eficiencia, las bibliotecas de procesamiento de imágenes como AForge.NET ofrecen algoritmos altamente optimizados. AForge.NET proporciona implementaciones avanzadas de detección de plantillas que superan con creces el rendimiento de las soluciones manuales.

Para integrar AForge.NET en tu proyecto, es necesario añadir las referencias a los paquetes NuGet AForge, AForge.Imaging y AForge.Imaging.Formats.

A continuación, se muestra un ejemplo de cómo utilizar la clase ExhaustiveTemplateMatching de AForge.NET para encontrar una subimagen:


using System;
using System.Drawing;
using System.Drawing.Imaging;
using AForge.Imaging;
using AForge.Imaging.Filters; // Para AForge.Imaging.Drawing (métodos de dibujo)

// ... dentro de un contexto adecuado, por ejemplo, un método de un formulario WinForms ...

private void ManejarBusquedaAForge()
{
    // Cargar y asegurar el formato de píxel adecuado para AForge (ej. 24bppRgb)
    Bitmap imagenFondo = CargarYConvertirBitmap(@"C:\ruta\a\mi\escenario.png", PixelFormat.Format24bppRgb);
    Bitmap imagenIcono = CargarYConvertirBitmap(@"C:\ruta\a\mi\boton.png", PixelFormat.Format24bppRgb);

    // Configurar el algoritmo de detección de plantillas con un umbral de similitud
    // Un valor de 0.92f significa 92% de similitud.
    ExhaustiveTemplateMatching detectorPlantillas = new ExhaustiveTemplateMatching(0.92f);

    // Ejecutar el proceso de búsqueda en la imagen principal
    TemplateMatch[] coincidenciasEncontradas = detectorPlantillas.ProcessImage(imagenFondo, imagenIcono);

    if (coincidenciasEncontradas.Length > 0)
    {
        // Opcional: Bloquear bits de la imagen principal para dibujar sobre ella eficientemente
        BitmapData datosBitmapFondo = imagenFondo.LockBits(
            new Rectangle(0, 0, imagenFondo.Width, imagenFondo.Height),
            ImageLockMode.ReadWrite, imagenFondo.PixelFormat);

        foreach (TemplateMatch coincidencia in coincidenciasEncontradas)
        {
            // Dibujar un rectángulo verde alrededor de cada área encontrada
            AForge.Imaging.Drawing.Rectangle(datosBitmapFondo, coincidencia.Rectangle, Color.Green);

            // Mostrar la ubicación de la coincidencia
            MessageBox.Show($"Coincidencia de AForge encontrada en: {coincidencia.Rectangle.Location}");
            // 'coincidencia' también contiene el valor de similitud ('Similarity')
        }
        imagenFondo.UnlockBits(datosBitmapFondo);

        // Si se desea visualizar la imagen con los rectángulos, se puede guardar o mostrar
        // imagenFondo.Save("escenario_con_detecciones.png");
    }
    else
    {
        MessageBox.Show("AForge.NET no encontró la plantilla en la imagen.");
    }
    
    // Liberar los recursos de los objetos Bitmap
    imagenFondo.Dispose();
    imagenIcono.Dispose();
}

/// <summary>
/// Carga una imagen desde un archivo y la convierte al formato de píxel especificado.
/// Esto es útil porque AForge.NET a menudo requiere formatos de píxel específicos.
/// </summary>
private Bitmap CargarYConvertirBitmap(string rutaArchivo, PixelFormat formatoObjetivo)
{
    using (Image imagenOriginal = Image.FromFile(rutaArchivo))
    {
        Bitmap bitmapConvertido = new Bitmap(imagenOriginal.Width, imagenOriginal.Height, formatoObjetivo);
        using (Graphics g = Graphics.FromImage(bitmapConvertido))
        {
            g.DrawImage(imagenOriginal, new Rectangle(0, 0, bitmapConvertido.Width, bitmapConvertido.Height));
        }
        return bitmapConvertido;
    }
}

El uso de AForge.NET simplifica enormemente el proceso de detección de plantillas, abstrayendo la complejidad de la manipulación de píxeles y ofreciendo una solución de alto rendimiento.

Etiquetas: C# procesamiento de imágenes AForge.NET Detección de Plantillas Manipulación de Píxeles

Publicado el 7-19 12:19