---
title: INFO911 Segmentation temps-réel semi-supervisée par distance d'histogramme de couleurs
---
INFO911 Segmentation temps-réel semi-supervisée par distance d'histogramme de couleurs
===
[TOC]
> [name=Jacques-Olivier Lachaud][time=December 2021][color=#907bf7]
###### tags: `info911` `tp`
Retour à [INFO911 (Main) Traitement et analyse d'image](https://codimd.math.cnrs.fr/s/UE_B59gMy)
Ce mini-projet vous fait construire progressivement une application qui segmente un flux vidéo en temps-réel et reconnait des "objets" que vous lui avez montré.
{%youtube JKhwMEDves8 %}
1. Le programme "apprend" les distributions de couleur du fond
2. Le programme "apprend" plusieurs distributions de couleurs sur mon visage et mes mains
3. On passe en mode reconnaissance, ça marche pas trop mal, sauf certaines zones du plafond dont les couleurs ressemblent aux couleurs de ma peau
4. Le programme "apprend" plusieurs distributions de couleurs sur ma tasse verte
5. On passe en mode reconnaissance, ça marche pas trop mal pour différencier tasse/peau/fond.
## 1. Set-up initial avec mini-application de capture vidéo
On vous donne le code ci-dessous, qui ouvre votre caméra et récupère les images toutes les 50 ms.
```cpp=
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <opencv2/core/utility.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>
using namespace cv;
using namespace std;
int main( int argc, char** argv )
{
Mat img_input, img_seg, img_d_bgr, img_d_hsv, img_d_lab;
VideoCapture* pCap = nullptr;
const int width = 640;
const int height= 480;
const int size = 50;
// Ouvre la camera
pCap = new VideoCapture( 0 );
if( ! pCap->isOpened() ) {
cout << "Couldn't open image / camera ";
return 1;
}
// Force une camera 640x480 (pas trop grande).
pCap->set( CAP_PROP_FRAME_WIDTH, 640 );
pCap->set( CAP_PROP_FRAME_HEIGHT, 480 );
(*pCap) >> img_input;
if( img_input.empty() ) return 1; // probleme avec la camera
Point pt1( width/2-size/2, height/2-size/2 );
Point pt2( width/2+size/2, height/2+size/2 );
namedWindow( "input", 1 );
imshow( "input", img_input );
bool freeze = false;
while ( true )
{
char c = (char)waitKey(50); // attend 50ms -> 20 images/s
if ( pCap != nullptr && ! freeze )
(*pCap) >> img_input; // récupère l'image de la caméra
if ( c == 27 || c == 'q' ) // permet de quitter l'application
break;
if ( c == 'f' ) // permet de geler l'image
freeze = ! freeze;
cv::rectangle( img_input, pt1, pt2, Scalar( { 255.0, 255.0, 255.0 } ), 1 );
imshow( "input", img_input ); // affiche le flux video
}
return 0;
}
```
Notez où on récupère l'image courante (ligne 39) et comment on récupère facilement sur quelle touche on presse (ligne 37), par exemple si on presse 'f' on peut geler la caméra.
## 2. Distribution de couleurs ou histogramme de couleurs
De la même façon que l'on peut définir la distribution des niveaux de gris d'une zone dans une image N & B (c'est l'histogramme), on peut définir une *distribution des couleurs* dans une zone d'une image couleur. On peut aussi parler d'**histogramme couleur**.
### Représentation d'une histogramme couleur
Le problème est que a priori notre histogramme couleur aurait une taille `256x256x256` pour l'espace RGB usuel. C'est beaucoup trop coûteux à stocker, mais surtout énormément de ces cases seraient vides.
Du coup, on va résumer l'histogramme dans beaucoup moins de cases, typiquement `8x8x8`, donc 512 cases (on peut faire plus genre `16x16x16` mais votre ordinateur va ramer).
```cpp
struct ColorDistribution {
float data[ 8 ][ 8 ][ 8 ]; // l'histogramme
int nb; // le nombre d'échantillons
ColorDistribution() { reset(); }
ColorDistribution& operator=( const ColorDistribution& other ) = default;
// Met à zéro l'histogramme
void reset();
// Ajoute l'échantillon color à l'histogramme:
// met +1 dans la bonne case de l'histogramme et augmente le nb d'échantillons
void add( Vec3b color );
// Indique qu'on a fini de mettre les échantillons:
// divise chaque valeur du tableau par le nombre d'échantillons
// pour que case représente la proportion des picels qui ont cette couleur.
void finished();
// Retourne la distance entre cet histogramme et l'histogramme other
float distance( const ColorDistribution& other ) const;
};
```
Il s'agit d'écrire les méthodes de cette structure (on fera `distance` plus loin). On s'en servira ainsi pour stocker la distribution de couleur d'une zone délimitée par les points `pt1` et `pt2` d'une image couleur `input`:
```cpp
ColorDistribution
getColorDistribution( Mat input, Point pt1, Point pt2 )
{
ColorDistribution cd;
for ( int y = pt1.y; y < pt2.y; y++ )
for ( int x = pt1.x; x < pt2.x; x++ )
cd.add( input.at<Vec3b>( y, x ) );
cd.finished();
return cd;
}
```
> Notez qu'on pourrait être plus intelligent :wink: et :
> 1) utiliser un tableau 1D de taille 512 plutôt qu'un tableau 3D 8x8x8
> 2) avoir moins de précision sur les bleus que sur rouge et vert (genre 4x8x8)
> 3) se mettre dans un autre espace de couleurs...
### Distance entre deux histogrammes couleurs
On se rappelle que l'on peut voir un histogramme comme une densité de probabilité et qu'on peut appliquer toute l'artillerie usuelle des statistiques. On utilisera la distance dite du $\chi_2$ (chi-2) entre deux histogrammes $h_1$ et $h_2$:

Ecrivez donc la méthode `ColorDistribution::distance`.
:::warning
Attention, il faut bien avoir normalisé les valeurs dans l'histogramme en divisant par le nombre d'échantillons.
:::
### Vérification
Vérifiez que votre code fonctionne en ajoutant une touche 'v', qui fait le calcul suivant:
```
1) Calcule la distribution couleur de la partie gauche et de la partie droite de l'écran
2) Calcule la distance entre les 2 distributions et l'affiche
```
| image |  |  |  |
| --- | ---- | -------- | -------- |
| distance | 0.294053 | 0.498558 | 0.43796 |
Faites quelques essais en mettant un cahier vert d'un côté et un cahier bleu de l'autre, etc.
## 3. Comparaison fond et objet
### Distributions de couleurs du fond
Comme le fond peut être assez varié (suivant votre décor), qu'éventuellement vous voudriez faire partie du fond, on ne va pas mémoriser une seule distribution pour le fond mais plusieurs. On va découper le fond en blocs 128x128 (sauf aux bords), du coup ça fera environ 20 distributions de couleurs qui décrivent le fond.
```cpp
std::vector<ColorDistribution> col_hists; // histogrammes du fond
std::vector<ColorDistribution> col_hists_object; // histogrammes de l'objet
```
Rajoutez une touche 'b' qui calcule ces histogrammes de couleurs sur les difféentes parties de l'image, et qui les mémorise dans le tableau `col_hists`. Le pseudo code est
```
const int bbloc = 128;
for (y=0; y <= height-bbloc; y += bbloc )
for (x=0; x <= width-bbloc; x += bbloc )
{
calcule ColorDistribution sur le bloc (x,y) -> (x+bbloc, y+bbloc)
le mémorise dans col_hists
}
int nb_hists_background = col_hists.size();
```
:::success
N'oubliez pas d'utiliser votre fonction `getColorDistribution(...)`
:::
### Distributions de couleurs d'un objet
Rajoutez une touche 'a' qui calcule l'histogramme de couleur de la partie qui est matérialisé par le rectangle blanc, et que le rejoute à l'autre tableau d'histogrammes de couleurs `col_hists_object`.
Ainsi, l'utilisateur pourra présenter plusieurs parties du même objet. Cela facilitera sa reconnaissance après.
## 4. Mode reconnaissance
Dès lors que les deux tableaux `col_hists` et `col_hists_objects` ne sont pas vides, l'utilisateur peut demander à basculer en mode "reconnaissance", touche 'r'.
Dans ce mode, on découpe l'image en blocs `8x8` ou `16x16` (comme vous voulez). Par bloc, on calcule son histogramme de couleurs `h` et on cherche l'histogramme dans `col_hists` et dans `col_hists_objects` qui a le plus petite distance avec `h`.
Si le plus proche est dans `col_hists` le bloc sera étiqueté "fond" sinon il sera donc étiqueté "objet".
On commence par écrire une fonction
```cpp
float minDistance( const ColorDistribution& h,
const std::vector< ColorDistribution >& hists );
```
qui retourne la plus petite distance entre `h` et les histogrammes de couleurs de `hists`.
Puis on écrit la fonction qui fabrique une nouvelle image, où chaque bloc est coloré selon qu'il est "fond" ou "objet".
```cpp
Mat recoObject( Mat input,
const std::vector< ColorDistribution >& col_hists, /*< les distributions de couleurs du fond */
const std::vector< ColorDistribution >& col_hists_object, /*< les distributions de couleurs de l'objet */
const std::vector< Vec3b >& colors, /*< les couleurs pour fond/objet */
const int bloc /*< taille de chaque bloc, 16 si 16x16 */ );
```
Le tableau `colors` contient 2 couleurs, mettons noir et rouge, pour désigner la couleur du fond et la couleur de l'objet.
Pour faire un affichage où on distingue la video sous la classification, on peut utiliser le code suivant:
```cpp
Mat output = img_input;
if ( reco )
{ // mode reconnaissance
Mat gray;
cvtColor(img_input, gray, COLOR_BGR2GRAY);
Mat reco = recoObject( img_input, col_hists, col_hists_object, colors, 8 );
cvtColor(gray, img_input, COLOR_GRAY2BGR);
output = 0.5 * reco + 0.5 * img_input; // mélange reco + caméra
}
else
cv::rectangle( img_input, pt1, pt2, Scalar( { 255.0, 255.0, 255.0 } ), 1 );
imshow( "input", output );
```
Si tout fonctionne, voilà ce que ça donne avec ma tasse à café.

