Suivi d'objets vidéo en utilisant OpenCV

C++Beginner
Pratiquer maintenant

Introduction

Dans ce laboratoire, nous allons implémenter le suivi d'objets vidéo à l'aide d'OpenCV.
Vous devez avoir terminé le cours "Construire le système solaire avec C++" avant d'apprendre ce projet.

Choses à apprendre

  • Les bases du C++
  • Les bases de g++
  • Représentation d'images
  • Application d'OpenCV
  • Algorithmes Meanshift et Camshift

Résultats finaux

Cet experiment va implémenter un programme qui peut suivre les planètes dans un système solaire. (Dans l'image suivante, nous avons sélectionné Jupiter sur l'orbite jaune, et vous pouvez voir que l'objet suivi est marqué par un éclipse rouge) :

image desc

Avant d'écrire ce projet, vous devez avoir terminé notre cours "Construire le système solaire avec C++".

Créer un fichier vidéo

Dans l'environnement LabEx, nous ne supportons pas l'environnement de la caméra. Ainsi, nous devons créer un fichier vidéo pour notre projet.

Installons l'outil d'enregistrement vidéo :

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

Après l'installation, nous pouvons trouver le logiciel d'enregistrement dans le menu des applications :

image desc

Ensuite, vous pouvez exécuter le programme du système solaire ./solarsystem et utiliser RecordMyDesktop pour enregistrer l'écran du bureau (10 à 30 secondes seront suffisantes), et l'enregistrer dans ~/Code/camshift avec le nom video :

image desc

Lorsque vous voulez terminer l'enregistrement, vous pouvez cliquer sur le bouton Stop dans le coin inférieur droit. Ensuite, vous obtiendrez un fichier video.ogv :

image desc

Fondements des images numériques

OpenCV est une bibliothèque de vision par ordinateur open source et multiplateforme. Contrairement au rendu d'images d'OpenGL, OpenCV implémente de nombreux algorithmes courants pour le traitement d'images et la vision par ordinateur. Avant d'apprendre OpenCV, nous devons comprendre certains concepts de base des images et des vidéos dans l'ordinateur.

Tout d'abord, nous devons comprendre comment une image est représentée dans l'ordinateur. Il existe deux manières courantes de stocker les images : l'une est la carte vectorielle et l'autre est la carte de pixels.

Dans la carte vectorielle, les images sont définies mathématiquement comme une série de points connectés par des lignes. L'élément graphique dans un fichier de carte vectorielle est appelé un objet. Chaque objet est une entité autonome, qui a des propriétés telles que la couleur, la forme, le contour, la taille et la position sur l'écran.

La plus courante est la carte de pixels. Par exemple, la taille d'une image est souvent de 1024 * 768. Cela signifie que l'image a 1024 pixels dans la direction horizontale et 768 pixels dans la direction verticale.

Le pixel est l'unité de base de la carte de pixels. En général, un pixel est un mélange des trois couleurs primaires (rouge, vert et bleu). Étant donné que la nature de l'ordinateur est la reconnaissance des nombres, dans la plupart des cas, nous représentons une couleur primaire en termes de luminosité allant de 0 à 255. En d'autres termes, pour la couleur primaire rouge, 0 signifie le plus foncé, c'est-à-dire noir, et 255 signifie le plus brillant, c'est-à-dire rouge pur.

Ainsi, un pixel peut être représenté comme un triplet (R,G,B), de sorte que blanc peut être représenté comme (255,255,255), et noir est (0,0,0). Ensuite, nous appelons cette image une image dans l'espace de couleur RGB. R, G et B deviennent les trois canaux de l'image ; et il existe de nombreux autres espaces de couleur en plus de l'espace de couleur RGB, tels que HSV, YCrCb etc.

Comme le pixel est à la carte de pixels, l'image est l'unité de base de la vidéo. Une vidéo est composée d'une série d'images dans lesquelles nous appelons chacune des images une trame. Et ce que nous appelons généralement le taux de trames vidéo signifie que cette vidéo contient autant d'images de trames par seconde. Par exemple, si le taux de trames est de 25, alors cette vidéo jouera 25 images de trames par seconde.

S'il y a 1000 millisecondes dans 1 seconde et disons que le taux de trames est rate, alors l'intervalle de temps entre les images de trames est 1000/rate.

Histogramme de couleur d'une image

Un histogramme de couleur est un outil pour décrire une image. Il est similaire à un histogramme normal, sauf que l'histogramme de couleur doit être calculé à partir d'une certaine image.

Si une image est dans l'espace de couleur RGB, alors nous pouvons compter le nombre d'occurrences de chaque couleur dans le canal R. Ainsi, nous pouvons obtenir un tableau de longueur 256 (table de recherche de probabilité de couleur). Divisez toutes les valeurs simultanément par le nombre total de pixels (largeur fois hauteur) dans l'image et convertissez la séquence résultante en un histogramme. Le résultat est un histogramme de couleur du canal R. De manière similaire, vous pouvez avoir les histogrammes dans le canal G et le canal B.

Projection arrière de l'histogramme

Il a été prouvé que dans l'espace de couleur RGB, l'histogramme est sensible aux variations de l'éclairage. Afin de réduire l'impact de ce changement sur l'effet de suivi, l'histogramme doit être projeté arrière. Ce processus est divisé en trois étapes :

  1. Tout d'abord, nous convertissons l'image de l'espace RGB vers l'espace HSV.
  2. Ensuite, nous calculons l'histogramme du canal H.
  3. La valeur de chaque pixel dans l'image est remplacée par la probabilité correspondante dans la table de recherche de probabilité de couleur pour obtenir une carte de distribution de probabilité de couleur.

Ce processus est appelé projection arrière et la carte de distribution de probabilité de couleur est une image en niveaux de gris.

Fondements d'OpenCV

Nous devons installer OpenCV d'abord :

sudo apt update
sudo apt-get install libopencv-dev

Nous supposons que vous connaissez déjà la syntaxe de base du C++. Vous savez que presque chaque programme utilisera le fichier d'en-tête #include <iostream> et using namespace std; ou std::cout. OpenCV a également son propre espace de noms.

Pour utiliser OpenCV, nous n'avons qu'à inclure le fichier d'en-tête suivant :

#include <opencv2/opencv.hpp>

Ensuite :

using namespace cv;

pour activer l'espace de noms OpenCV (ou directement en utilisant le préfixe cv:: pour toutes les API).

C'est votre première fois d'utiliser OpenCV et vous pouvez être peu familier avec les interfaces d'OpenCV, donc nous recommandons d'utiliser le préfixe cv:: pour apprendre les API d'OpenCV.

Écrivons notre premier programme pour lire notre vidéo enregistrée :

//
// main.cpp
//
#include <opencv2/opencv.hpp> // Fichier d'en-tête OpenCV

int main() {

    // créer un objet de capture vidéo
    // OpenCV offre l'objet VideoCapture et
    // traite la lecture d'une vidéo à partir d'un fichier comme si c'était la lecture d'une caméra.
    // Lorsque le paramètre d'entrée est un chemin de fichier, il lira un fichier vidéo ;
    // s'il s'agit d'un numéro d'identification de caméra (généralement 0),
    // il lira la caméra
    cv::VideoCapture video("video.ogv"); // lecture à partir d'un fichier
    // cv::VideoCapture video(0);        // lecture à partir de la caméra

    // conteneur pour l'image de trame lue, objet Mat dans OpenCV
    // La classe clé d'OpenCV est Mat, qui signifie Matrice
    // OpenCV utilise des matrices pour décrire les images
    cv::Mat frame;
    while(true) {

        // écrire les données vidéo dans frame, >> est surchargé par OpenCV
        video >> frame;

        // lorsqu'il n'y a pas de trame, rompre la boucle
        if(frame.empty()) break;

        // visualiser la trame actuelle
        cv::imshow("test", frame);

        // le taux de trames vidéo est de 15, donc nous devons attendre 1000/15 pour une lecture fluide
        // waitKey(int delay) est une fonction d'attente dans OpenCV
        // à ce stade, le programme attendra `delay` millisecondes pour une entrée clavier
        int key = cv::waitKey(1000/15);

        // rompre la boucle lorsqu'on appuie sur le bouton ECS du clavier
        if (key == 27) break;
    }
    // libérer la mémoire
    cv::destroyAllWindows();
    video.release();
    return 0;

}

Placez ce fichier main.cpp dans le même dossier que video.ogv dans ~/Code/camshift, et compilez le programme :

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

Lorsque nous exécutons le programme, nous pouvons voir la vidéo en train de jouer :

./main
image desc

Note

Vous pouvez observer l'erreur suivante :

libdc1394 error: Failed to initialize libdc1394

Il s'agit d'un bogue d'OpenCV et cela n'affecte pas notre exécution.

Si vous voulez éliminer le problème, vous pouvez exécuter le code suivant avant d'exécuter le programme :

sudo ln /dev/null /dev/raw1394

Algorithmes Meanshift et Camshift

  • Meanshift
  • Camshift
  • Pour définir l'événement de rappel de la souris pour sélectionner la cible à suivre
  • Pour lire l'image à partir du flux vidéo
  • Pour implémenter le Camshift

Meanshift

Les algorithmes Meanshift et Camshift sont deux algorithmes classiques pour le suivi d'objets. Camshift est basé sur Meanshift. Leur interprétation mathématique est complexe, mais l'idée de base est relativement simple. Donc, nous sautons ces faits mathématiques et présentons tout d'abord l'algorithme Meanshift.

Supposons qu'il y ait un ensemble de points rouges sur l'écran, le cercle bleu (fenêtre) doit être déplacé vers les points où se trouve la région la plus dense (ou où il y a le plus grand nombre de points) :

image desc

Comme montré dans l'image ci-dessus, marquez le cercle bleu comme C1 et le centre du cercle comme C1_o. Mais le barycentre de ce cercle est C1_r, marqué comme un cercle bleu plein.

Lorsque C1_o et C1_r ne se chevauchent pas, déplacez le cercle C1 vers le centre du cercle C1_r à plusieurs reprises. Finalement, il restera sur le cercle C2 à densité maximale.

Pour le traitement d'images, nous utilisons généralement l'histogramme projeté arrière de l'image. Lorsque la cible à suivre se déplace, il est clair que ce processus de mouvement peut être reflété par l'histogramme projeté arrière. Ainsi, l'algorithme Meanshift déplacera finalement notre fenêtre sélectionnée vers la position de la cible en mouvement. (L'algorithme a été prouvé convergeant à la fin.)

