Le traitement numérique des images

Cette page reprend l'article publié sur le site web Images des mathématiques.

Les appareils numériques photographient de manière très précise le monde qui nous entoure. L'utilisateur souhaite pouvoir stocker avec un encombrement minimal ses photos sur son disque dur. Il souhaite également pouvoir les retoucher afin d'améliorer leur qualité. Cet article présente les outils mathématiques et informatiques qui permettent d'effectuer ces différentes tâches.

Cet article présente quelques concepts du traitement mathématique des images numériques. Ces traitements permettent de stocker plus facilement les images et d'améliorer leur qualité. Les mathématiques utilisées dans cet article correspondent au niveau de la classe de troisième. Les mots clés en rouge pointent vers les pages Wikipédia correspondantes. Ils sont repris à la fin de l'article dans un glossaire.

Mot clefs : image, bits, carré, racine carrée, inverse, logarithme, moyenne, médiane.

Important : voir la page d'installation pour les détails de l'installation des toolboxes. $\newcommand{\dotp}[2]{\langle #1, #2 \rangle}$ $\newcommand{\enscond}[2]{\lbrace #1, #2 \rbrace}$ $\newcommand{\pd}[2]{ \frac{ \partial #1}{\partial #2} }$ $\newcommand{\umin}[1]{\underset{#1}{\min}\;}$ $\newcommand{\umax}[1]{\underset{#1}{\max}\;}$ $\newcommand{\umin}[1]{\underset{#1}{\min}\;}$ $\newcommand{\uargmin}[1]{\underset{#1}{argmin}\;}$ $\newcommand{\norm}[1]{\|#1\|}$ $\newcommand{\abs}[1]{\left|#1\right|}$ $\newcommand{\choice}[1]{ \left\{ \begin{array}{l} #1 \end{array} \right. }$ $\newcommand{\pa}[1]{\left(#1\right)}$ $\newcommand{\diag}[1]{{diag}\left( #1 \right)}$ $\newcommand{\qandq}{\quad\text{and}\quad}$ $\newcommand{\qwhereq}{\quad\text{where}\quad}$ $\newcommand{\qifq}{ \quad \text{if} \quad }$ $\newcommand{\qarrq}{ \quad \Longrightarrow \quad }$ $\newcommand{\ZZ}{\mathbb{Z}}$ $\newcommand{\CC}{\mathbb{C}}$ $\newcommand{\RR}{\mathbb{R}}$ $\newcommand{\EE}{\mathbb{E}}$ $\newcommand{\Zz}{\mathcal{Z}}$ $\newcommand{\Ww}{\mathcal{W}}$ $\newcommand{\Vv}{\mathcal{V}}$ $\newcommand{\Nn}{\mathcal{N}}$ $\newcommand{\NN}{\mathcal{N}}$ $\newcommand{\Hh}{\mathcal{H}}$ $\newcommand{\Bb}{\mathcal{B}}$ $\newcommand{\Ee}{\mathcal{E}}$ $\newcommand{\Cc}{\mathcal{C}}$ $\newcommand{\Gg}{\mathcal{G}}$ $\newcommand{\Ss}{\mathcal{S}}$ $\newcommand{\Pp}{\mathcal{P}}$ $\newcommand{\Ff}{\mathcal{F}}$ $\newcommand{\Xx}{\mathcal{X}}$ $\newcommand{\Mm}{\mathcal{M}}$ $\newcommand{\Ii}{\mathcal{I}}$ $\newcommand{\Dd}{\mathcal{D}}$ $\newcommand{\Ll}{\mathcal{L}}$ $\newcommand{\Tt}{\mathcal{T}}$ $\newcommand{\si}{\sigma}$ $\newcommand{\al}{\alpha}$ $\newcommand{\la}{\lambda}$ $\newcommand{\ga}{\gamma}$ $\newcommand{\Ga}{\Gamma}$ $\newcommand{\La}{\Lambda}$ $\newcommand{\si}{\sigma}$ $\newcommand{\Si}{\Sigma}$ $\newcommand{\be}{\beta}$ $\newcommand{\de}{\delta}$ $\newcommand{\De}{\Delta}$ $\newcommand{\phi}{\varphi}$ $\newcommand{\th}{\theta}$ $\newcommand{\om}{\omega}$ $\newcommand{\Om}{\Omega}$

In [2]:
using NtToolBox
using PyPlot
using Images
#using Autoreload
#arequire("NtToolBox")

Les pixels d'une image

Une image numérique en niveaux de gris est un tableau de valeurs. Chaque case de ce tableau, qui stocke une valeur, se nomme un pixel. En notant $n$ le nombre de lignes et $p$ le nombre de colonnes de l'image, on manipule ainsi un tableau de $n \times p$ pixels.

La figure ci-dessous montre une visualisation d'un tableau carré avec $n=p=240$, ce qui représente $240\times 240$=57600 pixels. Les appareils photos numériques peuvent enregistrer des images beaucoup plus grandes, avec plusieurs millions de pixels.

In [4]:
n = 256
name = "NtToolBox/src/data/hibiscus.png"
f = load_image(name, n, 1)
imageplot(f);

Les valeurs des pixels sont enregistrées dans l'ordinateur ou l'appareil photo numérique sous forme de nombres entiers entre 0 et 255, ce qui fait 256 valeurs possibles pour chaque pixel.

La valeur 0 correspond au noir, et la valeur 255 correspond au blanc. Les valeurs intermédiaires correspondent à des niveaux de gris allant du noir au blanc.

La figure ci-dessous montre un sous-tableau de $6 \times 6$ pixels extrait de l'image précédente. On peut voir à la fois les valeurs qui composent le tableau et les niveaux de gris qui permettent d'afficher l'image à l'écran.

In [3]:
slex = 19:23
sely = 62:66
floor(255*f[19:24,62:66]);

Stocker une image

Stocker de grandes images sur le disque dur d'un ordinateur prend beaucoup de place. Les nombres entiers sont stockés en écriture binaire, c'est-à-dire sous la forme d'une succession de 0 et de 1. Chaque 0 et chaque 1 se stocke sur une unité élémentaire de stockage, appelée bit.

Pour obtenir l'écriture binaire d'un pixel ayant comme valeur 179, il faut décomposer cette valeur comme somme de puissances de deux. On obtient ainsi $$ 179=2^7+2^5+2^4+2+1, $$ où l'on a pris soin d'ordonner les puissances de deux par ordre décroissant. Afin de faire mieux apparaître l'écriture binaire, on ajoute "$1 \times$" devant chaque puissance qui apparaît dans l'écriture, et "$0\times$" devant les puissances qui n'apparaissent pas $$ 179=1 \times 2^7 + 0 \times 26 + 1 \times 2^5 + 1 \times 24 + 0 \times 2^3 + 0 \times 22 + 1 \times 2^1 + 1 \times 2^0. $$

Avec une telle écriture, la valeur de chaque pixel, qui est un nombre entre 0 et 255, nécessite $$ \log_2(256) = 8 \text{ bits}. $$ La fonction $\log_2$ est le logarithme en base 2, et ce calcul exprime le fait que $$ 256=2^8 = 2 \times 2 \times 2 \times 2 \times 2 \times 2 \times 2 \times 2. $$ L'écriture binaire de la valeur 179 du pixel est ainsi $(1,0,1,1,0,0,1,1)$, où chaque 1 et chaque 0 correspond au facteur multiplicatif qui apparaît devant chaque puissance.

On peut écrire toute valeur entre 0 et 255 de cet manière, ce qui nécessite d'utilisation de 8 bits. Il y a en effet 256 valeurs possibles, et $256=2^8$. Pour stocker l'image complète, on a donc besoin de $$ n \times p \times 8 \text{ bits}. $$

Pour stocker l'image complète, on a donc besoin de $$ n \times p \times 8 \text{ bits}. $$ Pour l'image montrée aux figure précédentes, on a ainsi besoin de $$ 256 \times 256 \times 8 = 524288 \text{ bits}. $$

Pour l'image montrée à la première figure, on a ainsi besoin de $$ 240 \times 240 \times 8 = 460800 \text{ bits.} $$ On utilise le plus souvent l'octet (8 bits) comme unité, de sorte que cette image nécessite 57,6ko (kilo octets).

La résolution d'une image

Afin de réduire la place de stockage d'une image, on peut réduire sa résolution), c'est-à-dire diminuer le nombre de pixels.

La façon la plus simple d'effectuer cette réduction consiste à supprimer des lignes et des colonnes dans l'image de départ.

La figure suivante montre ce que l'on obtient si l'on retient une ligne sur 4 et une colonne sur 4.

In [4]:
sub = (l, k) -> l[collect(i for i in 1:length(l[:, 1]) if (i - 1 )%k == 0), collect(i for i in 1:length(l[1, :]) if (i - 1 )%k == 0)]
Out[4]:
(::#1) (generic function with 1 method)
In [5]:
imageplot(sub(f, 4))

On a ainsi divisé par $4 \times 4 = 16$ le nombre de pixels de l'image, et donc également réduit par 16 le nombre de bit nécessaire pour stocker l'image sur un disque dur.

La figure suivante montre les résultats obtenus en enlevant de plus en plus de lignes et de colonnes. Bien entendu, la qualité de l'image se dégrade vite.

In [6]:
klist = [2, 4, 8, 16]
clf
for i in 1:length(klist)
    k = klist[i]
    imageplot( sub(f,k), string("1 ligne/ colonne sur ", string(k)), [2, 2, i])
end

Quantifier une image

Une autre façon de réduire la place mémoire nécessaire pour le stockage consiste à utiliser moins de nombres entirers pour chaque valeur.

On peut par exemple utiliser uniquement des nombres entier entre 0 et 3, ce qui donnera une image avec uniquement 4 niveau de gris.

On peut effectuer une conversion de l'image d'origine vers une image avec 3 niveau de valeurs en effectuant les remplacements:

  • les valeurs dans $0,1,\ldots,63$ sont remplacées par la valeur 0,

  • les valeurs dans $64,1,\ldots,127$ sont remplacées par la valeur 1,

  • les valeurs dans $128,1,\ldots,191$ sont remplacées par la valeur 2,

  • les valeurs dans $192,\ldots,255$ sont remplacées par la valeur 3.

Une telle opération se nomme quantification).

La figure suivante montre l'image résultante avec 4 niveaux de couleurs. Les 4 valeurs sont affichées en utilisant 4 niveaux de gris allant du noir au blanc.

In [7]:
quant = (l,q) -> (round(q*rescale(f, 1e-3, 1 - 1e-3) - 1/2) + 1/2)/q
imageplot(quant(f,4), "4 niveaux de gris")
Out[7]:
PyObject <matplotlib.text.Text object at 0x0000000002364668>

Nous avons déjà vu que l'on pouvait représenter toute valeur entre 0 et 255 à l'aide de 8 bits en utilisant l'écriture binaire. De fa\c{c}on similaire, on vérifie que toute valeur entre 0 et 3 peut se représenter à l'aide de 2 bits. On obtient ainsi une réduction d'un facteur 8/2=4 de la place mémoire) nécessaire pour le stockage de l'image sur un disque dur.

La figure suivante montre les résultats obtenus en utilisant de moins en moins de niveaux de gris.

In [8]:
qlist = [16, 4, 3, 2]
for i in 1:length(qlist)
    q = qlist[i]
    f1 = quant(f,q)
    f1[1] = 0
    f1[2] = 1
    imageplot(f1, string(string(q), " niveaux de gris"), [2, 2, i])
end

Tout comme pour la réduction du nombre de pixels, la réduction du nombre de niveaux de gris influe beaucoup sur la qualité de l'image. Afin de réduire au maximum la taille d'une image sans modifier sa qualité, on utilise des méthodes plus complexes de compression d'image. La méthode la plus efficace s'appelle JPEG-2000. Elle utilise la théorie des ondelettes. Pour en savoir plus à ce sujet, vous pouvez consuler cet article d'Erwan Le Pennec.

Enlever le bruit par moyennes locales

Les images sont parfois de mauvaise qualité. Un exemple typique de défaut est le bruit qui apparait quand une photo est sous-exposée), c'est-à-dire qu'il n'y a pas assez de luminosité. Ce bruit se manifeste par de petites flucturation aléatoires des niveaux de gris. La figure ci-dessous montre une image bruitée.

