Algèbre linéaire et traitement des images

PREMIÈRE PARTIE : INTRODUCTION AUX IMAGES NUMÉRIQUES EN PYTHON

Étienne Coutant, Olivier Nocent, Frédéric Blanchard

L'objectif de cette activité est d'illustrer le rôle du calcul matriciel (applications linéaires, transformations) en traitement numérique des images. Chaque exercice consiste à créer un filtre modifiant le contenu d'un fichier image.

Elle comporte trois parties, et se termine par une section facultative présentant des ouvertures possibles :

  1. Introduction : les images numériques en python
  2. Manipulation des couleurs
  3. Transformations géométriques
  4. Pour aller plus loin (bonus)

1 Préparation

Les images qui seront utilisées dans les trois premières parties sont les suivantes :

Vous devez les télécharger (localement) puis les « téléverser » dans un répertoire images que vous aurez créé.

Pour pouvez aussi effectuer cette opération grâce aux instructions suivantes, depuis votre notebook jupyter :

import urllib.request,os
# Creation eventuelle du répertoire
try:
   os.mkdir('images')
except:
    print('Le répertoire existe déjà')
# Téléchargement des images
urllib.request.urlretrieve("https://iut-info.univ-reims.fr/users/coutant/img/solo-256px.png", "images/solo-256px.png")
urllib.request.urlretrieve("https://iut-info.univ-reims.fr/users/coutant/img/logo-starwars-256px.png", "images/logo-starwars-256px.png")

Le répertoire images devrait maintenant contenir les deux images suivantes :

Solo
Solo

Attention

Si vous travaillez avec votre distribution de python, il vaut mieux utiliser le module requests :

import requests,os

def download(url, destination):
  response = requests.get(url, verify=True)
  open(destination, 'wb').write(response.content)

# Creation eventuelle du répertoire
try:
   os.mkdir('images')
except:
    print('Le répertoire existe déjà')

# Téléchargement des images
download('https://iut-info.univ-reims.fr/users/coutant/img/solo-256px.png', 'images/solo-256px.png')
download('https://iut-info.univ-reims.fr/users/coutant/img/logo-starwars-256px.png', 'images/logo-starwars-256px.png')

Il est aussi nécessaire de charger les modules suivants :

# Modules
from math import *
import random
import numpy as np
import matplotlib.pyplot as plt

2 Introduction : les images matricielles en python

2.1 Les images matricielles (ou images bitmap)

Il existe de nombreux formats d'images numériques. On distingue deux catégories :

Celles qui nous intéressent ici sont les images matricielles (ou images bitmap).

Les images matricielles sont des tableaux de points, appelés pixels. Chaque pixel est caractérisé par :

Dans une image en niveaux de gris, le vecteur qui représente le pixel n'a qu'une composante.

Dans le cas d'une image en couleurs, il y a au moins trois canaux. Généralement, il s'agit des canaux associés au Rouge, au Vert et au Bleu. En utilisant le principe de synthèse additive la superposition de ces canaux lumineux permet d'obtenir les autres couleurs (nous en reparlerons dan la partie suivante). On peut représenter le tableau de pixels comme un tableau à trois dimensions (dont la profondeur représente le nombre de canaux spectraux). Dans ce cas, un pixel p est un vecteur à trois composantes p=(r_p,g_p, b_p) où :

Ainsi par exemple :

Ainsi ces pixels sont de couleurs respectives :

2.2 Implémentation en python

La structure de données pythontypiquement utilisée pour représenter ces tableaux est l'array de numpy. Les valeurs sont généralement de type float64.

Affichage facile

Pour afficher les images, nous vous proposons d'utiliser la fonction display(img_list, size, shape) ci-dessous, qui affiche une série d'images stockées dans une liste img_list. La taille de l'affichage d'une image est fixée à size pouces. Le booléen shape permet d'afficher ou non les dimensions du tableau au dessus de l'image.

def display(imglist,size=5, shape=True):
    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)
        if len(imglist[i].shape) > 2 :
            subfig = plt.imshow(imglist[i], vmin=0.0, vmax=1.0)
        else :
            subfig = plt.imshow(imglist[i],cmap="gray",vmin=0.0, vmax=1.0)
        subfig.axes.get_xaxis().set_visible(False)
        subfig.axes.get_yaxis().set_visible(False)
        if shape == True:
            a.set_title(str(imglist[i].shape))
    plt.show()

Nous allons maintenant créer une image de 4 pixels (2 \times 2) avec les niveaux de gris suivants :

img = np.array([[0.98, 0.43],
                [0.11, 0.79]])

display([img])

puis une image de 4 pixels (2 \times 2) avec les couleurs suivantes :

img = np.array([[[0.98, 0.06, 0.18], [0.78, 0.29, 0.91]],
                [[0.49, 0.51, 0.37], [0.53, 0.81, 0.55]]])

display([img])

Pour accéder, en lecture, comme en écriture, à un pixel, il suffit d'utiliser les [ ].

Exemple

img[0,1]
array([0.78, 0.29, 0.91])
# Accès en modification
img[0,1] = [0.0,1.0,0.0]

# Visualisation de l'image modifiée
display([img])

2.3 Lecture et écriture d'images à l'aide du module matplotlib

Le module matplotlib dispose de deux fonctions imread et imsave (dans le sous-module pyplot) qui permettent respectivement :

# Lecture de l'image téléchargée au début du TP
img = plt.imread("images/solo-256px.png")
#  Sauvegarde de l'image img dans un fichier
plt.imsave("images/solo-256px-copie.png", img)

Il existe aussi une fonction imshow, qui permet d'afficher une image contenue dans un tableau :

