Face Landmarks Detector con Dlib y OpenCV

Dlib en una biblioteca multipropósito que puede ser usada desde C++ y Python, contiene muchos algoritmos de aprendizaje de máquinas, compresión, análisis de imágenes, entre otras, esta librería es de código abierto y debemos compilarla para poder utilizarla, para nuestro tutorial utilizaremos OpenCV para la detección de rostros y Dlib para la obtención de las partes relevantes de la cara, ojos, boca, nariz, etc., cada una de estas partes definida por un conjunto de puntos preestablecidos.

La detección de cada una de las regiones de la cara tiene muchas aplicaciones, entre ellas, realidad aumentada, alineación de rostros, detección de emociones, análisis de expresiones faciales, entre otras.

Los face landmarks de nuestra modelo:

Los resultados de la imagen inferior los puedes obtener con el código de ejemplo proporcionado por la librería Dlib, el archivo es el siguiente: dlib/examples/face_landmark_detection_ex.cpp

lena-landmark

Como podemos ver los landmarks son un conjunto de puntos que definen las regiones de la cara, por ejemplo, la nariz está conformada por los puntos que van desde el número 28 al 36, en la imagen superior estos puntos están unidos por una línea.

facial_landmarks_68markup

Para poder localizar cada uno de estos puntos necesitaremos un modelo entrenado para esta tarea, dlib nos permite entrenar nuestros modelos propios pero en esta ocasión utilizaremos uno de los muchos que podemos encontrar en la web, descárgalo en el enlace: http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2

Detección de landmarks en tiempo real

Vamos a construir un pequeña aplicación que capture imágenes de nuestra webcam, si hay un rostro presente intentaremos ubicar las regiones que nos interesan del mismo.

Lo primero que haremos será usar OpenCV para realizar las capturas de la cámara y luego aplicaremos los clasificadores en cascada para detectar los rostros presentes, puedes tener más detalles en detección de rostros.

// captura imagenes de la camara
cv::VideoCapture cap(0);
if (!cap.isOpened()) {
     return 1;
}
 
// clasificador en cascada que detecta rostros frontales
cv::CascadeClassifier face_cascade;
face_cascade.load("files/data/haarcascade_frontalface_alt.xml");

Dlib tiene una función preparada para detectar caras, pero utilizaremos la de OpenCV pues ya la conocemos de tutoriales anteriores, usar la función de dlib no es difícil, puedes ver los ejemplos incluidos con la librería.

Lo siguiente que haremos será crear la ventana y cargar el modelo que detectará los landmarks.

// crear una ventana dlib
image_window win;
win.set_title("Landmark Detector :: Tutor de Programacion");

// detecta los landmarks
shape_predictor pose_model;
deserialize("files/data/shape_predictor_68_face_landmarks.dat") >> pose_model;

El proceso para detectar los rostros ya lo hemos estudiado anteriormente, como sabemos las regiones en donde se ha detectado una cara se guarda en un std::vector<cv::Rect> por lo que debemos convertirlo a std::vector<dlib::rectangle> ya que dlib requiere este tipo de objeto para identificar el rectángulo en donde se encuentra un rostro, esto es lo que hacemos al final del código mostrado abajo.

while (!win.is_closed())
{
    // Grab a frame
    cv::Mat temp, frame_gray;

    // convertir a grises y equalizar histograma
    cv::cvtColor(temp, frame_gray, CV_BGR2GRAY);
    cv::equalizeHist(frame_gray, frame_gray);

    std::vector<cv::Rect> faces;
    std::vector<dlib::rectangle> facesRect;

    // detectar los rostros 
    face_cascade.detectMultiScale(frame_gray, faces, 1.2, 3, cv::CASCADE_FIND_BIGGEST_OBJECT, cv::Size(50, 50));

    // guardar la region en donde se encuentra la cara
    for (cv::Rect& rc : faces) {
        facesRect.push_back(rectangle(rc.x, rc.y, rc.x + rc.width, rc.y + rc.height));
    }

   // obtener los landmarks ...
}

Ahora, por cada rostro ubicado con el proceso anterior detectaremos sus regiones de interés, antes de hacer algo primero debemos convertir el objeto cv::Mat a un objeto compatible con dlib, es fácil solo hacemos esto: cv_image<bgr_pixel> cimg(temp), luego podemos usar la función pose_model(cimg, facesRect[i]) para detectar los landmarks, el primer parámetro es la imagen de entrada y el segundo indica el rectángulo en donde se encuentra el rostro.