In [9]:
name = "NtToolBox/src/data/boat.png"
f = load_image(name, n)
sigma = .08
f = f + randn(n,n)*sigma
imageplot(clamP(f)) #We wrote clamP instead of clamp, because there is already a function clamp in module Base.

Afin d'enlever le bruit dans les images, il convient de faire subir une modification aux valeurs de pixels. L'opération la plus simple consiste à remplacer la valeur $a$ de chaque pixel par la moyenne de $a$ et des 8 valeurs $b,c,d,e,f,g,h,i$ des 8 pixels voisins de a.

Les valeurs des pixels sont positionnées comme suit : $$ \left[ \begin{array}{ccc}$ g & c & h \ b & a & d \ f & e & i \end{array}$

\right]

\left[ \begin{array}{ccc}$ 79 & 54 & 47 \\ 192 & 190 & 153 \\ 166 & 189 & 203 \end{array}$ \right] $$

On obtient ainsi une image modifiée en rempla\c{c}ant a par $$ \frac{a+b+c+d+e+f+g+h+i}{9} $$ puisque l'on fait la moyenne de 9 valeurs. Dans notre exemple, cette moyenne vaut $$ \frac{190+192+79+54+47+153+203+189+166}{9} \approx 141,4. $$ En effectuant cette opération pour chaque pixel, on supprime une partie du bruit, car ce bruit est constitué de fluctuations aléatoires, qui sont diminuées par un calcul de moyennes. La figure ci-dessous montre l'effet d'un tel calcul.

