Traitement d'images avec Python

Deuxième partie : transformations géométriques et compressions

1. Rappel

Vous pouvez télécharger le fichier PNG ci-dessous, utilisé dans les exemples, pour tester l'ensemble de vos filtres :

Configuration

Import des modules maths, numpy pour la manipulation de tableaux et matplotlib pour l'affichage des images.

In [43]:
from math import *
import numpy as np
import matplotlib.pyplot as plt
In [44]:
%matplotlib inline
#permet l'affichage des images à l'intérieur du notebook Jupyter

Import d'une fonction greyscale (vous pouvez tout à fait importer la votre, issue du TP précédent).

In [45]:
def greyscale(src):
    G = np.array([[0.2126, 0.7152, 0.0722],
                  [0.2126, 0.7152, 0.0722],
                  [0.2126, 0.7152, 0.0722]])
    h = src.shape[0]
    w = src.shape[1]
    dst = np.zeros(src.shape)
    for y in range(0,h):
        for x in range(0,w):
            dst[y,x]=np.dot(G,src[y,x])
    return dst

Chargement et affichage de l'image

Chargement d'une image à partir de son url et affichage dans le notebook.

In [46]:
img = plt.imread("https://iut-info.univ-reims.fr/users/coutant/img/solo-256px.png")

Nous définissons ci-dessous la fonction display(imglist,size), qui affiche une série d'images stockées dans une liste imglist, la taille de chaque image étant de size pouces.

In [47]:
def display(imglist,size):
    cols = len(imglist)
    fig = plt.figure(figsize=(size*cols,size*cols))
    for i in range(0,cols):
        a = fig.add_subplot(1, cols, i+1)
        subfig = plt.imshow(imglist[i])
        subfig.axes.get_xaxis().set_visible(False)
        subfig.axes.get_yaxis().set_visible(False)
In [48]:
display([img,img],5)

2. Transformations géométriques

Jusqu'à présent, nous avons utilisé les applications linéaires pour modifier les composantes d'un pixel. Avec les transformations géométriques, nous allons modifier les coordonnées $(x,y)$ d'un pixel afin de le déplacer au sein de l'image.

Toutes les transformations que nous allons implémenter se feront relativement à un centre passé en paramètre de chaque fonction. Par conséquent, avant d'appliquer la transformation, il faudra translater le pixel en lui retranchant les coordonnées du centre. Puis, après avoir appliqué la transformation, le pixel résultat sera à nouveau translaté dans le sens opposé en lui ajoutant les coordonnées du centre.

2.1 Symétrie par rapport à l'axe des abscisses

Une symétrie par rapport à l'axe des abscisses est définie comme suit :

$$ \left( \begin{array}{c} x' \\ y' \end{array} \right)= \begin{pmatrix} 1 & 0 \\ 0 & -1 \end{pmatrix} \left( \begin{array}{c} x - x_C \\ y - y_C \end{array} \right)+ \left( \begin{array}{c} x_C \\ y_C \end{array} \right) $$