Camshift

Après la description précédente, nous avons vu que l'algorithme Meanshift suit toujours une taille de fenêtre fixe, ce qui ne répond pas à nos besoins, car dans une vidéo, l'objet cible n'a pas nécessairement une taille importante.

C'est pourquoi Camshift a été créé pour résoudre ce problème. On peut également le voir dans le Meanshift Adaptatif Continu de Camshift.

Son idée de base est la suivante : D'abord, appliquer l'algorithme Meanshift. Une fois que les résultats de Meanshift convergent, Camshift met à jour la taille de la fenêtre, calcule une ellipse directionnelle pour correspondre à la fenêtre puis applique l'ellipse comme nouvelle fenêtre pour appliquer l'algorithme Meanshift.

OpenCV fournit une interface générique pour l'algorithme Camshift :

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

Le premier paramètre, probImage, est la projection arrière de l'histogramme de la cible. Le second paramètre, window, est la fenêtre de recherche de l'algorithme Camshift. Le troisième paramètre, criteria, est la condition de fin (termination) de l'algorithme.

Analyse

Après avoir compris l'idée de base de l'algorithme Camshift, nous pouvons analyser que la mise en œuvre de ce code se divise principalement en plusieurs étapes :

  1. Définir l'événement de rappel de la souris pour sélectionner la cible à suivre.
  2. Lire l'image à partir du flux vidéo.
  3. Implémenter le processus Camshift.

