cours1 min de lecture

Traiter une image, pixel par pixel

Transformer une image, c'est appliquer le même calcul aux trois nombres de chaque pixel. Écrire la règle, la retrouver à partir d'un avant et d'un après, puis l'écrire en Python.
programme

Introduction

Un filtre noir et blanc dans une application photo, une image passée en négatif, un effet « vision rouge » dans un jeu : aucun de ces effets ne redessine l'image. Un programme parcourt la grille, prend les trois nombres d'un pixel, en calcule trois nouveaux, et recommence, à l'identique, sur tous les pixels. Une transformation d'image est donc une règle de calcul sur un triplet, répétée autant de fois que l'image a de pixels.

Ce cours écrit ces règles, apprend à les retrouver quand seul le résultat est connu, puis les traduit en Python.

Une image, une grille de nombres

Une image numérique est une grille de cases, les pixels. Chaque case porte trois jauges : une rouge, une verte, une bleue, chacune de 0 (éteinte) à 255 (à fond). Transformer l'image, c'est régler les jauges, case par case, toujours avec la même règle.

Le triplet d'un pixel s'écrit (R, V, B) : rouge, vert, bleu, dans cet ordre. (255, 0, 0) est du rouge pur, (0, 0, 0) du noir, (255, 255, 255) du blanc, et un triplet aux trois composantes égales est un gris.

Que représente le pixel (0, 255, 0) ?

Trois règles à connaître

Chaque règle dit comment calculer le nouveau triplet à partir de l'ancien. La flèche ← se lit « devient ».

Le négatif

Chaque composante devient 255 moins sa valeur : ce qui était à fond s'éteint, ce qui était éteint passe à fond, le clair devient sombre.

R←255−RV←255−VB←255−BR \leftarrow 255 - R \qquad V \leftarrow 255 - V \qquad B \leftarrow 255 - B
AvantAprès
(0, 0, 0)(255, 255, 255)
(255, 255, 255)(0, 0, 0)
(220, 40, 40)(35, 215, 215)

Un seul canal conservé

Une composante reste, les deux autres tombent à 0. Pour le canal rouge seul :

R←RV←0B←0R \leftarrow R \qquad V \leftarrow 0 \qquad B \leftarrow 0

Il ne subsiste qu'une des trois couleurs de base : l'image entière prend une teinte rouge plus ou moins sombre. Le canal vert seul ou le canal bleu seul s'obtiennent de la même façon.

Les niveaux de gris

Les trois composantes prennent la même valeur, la moyenne des trois :

R, V, B←R+V+B3R,\ V,\ B \leftarrow \frac{R + V + B}{3}

La couleur disparaît, la clarté reste. (220, 40, 40) devient (100, 100, 100), car (220 + 40 + 40) ÷ 3 = 100.

Quelle opération transforme une composante en sa version « négatif » ?

Quel est le négatif du pixel (30, 90, 220) ?

Retrouver la règle d'un avant / après

Le problème se pose aussi dans l'autre sens : une image a été transformée, et seuls les triplets d'avant et d'après sont connus. Trois questions suffisent pour retrouver la règle :

  1. Les trois valeurs d'après sont-elles égales ? Alors ce sont les niveaux de gris, et la valeur commune est la moyenne d'avant.
  2. Certaines sont-elles tombées à 0 pendant qu'une autre n'a pas bougé ? Alors un seul canal est conservé.
  3. Avant et après font-ils 255 une fois additionnés, composante par composante ? Alors c'est le négatif.
PixelAvantAprès
A(220, 40, 40)(100, 100, 100)
B(240, 200, 160)(200, 200, 200)
C(90, 90, 90)(90, 90, 90)

Les trois valeurs d'après sont égales, et ce sont les moyennes d'avant : niveaux de gris. Le pixel C, déjà gris, ressort inchangé : c'est un indice et non un hasard, puisque la moyenne de trois nombres égaux est ce nombre.

La règle se lit sur les nombres, pas sur la couleur affichée. « L'image est devenue plus foncée » décrit un résultat ; V ← 0 et B ← 0 disent la règle.

