GeckoGeek.fr

Paradigme d'un lézard

Vendredi 14 Décembre 2018

Tutorial OpenCV : Isoler et Traquer une Couleur

Par Lya le 24/03/2010 dans Programmation, Réalité Augmentée | 56 commentaires

Continuons sur la lancée d’OpenCv. Nous avons vu dans le précédent billet comment récupérer le flux d’une vidéo ou d’une webcam. Avant d’afficher ce dernier, il est possible de faire subir aux images tout un tas de traitements (niark). L’un d’entre eux, qui est amusant et relativement facile à réaliser avec OpenCV, est le tracking d’objet.

Par les détails ou par la couleur

Globalement avec OpenCV vous pouvez utiliser facilement deux types de méthodes pour traquer des objets. Celles qui repèrent des détails caractéristiques de l’objet et celles qui se basent sur la détection de couleur. Le choix dépend de vos conditions.

Si votre objet contient des détails et qu’il n’a pas de couleur particulière (ie qui se détache fortement de son environnement) vous préférerez la première méthode. En revanche si vous souhaitez traquer un objet qui n’a pas de forme prédéfinie mais qui possède une couleur repérable alors vous appliquerez la seconde.

Dans ce tutoriel, nous allons nous intéresser à celle qui se base sur la couleur. L’algorithme que nous utiliserons sera légèrement différent (en terme d’utilisation) de celui du Camshift qui est présent dans les exemples fourni avec openCV (et que nous vous conseillons d’essayer d’ailleurs : OpenCV-2.0.0/samples/c/camshiftdemo.c).

Voici quelques vidéos de l’algorithme en action :



Le code est maintenant expliqué pas à pas. La version finale est présentée en bas de page.

Isoler une couleur

Je suppose à partir de là que vous avez votre code qui récupère un flux vidéo. Nous allons donc voir maintenant comment isoler une couleur choisie.

Binarisation

La binarisation consiste à séparer les pixels de l’image en deux classes distinctes, généralement représentées par deux couleurs, le blanc et le noir. Notre première classe correspondra à notre couleur à isoler, la seconde à toutes les autres couleurs. Ainsi, si notre couleur sélectionnée est le rouge, nous verrons sur notre image binarisée tous les éléments rouges apparaître en blanc sur un fond noir (ou l’inverse selon l’attribution des couleurs aux classes).

Si nous voulions isoler plusieurs couleurs, nous devrons réaliser une segmentation pour obtenir plusieurs classes (la binarisation est une segmentation qui produit toujours deux classes).

L’image hsv

Nous allons tout d’abord convertir notre image qui est en BGR (équivalent de RGB mais avec une inversion des canaux bleu et rouge) en HSV (Hue, Saturation, Value, en français : TSV – Teinte, Saturation, Valeur). Nous passons l’image en HSV car ainsi nous pourrons nous baser sur la teinte et la saturation de la couleur en laissant plus libre la « brillance » (V – value) de cette dernière. Ce qui nous permet, en partie seulement, d’éloigner les problèmes liés à l’éclairage.

IplImage *hsv;
hsv = cvCloneImage(image);
cvCvtColor(image, hsv, CV_BGR2HSV);

La conversion de RGB vers HSV est sans perte. Toutes les couleurs RGB ont une correspondance en HSV. (Attention, ce n’est pas le cas pour l’opération inverse, HSV vers RGB.). Remarque : sur OpenCV, la composante Hue n’est pas codée sur 8 bits (0 à 255) comme les autres mais de 0 à 180 (donc perte de précision).

Le masque

Nous devons maintenant créer un masque qui nous permettra de représenter l’image binarisée. Il doit donc être lui aussi de la même taille que notre image originale. Mais il n’a besoin que d’un seul canal (0 à 255).

IplImage *mask;
mask = cvCreateImage(cvGetSize(image), image->depth, 1);

Il nous faut le remplir. Supposons que nous avons la valeur de la couleur que nous recherchons en HSV. Théoriquement, nous devons mettre en blanc (par exemple) tous les pixels ayant les mêmes valeurs HSV que notre couleur. Pratiquement, le résultat ne serait pas très convaincant et nous ne récupérerions que quelques pixels.

Nous allons donc rechercher les pixels qui se situent dans un intervalle autour de cette couleur, et nous laisserons la valeur (V) de côté (ie conditions d’éclairage). Nous utiliserons donc une valeur de tolérance. Ici, elle sera la même pour la teinte et la saturation, mais elle pourrait très bien être différente.
Nous cherchons donc tous les pixels qui vérifient :