plt.imshow(img)
<matplotlib.image.AxesImage at 0x7fef4ac7d3d0>

...mais on lui préférera notre fonction display qui en simplifie l'usage.

Pour accéder au pixel situé à la i-ème ligne et à la j-ème colonne de l'image on utilise donc simplement img[i,j]. Par exemple, le pixel situé sur la ligne 78 et la colonne 95 de l'image précédente correspond au vecteur :

p = img[78,95]
print(p)
[0.7411765 0.6627451 0.5803922]

Les composantes R, G et B de ce pixel p sont :

print("Rouge : ", p[0])
print("Vert : ", p[1])
print("Bleu : ", p[2])
Rouge :  0.7411765
Vert :  0.6627451
Bleu :  0.5803922

Remarque

On peut naturellement accéder directement à une composante d'un pixel en spécifiant son indice :

img[78, 95 ,0]
0.7411765

Vérification avec Gimp

Vous pouvez vérifier -ici avec Gimp, mais tout autre logiciel d'imagerie fera l'affaire- que ces valeurs numériques sont cohérentes.

Attention : dans Gimp, les pixels sont repérés de la façon suivante
(colonne,ligne) contrairement à notre représentation.

3 Exercices d'échauffement

3.1 Inverse

Écrire une fonction inverse(img) qui crée une nouvelle image dont les couleurs des pixels sont obtenues par complémentaire à 1 des couleurs de l'image img. En d'autres termes, pour chaque pixel p^{\prime}_{i,j} = (r^{\prime}_{i,j},g^{\prime}_{i,j},b^{\prime}_{i,j}) de la nouvelle image, on a :

\left\{\begin{array}{ccc} r^{\prime}_{i,j} & = & 1 - r_{i,j}\\ g^{\prime}_{i,j} & = & 1 - g_{i,j}\\ b^{\prime}_{i,j} & = & 1 - b_{i,j} \end{array}\right.

p_{i,j} = (r_{i,j},g_{i,j},b_{i,j}) est le pixel correspondant dans l'image de départ.

Exemple

display([img,inverse(img)],5)

3.2 En rouge, en vert, en bleu

Écrire une fonction keep_red(img) qui annule les valeurs sur les composantes vertes et bleue d'une copie de img et retourne le résultat (puis sur le même principe écrire keep_green(img) et keep_blue(img)).

Exemple :

display([keep_red(img),keep_green(img),keep_blue(img)],5)

3.3 Morceaux choisis

Écrire une fonction extract(img, l1, c1, l2, c2) qui retourne une nouvelle image avec les pixels de img situés entre les lignes l1 et l2 et les colonnes c1 et c2.

Schema extraction rectangulaire

Exemple :

img2 = extract(img, 23, 57, 95, 111)
display([img2],5)

3.4 Images aléatoires

  1. Écrire une fonction randimg_gsc(height, width) qui crée une image de taille height\timeswidth dans laquelle le niveau de gris de chaque pixel est choisi au hasard.

  2. Écrire une fonction randimg_rgb(height, width) qui crée une image de taille height\timeswidth dans laquelle les valeurs de rouge, vert, et bleu de chaque pixel sont choisies au hasard.

Indication : la fonction random.random() du module random (ne pas oublier de l'importer avec import random) permettent de générer aléatoirement uniformément un réel compris entre 0 et 1.

Exemples :

display([randimg_gsc(256,256), randimg_rgb(256,256)],5)

3.5 Imagettes

Construire les tableaux permettant de reproduire les images suivantes :

display([im1, im2, im3, im4, im5],4)

4 Exercices bonus

Les exercices qui suivent sont facultatifs. Ne les faites qui si vous avez une avance suffisante. La suite est encore longue !

4.1 Imagettes, encore

Construire les tableaux permettant de reproduire à peu près les images suivantes :

display([im5,im6],5)

Remarque : sur l'image de gauche, le motif artefactuel est dû à un phénomène appelé moiré

4.2 Histogrammes

Voici une fonction count_histo_gsc(img), qui :

Cette fonction construit un histogramme des niveaux de gris de l'image passée en paramètre.

def count_histo_gsc(img):
    [height, width] = np.shape(img)
    lcount = [0] * 256
    for i in range(height):
        for j in range(width):
            int_gsv = int(round(img[i,j] * 255))
            lcount[int_gsv] += 1
    return lcount

Ensuite, la fonction draw_histo construit une image de l'histogramme des fréquences. Grosso modo, on dessine une ligne verticale proportionnelle au nombre de fois où le niveau de gris correspondant apparaît dans l'image.

def draw_histo(img):
    [height, width] = np.shape(img)
    counth = count_histo_gsc(img)
    # Attention cascade ; on transforme les comptages 
    # en hauteurs de barres en pixels (le max est à 255) :
    ch = [int(i * 255 / max(counth)) for i in counth]
    img_histo = np.zeros((256,256),dtype='float32')
    for g in range(256):
        if ch[g] > 0:
            for k in range(255-ch[g],256):
                img_histo[k,g] = 1.0
    return img_histo

Exemple d'utilisation :

img = plt.imread("https://upload.wikimedia.org/wikipedia/commons/f/fa/Grayscale_8bits_palette_sample_image.png")
display([draw_histo(img), img], 5)

Écrire la fonction draw_histo_rgb(img) qui retourne une liste des trois images des histogrammes des composantes chromatiques : rouge, vert, bleu.

img = plt.imread("./images/solo-256px.png")
display(draw_histo(img),5)

5 Conclusion

Voilà. Cette introduction à l'image en python est terminée. Mais... quel rapport avec l'algèbre linéaire ? On y vient. Dans la partie suivante, on utilise les outils de l'algèbre linéaire pour transformer les couleurs d'une image.