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) :
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.
Après l'installation, nous pouvons trouver le logiciel d'enregistrement dans le menu des applications :
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 :
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 :
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 :
Tout d'abord, nous convertissons l'image de l'espace RGB vers l'espace HSV.
Ensuite, nous calculons l'histogramme du canal H.
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.
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
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) :
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 :
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 :
Définir l'événement de rappel de la souris pour sélectionner la cible à suivre.
Lire l'image à partir du flux vidéo.
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 :
Dans l'image ci-dessus, nous avons sélectionné Jupiter et la fenêtre de suivi est une ellipse rouge.