H – tolérance <= Hpixel < H + tolérance && S - tolérance <= Spixel < S + tolérance. Une fonction opencv nous permet d’appliquer cette opération directement à tous les pixels de notre image HSV et met à jour directement notre masque. Si l’expression est vérifiée pour un pixel donné de l’image, elle met le pixel correspondant du masque à 255 (blanc), sinon, elle le met à 0 (noir). [cpp]cvInRangeS(hsv, cvScalar(h - tolerance -1, s - tolerance, 0), cvScalar(h + tolerance -1, s + tolerance, 255), mask);[/cpp]

Opérateurs morphologiques et isolation d’un objet

Nous avons maintenant une image en noir et blanc qui nous indique où se trouve notre couleur dans l’image par des « tâches blanches ». Or nous voulons suivre un objet de couleur, il va donc nous falloir l’isoler.

Globalement (à moins que vous choisissez la couleur la plus présente dans votre environnement) l’objet correspondra à la tâche la plus grosse et la plus dense. Il nous faut donc nous débarrasser de ces pixels éparpillés ça et là qui étaient contenus dans l’intervalle mais qui ne représentent pas notre objet.

C’est là qu’interviennent les opérateurs morphologiques. Nous allons effectuer une ouverture morphologique. L’ouverture consiste à l’application successive de deux opérateurs morphologiques. D’abord nous effectuons une érosion. Cela nous permet de supprimer les pixels « isolés » qui ne correspondent pas à notre objet de couleur. Ensuite nous effectuons une dilatation qui nous permet de renforcer les groupes denses de pixels (donc notre objet).

Pour rappel, dans notre image nous avons mis le blanc en couleur repérée, donc il nous faut inverser l’ouverture (car elle est basée sur les pixels noirs).

On crée d’abord le noyau de notre opération morphologique. Ici je l’ai choisi un peu plus grand que le noyau de base (3,3) afin de pouvoir supprimer les petits amas de pixels parasites. La forme circulaire a été choisie car elle correspond plus à un environnement extérieur (bien que les tables, armoires, & co soient carrées).

IplConvKernel *kernel;
kernel = cvCreateStructuringElementEx(5, 5, 2, 2, CV_SHAPE_ELLIPSE);

Ensuite nous appliquons l’ouverture (à l’envers) à notre masque (pour les dilatations et érosions, l’image temporaire n’est pas nécessaire).

cvDilate(mask, mask, kernel, 1);
cvErode(mask, mask, kernel, 1);

Avec cela, vous ne verrez apparaître en blanc que votre objet (si sa couleur n’est pas récurrente dans le fond de l’image, cela va de soi).

Choisir la couleur

Le plus simple est de choisir la couleur directement sur l’image du flux vidéo, en cliquant dessus. C’est relativement simple à effectuer. Sur la fenêtre dans laquelle nous affichons le flux, nous ajoutons une fonction de callback sur l’event de la souris :

cvSetMouseCallback("GeckoGeek Color Tracking", getObjectColor);

N’oubliez pas que les fenêtres sont identifiées par leur nom, donc ne vous trompez pas en les désignant.
Quand un événement souris est détecté, la fonction « getObjectColor » est appelée.

Elle est définit ainsi :

void getObjectColor(int event, int x, int y, int flags, void *param = NULL)

Il nous suffit maintenant de la définir.

Avant tout, il faut détecter l’événement « clic gauche » :

if(event == CV_EVENT_LBUTTONUP)

Ensuite, nous convertissons l’image (qui est en BGR) en HSV comme nous l’avons vu précédemment et nous récupérons le pixel sélectionné (en x, y) :

CvScalar pixel;
pixel = cvGet2D(hsv, y, x);

Il ne reste qu’à mettre à jour les valeurs de notre couleur choisie :

h = (int)pixel.val[0];
s = (int)pixel.val[1];
v = (int)pixel.val[2];

Traquer la couleur

En analysant le déplacement de la « tâche » qui représente notre objet sur les images binarisées nous pouvons le suivre. Une possibilité est de calculer le centre de la tâche et de regarder le déplacement de ce centre.

Calcul du barycentre

Il nous faut récupérer les coordonnées x et y de notre tâche afin de calculer son barycentre. Pour cela nous parcourons le masque et nous additionnons les coordonnées (les x et les y respectivement) de tous les pixels blancs (255) dans notre cas. Nous comptons au passage le nombre de point afin de pouvoir diviser nos sommes.