// convertir el objeto cv::Mat 
cv_image<bgr_pixel> cimg(temp);

// guarda los puntos obtenidos
std::vector<image_window::overlay_circle> points;

// detectar los landmarks para cada rostro encontrado
for (unsigned long i = 0; i < facesRect.size(); ++i) {
    full_object_detection shape = pose_model(cimg, facesRect[i]);

    // guardar las coordenada de cada landmark
    for (unsigned int n = 0; n < shape.num_parts(); n++) {
        points.push_back(image_window::overlay_circle(shape.part(n), 2, color));
    }
}

Al final creamos un image_window::overlay_circle para dibujar un círculo en cada uno de los puntos, al aplicar la función pose_model(...) esta nos devuelve un full_object_detection usando sus métodos num_parts() obtenemos la cantidad de puntos encontrados, con part(int) obtenemos las coordenada X, Y del punto indicado.

Punto de interés de un rostro

Si deseamos por ejemplo obtener la región rectangular que corresponde a la nariz, solo debemos usar shape.part(n) para obtener las coordenadas de la misma, recordando que n desde estar entre 28 y 36 ya que este es el rango que le corresponde a la nariz.

// guarda los puntos de la region indicada
std::vector<cv::Point> nariz;

// detectar los landmarks para cada rostro encontrado
for (unsigned long i = 0; i < facesRect.size(); ++i) {
    full_object_detection shape = pose_model(cimg, facesRect[i]);

    // guardar las coordenada de cada landmark
    for (unsigned int n = 0; n < shape.num_parts(); n++) {
        // guardar las coordenadas de la nariz
        if (n >= 28 && n < 36) {
            nariz.push_back(cv::Point(shape.part(n).x(), shape.part(n).y()));
        }
    }
}

// obtener el rectangulo de la nariz y dibujarlo
cv::Rect rectNariz = cv::boundingRect(nariz);
cv::rectangle(temp, rectNariz, cv::Scalar(125, 125, 0));

Como puedes ver guardamos las coordenadas que corresponden a la región que nos interesa ubicar, luego para obtener el rectángulo usamos la función cv::bundingRect(...) la cual nos devuelve el cv::Rect que encierra el conjunto de puntos indicado, al final utilizamos la función cv::rectangle(...) para dibujarla sobre la imagen.

El código completo:

#include <opencv2/opencv.hpp>

#include <dlib/opencv.h>
#include <dlib/image_processing/frontal_face_detector.h>
#include <dlib/image_processing/render_face_detections.h>
#include <dlib/image_processing.h>
#include <dlib/gui_widgets.h>

using namespace dlib;
using namespace std;