## 5. Développements possibles
### Ajouter d'autres objets (facile, sympa)
Le plus simple est d'avoir un tableau de tableau d'histogrammes de couleurs `all_col_hists`, qui contient les histogrammes de couleur du fond, du premier objet, du deuxième objet, etc.
La fonction `recoObjet` devient:
```cpp
Mat
recoObject( Mat input,
const std::vector< std::vector< ColorDistribution > >& all_col_hists,
const std::vector< Vec3b >& colors,
const int bloc );
```
Et on étoffe le tableau de couleurs `colors` avec d'autres couleurs.
Il faut aussi faire une touche pour ajouter un nouvel objet.
|  |  |  |
| ------------------------------------------------------------------------------------------ | --- | ------------------------------------------------------------------------------------------ |
### Optimiser la vitesse d'exécution (facile, sympa si ça rame)
Un gros problème de l'approche est son temps de calcul proportionnel au nombre de distributions apprises. Or, certaines distributions apprises ne sont pas très utiles, car elles sont très proches d'autres distributions. On peut donc simplifier les distributions de couleurs en éliminant les distributions qui ressemblent trop aux autres déjà apprises, c'est-à-dire celles dont la distance aux autres distributions déjà apprises est très petite.
Une façon de faire est de ne mémoriser une distribution de couleur que si sa distance minimale à toutes les autres distributions est supérieure à un seuil donné (à choisir avec trackbar par exemple, 0.05 par défaut ?).
### Améliorer la robustesse de l'analyse des couleurs (plus difficile)
On peut par exemple tester d'autres espaces de couleurs (que l'on peut aussi mettre dans un histogramme de couleurs). On peut aussi comparer les distances moyennes plutôt que la distance minimal, ou la moyenne des deux-trois plus petites.
On peut aussi faire la **relaxation** des labels trouvés, c'est-à-dire que l'on regarde les labels trouvés dans le voisinage 3x3 (par exemple) d'un bloc, et on met au centre le label le plus fréquent (s'il est plus fréquent que le label qui était au centre). Cela élimine certaines classifications parasites.
On peut aussi abstraire les distributions de couleurs comme des lois normales multi-variées, puis comparer ces lois normales via la [distance de Bhattacharyya](https://fr.wikipedia.org/wiki/Distance_de_Bhattacharyya) par exemple.
### Mieux capturer la géométrie des objets (plus difficile, résultat cool)
La classification obtenue est indépendante case par case. Il serait plus intéressant de grouper les régions puis de mieux délimiter les objets sur le fond. Beaucoup d'approches sont possibles, mais on peut suggérer :
- regrouper les blocs, mettons par 4 blocs x 4 blocs, ainsi
- s'il n'y a que le même label $l>0$ ou du label fond ($l=0$), le groupe est labélisé $l$
- sinon mettre comme indécis, soit mettre le label le plus fréquent (si nettement plus fréquent que les autres)
- utiliser ces labels comme marqueurs pour un processus de segmentation plus précis genre **watershed** ou **grabcut**, le fond étant aussi marqué.
- attention, un bloc de fond qui touche un bloc labelisé n'est pas marqué, pour laisser l'algorithme trouver le contour séparant les objets du fond.
Ci-dessous j'ai juste mis un marqueur du label trouvé au milieu du bloc, si deux labels sont identiques sur 2 blocs adjacents, je trace une ligne entre avec ce marqueur. Enfin, seuls les blocs labélisés 0 (fond) qui sont entourés d'autres blocs labélisés 0 sont marqués.
|  |  |
| ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|  |  |
| avec distributions de couleurs via histogrammes | avec distributions de couleurs via lois normales multi-variées |