Plus bas, nous continuons à modifier le code dans main.cpp.

Pour sélectionner l'objet suivi par la fonction de rappel de la souris

OpenCV est différent d'OpenGL. Il y a cinq paramètres spécifiés pour la fonction de rappel de la souris. Les trois premiers sont ceux dont nous avons le plus besoin : En utilisant la valeur de event, nous pouvons obtenir l'événement du clic gauche de la souris (CV_EVENT_LBUTTONDOWN), l'événement du relâchement du clic gauche de la souris (CV_EVENT_LBUTTONUP) et ainsi de suite :

bool selectObject = false; // Utilisé pour savoir si un objet a été sélectionné ou non
int trackObject = 0;       // 1 signifie qu'il y a un objet à suivre, 0 signifie qu'il n'y a pas d'objet, et -1 signifie que les propriétés Camshift n'ont pas été calculées
cv::Rect selection;        // Sauvegarde la région sélectionnée par la souris
cv::Mat image;             // Cache l'image provenant de la vidéo

// Fonction de rappel de la souris d'OpenCV :
// void onMouse(int event, int x, int y, int flag, void *param)
// Le quatrième paramètre `flag` représente l'état supplémentaire,
// param signifie le paramètre utilisateur, nous n'en avons pas besoin, donc, pas de nom.
void onMouse( int event, int x, int y, int, void* ) {
    static cv::Point origin;
    if(selectObject) {
        // Déterminer la hauteur, la largeur et la position du coin supérieur gauche de la sélection
        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);

        // & est remplacé par cv::Rect
        // Cela signifie l'intersection de deux régions,
        // Le but principal ici est de traiter la région en dehors de la région sélectionnée
        selection &= cv::Rect(0, 0, image.cols, image.rows);
    }

    switch(event) {
            // Traitement du clic gauche enfoncé
        case CV_EVENT_LBUTTONDOWN:
            origin = cv::Point(x, y);
            selection = cv::Rect(x, y, 0, 0);
            selectObject = true;
            break;
            // Traitement du relâchement du clic gauche
        case CV_EVENT_LBUTTONUP:
            selectObject = false;
            if( selection.width > 0 && selection.height > 0 )
                trackObject = -1; // L'objet à suivre n'a pas eu ses propriétés Camshift calculées
            break;
    }
}