int main()
{
    try
    {
        // captura imagenes de la camara
        cv::VideoCapture cap("files/data/video.avi");
        if (!cap.isOpened()) {
            return 1;
        }

        // clasificador en cascada que detecta rostros frontales
        cv::CascadeClassifier face_cascade;
        face_cascade.load("files/data/haarcascade_frontalface_alt.xml");

        // crear una ventana dlib
        image_window win;
        win.set_size(420, 380);
        win.set_title("Face Landmark Detector :: Tutor de Programacion");

        // detecta los landmarks
        shape_predictor pose_model;
        deserialize("files/data/shape_predictor_68_face_landmarks.dat") >> pose_model;

        const rgb_pixel color(255, 255, 0);
        const cv::Scalar scalar(255, 255, 0);


        // Grab and process frames until the main window is closed by the user.
        while (!win.is_closed())
        {
            // Grab a frame
            cv::Mat temp, frame_gray;

            // detener so no se ha podido leer la camara
            if (!cap.read(temp)) {
                break;
            }

            // convertir a grises y equalizar histograma
            cv::cvtColor(temp, frame_gray, CV_BGR2GRAY);
            cv::equalizeHist(frame_gray, frame_gray);

            std::vector<cv::Rect> faces;
            std::vector<dlib::rectangle> facesRect;

            // detectar los rostros 
            face_cascade.detectMultiScale(frame_gray, faces, 1.1, 3, cv::CASCADE_FIND_BIGGEST_OBJECT, cv::Size(50, 50));

            // guardar la region en donde se encuentra la cara
            for (cv::Rect& rc : faces) {

                cv::rectangle(temp, rc, scalar, 1, cv::LINE_AA);

                facesRect.push_back(rectangle(rc.x, rc.y, rc.x + rc.width, rc.y + rc.height));
            }

            // Turn OpenCV's Mat into something dlib can deal with.  Note that this just
            // wraps the Mat object, it doesn't copy anything.  So cimg is only valid as
            // long as temp is valid.  Also don't do anything to temp that would cause it
            // to reallocate the memory which stores the image as that will make cimg
            // contain dangling pointers.  This basically means you shouldn't modify temp
            // while using cimg.
            cv_image<bgr_pixel> cimg(temp);

            // guarda los puntos obtenidos
            std::vector<image_window::overlay_circle> points;
            std::vector<full_object_detection> detects;
            std::vector<cv::Point> nariz;

            // detectar los landmarks para cada rostro encontrado
            for (unsigned long i = 0; i < facesRect.size(); ++i) {
                full_object_detection shape = pose_model(cimg, facesRect[i]);
                detects.push_back(shape);

                // guardar las coordenada de cada landmark
                for (unsigned int n = 0; n < shape.num_parts(); n++) {
                    point pt = shape.part(n);
                    points.push_back(image_window::overlay_circle(pt, 2, color));

                    // captura los puntos correspondientes al area de la nariz
                    if (n >= 28 && n < 36) {
                        nariz.push_back(cv::Point(pt.x(), pt.y()));
                    }
                }
            }

            //// dibujar la region de la nariz
            //cv::Rect rectNariz = cv::boundingRect(nariz);
            //cv::rectangle(temp, rectNariz, cv::Scalar(0, 0, 255));

            // Display it all on the screen
            win.clear_overlay();                               
            win.set_image(cimg);                              // establecer la imagen en la ventana
            win.add_overlay(points);                          // mostrar los puntos de los landmarks
            win.add_overlay(render_face_detections(detects)); // dibujar los landmarks como lineas 
        }
    }
    catch (serialization_error& e) {
        cout << endl << e.what() << endl;
    }
    catch (exception& e) {
        cout << e.what() << endl;
    }
}

DLib & OpenCV - Face Landmarks Detection

Instrucciones para compilar

  1. Primero descarga el proyecto y luego descomprime opencv-dlib-landmark en una carpeta de tu preferencia.
  2. Luego descarga el modelo, lo descomprimes y lo guardas en la carpeta /opencv-dlib-landmark/data.
  3. Utiliza CMake para generar el proyecto para tú compilador preferido, debe tener soporte para C++ 11.
  4. Abre el proyecto, cambia a modo Release, compila y ejecuta, debes asegurarte de tener una cámara conectada. 

Asumimos que ya tienes instalado OpenCV, la biblioteca Dlib está incluida en el proyecto y esta preparada para compilarse con el mismo.

Comentarios

  1. Hola, Soy Sergio. Excelente página para empezar. He tenido problemas en la compilación, queda en 98% y sale error en "source.cpp", exactamente en CV_BGR2GRAY. Tengo OpenCV 3.0.0 beta instalado. ¿Alguna idea?

    Gracias

    ResponderEliminar
    Respuestas
    1. Es difícil saber el problema con poca información, recuerda que Dlib requiere un compilador con soporte para C++ 11, otra cosa es que las pruebas las he realizado con OpenCV 3.3.0.

      Eliminar
  2. Respuestas
    1. La librería DLib esta incluida dentro del proyecto y la misma se compila usando CMake, puedes ver el archivo CMakeList.txt que contiene el script de compilación.

      Eliminar
    2. Gracias por tu respuesta, ya lo solucioné esa parte, perdón por ser preguntón. Ahora que estoy ejecutando el archivo de ejemplo dlib/examples/face_landmark_detection_ex.cpp me aparece que no se encuentra el archivo PDB o no se puede abrir, ¿podrás saber a que se debe?. Gracias

      Eliminar

Publicar un comentario

Temas relacionados

Entradas populares de este blog

tkinter Grid

tkinter Canvas

Histogramas OpenCV Python

Python Binance API