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
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.
| Avant | Aprè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 :
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 :
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 :
- 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.
- Certaines sont-elles tombées à 0 pendant qu'une autre n'a pas bougé ? Alors un seul canal est conservé.
- Avant et après font-ils 255 une fois additionnés, composante par composante ? Alors c'est le négatif.
| Pixel | Avant | Aprè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.
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.
return. À l'appel, Python les
regroupe en un trio affiché entre parenthèses, par exemple (245, 55, 205).def negatif(r, v, b):définit une fonctionnegatifqui attend trois nombres ;return 255 - r, 255 - v, 255 - brenvoie 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 :
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 :
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
-5ou300est 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).