La figure ci-dessous montre l'effet d'un tel moyennage.

In [10]:
roll2d = (f, i, j) -> circshift(f, (i, j))
function filt_moy(f, k)
    n = size(f)[1]
    g = zeros(n, n)
    for i in -k:k
        for j in -k:k
            g = g + roll2d(f, i, j)
        end
    end
    return g/((2*k + 1)^2)
end
imageplot(clamP(f), "Image bruitée", [1, 2, 1])
imageplot(clamP(filt_moy(f, 1)), "Image moyennée", [1, 2, 2])
Out[10]:
PyObject <matplotlib.text.Text object at 0x000000002C139630>

Tout le bruit n'a pas été enlevé par cette opération. Afin d'enlever plus de bruit, on peut moyenner plus de valeurs autour de chaque pixel. La figure suivante montre le résultat obtenu en moyennant de plus en plus de valeurs.

In [11]:
klist = [1, 2, 3, 4]
for i in 1:length(klist)
    k = klist[i]
    f1 = filt_moy(f, k)
    imageplot(clamP(f1), string("Moyenne de ", string((2*k+1)^2), " pixels"), [2, 2, i])
end

Le moyennage des pixels est très efficace pour enlever le bruit dans les images, malheureusement il détruit également une grande partie de l'information de l'image. on peut en effet s'appercevoir que les images obtenues par moyennage sont floues. Ceci est en particulier visible près des contours, qui ne sont pas nets.

