Flujo Óptico Lucas-Kanade con OpenCV

El flujo óptico es una técnica de análisis de imágenes que permite determinar el movimiento de un objeto dentro de una secuencia de imágenes, se puede aplicar en: detección de movimiento, seguimiento de objetos, compresión de video, estabilización de videos, etc., la biblioteca OpenCV implementa varios métodos para calcular el flujo óptico, en este tutorial veremos el algoritmo propuesto por Lucas-Kanade.

Método de Lucas Kanade con OpenCV

Este es implementado por la función cv::calcOpticalFlowPyrLK() la misma trabaja con imágenes a escala de grises, debemos pasarle la imagen actual y la anterior, además es necesario proporcionarle una serie de puntos para los cuales se intentará calcular el flujo, la función devuelve las nuevas posiciones de dichos puntos junto con un código para cada que indica si ha sido posible detectar el flujo, los valores son 1 en caso de éxito y 0 de lo contrario. 

Flujo óptico - Lucas Kanade Lucas Kanade flujo óptico con OpenCV

Nuestra aplicación demostrativa utiliza la webcam para la captura de la secuencia de video en tiempo real, al iniciar el programa primero debes presionar una tecla cualquiera que no sea ESC, esto inicia la captura de los puntos de interés, luego mueves el objeto para ver el flujo.

#include <iostream>
#include <vector>
#include <deque>

#include <opencv2\opencv.hpp>

using namespace cv;
using namespace std;

void main() 
{
    String window = "OpticalFlow :: Lucas-Kanade";
    VideoCapture capture(0);

    deque<vector<Point2f>> records;
    vector<Point2f> points[2];

    Mat prev_gray;

    Scalar COLOR(255, 255, 0);

    // crear la ventana
    namedWindow(window);

    // bucle de captura de video
    while (true) {

        // capturar el cuadro actual
        Mat frame, gray;
        capture >> frame;

        // si no hay datos continuar
        if (frame.empty()) continue;

        // convertir a escala de grises
        cvtColor(frame, gray, COLOR_BGR2GRAY);

        // verificar si hay puntos anteriores
        if (!points[0].empty()) 
        {
            vector<uchar> status;

            // aseguarse de que la imagen anterior no esta vacia
            if (prev_gray.empty()) {
                gray.copyTo(prev_gray);
            }

            // calcular flujo optico con el metodo de Lucas-Kanade
            calcOpticalFlowPyrLK(prev_gray, gray, points[0], points[1], status, cv::noArray());

            // guardar los puntos obtenidos
            records.push_front(points[1]);

            // guardar solo un maximo de 10 conjuntos de puntos anteriores
            if (records.size() >= 10) {
                records.pop_back();
            }

            // dibujar los datos
            for (size_t i = 0; i < points[1].size(); i++)
            {
                if (!status[i]) continue;

                // dibujar el conjunto de lineas guardo previamente, 
                // estos representan el movimiento de los puntos.
                for (size_t j = 0; j < records.size() - 1; j++)
                    line(frame, records[j][i], records[j + 1][i], COLOR, 1, LINE_AA);

                // dibujar los puntos de interes
                circle(frame, points[1][i], 3, COLOR, FILLED, LINE_AA);
            }
        }

        // esperar por 30 ms ha que se presione una tecla
        char key = (char)waitKey(30);

        // si la tecla es ESC salir, cualquier otra captura los puntos caracteristicos.
        if (key == 27) break;
        else if (key != -1)
        {
            // obtener los puntos de interes
            goodFeaturesToTrack(gray, points[1], 100, 0.01, 10);

            // limpiar los puntos previos
            records.clear();
        }

        // mostrar la imagen
        imshow(window, frame);

        // intercambiar los puntos, los actuales pasan a ser los anteriores.
        std::swap(points[1], points[0]);

        // intercambiar las imagenes, la actual es ahora la anterior.
        cv::swap(prev_gray, gray);
    }
}

Lo primero a tener en cuenta es que requerimos los puntos de interés, podemos crearlos manualmente usando el ratón, es decir, haciendo clic en cada punto y guardando dicha posición para luego proporcionársela al algoritmo, en este ejemplo usaremos la función goodFeatureToTrack(...) para obtener dichos puntos, puedes ver como se utiliza la misma en detección de esquinas.

// esperar por 30 ms ha que se presione una tecla
char key = (char)waitKey(30);

// si la tecla es ESC salir, cualquier otra captura los puntos caracteristicos.
if (key == 27) break;
else if (key != -1)
{
    // obtener los puntos de interes
    goodFeaturesToTrack(gray, points[1], 100, 0.01, 10);
}

Los parámetros son: primero la imagen en donde ubicaremos los puntos, luego la variable en donde se almacenarán los mismos, seguido de la cantidad máxima de puntos a obtener, sigue el nivel de calidad y la distancia mínima entre dichos puntos.

Una vez tenemos los puntos, podemos aplicar el algoritmo de Lucas-Kanade, la variable points nos servirá para guardar los puntos obtenidos en el paso anterior, y la nueva ubicación de los mismos proporcionada por el siguiente método:

vector<Point2f> points[2];
vector<uchar> status;

// calcular flujo optico con el metodo de Lucas-Kanade
calcOpticalFlowPyrLK(prev_gray, gray, points[0], points[1], status, cv::noArray());

A esta función le pasamos las imágenes previas y la actual, en escala de grises, luego los puntos anteriores y el lugar donde guardar los nuevos, le sigue la variable en donde se indica si ha sido posible encontrar el flujo del punto, podemos configurar más parámetros pero en esta ocasión usaremos sus valores por defecto, para más detalles ver la documentación.

Para dibujar el flujo solo recorremos los puntos almacenados y dibujamos:

for (size_t i = 0; i < points[1].size(); i++)
{
    if (!status[i]) continue;

    // dibujar el conjunto de lineas guardo previamente, 
    // estos representan el movimiento de los puntos.
    for (size_t j = 0; j < records.size() - 1; j++)
        line(frame, records[j][i], records[j + 1][i], COLOR, 1, LINE_AA);

    // dibujar los puntos de interes
    circle(frame, points[1][i], 3, COLOR, FILLED, LINE_AA);
}

Finalmente mostramos la imagen, además almacenamos la imagen actual para utilizarla en la siguiente pasada, hacemos lo mismo con los puntos.

// mostrar la imagen
imshow(window, frame);

// intercambiar los puntos, los actuales pasan a ser los anteriores.
std::swap(points[1], points[0]);

// intercambiar las imagenes, la actual es ahora la anterior.
cv::swap(prev_gray, gray);

En siguientes tutoriales veremos otros métodos que podemos aplicar con OpenCV, luego estudiaremos algunas de las aplicaciones del flujo óptico, quizás estabilización de video.

Descargar proyecto: flujo-optico-lk.zip

Comentarios

Entradas populares de este blog

Conectar SQL Server con Java

Entrenar OpenCV en Detección de Objetos

Procesamiento de imágenes en OpenCV

Acceso a la webcam con OpenCV

Conociendo la clase cv::Mat de OpenCV