Un pixel passe de (240, 200, 160) à (240, 0, 0). Quelle transformation a été appliquée ?

Un pixel passe de (60, 120, 200) à (195, 135, 55). Quelle transformation a été appliquée ?

Ce qu'une transformation ne change pas

Une transformation modifie les valeurs des pixels, pas leur nombre. Le négatif d'une image de 1000 × 500 contient toujours 500 000 pixels de trois octets : son poids, sans compression, reste de 1,5 Mo. Même une image passée en niveaux de gris garde ses trois composantes par pixel ; elles sont simplement égales.

Une image de 800 × 600 passe en négatif. Que devient son poids, sans compression ?

La même règle en Python

Une règle sur un triplet s'écrit naturellement comme une fonction : elle reçoit les trois nombres r, v, b et renvoie les trois nouveaux.

Renvoyer plusieurs valeurs. Une fonction Python peut renvoyer plusieurs valeurs séparées par des virgules dans le return. À l'appel, Python les regroupe en un trio affiché entre parenthèses, par exemple (245, 55, 205).
⏵ Ctrl+↵ pour exécuter
Aucune exécution pour l'instant.
Lecture du code, ligne par ligne :
  • def negatif(r, v, b): définit une fonction negatif qui attend trois nombres ;
  • return 255 - r, 255 - v, 255 - b renvoie les trois nouvelles valeurs, dans l'ordre rouge, vert, bleu ;
  • print(negatif(10, 200, 50)) appelle la fonction sur un pixel et affiche le résultat.

Les niveaux de gris demandent une moyenne entière, car une composante est un entier de 0 à 255 :

⏵ Ctrl+↵ pour exécuter
Aucune exécution pour l'instant.
7 / 3 vaut 2.333… (un nombre à virgule) ; 7 // 3 vaut 2 (la partie entière seulement). D'où // pour une composante.

Le canal rouge seul tient en une ligne :

⏵ Ctrl+↵ pour exécuter
Aucune exécution pour l'instant.

Quel pixel obtient-on en appliquant canal_rouge à (120, 200, 80) ?

Et sur une image entière ?

Tout ce qui précède porte sur un seul pixel. Pour une image complète, le programme parcourt toutes les cases de la grille et applique la même fonction à chacune.

Démonstration

Le code ci-dessous est à lire, pas à reproduire. Il applique negatif à toute une image avec la bibliothèque Python Pillow. La fonction negatif est exactement celle écrite plus haut ; le reste sert à ouvrir l'image, à la parcourir case par case et à enregistrer le résultat.

from PIL import Image

def negatif(r, v, b):
    return 255 - r, 255 - v, 255 - b

source = Image.open("photo.jpg")
largeur, hauteur = source.size
resultat = Image.new("RGB", (largeur, hauteur))

for x in range(largeur):
    for y in range(hauteur):
        r, v, b = source.getpixel((x, y))
        resultat.putpixel((x, y), negatif(r, v, b))

resultat.save("negatif.jpg")

Ce qui fait l'effet tient dans la fonction qui transforme un pixel. Le reste est le même pour toutes les transformations.

Pièges courants

  • Le négatif porte sur les trois composantes, une par une. N'en inverser qu'une est l'erreur la plus fréquente.
  • Toujours dans l'ordre R, V, B.
  • Les valeurs restent entre 0 et 255. Une règle qui produit -5 ou 300 est mal posée.
  • Une transformation ne change pas le poids : les valeurs changent, leur nombre non.
  • Décrire n'est pas écrire la règle : « plus foncé » ne dit pas quel calcul a été fait.

Pour aller plus loin

Le module Pixels du labo applique ces transformations à une vraie image et affiche, dans une fenêtre de zoom, les triplets d'un même pixel avant et après. Son défi à paliers pose le problème inverse : retrouver l'opération qui produit une image donnée.

D'autres règles suivent le même principe : éclaircir (ajouter une constante à chaque composante, sans dépasser 255), augmenter le contraste (écarter les valeurs du milieu), virer au sépia (mélanger les composantes avec des coefficients fixes).