Enlever le bruit par médiane

Afin de réduire ce flou, il faut remplacer le moyennage par une opération un peu plus complexe, que l'on nomme mediane.

Etant donné la valeur $a$ d'un pixel, et les valeurs $b,c,d,e,f,g,h,i$, on commence par les classer par croissant.

Dans l'exemple du voisinage de 9 pixels utilisé à la section précédente, on obtient les 9 valeurs classées $$ 47,54,79,153,166,189,190,192,203. $$ La médiane des neuf valeurs $a,b,c,d,e,f,g,h,i$ est la $5^\text{e}$ valeur de ce classement (c'est-à-dire la valeur centrale de ce classement).

Dans notre cas, la médiane est donc 166. Notez que ce nombre est en général différent de la moyenne, qui vaut, pour notre exemple 141,4.

La figure ci-dessous compare le débruitage obtenu en effectuant la moyenne et la médiane de 9 pixels voisins.

In [12]:
function filt_med(f, k)
    n = size(f)[1]
    g = zeros(n, n, (2*k+1)^2)
    s = 1
    for i in -k:k
        for j in -k:k
            g[:,:,s] = roll2d(f, i, j)
            s = s+1
        end
    end
    return median(g, 3)[:, :]
end
imageplot(clamP(filt_moy(f, 1)), "Moyenne de 9 nombres", [1, 2, 1])
imageplot(clamP(filt_med(f, 1)), "Médiane de 9 nombres", [1, 2, 2])