Pour lire des images à partir d'un flux vidéo

Nous avons implémenté la structure de lecture d'un flux vidéo. Écrivons plus de détails :

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

    // 1. enregistrer la fonction de rappel pour l'événement de la souris
    cv::setMouseCallback("CamShift at LabEx", onMouse, NULL);

    cv::Mat frame;

    // 2. lire l'image à partir de la vidéo
    while(true) {
        video >> frame;
        if(frame.empty()) break;

        // écrire l'image à partir de frame dans la variable globale image pour le cache
        frame.copyTo(image);

        // dessiner un rectangle si un objet est sélectionné
        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;
    }
    // libérer la mémoire allouée
    cv::destroyAllWindows();
    video.release();
    return 0;
}

Note :

ROI (Region of Interest) : En traitement d'images, n'importe quelle région à traiter peut être une région d'intérêt, à savoir une ROI.

Pour implémenter Camshift avec OpenCV

Pour calculer l'histogramme projeté arrière de la cible à suivre, il est nécessaire d'utiliser la fonction cvtColor, qui peut convertir l'image d'origine de l'espace de couleur RGB en l'espace de couleur HSV. Le calcul de l'histogramme doit être effectué après avoir sélectionné la cible initiale, donc :

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;             // fenêtre de suivi
    int hsize = 16;                   // pour l'histogramme
    float hranges[] = {0,180};        // pour l'histogramme
    const float* phranges = hranges;  // pour l'histogramme

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

        // transférer dans l'espace HSV
        cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
        // traitement lorsqu'il y a un objet
        if(trackObject) {

            // ne traiter que H : 0~180, S : 30~256, V : 10~256, filtrer les autres et copier le reste dans mask
            cv::inRange(hsv, cv::Scalar(0, 30, 10), cv::Scalar(180, 256, 10), mask);
            // séparer le canal h de hsv
            int ch[] = {0, 0};
            hue.create(hsv.size(), hsv.depth());
            cv::mixChannels(&hsv, 1, &hue, 1, ch, 1);

            // extraction de propriétés si les propriétés de l'objet à suivre n'ont pas été calculées
            if( trackObject < 0 ) {

                // configurer le canal h et la ROI de mask
                cv::Mat roi(hue, selection), maskroi(mask, selection);
                // calculer l'histogramme de la ROI
                calcHist(&roi, 1, 0, maskroi, hist, 1, &hsize, &phranges);
                // normalisation de l'histogramme
                normalize(hist, hist, 0, 255, CV_MINMAX);

                // définir la fenêtre de suivi
                trackWindow = selection;

                // marquer que les propriétés de l'objet à suivre ont été calculées
                trackObject = 1;
            }
            // projection arrière de l'histogramme
            calcBackProject(&hue, 1, 0, hist, backproj, &phranges);
            // extraire la région commune
            backproj &= mask;
            // appeler l'algorithme Camshift
            cv::RotatedRect trackBox = CamShift(backproj, trackWindow, cv::TermCriteria( CV_TERMCRIT_EPS | CV_TERMCRIT_ITER, 10, 1 ));
            // traitement lorsque la région est trop petite pour être dessinée
            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);
            }
            // dessiner la zone de suivi
            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;
}

