Estimar postura 3D

En el tutorial anterior aprendimos a extraer los landmarks de un rosto, ahora utilizaremos esa información para intentar estimar los distintos ángulos de rotación que determinan la pose de la cabeza, con esto podremos saber, por ejemplo, la dirección en la que esta mirando una persona, con esto podríamos construir una aplicación que monitoreé a un conductor y le envié una alerta cuando este quite su mirada de la carretera.

Para estimar la postura 3D de un objeto a partir de una imagen 2D requerimos tener un modelo tridimensional aproximado del objeto que deseamos analizar, en nuestro caso una cabeza humana, luego extraemos las coordenadas de los puntos del modelo 3D que correspondan con los landmarks obtenidos en la imagen 2D, con esta información podemos usar la función cv::solvePnP(...) para estimar la postura.

lena-pose

No es necesario crear un modelo 3D de para el rostro con el cual trabajaremos, podemos usar un modelo genérico, es más tampoco requerimos crear el modelo solo necesitamos conocer las coordenadas de cada uno de los puntos correspondientes, para nuestro proyecto usaremos las siguientes coordenadas obtenidas de la web:

std::vector<cv::Point3d> object_pts;

object_pts.push_back(cv::Point3d(6.825897, 6.760612, 4.402142));     //#33 left brow left corner
object_pts.push_back(cv::Point3d(1.330353, 7.122144, 6.903745));     //#29 left brow right corner
object_pts.push_back(cv::Point3d(-1.330353, 7.122144, 6.903745));    //#34 right brow left corner
object_pts.push_back(cv::Point3d(-6.825897, 6.760612, 4.402142));    //#38 right brow right corner
object_pts.push_back(cv::Point3d(5.311432, 5.485328, 3.987654));     //#13 left eye left corner
object_pts.push_back(cv::Point3d(1.789930, 5.393625, 4.413414));     //#17 left eye right corner
object_pts.push_back(cv::Point3d(-1.789930, 5.393625, 4.413414));    //#25 right eye left corner
object_pts.push_back(cv::Point3d(-5.311432, 5.485328, 3.987654));    //#21 right eye right corner
object_pts.push_back(cv::Point3d(2.005628, 1.409845, 6.165652));     //#55 nose left corner
object_pts.push_back(cv::Point3d(-2.005628, 1.409845, 6.165652));    //#49 nose right corner
object_pts.push_back(cv::Point3d(2.774015, -2.080775, 5.048531));    //#43 mouth left corner
object_pts.push_back(cv::Point3d(-2.774015, -2.080775, 5.048531));   //#39 mouth right corner
object_pts.push_back(cv::Point3d(0.000000, -3.116408, 6.097667));    //#45 mouth central bottom corner
object_pts.push_back(cv::Point3d(0.000000, -7.415691, 4.070434));    //#6 chin corner

Los puntos correspondientes de la imagen 2D serán obtenidos usando dlib para detectar los landmarks del rostro capturado por la webcam, de este modo podremos estimar la postura de nuestro propio rostro, el proceso ya lo vimos en el tutorial antes mencionado.

cv::Mat temp, frame_gray;

if (!cap.read(temp)) { continue; }

// 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) {
    facesRect.push_back(dlib::rectangle(rc.x, rc.y, rc.x + rc.width, rc.y + rc.height));
}

// tipo de imagen requerido por dlib
cv_image<bgr_pixel> cimg(temp);

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

// detectar los landmarks para cada rostro encontrado
for (unsigned long i = 0; i < facesRect.size(); ++i) 
{
    // obtener el landmark del rostro que se encuantra en el rectangulo indicado
    full_object_detection shape = pose_model(cimg, facesRect[i]);
    detects.push_back(shape);

    image_pts.push_back(cv::Point2d(shape.part(17).x(), shape.part(17).y())); //#17 left brow left corner
    image_pts.push_back(cv::Point2d(shape.part(21).x(), shape.part(21).y())); //#21 left brow right corner
    image_pts.push_back(cv::Point2d(shape.part(22).x(), shape.part(22).y())); //#22 right brow left corner
    image_pts.push_back(cv::Point2d(shape.part(26).x(), shape.part(26).y())); //#26 right brow right corner
    image_pts.push_back(cv::Point2d(shape.part(36).x(), shape.part(36).y())); //#36 left eye left corner
    image_pts.push_back(cv::Point2d(shape.part(39).x(), shape.part(39).y())); //#39 left eye right corner
    image_pts.push_back(cv::Point2d(shape.part(42).x(), shape.part(42).y())); //#42 right eye left corner
    image_pts.push_back(cv::Point2d(shape.part(45).x(), shape.part(45).y())); //#45 right eye right corner
    image_pts.push_back(cv::Point2d(shape.part(31).x(), shape.part(31).y())); //#31 nose left corner
    image_pts.push_back(cv::Point2d(shape.part(35).x(), shape.part(35).y())); //#35 nose right corner
    image_pts.push_back(cv::Point2d(shape.part(48).x(), shape.part(48).y())); //#48 mouth left corner
    image_pts.push_back(cv::Point2d(shape.part(54).x(), shape.part(54).y())); //#54 mouth right corner
    image_pts.push_back(cv::Point2d(shape.part(57).x(), shape.part(57).y())); //#57 mouth central bottom corner
    image_pts.push_back(cv::Point2d(shape.part(8).x(), shape.part(8).y()));   //#8 chin corner

   //...

}