for(x = 0; x < mask->width; x++) {
	for(y = 0; y < mask->height; y++) {
		if(((uchar *)(mask->imageData + y*mask->widthStep))[x] == 255) {
			sommeX += x;
			sommeY += y;
			(*nbPixels)++;
		}
	}
}

Si nous avons une « tâche » dans l’image (donc des pixel blancs), on calcule le barycentre, sinon on donne au centre une valeur en dehors de l’image.

cvPoint((int)(sommeX / (*nbPixels)), (int)(sommeY / (*nbPixels)));

Ajout d’un marqueur et lissage du déplacement

Nous pouvons maintenant ajouter un marqueur sur notre image qui sera centré sur le barycentre calculé. Par exemple sur la vidéo nous avons ajouté un cercle rouge et parfois une image. Vous pouvez y mettre ce que vous voulez.

if (nbPixels > 10)
	cvDrawCircle(image, objectPos, 15, CV_RGB(255, 0, 0), -1);
cvShowImage("GeckoGeek Color Tracking", image);

Nous n’affichons qu’au delà de dix pixels trouvés afin de ne pas afficher de cercle si l’objet n’est pas présent mais que quelques points de couleurs sont détectés.

Par contre, il va nous falloir lisser le déplacement. Vous ne détecterez pas toujours le même nombre de points, surtout si vous bougez très légèrement. Votre objet n’aura pas bougé à l’image, mais votre centre lui (et donc votre marqueur) bougera sans cesse. Il faut aussi donner une impression de suivi lorsque l’objet se déplace très vite (et non une apparition à un endroit puis à un autre sans rien entre les deux).

Du coup, nous allons créer des pas, un pas minimum et un pas maximum, et nous allons considérer la distance sur laquelle il faut se déplacer sur x et sur y. Nous recalculons ce déplacement à chaque image pour mettre à jour la nouvelle position. On sauvegarde dans objectPos la position de l’image précédente (donc la position actuelle avant mise à jour). Le barycentre calculé sur l’image est stocké lui dans objectNextPos, la position à atteindre.

if (abs(objectPos.x - objectNextPos.x) > STEP_MIN) {
	objectNextStepX = max(STEP_MIN, min(STEP_MAX, abs(objectPos.x - objectNextPos.x) / 2));
	objectPos.x += (-1) * sign(objectPos.x - objectNextPos.x) * objectNextStepX;
}
if (abs(objectPos.y - objectNextPos.y) > STEP_MIN) {
	objectNextStepY = max(STEP_MIN, min(STEP_MAX, abs(objectPos.y - objectNextPos.y) / 2));
	objectPos.y += (-1) * sign(objectPos.y - objectNextPos.y) * objectNextStepY;
}

Ainsi le déplacement de notre objet est lissé. La méthode que nous utilisons est très simple est très basique. Il est possible de la remplacer par différents algorithmes qui donneront des résultats encore plus satisfaisants.

Libération de la mémoire

N’oubliez pas qu’à chaque fois que vous créez un objet, il vous faudra le supprimer.

Il faut donc supprimer (je cite en vrac) les images hsv, le masque, et le noyau des opérateurs morphologiques.

cvReleaseImage(&hsv);
cvReleaseImage(&mask);
cvReleaseStructuringElement(&kernel);

Et bien sûr les fenêtres et la capture.

Tout en un

A noter que dans l’algorithme présenté, les paramètres ont été réglés pour que l’on puisse détecter aussi bien des gros comme des tout petits objets et des couleurs très variées voir parfois proche de l’environnement de fond. Dans la vidéo d’illustration, Pour tous les objets présentés, quelque soit leur forme, leur taille et leur couleur, aucun des paramètres n’a été modifié, nous nous contentions seulement de cliquer sur la nouvelle couleur à traquer. Vous voyez donc bien ainsi qu’il est possible d’améliorer grandement la résultat si vous voulez l’utiliser dans un cas précis avec moins de variations (même forme, même couleur, etc.).

A vous donc de l’adapter à vos besoin et de faire varier les paramètres (tolérance, opérateur morphologiques et noyaux, les pas …). Vous pouvez également sélectionner une zone comme « couleur modèle » plutôt qu’un pixel, ou effectuer une détection de contour sur l’image binarisée avant de calculer le barycentre, adapter la taille du marqueur en fonction des homothéties (objet qui avance et qui recule), si votre objet à une forme orientée vous pouvez effectuer des rotations (préférable de le faire avec une mire quand même, car plus précis), vous pouvez aussi repérer plusieurs objets (composantes connexes), bref il y a beaucoup de possibilités.