Résumé

Voici tout ce que nous avons écrit dans ce projet :

#include <opencv2/opencv.hpp>

bool selectObject = false; // Utilisé pour savoir si un objet a été sélectionné ou non
int trackObject = 0;       // 1 signifie qu'il y a un objet à suivre, 0 signifie qu'il n'y a pas d'objet, et -1 signifie que les propriétés Camshift n'ont pas été calculées
cv::Rect selection;        // Sauvegarde la région sélectionnée par la souris
cv::Mat image;             // Cache l'image provenant de la vidéo

// Fonction de rappel de la souris d'OpenCV :
// void onMouse(int event, int x, int y, int flag, void *param)
// Le quatrième paramètre `flag` représente l'état supplémentaire,
// param signifie le paramètre utilisateur, nous n'en avons pas besoin, donc, pas de nom.
void onMouse( int event, int x, int y, int, void* ) {
    static cv::Point origin;
    if(selectObject) {
        // Déterminer la hauteur, la largeur et la position du coin supérieur gauche de la sélection
        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);

        // & est remplacé par cv::Rect
        // Cela signifie l'intersection de deux régions,
        // Le but principal ici est de traiter la région en dehors de la région sélectionnée
        selection &= cv::Rect(0, 0, image.cols, image.rows);
    }

    switch(event) {
            // Traitement du clic gauche enfoncé
        case CV_EVENT_LBUTTONDOWN:
            origin = cv::Point(x, y);
            selection = cv::Rect(x, y, 0, 0);
            selectObject = true;
            break;
            // Traitement du relâchement du clic gauche
        case CV_EVENT_LBUTTONUP:
            selectObject = false;
            if( selection.width > 0 && selection.height > 0 )
                trackObject = -1; // L'objet à suivre n'a pas eu ses propriétés Camshift calculées
            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;             // Fenêtre de suivi
    int hsize = 16;                   // Pour l'histogramme
    float hranges[] = {0,180};        // Pour l'histogramme
    const float* phranges = hranges;  // Pour l'histogramme

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

        // Transférer dans l'espace HSV
        cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
        // Traitement lorsqu'il y a un objet
        if(trackObject) {

            // Ne traiter que H : 0~180, S : 30~256, V : 10~256, filtrer les autres et copier le reste dans mask
            cv::inRange(hsv, cv::Scalar(0, 30, 10), cv::Scalar(180, 256, 256), mask);
            // Séparer le canal h de hsv
            int ch[] = {0, 0};
            hue.create(hsv.size(), hsv.depth());
            cv::mixChannels(&hsv, 1, &hue, 1, ch, 1);

            // Extraction de propriétés si les propriétés de l'objet à suivre n'ont pas été calculées
            if( trackObject < 0 ) {

                // Configurer le canal h et la ROI de mask
                cv::Mat roi(hue, selection), maskroi(mask, selection);
                // Calculer l'histogramme de la ROI
                calcHist(&roi, 1, 0, maskroi, hist, 1, &hsize, &phranges);
                // Normalisation de l'histogramme
                normalize(hist, hist, 0, 255, CV_MINMAX);

                // Définir la fenêtre de suivi
                trackWindow = selection;

                // Marquer que les propriétés de l'objet à suivre ont été calculées
                trackObject = 1;
            }
            // Projection arrière de l'histogramme
            calcBackProject(&hue, 1, 0, hist, backproj, &phranges);
            // Extraire la région commune
            backproj &= mask;
            // Appeler l'algorithme Camshift
            cv::RotatedRect trackBox = CamShift(backproj, trackWindow, cv::TermCriteria( CV_TERMCRIT_EPS | CV_TERMCRIT_ITER, 10, 1 ));
            // Traitement lorsque la région est trop petite pour être dessinée
            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);
            }
            // Dessiner la zone de suivi
            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;
}

Recompilons main.cpp :

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

et exécutons-le :

./main

Maintenant, nous pouvons sélectionner l'objet dans le programme et le suivi est en cours :

image desc

Dans l'image ci-dessus, nous avons sélectionné Jupiter et la fenêtre de suivi est une ellipse rouge.