Seguimiento de objetos de video utilizando OpenCV

C++C++Beginner
Practicar Ahora

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

En este laboratorio, implementaremos el seguimiento de objetos de video utilizando OpenCV.
Debes terminar el curso "Construyendo el Sistema Solar con C++" antes de aprender este proyecto.

Cosas que aprender

  • Bases de C++
  • Bases de g++
  • Representación de imágenes
  • Aplicación de OpenCV
  • Algoritmos Meanshift & Camshift

Resultados finales

Este experimento implementará un programa que puede seguir los planetas en un sistema solar. (En la siguiente imagen, seleccionamos Júpiter de la órbita amarilla y se puede ver que el objeto seguido ha sido marcado con un eclipse rojo):

image desc

Antes de escribir este proyecto, debes terminar nuestro curso "Construyendo el Sistema Solar con C++".

Para crear un archivo de video

En el entorno de LabEx, no soportamos el entorno de cámara. Por lo tanto, necesitamos crear un archivo de video para nuestro proyecto.

Instalemos la herramienta de grabación de video:

sudo apt-get update && sudo apt-get install gtk-recordmydesktop

Después de la instalación, podemos encontrar el software de grabación en el menú de aplicaciones:

image desc

Luego, puedes ejecutar el programa del sistema solar ./solarsystem y usar RecordMyDesktop para grabar la pantalla de escritorio (10 a 30 segundos estarán bien), y guardarla en ~/Code/camshift con el nombre video:

image desc

Cuando quieras terminar la grabación, puedes hacer clic en el botón Detener en la esquina inferior derecha. Luego obtendrás un archivo video.ogv:

image desc

Bases de las Imágenes Digitales

OpenCV es una biblioteca de visión artificial de código abierto y multiplataforma. A diferencia de la renderización de imágenes de OpenGL, OpenCV implementa muchos algoritmos comunes para el procesamiento de imágenes y la visión artificial. Antes de aprender OpenCV, necesitamos entender algunos conceptos básicos de imágenes y videos en la computadora.

En primer lugar, debemos entender cómo se representa la imagen en la computadora. Hay dos maneras comunes de almacenar imágenes: una es el mapa vectorial y la otra es el mapa de píxeles.

En el mapa vectorial, las imágenes se definen matemáticamente como una serie de puntos conectados por líneas. El elemento gráfico en un archivo de mapa vectorial se llama objeto. Cada objeto es una entidad independiente, que tiene propiedades como color, forma, contorno, tamaño y posición en la pantalla.

La más común es el mapa de píxeles. Por ejemplo, el tamaño de una imagen es a menudo 1024 * 768. Esto significa que la imagen tiene 1024 píxeles en la dirección horizontal y 768 píxeles en la dirección vertical.

El píxel es la unidad básica del mapa de píxeles. Por lo general, un píxel es una mezcla de los tres colores primarios (rojo, verde y azul). Dado que la naturaleza de la computadora es el reconocimiento de números, en general representamos un color primario en términos de brillo de 0 a 255. En otras palabras, para el color primario rojo, 0 significa el más oscuro, es decir, negro, y 255 significa el más brillante, es decir, rojo puro.

Así, un píxel se puede representar como un tríada (R,G,B), de modo que el blanco se puede representar como (255,255,255) y el negro es (0,0,0). Entonces llamamos a esta imagen una imagen en el espacio de color RGB. R, G y B se convierten en los tres canales de la imagen; y hay muchos otros espacios de color además del espacio de color RGB, como HSV, YCrCb, etc.

Como el píxel es al mapa de píxeles, la imagen es la unidad básica del video. Un video consta de una serie de imágenes en las que llamamos a cada una de las imágenes un fotograma. Y lo que por lo general llamamos tasa de fotogramas del video significa que este video contiene tantas imágenes de fotograma por segundo. Por ejemplo, si la tasa de fotogramas es 25, entonces este video reproducirá 25 fotogramas por segundo.

