
Compartir:
Antiguo desarrollador .NET Advocate @Vonage, ingeniero de software poliglota full-stack, AI/ML
Detección de rostros en tiempo real en .NET con OpenCV y Vonage Video API
Tiempo de lectura: 12 minutos
Nota: Es posible que algunas de las herramientas o métodos descritos en este artículo ya no reciban soporte o no estén actualizados. Para obtener contenido actualizado o soporte, consulta nuestras últimas publicaciones o contáctanos en el Slack de la comunidad de Vonage
La visión por ordenador es mi campo favorito de la informática. Combina mis cuatro asignaturas favoritas -Programación, Álgebra Lineal, Probabilidades y Cálculo- en algo práctico y potente. En este artículo, vamos a ver una aplicación interesante de la visión por computador, la detección de rostros, e integrar esta función en una OpenTok Windows Presentation Framework(WPF) App.
Línea de base
Para ayudarnos a empezar, trabajaremos a partir del archivo CustomVideoRender Video de Vonage. En este momento este ejemplo añade una sombra de color azul a su marco de vídeo cuando se activa el filtro. Vamos a eliminar ese sombreado azul y añadir la detección facial al renderizador en su lugar. Y si te lo crees, esa función de detección facial va a ser unas 30 veces más rápida que el filtro azul. Para lograr esta hazaña vamos a aplicar el filtro Viola-Jones para la detección de rasgos utilizando Emgu CV.
Dame el código
Si no te apetece seguir todo este tutorial, puedes encontrar un ejemplo funcional en GitHub. Sólo asegúrate de cambiar los parámetros como se indica en la Guía de inicio rápido del repositorio principal de muestras
Breve reseña de Viola-Jones
No voy a profundizar demasiado en cómo funciona el método Viola-Jones, pero para los interesados, he aquí un breve contexto. El quid del algoritmo Viola-Jones es triple. En primer lugar, utiliza unas características llamadas Haar, que pueden parecer unas tontas formas en blanco y negro.
Haar-like feature shapes source: Source https://scc.ustc.edu.cn/
Pero, en realidad, son detectores de características muy simples que pueden decirnos mucho sobre el sombreado relativo de una imagen:
Harr-like Features over Faces source http://www.willberger.org/cascade-haar-explained/
Cuando se superpone a una imagen, la suma de la región blanca se resta de la suma de la región negra, lo que nos indica la diferencia de sombreado entre las regiones. Esos cálculos, cuando se realizan en cascada sobre muchas características, pueden darnos una buena idea de en qué parte de una imagen puede estar un rostro. Como estas características son tan sencillas, no varían con la escala, lo que significa que pueden encontrar rostros en una imagen independientemente de su tamaño.
Este método hace un excelente trabajo detectando caras en una imagen, pero si no fuera por la última gran innovación del artículo, este método sería paralizantemente lento, en lugar de luminosamente rápido. Introdujeron el concepto de imagen integral: una imagen integral es una imagen en la que cada píxel es la suma de la región situada por encima y a la izquierda del píxel. Calculando esto sobre una imagen de entrada, podemos realizar cálculos sobre características tipo Haar con una complejidad de tiempo de O(1) en lugar de O(N*M), donde N y M son la altura y la anchura respectivamente de la característica tipo Haar. Esto hace que la combinatoria no sólo funcione, sino que juegue a nuestro favor, ya que estamos intentando construir un detector de caras que funcione rápidamente.
Requisitos previos
Visual Studio - Estoy usando 2019, aunque las versiones anteriores deberían funcionar
Mínimo .NET Framework 4.6.1 - puedes utilizar desde 4.5.2, pero tendrás que utilizar EmguCV en lugar de Emgu.CV para tu paquete NuGet de OpenCV.
Ejemplo de CustomVideoRenderer - Este es el ejemplo que vamos a adaptar.
Una cuenta de Video API de Vonage - si no tienes una regístrate aquí.
Una clave de API, un identificador de sesión y un token de tu cuenta de API de Video de Vonage - consulta la Inicio rápido en el repositorio para obtener más detalles.
Primeros pasos
Actualización a 4.6.1
En primer lugar, vamos a abrir el archivo de la solución CustomVideoRenderer. En MainWindow.xaml.cs pon tus credenciales si aún no lo has hecho. Luego actualiza el csproj para que tenga como objetivo .NET Framework 4.6.1.
Para ello, abra la solución en Visual Studio, haga clic con el botón derecho del ratón en el archivo del proyecto y haga clic en "Propiedades". A continuación, en la pestaña Aplicaciones, cambie el marco de trabajo de destino a 4.6.1.
Upgrade .NET version
Añadir paquetes NuGet
A continuación, añade los siguientes paquetes NuGet a los que ya están en la aplicación:
Emgu.CV.runtime.windows - Estoy usando 4.2.0.3662
WriteableBitmapEx - Estoy usando 1.6.5
Agarra las funciones tipo Haar
Obtenga los dos archivos siguientes de OpenCV:
haarcascade_frontalface_default.xml
haarcascade_profileface.xml
Coloque estos archivos junto al proyecto y configúrelos para que se copien en el directorio de compilación cuando se compile. Esto puede implicar el establecimiento de un evento post-construcción si su instancia de Visual Studio es tan poco cooperativa como la mía:
copy $(ProjectDir)\haarcascade_profileface.xml $(ProjectDir)$(OutDir)
copy $(ProjectDir)\haarcascade_frontalface_default.xml $(ProjectDir)$(OutDir)
Displaying Post Build Events Screen
En este punto, usted debe ser capaz de disparar la aplicación y conectarse a una llamada. Dado que Windows impide que más de una aplicación acceda a tu cámara al mismo tiempo, es posible que debas unirte a la llamada desde otra computadora utilizando la Zona de juegos de Video API de Vonage
Running in Playground
Ahora si conectamos nuestra llamada debería verse algo como esto en la Windows App:
Display Without Filter Windows App
Y si activamos el botón de filtro se verá más como:
Display With Blue Filter Windows App
Lo que ha ocurrido hasta ahora
Hasta ahora, lo único que ocurre es que si pulsas el botón "cambiar filtro", la aplicación aplicará un tinte azul a cada fotograma que entre en el renderizador.
¿Cómo ocurre esto?
En lugar de utilizar el VideoRenderer estándar, estamos creando nuestro propio renderizador personalizado, SampleVideoRendererque extiende Control e implementa IVideoRenderer de la Video API de Vonage. Esta interfaz es bastante simple: tiene un método, RenderFramedel que tomamos el fotograma y lo dibujamos en un mapa de bits en el control. Esto nos permite intervenir cada vez que aparece un fotograma, aplicarle lo que queramos y hacer que se renderice.
Añadir visión por ordenador
Así que con este Custom Renderer, tenemos todo lo que necesitamos para empezar a añadir el CV a nuestra aplicación. Abramos SampleVideoRenderer.cs y antes de hacer nada añadamos los siguientes imports:
using Emgu.CV;
using Emgu.CV.Structure;
using System.Diagnostics;
using System.Drawing;
using System.Collections.Concurrent;
using System.IO;
using System.Threading;Ya que estás aquí, renombra EnableBlueFilter a DetectingFaces (asegúrate de usar la función de renombrado de tu IDE) y haz que sea una propiedad pública get, privada set en lugar de un campo público como este:
public bool DetectingFaces { get; private set; }Esto romperá algunas cosas, pero pronto se verá cómo arreglarlas. Por ahora, vamos a seguir adelante.
Constantes
Añade las siguientes constantes a tu Renderizador:
private const double SCALE_FACTOR = 4;
private const int INTERVAL = 33;
private const double PIXEL_POINT_CONVERSION = (72.0 / 96.0);El SCALE_FACTOR es la escala a la que vamos a reducir las imágenes para su procesamiento-4 significa que redimensionaremos las imágenes a una cuarta parte del tamaño antes de ejecutar la detección. El INTERVAL es el número de milisegundos entre imágenes que intentaremos capturar del flujo. 33 es aproximadamente el número de milisegundos entre fotogramas en un flujo de 30FPS, por lo que el parámetro as-is significa que se está ejecutando a toda velocidad. El parámetro PIXEL_POINT_CONVERSION es la proporción de píxeles por punto en una pantalla de 96 DPI (que es lo que estoy usando). Naturalmente, esto puede ser mejor calculado cuando estamos factorizando para la conciencia DPI, pero vamos a utilizar esa relación como evangelio por ahora. Sólo necesitamos esto porque por alguna razón la librería Bitmap Extensions que estamos usando parece querer dibujar X en puntos e Y en píxeles 🤷♂️.
Crear nuestros clasificadores
Ya he explicado brevemente cómo funcionan las funciones similares a Haar, pero para conocerlas más a fondo, no dude en consultar el artículo de Viola-Jones de Viola-Jones. Lo bueno de OpenCV (y EmguCV por extensión) es que gran parte de todo esto está abstraído de nosotros.
Ahora continuamos con nuestro SampleVideoRenderer. Baja y añade dos CascadeClassifiers estáticos como campos:
static CascadeClassifier _faceClassifier;
static CascadeClassifier _profileClassifier;A continuación, en el constructor inicializarlos con sus respectivos archivos:
_faceClassifier = new CascadeClassifier(@"haarcascade_frontalface_default.xml");
_profileClassifier = new CascadeClassifier(@"haarcascade_profileface.xml");Estos archivos XML describen las características de Haar para el clasificador lo suficientemente bien como para entrenarlo. Así que en este punto, ¡hemos entrenado al clasificador!
Ahora clasifiquemos
Algunas estructuras necesarias
Mientras estamos clasificando, no vamos a querer bloquear el hilo principal. Así que vamos a implementar el patrón productor-consumidor. Vamos a utilizar BlockingCollections. Específicamente, vamos a usar un ConcurrentStack, porque los cuadros más relevantes y más recientes son uno y el mismo. Añade los siguientes campos a nuestra clase:
private System.Drawing.Rectangle[] _faces = new System.Drawing.Rectangle[0];
private BlockingCollection<Image<Bgr, byte>> _images = new BlockingCollection<Image<Bgr, byte>>(new ConcurrentStack<Image<Bgr, byte>>());
private CancellationTokenSource _source;
private Stopwatch _watch = Stopwatch.StartNew();La matriz _faces va a contener las caras que hemos detectado con nuestro clasificador, mientras que el array _images collection, inicializada con un ConcurrentStack, va a ser la colección LIFO de imágenes que vamos a procesar. El CancellationTokenSource es lo que vamos a utilizar para salir del bucle de procesamiento cuando llegue el momento. El Stopwatch va a servir como nuestro cronómetro para evitar que intentemos detectar fotogramas demasiado rápido.
Bucle de procesamiento
Ahora vamos a implementar nuestro bucle de procesamiento. Añade el siguiente método a tu código:
private void DetectFaces(CancellationToken token)
{
System.Threading.ThreadPool.QueueUserWorkItem(delegate
{
try
{
while (true)
{
var image = _images.Take(token);
_faces = _faceClassifier.DetectMultiScale(image);
if(_faces.Length == 0)
{
_faces = _profileClassifier.DetectMultiScale(image);
}
if (_images.Count > 25)
{
_images = new BlockingCollection<Image<Bgr, byte>>(new ConcurrentStack<Image<Bgr, byte>>());
GC.Collect();
}
}
}
catch (OperationCanceledException)
{
//exit gracefully
}
}, null);
}En este método ocurren muchas cosas. Primero, vamos a ejecutar la operación en uno de los Daemons disponibles del ThreadPool. Luego, vamos a procesar en un bucle cerrado. Llamamos Take a la colección bloqueante para extraer una imagen de la pila. Esta llamada a Take se bloqueará si no hay nada en la colección, y cuando señalemos la cancelación lanzará una OperationCanceledException, que capturaremos a continuación, para salir del bucle con elegancia. Con la imagen, asignará la _faces al resultado de DetectMultiScaleque es el método de detección de caras. Si no encuentra nada lo intentará de nuevo con el clasificador de caras de perfil.
Cuando todo esto está hecho, comprobamos la colección de imágenes para ver si está por encima de algún límite (estamos usando 25 como ejemplo aquí). Si excede ese límite, porque el clasificador se ha quedado atrás, vamos a limpiar la colección reiniciándola, y entonces vamos a decirle al recolector de basura que venga y recoja esas imágenes. ¿Por qué llamar al recolector de basura? Bueno, ese es el tema de otra entrada del blog, pero esencialmente si tus objetos son demasiado grandes (por encima de 85.000 bytes), son empujados al montón de objetos grandes, al que el recolector de basura asigna una prioridad más baja que a otros objetos (ya que es bastante caro computacionalmente liberar la memoria). Lo que esto significa en la práctica es que si usted está tratando con objetos grandes con relativa rapidez es posible que desee asegurarse de que se limpian o tendrá un uso de memoria considerable.
Ahora bien, si usted sigue mis directrices de rendimiento a continuación, usted nunca tendrá que golpear ese código, pero lo estoy dejando en sólo para que cuando la gente está afinando que no ven picos masivos en el uso de memoria.
Conmutación del bucle de detección
Ahora añade el siguiente código a tu Renderizador:
public void ToggleFaceDetection(bool detectFaces)
{
DetectingFaces = detectFaces;
if (!detectFaces)
{
_source?.Cancel();
}
else
{
_source?.Dispose();
_source = new CancellationTokenSource();
var token = _source.Token;
DetectFaces(token);
}
}Esto va a gestionar la alternancia del detector de caras para tu renderizador. Si lo configuras para que se detenga, le dirá a la fuente de tokens que se cancele, sacándote del bucle con elegancia. Si le dices que comience, se deshará del viejo CancellationTokenSource, lo reiniciará, tomará un token, y comenzará el bucle de procesamiento con ese token.
Añadamos también un finalizador para asegurarnos de que la tarea de detección de caras se cancela cuando el renderizador se está apagando:
~SampleVideoRenderer()
{
_source?.Cancel();
} Puesta en común
Hasta ahora, hemos sentado todas las bases que vamos a necesitar para hacer la detección facial. A partir de aquí es sólo una cuestión de conseguir que nuestro renderizador realice la detección facial en cada fotograma. Ahora vamos al método RenderFrame en el SampleVideoRenderer. Elimina los dos bucles for anidados y reemplaza ese código por:
using (var image = new Image<Bgr, byte>(frame.Width, frame.Height, stride[0], buffer[0]))
{
if (_watch.ElapsedMilliseconds > INTERVAL)
{
var reduced = image.Resize(1.0 / SCALE_FACTOR, Emgu.CV.CvEnum.Inter.Linear);
_watch.Restart();
_images.Add(reduced);
}
}
DrawRectanglesOnBitmap(VideoBitmap,_faces);Esto va a tirar de la imagen directamente desde el búfer de nuestro filtro anterior estaba copiando también, a continuación, empuje la nueva imagen en nuestra pila de bloqueo, y luego va a dibujar los rectángulos en las caras detectadas. Debajo del método RenderFrame añadimos el método DrawRectanglesOnBitmap que tendrá el siguiente aspecto:
public static void DrawRectanglesOnBitmap(WriteableBitmap bitmap, Rectangle[] rectangles)
{
foreach (var rect in rectangles)
{
var x1 = (int)((rect.X * (int)SCALE_FACTOR) * PIXEL_POINT_CONVERSION);
var x2 = (int)(x1 + (((int)SCALE_FACTOR * rect.Width) * PIXEL_POINT_CONVERSION));
var y1 = rect.Y * (int)SCALE_FACTOR;
var y2 = y1 + ((int)SCALE_FACTOR * rect.Height);
bitmap.DrawLineAa(x1, y1, x2, y1, strokeThickness: 5, color: Colors.Blue);
bitmap.DrawLineAa(x1, y1, x1, y2, strokeThickness: 5, color: Colors.Blue);
bitmap.DrawLineAa(x1, y2, x2, y2, strokeThickness: 5, color: Colors.Blue);
bitmap.DrawLineAa(x2, y1, x2, y2, strokeThickness: 5, color: Colors.Blue);
}
}Esto dibujará el rectángulo como 4 líneas separadas en el mapa de bits y lo mostrará. PIXEL_POINT_CONVERSION sólo en la x.
Una última cosa antes de la prueba
Me he dado cuenta de que el elemento PublisherVideo de la MainWindow es un poco pequeño para poder ver lo que ocurre en él. Así que para mis pruebas, he duplicado o cuadruplicado el tamaño de la ventana. Para ello, basta con ajustar la altura y la anchura en la línea 12 de MainWindow.xaml.
Preparados, listos, prueba
Ya estamos listos: enciende la aplicación y pulsa el botón Toggle Filter en la esquina superior izquierda de la pantalla. Esto activará el filtro. Deberías verlo en tu vista previa, y si te conectas a una llamada podrás ver que la detección facial también funciona en los participantes remotos.
Display Example With Face Detection
Verá que este medio de detección de características es a la vez preciso y rápido. El filtro se ejecuta en unos 10ms, comparado con los ~30ms del filtro azul que fue modificado. Y puesto que el procesamiento principal se ejecuta en un hilo de trabajo, y el dibujo real tarda menos de un milisegundo, esto es en realidad unas treinta veces más rápido, lo que significa añadir la detección facial es prácticamente gratis desde una perspectiva UX.
Ajuste de parámetros
Ninguna discusión sobre Visión por Computador estaría completa sin un pequeño comentario sobre el ajuste paramétrico. Hay todo tipo de parámetros que potencialmente podría ajustar aquí, pero sólo voy a centrarme en dos:
Intervalo entre fotogramas
Factor de escala
Como mencioné antes, los 33 milisegundos entre fotogramas me funcionaron especialmente si establecía el factor de escala adecuadamente. El factor de escala fue la pieza más importante para el rendimiento. Si estableces el factor de escala en 1 -en otras palabras, intenta tomar una imagen completa (en mi caso 1280x720)- eso son 921.000 píxeles a procesar cada 33 milisegundos, lo que tiene un coste de rendimiento sustancial. En mi máquina, se ejecutaría a unos 200ms por fotograma, reduciendo mi CPU, y sin añadir la llamada explícita al recolector de basura, causaría que el uso de memoria explotara. Recuerde que el factor de escala es cuadrático, por lo que establecer el factor de escala a 4 resultados en el número de píxeles disminuyendo en un factor de 16. En mis pruebas, no observé ninguna pérdida de precisión al cambiar el tamaño.
Llegar más lejos
Vamos a dejar esto aquí por ahora, pero espero que este post inspire al lector a reconocer el inmenso potencial que OpenCV tiene en .NET. Algunas aplicaciones interesantes que usted podría utilizar esto, de la parte superior de mi cabeza:
Añadir filtros e integrar RA en tus aplicaciones. Echa un vistazo a algunos artículos sobre Homografías y algoritmos de seguimiento de características. Personalmente me gusta ORB (aunque sólo sea porque es mucho más libre que otros algoritmos de seguimiento de rasgos).
Puedes integrar Far End Camera Control (FECC) en tu aplicación y hacer que los movimientos de la cámara sigan tu cara.
Una vez que haya encontrado el ROI de la cara en su imagen, puede ejecutar de forma mucho más eficiente cosas como análisis de sentimientos.
Como cabe imaginar, es el primer paso del reconocimiento facial.
Recursos
Puedes encontrar un ejemplo de este tutorial en GitHub aquí
Para todo lo que quieras saber sobre la API de Video de Vonage, visita nuestro sitio
Para cualquier cosa que quieras saber sobre OpenCV consulta su docs
Echa un vistazo a página wiki de Emgu para aprender más sobre el uso de Emgu en particular. Si eres un fan de OpenCv Python como yo, no tendrás ningún problema en usar Emgu