---
title: INFO911 (TP2) Traitement d'image bas-niveau
---
INFO911 (TP2) Traitement d'image bas-niveau - filtrage spatial
===
[TOC]
> [name=Jacques-Olivier Lachaud][time=Novembre 2020][color=#907bf7]
###### tags: `info911` `tp`
Retour à [INFO911 (Main) Traitement et analyse d'image](https://codimd.math.cnrs.fr/s/UE_B59gMy)
Ce TP étudie principalement le filtrage spatial des images, via l'opérateur de convolution. Vous verrez aussi quelques applications des filtres différentiels.
## 1. Filtrage avec OpenCV
[OpenCV](https://opencv.org) fournit des fonctions pour faire du filtrage (spatial et temporel). Lorsqu'on veut utiliser des masques de convolutions, le plus simple est d'utiliser [`cv::filter2D`](https://docs.opencv.org/3.4/d4/d86/group__imgproc__filter.html#ga27c049795ce870216ddfb366086b5a04). Bizarrement, cette fonction ne met pas en oeuvre la ==convolution== $I \ast M$ mais la ==corrélation== $I \star M$. Pas de panique, il s'agit juste de faire la symétrie centrale sur le noyau !
$$
I \star M = I \ast \mathrm{Sym}(M)
$$
Par exemple, ça ne change rien sur le filtre moyenneur $M$:
$$
M:=\begin{array}{|c|c|c|} \hline 1/16 & 2/16 & 1/16 \\ \hline 2/16 & \fbox{4/16 } & 2/16 \\ \hline 1/16 & 2/16 & 1/16 \\ \hline\end{array} \quad \mathrm{Sym}(M) = M
$$
Pour les filtres de Sobel il faudra faire la symétrie centrale. Pour $S_x$ ça donne:
$$
S_x:=\begin{array}{|c|c|c|} \hline 1/4 & 0 & -1/4 \\ \hline 2/4 & \fbox{0 } & -2/4 \\ \hline 1/4 & 0 & -1/4 \\ \hline\end{array} \quad \mathrm{Sym}(S_x)=\begin{array}{|c|c|c|} \hline -1/4 & 0 & 1/4 \\ \hline -2/4 & \fbox{0 } & 2/4 \\ \hline -1/4 & 0 & 1/4 \\ \hline\end{array}
$$
> La fonction `cv::filter2D` est même multi-canal, et peut travailler indifféremment sur les images couleurs ou niveaux de gris (ou même les images de flottants, mes images 3D, etc).[color=#907bf7]
## 2. Filtre moyenneur
C'est le filtre de base qui élimine assez bien les bruits qui suivent une loi normale. Reprenez le bout de code initial ci-dessous qui charge une image.
```cpp=
#include "opencv2/imgproc.hpp"
#include <opencv2/highgui.hpp>
using namespace cv;
int main( int argc, char* argv[])
{
namedWindow( "Youpi"); // crée une fenêtre
Mat input = imread( argv[ 1 ] ); // lit l'image donnée en paramètre
if ( input.channels() == 3 )
cv::cvtColor( input, input, COLOR_BGR2GRAY );
while ( true ) {
int keycode = waitKey( 50 );
int asciicode = keycode & 0xff;
if ( asciicode == 'q' ) break;
imshow( "Youpi", input ); // l'affiche dans la fenêtre
}
imwrite( "result.png", input ); // sauvegarde le résultat
}
```
Ecrivez ensuite la fonction
```cpp=
cv::Mat filtreM( cv::Mat input, cv::Mat M );
```
qui fait la convolution de l'image en entrée avec le masque $M$.
Changez ensuite votre programme pour que l'image soit moyennée à chaque appui sur la touche `a` (pour *average*).
| image $I$ | $I \ast M$ | $I \ast M \ast M \ast \ldots \ast M$ |
| -------- | -------- | -------- |
|  |  |  |
:::info
Que se passe-t-il si vous appliquez le filtre moyenneur 100 fois, 1000 fois ?
:::
## 3. Filtre médian
Mettez à jour votre programme pour qu'il applique le filtre médian de taille $3 \times 3$ lorsque vous appuyez sur la touche `m` (pour *median*). Vous utiliserez la fonction `cv::medianBlur`.
| 16 fois $\ast M$ | 16 fois `medianBlur` |
| -------- | -------- |
|  |  |
:::info
Que se passe-t-il si vous appliquez le filtre médian 100 fois, 1000 fois ?
:::
## 4. Rehaussement de contraste
Le rehaussement de contraste se fait par soustraction d'une fraction $\alpha$ de son Laplacien.
Masque ==Laplacien== $L:=\begin{array}{|c|c|c|} \hline 0 & 1 & 0 \\ \hline 1 & -4 & 1 \\ \hline 0 & 1 & 0 \\ \hline\end{array}$.
Filtre rehausseur $R_\alpha \ast I = I - \alpha L \ast I$, donc $R_\alpha = \delta_{(0,0)} - \alpha L$, où $\delta_{(0,0)}$ est l'impulsion centrée en (0,0). Le plus simple est donc de créer cette matrice $3 \times 3$ directement.
Vous pouvez ajouter un slider, et faire en sorte que chaque fois qu'on appuie sur `s` l'image soit rehaussée en fonction du coefficient choisi sur le slider.
```cpp=
// OpenCV old-school
int alpha = 20;
createTrackbar( "alpha (en %)", "Filter", &alpha, 200, NULL);
...
// alpha a toujours la valeur du trackbar.
```
```cpp=
// OpenCV recommended now
int alpha = 20;
createTrackbar( "alpha (en %)", "Filter", nullptr, 200, NULL)`
setTrackbarPos( "alpha (en %)", "Filter", alpha ); // init à 20
...
// récupère la valeur courante de alpha
alpha = getTrackbarPos( "alpha (en %)", "Filter" );
```
|  |  |  |
| ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| original | rehaussement $\ast R_{0,6}$ | rehaussement $\ast R_{0,6}$ 4 fois |
| | | |
:::info
A votre avis, d'où viennent les petits traits assez réguliers qui forment comme des carrés ?
Vous pouvez aussi tester de faire des moyennes/médians avant de rehausser.
:::
:::warning
Les trackbars ne retourne que des valeurs entières entre 0 et le max donné au début. Si vous voulez un double, par exemple entre 0 et 1, faites un trackbar avec valeur max 1000, et diviser l'entier par 1000.0.
:::
## 5. Filtres dérivatifs
On va maintenant approcher les *dérivées partielles* de l'image par les filtres de Sobel $S_x$ et $S_y$. En effet, si on considère que $I$ est la discrétisation de pas $\Delta x, \Delta y$ d'une image continue, on a les relations:
$$
I_x := I \ast S_x[i,j] \approx \frac{\partial I}{\partial x}(i\Delta x, j\Delta y), \quad
I_y := I \ast S_y[i,j] \approx \frac{\partial I}{\partial y}(i\Delta x, j\Delta y).
$$
:::warning
En tout point $[i,j]$ le vecteur $\mathbf{v}=(I_x[i,j],I_y[i,j])$ définit la direction **perpendiculaire** au contour (en fait celle qui fait varier le plus l'intensité). Ce vecteur s'appelle ==gradient== de l'image en ce point. On le note $\nabla I := (I_x,I_y)$, et correspond à une image de vecteurs 2D (ou champ de vecteurs).
On peut avoir la direction **tangente** d'un contour simplement en tournant le vecteur de 90°, i.e. $\mathbf{v}^\perp=(-I_y[i,j],I_x[i,j])$.
:::
Ajoutez dans votre code le calcul des deux images $I_x$ et $I_y$ via l'application des filtres de Sobel $S_x$ et $S_y$ (mettons sur les touches `x`et `y`).
:::danger
Contrairement aux filtres précédents, les filtres dérivatifs calculent des valeurs signées (les dérivées peuvent être positives: on passe du sombre au clair; ou négatives: on passe du clair au sombre). Par convention, on met le zéro à la valeur de gris 128. On utilisera opportunément le paramètre `delta` de `cv::filter2D` pour faire ce décalage.
:::
| $S_x$ Filtre différentiel en x (Sobel) | $S_y$ Filtre différentiel en y (Sobel) |
| -------- | -------- |
|  |  |
:::info
Que se passe-t-il si vous appuyez 2 fois sur `x` ? Vous calculez donc $I \ast S_x \ast S_x$. C'est (à peu près) la dérivée selon x de la dérivée selon x de $I$, c'est donc sa dérivée seconde selon x:
$$
I_{xx} := I \ast S_x \ast S_x[i,j] \approx \frac{\partial^2 I}{\partial x^2}(i\Delta x, j\Delta y)
$$
$$
I_{xy} := I \ast S_x \ast S_y[i,j] \approx \frac{\partial^2 I}{\partial x \partial y}(i\Delta x, j\Delta y)
$$
$$
I_{yy} := I \ast S_y \ast S_y[i,j] \approx \frac{\partial^2 I}{\partial y^2}(i\Delta x, j\Delta y)
$$
:::
## 6. Gradient d'une image et contours
On va quantifier la "force" d'un contour dans l'image. Les contours étant de zones de fortes variations de l'image, il est naturel de mesure la "force" d'un contour en fonction du ==gradient== $\nabla I$ de l'image (voir plus haut).
Comme on s'intéresse à la "force" du contour, on va calculer la magnitude (ou longueur) de ce vecteur, qui est sa norme euclidienne (i.e. racine carrée de la somme des carrés des composantes):
$$
G[i,j]:= \| \nabla I[i,j] \| = \sqrt{ I_x[i,j]^2 + I_y[i,j]^2}.
$$
Calculez donc cette image $G$ à partir de vos images $I \ast S_x$ et $I \ast S_y$. Cette image est elle positive et toujours inférieure à $128\sqrt{2}$ car on a codé les valeurs signées sur 8 bits.
On pourra la mettre sur la touche `g`.

:::warning
On ne peut pas calculer $G$ par une convolution directe. Vous serez obligé de faire un parcours de vos images $I \ast S_x$ et $I \ast S_y$ pour calculer $G$.
:::
:::info
Pourquoi est-ce que la norme du gradient n'est pas un filtre linéaire et invariant par translation ? Où est le problème ?
:::
## 7. Détection de contours à la Marr-Hildreth
Lorsque le Laplacien d'une image change de signe dans un voisinage, cela veut dire qu'il existe un point (continu) dans ce voisinage où le laplacien de l'image s'annule. Cela indique une inflexion dans l'image vue comme une carte d'altitude et donc le milieu d'une forte pente, i.e. un contour.
En même temps, beaucoup de ces points correspondent à des contours très faibles. On va donc les éliminer en ne gardant que les points où le laplacien change de signe et où la force du contour est importante.
On se donne donc un seuil $T$, donné par un `trackBar`. Ensuite on fabrique une image où les contours seront en noir et le reste en blanc. Un point $[i,j]$ est un ==contour== si et seulement si $I \ast L$ change de signe dans le voisinage de $[i,j]$ et $G[i,j] \ge T$.
| Seuil T=20 | Seuil T=40 |
| -------- | -------- |
|  |  |
:::info
Le principal défaut de cette approche est que les contours peuvent être épais. Il n'y a pas non plus d'adaptation au contraste local. En cours, on voit que la méthode de Canny donne des résultats plus satisfaisants, au prix de plusieurs paramètres.
:::
## 8. Rendu artistique type 'esquisse'
On peut utiliser les calculs précédents pour faire un rendu plus artistique. L'idée est assez simple. Lorsqu'on détecte un point de contours, plutôt que de simplement tracer un point dans l'image, on va:
* tracer un segment de longueur proportionnel à la force du contour
* centré sur le point
* orienté dans la direction du contour avec un petit aléa
De plus on n'affichera qu'une partie de ces lignes (aléatoirement).
Voilà ce que l'on peut faire (c'est une proposition, on peut varier).
Vous mettez 3 `trackBar` qui servent à définir 3 paramètres:
* le seuil $T$ (déjà fait)
* la proportion $t$ de traits à tracer (en %)
* la longueur $l$ des traits (en %, des valeurs jusqu'à 1000, pour faire une longeur 10)
Ensuite l'algorithme est, pour chaque pixel $[i,j]*:
* Si $[i,j]$ est un point de contour (cf plus haut)
* Alors Si `rand01() < t/100.0` (avec `rand01():=rand()/(double)RAND_MAX`)
* Alors $\theta = \mathrm{atan2}( -I_y, I_x) + \pi/2 + 0.02(rand01()-0.5)$
* Soit $g$ le niveau de gris $G[i,j]$
* Soit $l'=g/255.*l/100.$
* on trace le segment de $(x+l'\cos(\theta), y+l'\sin(\theta))$ à $(x-l'\cos(\theta), y-l'\sin(\theta))$
On pourra utiliser `cv::line` pour tracer les lignes. Vous pourrez choisir une épaisseur et une couleur.

:::info
Si on veut faire des vidéos à la place d'une image fixe, c'est très facile
```cpp
// ouvre le flux video
VideoCapture cap(0);
if(!cap.isOpened()) return -1;
...
// récupère l'image
Mat input;
cap >> input;
```
Si ça rame trop, vous pouvez forcer une résolution inférieure sur la caméra. Ici, je force 640x480 (attention, les caméras ont des résolutions fixes définies, donc ça marche pas tout le temps).
```cpp
cap.set( CAP_PROP_FRAME_WIDTH, 640 );
cap.set( CAP_PROP_FRAME_HEIGHT, 480 );
int width = cap.get( CAP_PROP_FRAME_WIDTH );
int height = cap.get( CAP_PROP_FRAME_HEIGHT );
std::cout << "Camera is " << width << " x " << height << std::endl;
:::
## 9. Evaluation
A la fin de la séance, chaque groupe doit me montrer où il en est et ses réalisations fonctionnelles.