Ahora, con los datos obtenidos podemos invocar a la función cv::solvePnP(...) a ella debemos pasarle el ambos conjuntos de coordenadas las 3D y 2D, además de la matriz de cámara y sus coeficientes de distorsión, por simplicidad asumiremos que nuestra cámara no distorsiona la imagen.

int max_d = MAX(temp.rows, temp.cols);

// matriz de camara
cv::Mat cam_matrix = (Mat_<double>(3, 3) << max_d, 0, temp.cols / 2.0,
                                            0, max_d, temp.rows / 2.0,
                                            0, 0, 1.0);

//calc pose 3D
cv::solvePnP(object_pts, image_pts, cam_matrix, cv::noArray(), rotation_vec, translation_vec);

La matriz de cámara se define de la siguiente manera:

camara-matrix

Los valores cx y cy se encuentran aproximadamente en el centro de la imagen, mientras que fx y fy se aproxima al ancho de la imagen.

Los tres últimos parámetros enviados, indican lo siguiente: cv::noArray() no utilizaremos los coeficientes de distorsión, rotation_vec y translation_vec almacenan las vectores de rotación y traslación resultantes.

El siguiente fragmento de código estará encargado de dibujar los ejes (X, Y, Z) sobre los cuales se define la rotación de la cabeza, usaremos la función cv::projectPoint(...) para proyectar los puntos 3D en una imagen 2D, luego dibujamos cada uno de los ejes proyectados usando la función de dibujo cv::line(...).

// puntos 3D para generar los ejes de rotacion
std::vector<cv::Point3d> reprojectsrc;
reprojectsrc.push_back(cv::Point3d(0.0, 0.0, 0.0));
reprojectsrc.push_back(cv::Point3d(15.0, 0.0, 0.0));
reprojectsrc.push_back(cv::Point3d(0.0, 15.0, 0.0));
reprojectsrc.push_back(cv::Point3d(0.0, 0.0, 15.0));

// puntos 3D proyectados en 2D
std::vector<cv::Point2d> reprojectdst;
reprojectdst.resize(4);

// ...

// reproject
cv::projectPoints(reprojectsrc, rotation_vec, translation_vec, cam_matrix, cv::noArray(), reprojectdst);

// dibujar los ejes de rotacion 
line(temp, reprojectdst[0], reprojectdst[1], cv::Scalar(0, 0, 255), 2, cv::LINE_AA);
line(temp, reprojectdst[0], reprojectdst[2], cv::Scalar(0, 255, 0), 2, cv::LINE_AA);
line(temp, reprojectdst[0], reprojectdst[3], cv::Scalar(255, 0, 0), 2, cv::LINE_AA);

Para finalizar vamos a obtener los ángulos Euler (yaw, pitch, roll), lo primero que necesitamos es convertir el vector de rotación a una matriz de rotación para ello utilizamos cv::Rodrigues(...) luego concatenamos la matriz obtenida previamente con el vector de translación por medio de cv::hconcat(...) la matriz obtenida la descomponemos en sus diversos componentes, cv::decomposeProjectionMatrix(...), entre ellos el que nos interesa, los ángulos Euler, también obtenemos otros componentes pero no los requerimos.

cv::Mat out_intrinsics = cv::Mat(3, 3, CV_64FC1);
cv::Mat out_rotation = cv::Mat(3, 3, CV_64FC1);
cv::Mat out_translation = cv::Mat(3, 1, CV_64FC1);

cv::Mat pose_mat = cv::Mat(3, 4, CV_64FC1); 
cv::Mat euler_angle = cv::Mat(3, 1, CV_64FC1);

cv::Rodrigues(rotation_vec, rotation_mat);
cv::hconcat(rotation_mat, translation_vec, pose_mat);
cv::decomposeProjectionMatrix(pose_mat, out_intrinsics, out_rotation, out_translation, cv::noArray(), cv::noArray(), cv::noArray(), euler_angle);

Finalmente mostramos la información obtenida en la imagen:

// dibujar pitch 
cv::String txtPitch = cv::format("PITCH: %.2f", euler_angle.at<double>(0));
cv::Size size = cv::getTextSize(txtPitch, font, 1.0, 1, &baseline);
cv::Point pos = cv::Point(reprojectdst[1].x - size.width, reprojectdst[1].y);
cv::putText(temp, txtPitch, pos, font, 1.0, cv::Scalar::all(255), 1, cv::LINE_AA);

// dibujar yaw
cv::String txtYaw = cv::format("YAW: %.2f", euler_angle.at<double>(1));
cv::putText(temp, txtYaw, reprojectdst[2], font, 1.0, cv::Scalar::all(255), 1, cv::LINE_AA);

// dibujar roll
cv::String txtRoll = cv::format("ROLL: %.2f", euler_angle.at<double>(2));
cv::putText(temp, txtRoll, reprojectdst[3], font, 1.0, cv::Scalar::all(255), 1, cv::LINE_AA);

Estimar pose 3D

Puedes descargar el proyecto, el mismo está preparado para ser generado usando CMake, las instrucciones de compilación son idénticas a las definidas en el tutorial previo, el programa está preparado para ser usado con la webcam pero puedes cambiar a un video fácilmente, recuerda que debes descargar y descomprimir el modelo pre entrenado para dlib shape_predictor_68_face_landmarks.dat.bz2 y copiarlo en la carpeta data antes de generar con CMake.

Descargar: Estimar Postura 3D – OpenCV + Dlib

Comentarios

Publicar un comentario

Temas relacionados

Entradas populares de este blog

tkinter Grid

tkinter Canvas

Histogramas OpenCV Python

Python Binance API