Une partie de ces paramètres peut être aussi représentée sous forme de trackbars dans la fenêtre que vous affichez, ce qui vous permettra de les régler plus facilement sans avoir à relancer le programme.

Voici le code que nous venons de voir en entier :

/*
 * Code written by Lya (GeckoGeek.fr)
 */

#include "opencv/highgui.h"
#include "opencv/cv.h"

#include <iostream>
#include <stdlib.h>
#include <stdio.h>

// Maths methods
#define max(a, b) ((a) > (b) ? (a) : (b))
#define min(a, b) ((a) < (b) ? (a) : (b))  
#define abs(x) ((x) > 0 ? (x) : -(x))
#define sign(x) ((x) > 0 ? 1 : -1)

// Step mooving for object min & max
#define STEP_MIN 5
#define STEP_MAX 100 

IplImage *image;

// Position of the object we overlay
CvPoint objectPos = cvPoint(-1, -1);
// Color tracked and our tolerance towards it
int h = 0, s = 0, v = 0, tolerance = 10;

/*
 * Transform the image into a two colored image, one color for the color we want to track, another color for the others colors
 * From this image, we get two datas : the number of pixel detected, and the center of gravity of these pixel
 */
CvPoint binarisation(IplImage* image, int *nbPixels) {

	int x, y;
	CvScalar pixel;
	IplImage *hsv, *mask;
	IplConvKernel *kernel;
	int sommeX = 0, sommeY = 0;
	*nbPixels = 0;

	// Create the mask &initialize it to white (no color detected)
	mask = cvCreateImage(cvGetSize(image), image->depth, 1);

	// Create the hsv image
	hsv = cvCloneImage(image);
	cvCvtColor(image, hsv, CV_BGR2HSV);

	// We create the mask
	cvInRangeS(hsv, cvScalar(h - tolerance -1, s - tolerance, 0), cvScalar(h + tolerance -1, s + tolerance, 255), mask);

	// Create kernels for the morphological operation
	kernel = cvCreateStructuringElementEx(5, 5, 2, 2, CV_SHAPE_ELLIPSE);

	// Morphological opening (inverse because we have white pixels on black background)
	cvDilate(mask, mask, kernel, 1);
	cvErode(mask, mask, kernel, 1);  

	// We go through the mask to look for the tracked object and get its gravity center
	for(x = 0; x < mask->width; x++) {
		for(y = 0; y < mask->height; y++) { 

			// If its a tracked pixel, count it to the center of gravity's calcul
			if(((uchar *)(mask->imageData + y*mask->widthStep))[x] == 255) {
				sommeX += x;
				sommeY += y;
				(*nbPixels)++;
			}
		}
	}

	// Show the result of the mask image
	cvShowImage("GeckoGeek Mask", mask);

	// We release the memory of kernels
	cvReleaseStructuringElement(&kernel);

	// We release the memory of the mask
	cvReleaseImage(&mask);
	// We release the memory of the hsv image
    	cvReleaseImage(&hsv);

	// If there is no pixel, we return a center outside the image, else we return the center of gravity
	if(*nbPixels > 0)
		return cvPoint((int)(sommeX / (*nbPixels)), (int)(sommeY / (*nbPixels)));
	else
		return cvPoint(-1, -1);
}

/*
 * Add a circle on the video that fellow your colored object
 */
void addObjectToVideo(IplImage* image, CvPoint objectNextPos, int nbPixels) {

	int objectNextStepX, objectNextStepY;

	// Calculate circle next position (if there is enough pixels)
	if (nbPixels > 10) {

		// Reset position if no pixel were found
		if (objectPos.x == -1 || objectPos.y == -1) {
			objectPos.x = objectNextPos.x;
			objectPos.y = objectNextPos.y;
		}

		// Move step by step the object position to the desired position
		if (abs(objectPos.x - objectNextPos.x) > STEP_MIN) {
			objectNextStepX = max(STEP_MIN, min(STEP_MAX, abs(objectPos.x - objectNextPos.x) / 2));
			objectPos.x += (-1) * sign(objectPos.x - objectNextPos.x) * objectNextStepX;
		}
		if (abs(objectPos.y - objectNextPos.y) > STEP_MIN) {
			objectNextStepY = max(STEP_MIN, min(STEP_MAX, abs(objectPos.y - objectNextPos.y) / 2));
			objectPos.y += (-1) * sign(objectPos.y - objectNextPos.y) * objectNextStepY;
		}

	// -1 = object isn't within the camera range
	} else {

		objectPos.x = -1;
		objectPos.y = -1;

	}

	// Draw an object (circle) centered on the calculated center of gravity
	if (nbPixels > 10)
		cvDrawCircle(image, objectPos, 15, CV_RGB(255, 0, 0), -1);

	// We show the image on the window
	cvShowImage("GeckoGeek Color Tracking", image);

}