Exercice 1
Écrire une fonction symetrieAbs(src,centerx,centery) qui applique une symétrie par rapport à l'axe des abscisses à une image src passée en paramètre.
(on supposera que le centre du repère est placé en (centerx, centery)

In [50]:
display([img,symetrieAbs(img,128,128)],5)

2.2 Changement d'échelle

Un changement d'échelle de centre $(x_C,y_C)$ et de rapport $\lambda$ est défini comme suit :

$$ \left( \begin{array}{c} x' \\ y' \end{array} \right)= \begin{pmatrix} \lambda & 0 \\ 0 & \lambda \end{pmatrix} \left( \begin{array}{c} x - x_C \\ y - y_C \end{array} \right)+ \left( \begin{array}{c} x_C \\ y_C \end{array} \right) $$

Exercice 2
Écrire une fonction zoom(src,centerx,centery,factor) qui applique un changement d'échelle de rapport factor et de centre (centerx,centery) à une image src passée en paramètre.

Attention. Pour vous assurer d'obtenir des coordonnées $(x',y')$ entières, dans le cas où factor est $<1$, vous pourrez utiliser mon_array.astype(int).

In [52]:
display([img,zoom(img,128,128,0.5)],5)

On constate qu'avec un facteur de changement d'échelle $>1$, l'image résultat est creuse. En effet, en plus de déplacer le pixel, la fonction devrait affecter la même couleur aux pixels voisins dans un carré de coté factor.

In [53]:
display([img,zoom(img,64,64,4)],5)

Pour éviter d'obtenir une image creuse, procédons autrement : pour faire un zoom, nous allons parcourir les pixels de l'image destination, puis appliquer la transformation inverse pour retrouver le pixel antécédent.

Exercice 3
Écrire une fonction smartzoom(src,centerx,centery,factor) qui corrige le défaut de la fonction zoom en appliquant la transformation inverse aux pixels de l'image destination.

In [55]:
display([img,smartzoom(img,128,128,0.5),smartzoom(img,64,64,4)],5)

2.3 Rotation

Une rotation dans le plan de centre $(x_C,y_C)$ et d'angle $\alpha$ est définie comme suit :

$$ \left( \begin{array}{c} x' \\ y' \end{array} \right)= \begin{pmatrix} \cos\alpha & -\sin\alpha \\ \sin\alpha & \cos\alpha \end{pmatrix} \left( \begin{array}{c} x - x_C \\ y - y_C \end{array} \right)+ \left( \begin{array}{c} x_C \\ y_C \end{array} \right) $$

Exercice 4
Écrire une fonction rotate(src,centerx,centery,alpha) qui applique une rotation de centre (centerx,centery) et d'angle alpha à une image src passée en paramètre.

Attention. Pour vous assurez d'obtenir des coordonnées $(x',y')$ entières, vous pourrez utiliser mon_array.astype(int).

In [57]:
display([rotate(img,128,128,pi/2),rotate(img,128,128,pi/3),rotate(img,128,128,2*pi/3)],5)

Remarque : Comme l'axe vertical est orienté vers le bas, le sens trigonométrique est similaire au sens des aiguilles d'une montre.

On constate quelques jolis effets de Moiré. Ceci est dû à la nature discrète de la grille de pixels de l'image. Après transformation, les coordonnées du pixel sont modifiées pour n'en conserver que la partie entière, ce qui explique que certaines cases de l'image ne sont jamais remplies.

Exercice 5
Pour corriger ce défaut, écrire une fonction smartrotate(src,centerx,centery,alpha) qui parcourt les pixels de l'image destination, et applique une rotation d'angle -alpha pour retrouver le pixel antécédent.

In [59]:
display([smartrotate(img,128,128,pi/2),smartrotate(img,128,128,pi/3),smartrotate(img,128,128,2*pi/3)],5)

2.4 Twist

Le twist consiste en une rotation du plan de centre $(x_C,y_C)$, dont l'angle $\alpha$ est fonction de la distance du pixel au centre.

$$ \left( \begin{array}{c} x' \\ y' \end{array} \right)= \begin{pmatrix} \cos\alpha & -\sin\alpha \\ \sin\alpha & \cos\alpha \end{pmatrix} \left( \begin{array}{c} x - x_C \\ y - y_C \end{array} \right)+ \left( \begin{array}{c} x_C \\ y_C \end{array} \right) $$

$$\alpha = \rho \sqrt{ (x - x_C)^2 + (y - y_C)^2 }$$

Exercice 6
Écrire une fonction twist(src,centerx,centery,rho) qui applique un twist de centre (centerx,centery) et de facteur rho à une image src passée en paramètre.

In [61]:
display([twist(img,128,128,0.01),twist(img,128,128,0.02),twist(img,128,128,0.05)],5)

3. Compression d'images

Le principe de la compression destructive consiste à réduire la taille d'un fichier image en acceptant de perdre de l'information et donc d'altérer l'image d'origine. La technique présentée par la suite est détaillée dans le document Applications_changements_de_base.pdf, et elle est résumé ci-dessous.
Elle permet de diviser la taille d'un fichier par 4.

La technique proposée sera uniquement appliquée à des images de luminance (i.e. en niveaux de gris). Pour des considérations de symétrie, la luminance d'un pixel sera représentée par un nombre $l \in [-1,1]$ lors de l'étape de compression, $-1$ correspondant au noir et $1$ au blanc.

3.1 Compression dans la base canonique

Une première version naïve de la compression consiste à diviser l'image en paquets carrés de 4 pixels, représentés par des vecteurs de $[-1,1]^4$. Au sein de chaque paquet, on identifie le pixel avec la plus grande luminance en valeur absolue. Seul ce pixel prépondérant est conservé lors de la compression, les luminances des autres pixels étant fixées à $0$.

Pour supprimer l'effet dentelle engendré par $3/4$ de pixels gris avec une luminance à $0$, on peut dupliquer la luminance du pixel prépondérant dans les 3 autres.

Cette première version de la compression peut-être réalisée en 3 étapes :

  • Transcodage des luminances de $[0,1]$ vers $[-1,1]$
  • Pour chaque paquet de 4 pixels
    • Identifier le pixel prépondérant avec la plus grande luminance en valeur absolue
    • Dupliquer la luminance du pixel prépondérant dans les 3 autres pixels
  • Transcodage des luminances de $[-1,1]$ vers $[0,1]$

Exercice 7
Écrire une fonction compression(src) qui modifie une image de luminance src selon la méthode de compression naïve décrite précédemment.

In [63]:
img_nb = greyscale(img)
display([img_nb,compression(img_nb)],5)

3.2 Bonus : compression dans une base adaptée

Afin d'atténuer l'effet de pixelisation, il paraît judicieux de privilégier une autre base de vecteurs dans $[-1,1]^4$ qui rend mieux compte des différences de luminance au sein d'un paquet de 4 pixels. Cette nouvelle base est la base $B'$ :
$B'=\left\{(1,1,1,1),(1,1,-1,-1),(1,-1,1,-1),(1,-1,-1,1)\right\}$, correspondant aux contrastes suivants dans le paquet de 4 pixels :

* pas de contrastes
* contraste horizontal
* contraste vertical
* contraste diagonal

Une version smart de l'algorithme de compression consiste à :

  • Transcodage des luminances de $[0,1]$ vers $[-1,1]$
  • Pour chaque paquet de 4 pixels
    • Appliquer le changement de base de $B$ vers $B'$
    • Identifier dans $B'$ la coordonnée prépondérante avec la plus grande luminance en valeur absolue
    • Annuler les 3 autres coordonnées
    • Appliquer le changement de base de $B'$ vers $B$
  • Transcodage des luminances de $[-1,1]$ vers $[0,1]$

Exercice 8
Écrire une fonction R smartcompression(src) qui modifie une image de luminance src selon la méthode de compression smart décrite précédemment.

Remarque : Pour inverser la matrice de passage P, on pourra utiliser np.linalg.inv(P). On pourra aussi utiliser sa fonction inverse(P) créée lors du TP du semestre 1 sur le pivot de Gauss.

In [65]:
display([img_nb,compression(img_nb),smartcompression(img_nb)],5)