Si hay 1000 milisegundos en 1 segundo y digamos que la tasa de fotogramas es rate, entonces el intervalo de tiempo entre las imágenes de fotograma es 1000/rate.

Histograma de Color de Imagen

Un histograma de color es una herramienta para describir una imagen. Es similar a un histograma normal, excepto que el histograma de color debe calcularse a partir de una determinada imagen.

Si una imagen está en un espacio de color RGB, entonces podemos contar la cantidad de veces que aparece cada color en el canal R. De esta manera, podemos obtener una matriz de 256 longitudes (tabla de búsqueda de probabilidad de color). Divide todos los valores simultáneamente por el número total de píxeles (ancho por alto) en la imagen y convierte la secuencia resultante en un histograma. El resultado es un histograma de color del canal R. De manera similar, puedes tener los histogramas en el canal G y el canal B.

Proyección Inversa del Histograma

Se ha comprobado que en el histograma del espacio de color RGB es sensible a los cambios en la iluminación. Con el fin de reducir el impacto de este cambio en el efecto de seguimiento, el histograma necesita ser proyectado hacia atrás. Este proceso se divide en tres pasos:

  1. En primer lugar, convertimos la imagen del espacio RGB al espacio HSV.
  2. Luego calculamos el histograma del canal H.
  3. El valor de cada píxel en la imagen se reemplaza con la probabilidad correspondiente en la tabla de búsqueda de probabilidad de color para obtener un mapa de distribución de probabilidad de color.

Este proceso se llama proyección inversa y el mapa de distribución de probabilidad de color es una imagen en escala de grises.

Bases de OpenCV

Primero, necesitamos instalar OpenCV:

sudo apt update
sudo apt-get install libopencv-dev

Asumiremos que ya conoces la sintaxis básica de C++. Sabes que casi cada programa usará el archivo de encabezado #include <iostream> y using namespace std; o std::cout. OpenCV también tiene su propio espacio de nombres.

Para usar OpenCV, solo necesitamos incluir el siguiente archivo de encabezado:

#include <opencv2/opencv.hpp>

Luego:

using namespace cv;

para habilitar el espacio de nombres de OpenCV (o directamente usando el prefijo cv:: para todas las API).

Esta es tu primera vez usando OpenCV y es posible que no estés familiarizado con las interfaces de OpenCV, por lo que recomendamos usar el prefijo cv:: para aprender las API de OpenCV.

Escribamos nuestro primer programa para leer el video grabado:

//
// main.cpp
//
#include <opencv2/opencv.hpp> // Archivo de encabezado de OpenCV

int main() {

    // crear un objeto de captura de video
    // OpenCV ofrece el objeto VideoCapture y
    // trata la lectura de un video desde un archivo como si fuera la lectura desde una cámara.
    // cuando el parámetro de entrada es una ruta de archivo, leerá un archivo de video;
    // si es un número de identificador de cámara (por lo general es 0),
    // leerá la cámara
    cv::VideoCapture video("video.ogv"); // leyendo desde un archivo
    // cv::VideoCapture video(0);        // leyendo desde la cámara

    // contenedor para el fotograma de imagen leído, objeto Mat en OpenCV
    // La clase principal en OpenCV es Mat, que significa Matriz
    // OpenCV usa matrices para describir imágenes
    cv::Mat frame;
    while(true) {

        // escribir los datos del video en frame, >> está sobrescrito por OpenCV
        video >> frame;

        // cuando no hay un fotograma, romper el bucle
        if(frame.empty()) break;

        // visualizar el fotograma actual
        cv::imshow("test", frame);

        // la tasa de fotogramas del video es 15, por lo que debemos esperar 1000/15 para reproducirlo suavemente
        // waitKey(int delay) es una función de espera en OpenCV
        // en este punto, el programa esperará `delay` milisegundos para la entrada del teclado
        int key = cv::waitKey(1000/15);

        // romper el bucle cuando se haga clic en el botón ECS del teclado
        if (key == 27) break;
    }
    // liberar la memoria
    cv::destroyAllWindows();
    video.release();
    return 0;

}