/*
 * Get the color of the pixel where the mouse has clicked
 * We put this color as model color (the color we want to tracked)
 */
void getObjectColor(int event, int x, int y, int flags, void *param = NULL) {

	// Vars
	CvScalar pixel;
	IplImage *hsv;

	if(event == CV_EVENT_LBUTTONUP)	{

		// Get the hsv image
		hsv = cvCloneImage(image);
		cvCvtColor(image, hsv, CV_BGR2HSV);

		// Get the selected pixel
		pixel = cvGet2D(hsv, y, x);

		// Change the value of the tracked color with the color of the selected pixel
		h = (int)pixel.val[0];
		s = (int)pixel.val[1];
		v = (int)pixel.val[2];

		// Release the memory of the hsv image
    		cvReleaseImage(&hsv);

	}

}

int main() {

	// Image & hsvImage
	IplImage *hsv;
	// Video Capture
	CvCapture *capture;
	// Key for keyboard event
	char key;

	// Number of tracked pixels
	int nbPixels;
	// Next position of the object we overlay
	CvPoint objectNextPos;

	// Initialize the video Capture (200 => CV_CAP_V4L2)
 	capture = cvCreateCameraCapture(200);

	// Check if the capture is ok
    	if (!capture) {
		printf("Can't initialize the video capture.\n");
        	return -1;
 	}

	// Create the windows
   	cvNamedWindow("GeckoGeek Color Tracking", CV_WINDOW_AUTOSIZE);
   	cvNamedWindow("GeckoGeek Mask", CV_WINDOW_AUTOSIZE);
	cvMoveWindow("GeckoGeek Color Tracking", 0, 100);
	cvMoveWindow("GeckoGeek Mask", 650, 100);

	// Mouse event to select the tracked color on the original image
	cvSetMouseCallback("GeckoGeek Color Tracking", getObjectColor);

	// While we don't want to quit
	while(key != 'Q' && key != 'q') {

		// We get the current image
		image = cvQueryFrame(capture);

		// If there is no image, we exit the loop
		if(!image)
			continue;

		objectNextPos = binarisation(image, &nbPixels);
		addObjectToVideo(image, objectNextPos, nbPixels);

		// We wait 10 ms
		key = cvWaitKey(10);

	}

	// Destroy the windows we have created
	cvDestroyWindow("GeckoGeek Color Tracking");
	cvDestroyWindow("GeckoGeek Mask");

	// Destroy the capture
	cvReleaseCapture(&capture);

	return 0;

}

Et pour compiler le tout :

g++ monProgramme.cpp -o monProg -lcv -lhighgui

ou bien

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

Voilà, à vous de jouer ;-]

Commentaires (56)
  1. Johnf134 le 3 Sep 2014 à 16:31

    I like what you guys are up too. This kind of clever work and exposure! bkkafkdbaedk

  2. maxi le 20 Apr 2015 à 14:27

    hi,
    First of all thank you for the tutorial is my great help. I have another concern
    I veudrait namely: how to delineate my detection zone? (ie to ensure that the program remains part of an area)

  3. Coldfire le 26 Oct 2015 à 12:26

    Hi tout le monde !!
    voilà je veux bosser sur la reconnaissance des plaques d’immatriculation avec la bibliothèque opencv mais franchement je ne sais pas par où commencer :p alors si qqun peut m’aider son aide est la bienvenue :p

  4. Coldfire le 26 Oct 2015 à 12:59

    salut tt le monde!!
    voilà je veux réaliser un application opencv pour reconnaitre les plaques d’immatricualtion mais je ne sais pas par où commencer !!
    donnez moi des idées :p

  5. mounia le 8 Jan 2016 à 21:40

    j veux calculer le vecteur de mouvent d un main svp !

  6. lyra le 25 Nov 2016 à 12:18

    si a la place d’un point rouge je veux charger un model 3D et faire en sorte que ce soit lui qui représente le centre de gravité je dois faire comment svp? merci !


Laisser un commentaire