Coloca este archivo main.cpp en la misma carpeta con video.ogv en ~/Code/camshift, y compila el programa:

g++ main.cpp `pkg-config opencv --libs --cflags opencv` -o  main

Cuando ejecutamos el programa, podemos ver que el video está reproduciéndose:

./main
image desc

Nota

Es posible que observes el siguiente error:

libdc1394 error: Failed to initialize libdc1394

Este es un error de OpenCV y no afecta nuestra ejecución.

Si quieres eliminar el problema, puedes ejecutar el siguiente código antes de ejecutar el programa:

sudo ln /dev/null /dev/raw1394

Algoritmos Meanshift y Camshift

  • Meanshift
  • Camshift
  • Establecer el evento de devolución de llamada del mouse para seleccionar el objetivo a seguir
  • Leer la imagen desde el flujo de video
  • Implementar el Camshift

Meanshift

Los algoritmos Meanshift y Camshift son dos algoritmos clásicos para el seguimiento de objetos. Camshift se basa en Meanshift. Su interpretación matemática es compleja, pero la idea básica es relativamente simple. Entonces, omitiremos esos hechos matemáticos y primero presentaremos el algoritmo Meanshift.

Asumiendo que hay un conjunto de puntos rojos en la pantalla, el círculo azul (ventana) debe moverse hacia los puntos donde hay la región más densa (o donde hay más puntos):

image desc

Como se muestra en la imagen anterior, marque el círculo azul como C1 y el centro del círculo como C1_o. Pero el centroide de este círculo es C1_r, marcado como un círculo sólido azul.

Cuando C1_o y C1_r no se superponen, mueva el círculo C1 repetidamente hacia el centro del círculo C1_r. Finalmente, se quedará en el círculo de mayor densidad C2.

Para el procesamiento de imágenes, generalmente usamos el histograma proyectado hacia atrás de la imagen. Cuando el objetivo a seguir se mueve, es claro que este proceso de movimiento puede reflejarse por el histograma proyectado hacia atrás. Entonces, el algoritmo Meanshift finalmente moverá nuestra ventana seleccionada a la posición del objetivo en movimiento. (El algoritmo ha demostrado convergencia al final.)

Camshift

Después de la descripción anterior, vimos que el algoritmo Meanshift siempre sigue un tamaño de ventana fijo, lo cual no se ajusta a nuestras necesidades, ya que en un video, el objeto objetivo no tiene por qué ser grande.

Por lo tanto, se creó Camshift para mejorar este problema. Esto también se puede ver en el Meanshift Adaptativo Continuo de Camshift.

Su idea básica es: primero aplicar el algoritmo Meanshift. Una vez que los resultados de Meanshift convergen, Camshift actualiza el tamaño de la ventana, calcula una elipse direccional para ajustarse a la ventana y luego aplica la elipse como una nueva ventana para aplicar el algoritmo Meanshift.

OpenCV proporciona una interfaz genérica para el algoritmo Camshift:

RotatedRect CamShift(InputArray probImage, Rect& window, TermCriteria criteria)

El primer parámetro, probImage, es la proyección inversa del histograma del objetivo. El segundo parámetro, window, es la ventana de búsqueda del algoritmo Camshift. El tercer parámetro, criteria, es la condición de finalización (terminación) del algoritmo.

Análisis

Después de entender la idea básica del algoritmo Camshift, podemos analizar que la implementación de este código se divide principalmente en varios pasos:

  1. Establecer el evento de devolución de llamada del mouse para seleccionar el objetivo a seguir.
  2. Leer la imagen desde el flujo de video.
  3. Implementar el proceso Camshift.

A continuación, continuaremos modificando el código en main.cpp.

Seleccionar el objeto a seguir mediante la función de devolución de llamada del mouse

OpenCV es diferente de OpenGL. Hay cinco parámetros especificados para la función de devolución de llamada del mouse. Los tres primeros son los que más necesitamos: A través del valor de event, podemos obtener el evento de presionar el botón izquierdo del mouse (CV_EVENT_LBUTTONDOWN), el evento de soltar el botón izquierdo del mouse (CV_EVENT_LBUTTONUP) y así sucesivamente:

bool selectObject = false; // se utiliza para indicar si se ha seleccionado un objeto o no
int trackObject = 0;       // 1 significa que hay un objeto en seguimiento, 0 significa que no hay objeto y -1 significa que no se ha calculado la propiedad Camshift
cv::Rect selection;        // guarda la región seleccionada por el mouse
cv::Mat image;             // caché de la imagen del video

// Función de devolución de llamada del mouse de OpenCV:
// void onMouse(int event, int x, int y, int flag, void *param)
// el cuarto parámetro `flag` representa el estado adicional,
// param significa el parámetro del usuario, no los necesitamos, entonces, no tienen nombres.
void onMouse( int event, int x, int y, int, void* ) {
    static cv::Point origin;
    if(selectObject) {
        // determinando la altura, el ancho y la posición de la esquina superior izquierda de la selección
        selection.x = MIN(x, origin.x);
        selection.y = MIN(y, origin.y);
        selection.width = std::abs(x - origin.x);
        selection.height = std::abs(y - origin.y);

        // & se sobrescribe por cv::Rect
        // significa la intersección de dos regiones,
        // el propósito principal aquí es procesar la región fuera de la región seleccionada
        selection &= cv::Rect(0, 0, image.cols, image.rows);
    }

    switch(event) {
            // procesando el evento de presionar el botón izquierdo
        case CV_EVENT_LBUTTONDOWN:
            origin = cv::Point(x, y);
            selection = cv::Rect(x, y, 0, 0);
            selectObject = true;
            break;
            // procesando el evento de soltar el botón izquierdo
        case CV_EVENT_LBUTTONUP:
            selectObject = false;
            if( selection.width > 0 && selection.height > 0 )
                trackObject = -1; // objeto en seguimiento, no se ha calculado la propiedad Camshift
            break;
    }
}

Leer imágenes desde un flujo de video

Hemos implementado la estructura para leer un flujo de video. Vamos a escribir más detalles:

int main() {
    cv::VideoCapture video("video.ogv");
    cv::namedWindow("CamShift at LabEx");

    // 1. registrar la devolución de llamada del evento del mouse
    cv::setMouseCallback("CamShift at LabEx", onMouse, NULL);

    cv::Mat frame;

    // 2. leer la imagen desde el video
    while(true) {
        video >> frame;
        if(frame.empty()) break;

        // escribir la imagen del fotograma en la variable global image para el caché
        frame.copyTo(image);

        // dibujar un rectángulo si se está seleccionando un objeto
        if( selectObject && selection.width > 0 && selection.height > 0 ) {
            cv::Mat roi(image, selection);
            bitwise_not(roi, roi);
        }
        imshow("CamShift at LabEx", image);
        int key = cv::waitKey(1000/15.0);
        if(key == 27) break;
    }
    // liberar la memoria asignada
    cv::destroyAllWindows();
    video.release();
    return 0;
}

Nota:

ROI (Región de Interés): En el procesamiento de imágenes, cualquier región que se vaya a procesar puede ser una región de interés, es decir, ROI.

Implementar Camshift con OpenCV

El histograma proyectado hacia atrás para calcular el objeto seguido necesita utilizar la función cvtColor, que puede convertir la imagen original del espacio de color RGB al espacio de color HSV. El cálculo del histograma debe ser después de seleccionar el objetivo inicial, por lo tanto:

int main() {
    cv::VideoCapture video("video.ogv");
    cv::namedWindow("CamShift at LabEx");
    cv::setMouseCallback("CamShift at LabEx", onMouse, NULL);

    cv::Mat frame;
    cv::Mat hsv, hue, mask, hist, backproj;
    cv::Rect trackWindow;             // ventana de seguimiento
    int hsize = 16;                   // para el histograma
    float hranges[] = {0,180};        // para el histograma
    const float* phranges = hranges;  // para el histograma

    while(true) {
        video >> frame;
        if(frame.empty()) break;
        frame.copyTo(image);

        // transferir al espacio HSV
        cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
        // procesamiento cuando hay un objeto
        if(trackObject) {

            // solo procesar H: 0~180, S: 30~256, V: 10~256, filtrar el resto y copiar el resto al máscara
            cv::inRange(hsv, cv::Scalar(0, 30, 10), cv::Scalar(180, 256, 10), mask);
            // separar el canal h de hsv
            int ch[] = {0, 0};
            hue.create(hsv.size(), hsv.depth());
            cv::mixChannels(&hsv, 1, &hue, 1, ch, 1);

            // extracción de propiedades si el objeto seguido no ha sido calculado
            if( trackObject < 0 ) {

                // configurar el canal h y la ROI de la máscara
                cv::Mat roi(hue, selection), maskroi(mask, selection);
                // calcular el histograma de la ROI
                calcHist(&roi, 1, 0, maskroi, hist, 1, &hsize, &phranges);
                // normalización del histograma
                normalize(hist, hist, 0, 255, CV_MINMAX);

                // configurar la ventana de seguimiento
                trackWindow = selection;

                // marcar que el objeto seguido ha sido calculado
                trackObject = 1;
            }
            // proyección hacia atrás del histograma
            calcBackProject(&hue, 1, 0, hist, backproj, &phranges);
            // obtener la región común
            backproj &= mask;
            // llamar al algoritmo Camshift
            cv::RotatedRect trackBox = CamShift(backproj, trackWindow, cv::TermCriteria( CV_TERMCRIT_EPS | CV_TERMCRIT_ITER, 10, 1 ));
            // procesamiento de región demasiado pequeña para dibujar
            if( trackWindow.area() <= 1 ) {
                int cols = backproj.cols, rows = backproj.rows, r = (MIN(cols, rows) + 5)/6;
                trackWindow = cv::Rect(trackWindow.x - r, trackWindow.y - r,
                                       trackWindow.x + r, trackWindow.y + r) & cv::Rect(0, 0, cols, rows);
            }
            // dibujar el área de seguimiento
            ellipse( image, trackBox, cv::Scalar(0,0,255), 3, CV_AA );

        }


        if( selectObject && selection.width > 0 && selection.height > 0 ) {
            cv::Mat roi(image, selection);
            bitwise_not(roi, roi);
        }
        imshow("CamShift at LabEx", image);
        int key = cv::waitKey(1000/15.0);
        if(key == 27) break;
    }
    cv::destroyAllWindows();
    video.release();
    return 0;
}

Resumen

A continuación se muestra todo lo que escribimos en este proyecto:

#include <opencv2/opencv.hpp>

bool selectObject = false; // se utiliza para indicar si se ha seleccionado un objeto o no
int trackObject = 0;       // 1 significa que hay un objeto en seguimiento, 0 significa que no hay objeto y -1 significa que no se ha calculado la propiedad Camshift
cv::Rect selection;        // guarda la región seleccionada por el mouse
cv::Mat image;             // caché de la imagen del video

// Función de devolución de llamada del mouse de OpenCV:
// void onMouse(int event, int x, int y, int flag, void *param)
// el cuarto parámetro `flag` representa el estado adicional,
// param significa el parámetro del usuario, no los necesitamos, entonces, no tienen nombres.
void onMouse( int event, int x, int y, int, void* ) {
    static cv::Point origin;
    if(selectObject) {
        // determinando la altura, el ancho y la posición de la esquina superior izquierda de la selección
        selection.x = MIN(x, origin.x);
        selection.y = MIN(y, origin.y);
        selection.width = std::abs(x - origin.x);
        selection.height = std::abs(y - origin.y);

        // & se sobrescribe por cv::Rect
        // significa la intersección de dos regiones,
        // el propósito principal aquí es procesar la región fuera de la región seleccionada
        selection &= cv::Rect(0, 0, image.cols, image.rows);
    }

    switch(event) {
            // procesando el evento de presionar el botón izquierdo
        case CV_EVENT_LBUTTONDOWN:
            origin = cv::Point(x, y);
            selection = cv::Rect(x, y, 0, 0);
            selectObject = true;
            break;
            // procesando el evento de soltar el botón izquierdo
        case CV_EVENT_LBUTTONUP:
            selectObject = false;
            if( selection.width > 0 && selection.height > 0 )
                trackObject = -1; // objeto en seguimiento, no se ha calculado la propiedad Camshift
            break;
    }
}

int main( int argc, const char** argv ) {
    cv::VideoCapture video("video.ogv");
    cv::namedWindow("CamShift at LabEx");
    cv::setMouseCallback("CamShift at LabEx", onMouse, NULL);

    cv::Mat frame, hsv, hue, mask, hist, backproj;
    cv::Rect trackWindow;             // ventana de seguimiento
    int hsize = 16;                   // para el histograma
    float hranges[] = {0,180};        // para el histograma
    const float* phranges = hranges;  // para el histograma

    while(true) {
        video >> frame;
        if(frame.empty()) break;
        frame.copyTo(image);

        // transferir al espacio HSV
        cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
        // procesamiento cuando hay un objeto
        if(trackObject) {

            // solo procesar H: 0~180, S: 30~256, V: 10~256, filtrar el resto y copiar el resto al máscara
            cv::inRange(hsv, cv::Scalar(0, 30, 10), cv::Scalar(180, 256, 256), mask);
            // separar el canal h de hsv
            int ch[] = {0, 0};
            hue.create(hsv.size(), hsv.depth());
            cv::mixChannels(&hsv, 1, &hue, 1, ch, 1);

            // extracción de propiedades si el objeto seguido no ha sido calculado
            if( trackObject < 0 ) {

                // configurar el canal h y la ROI de la máscara
                cv::Mat roi(hue, selection), maskroi(mask, selection);
                // calcular el histograma de la ROI
                calcHist(&roi, 1, 0, maskroi, hist, 1, &hsize, &phranges);
                // normalización del histograma
                normalize(hist, hist, 0, 255, CV_MINMAX);

                // configurar la ventana de seguimiento
                trackWindow = selection;

                // marcar que el objeto seguido ha sido calculado
                trackObject = 1;
            }
            // proyección hacia atrás del histograma
            calcBackProject(&hue, 1, 0, hist, backproj, &phranges);
            // obtener la región común
            backproj &= mask;
            // llamar al algoritmo Camshift
            cv::RotatedRect trackBox = CamShift(backproj, trackWindow, cv::TermCriteria( CV_TERMCRIT_EPS | CV_TERMCRIT_ITER, 10, 1 ));
            // procesamiento de región demasiado pequeña para dibujar
            if( trackWindow.area() <= 1 ) {
                int cols = backproj.cols, rows = backproj.rows, r = (MIN(cols, rows) + 5)/6;
                trackWindow = cv::Rect(trackWindow.x - r, trackWindow.y - r,
                                       trackWindow.x + r, trackWindow.y + r) & cv::Rect(0, 0, cols, rows);
            }
            // dibujar el área de seguimiento
            ellipse( image, trackBox, cv::Scalar(0,0,255), 3, CV_AA );

        }


        if( selectObject && selection.width > 0 && selection.height > 0 ) {
            cv::Mat roi(image, selection);
            bitwise_not(roi, roi);
        }
        imshow("CamShift at LabEx", image);
        int key = cv::waitKey(1000/15.0);
        if(key == 27) break;
    }
    cv::destroyAllWindows();
    video.release();
    return 0;
}

Vamos a recompilar main.cpp:

g++ main.cpp $(pkg-config opencv --libs --cflags opencv) -o main

y ejecutarlo:

./main

Ahora, podemos seleccionar el objeto en el programa y el seguimiento está en marcha:

image desc

En la imagen anterior, seleccionamos Júpiter y la ventana de seguimiento es una elipse roja.