Photo d'origine, rendu en carrés et rendu en cercles (blocs de 16 pixels).
Crédits : NASA (domaine public), photo de l'astronaute Eileen Collins.
Pixeliser une image, c'est la rendre volontairement moins précise en la découpant en blocs carrés de pixels, puis en remplaçant chaque bloc par un carré uni dont la couleur est la moyenne des couleurs du bloc.
Plus les blocs sont grands, plus l'image est méconnaissable. On peut ainsi imaginer un jeu où il faut deviner un personnage à partir d'une image très pixelisée, puis de moins en moins pixelisée à chaque essai raté.
Le traitement est toujours le même :
Comme on répète ce schéma pour tous les blocs, et que l'on connaît à l'avance le nombre de blocs (il dépend des dimensions de l'image et de la taille des blocs), on utilise des boucles Pour.
Une image est un ensemble de pixels rangés en lignes et en colonnes. Chaque pixel est représenté par un tuple (R, V, B) de trois entiers compris entre 0 et 255 (nuances de rouge, vert et bleu).
On mémorise donc une image dans un tableau de tableaux : chaque élément du tableau est une ligne de l'image, et chaque ligne est un tableau de pixels. Ainsi, image[ligne][colonne] désigne le pixel situé à la ligne ligne et à la colonne colonne (comme jeu[ligne][colonne] dans le projet morpion).
Voici par exemple une mini-image de 4 × 4 pixels, que l'on utilisera pour les tests :
R = (255, 0, 0) # rouge
B = (0, 0, 255) # bleu
W = (255, 255, 255) # blanc
N = (0, 0, 0) # noir
mini = [[R, R, B, B],
[R, R, B, B],
[W, N, N, N],
[W, W, N, N]]
Avec des blocs de taille 2, cette image contient 4 blocs : un bloc rouge en haut à gauche, un bloc bleu en haut à droite, un bloc blanc et noir en bas à gauche et un bloc noir en bas à droite.
Pour lire et enregistrer de vraies images, on vous fournit le fichier outils.py qui contient deux fonctions :
charger_image(nom_fichier) : renvoie le tableau de tableaux de tuples correspondant à l'image (réduite automatiquement à 800 pixels maximum de côté) ;sauvegarder_image(image, nom_fichier) : enregistre le tableau de tableaux image dans un fichier image.Pour les utiliser, placez outils.py dans le même dossier que votre programme et écrivez en haut de celui-ci :
from outils import charger_image, sauvegarder_image
Dans un projet, la première phase consiste toujours à décomposer le problème en différentes tâches simples.
Pour cela, il est nécessaire de réfléchir de manière plus fine au problème en détaillant davantage le principe de l'algorithme. C'est cette analyse qui va permettre d'identifier les différentes tâches attendues dans notre algorithme/programme.
Voici le principe de notre algorithme :
On charge l'image
Pour chaque bloc de l'image :
On extrait les pixels du bloc
On calcule la couleur moyenne de ces pixels
On dessine, à la place du bloc, un carré de cette couleur
On enregistre l'image obtenue
On peut désormais identifier les tâches principales à accomplir pour programmer notre traitement. Chacune de ces tâches sera programmée par une fonction. Voici les fonctions principales nécessaires :
extraire_bloc() : fonction qui renvoie les pixels d'un bloc de l'image ;moyenne_couleurs() : fonction qui calcule la couleur moyenne d'un bloc ;dessiner_carre() : fonction qui remplit un bloc de l'image avec une couleur unie ;dessiner_cercle() : fonction qui dessine, à la place d'un bloc, un cercle d'une couleur sur un fond d'une autre couleur (pour obtenir un autre rendu que des carrés).On utilisera les variables suivantes :
image est le tableau de tableaux contenant les pixels de l'image ;taille est un entier : la longueur (en pixels) du côté d'un bloc ;ligne et colonne sont des entiers : la position du pixel situé en haut à gauche d'un bloc ;bloc est un tableau de tableaux contenant les pixels d'un bloc (c'est une petite image) ;couleur est un tuple (R, V, B).Les fonctions principales étant identifiées, il faut maintenant réfléchir aux entrées et aux sorties de chacune d'elle : c'est-à-dire aux paramètres de la fonction (de quoi a-t-elle besoin pour travailler ?) et aux valeurs renvoyées par la fonction (que doit-elle renvoyer ?)
extraire_blocLa fonction extraire_bloc a besoin de l'image, de la position du coin haut-gauche du bloc (ligne et colonne) et de la taille du bloc. Elle possède donc quatre paramètres. Elle renvoie un nouveau tableau de tableaux de taille taille × taille contenant les pixels du bloc.
def extraire_bloc(image, ligne, colonne, taille):
"""Renvoie le tableau de tableaux des pixels du bloc de côté taille
dont le pixel en haut à gauche est image[ligne][colonne]."""
# sera à compléter !
Exemples :
>>> extraire_bloc(mini, 0, 2, 2) # bloc en haut à droite
[[(0, 0, 255), (0, 0, 255)], [(0, 0, 255), (0, 0, 255)]]
>>> extraire_bloc(mini, 2, 0, 2) # bloc en bas à gauche
[[(255, 255, 255), (0, 0, 0)], [(255, 255, 255), (255, 255, 255)]]
Faites un schéma de la mini-image sur papier et entourez le bloc qui commence en (2, 0). Le pixel situé à la ligne i et à la colonne j du bloc correspond à quel pixel de l'image ? Essayez avec i = 0, j = 0, puis i = 1, j = 0, puis i = 1, j = 1.
Le pixel (i, j) du bloc est le pixel image[ligne + i][colonne + j] de l'image. On peut alors construire le bloc par compréhension, comme dans le cours sur les tableaux de tableaux :
def extraire_bloc(image, ligne, colonne, taille):
return [[... for j in range(...)] for i in range(...)]
moyenne_couleursLa fonction moyenne_couleurs a besoin de connaître le bloc dont elle doit calculer la couleur moyenne. Et c'est tout ! Ce sera donc son seul paramètre. Elle renvoie un tuple (R, V, B) dont chaque composante est la moyenne des composantes correspondantes des pixels du bloc. Attention, ces composantes doivent être des entiers.
def moyenne_couleurs(bloc):
"""Renvoie le tuple (R, V, B) des moyennes entières des composantes
rouge, verte et bleue des pixels du tableau de tableaux bloc."""
# sera à compléter !
Exemples :
>>> moyenne_couleurs([[(255, 0, 0), (0, 0, 255)], [(255, 0, 0), (0, 0, 255)]])
(127, 0, 127)
>>> moyenne_couleurs(extraire_bloc(mini, 2, 0, 2)) # 3 pixels blancs, 1 noir
(191, 191, 191)
Il s'agit de l'algorithme de calcul de moyenne vu en cours, mais fait trois fois en même temps : une somme pour le rouge, une pour le vert, une pour le bleu. Pour parcourir tous les pixels d'un tableau de tableaux, cherchez dans le cours sur les tableaux (Thème 2, Chapitre 2) : partie « Parcourir un tableau de tableaux ».
Voici une fonction incomplète (et pas forcément satisfaisante).
def moyenne_couleurs(bloc):
somme_r = 0
...
for ligne in bloc:
for pixel in ligne:
somme_r = somme_r + pixel[0]
...
nb_pixels = ...
return (somme_r / nb_pixels, ..., ...)
dessiner_carreLa fonction dessiner_carre a pour rôle de remplacer tous les pixels d'un bloc de l'image par une même couleur. Elle a donc besoin de l'image, de la position et de la taille du bloc, et de la couleur à utiliser : cinq paramètres. Elle modifie le tableau image et ne renvoie rien (comme la fonction jouer du projet morpion).
def dessiner_carre(image, ligne, colonne, taille, couleur):
"""Remplace par couleur tous les pixels du bloc de côté taille
dont le pixel en haut à gauche est image[ligne][colonne]."""
# sera à compléter !
Exemple :
>>> dessiner_carre(mini, 2, 0, 2, (191, 191, 191))
>>> mini
[[(255, 0, 0), (255, 0, 0), (0, 0, 255), (0, 0, 255)],
[(255, 0, 0), (255, 0, 0), (0, 0, 255), (0, 0, 255)],
[(191, 191, 191), (191, 191, 191), (0, 0, 0), (0, 0, 0)],
[(191, 191, 191), (191, 191, 191), (0, 0, 0), (0, 0, 0)]]
Comme pour extraire_bloc, le pixel (i, j) du bloc est le pixel image[ligne + i][colonne + j] de l'image.
Voici le début d'une fonction envisageable.
def dessiner_carre(image, ligne, colonne, taille, couleur):
for i in range(taille):
for j in ...:
...
dessiner_cercleLa fonction dessiner_cercle remplace elle aussi tous les pixels d'un bloc : ceux qui sont à l'intérieur du cercle inscrit dans le bloc prennent la couleur couleur, les autres prennent la couleur fond. Elle a donc les mêmes paramètres que dessiner_carre, plus la couleur de fond. Elle modifie le tableau image et ne renvoie rien.
def dessiner_cercle(image, ligne, colonne, taille, couleur, fond):
"""Remplace les pixels du bloc de côté taille dont le pixel en haut
à gauche est image[ligne][colonne] : ceux situés dans le cercle inscrit
dans le bloc prennent la couleur couleur, les autres la couleur fond."""
# sera à compléter !
Exemple : la fonction ne vérifie pas le type des couleurs. Pour la tester, on peut donc utiliser des caractères à la place des tuples, c'est plus lisible !
>>> grille = [['.' for j in range(6)] for i in range(6)]
>>> dessiner_cercle(grille, 0, 0, 6, 'C', 'F')
>>> for l in grille:
... print(l)
['F', 'C', 'C', 'C', 'C', 'F']
['C', 'C', 'C', 'C', 'C', 'C']
['C', 'C', 'C', 'C', 'C', 'C']
['C', 'C', 'C', 'C', 'C', 'C']
['C', 'C', 'C', 'C', 'C', 'C']
['F', 'C', 'C', 'C', 'C', 'F']
Un point est à l'intérieur d'un cercle si sa distance au centre est inférieure ou égale au rayon. Pour éviter la racine carrée, on compare les carrés : le pixel (i, j) est dans le cercle de centre (ci, cj) et de rayon r si `(i - ci)2 + (j - cj)2
Dans le repère du bloc (pixels numérotés de 0 à taille - 1), on peut prendre comme centre le point ((taille - 1) / 2, (taille - 1) / 2) et comme rayon taille / 2.
def dessiner_cercle(image, ligne, colonne, taille, couleur, fond):
centre = (taille - 1) / 2
rayon = taille / 2
for i in range(taille):
for j in range(taille):
if ...:
image[...][...] = couleur
else:
...
Le traitement complet est lui-même écrit sous la forme d'une fonction pixeliser, qui prend en paramètres le nom du fichier image et la taille des blocs. Elle enregistre l'image pixelisée dans un nouveau fichier dont le nom commence par pixel_, et ne renvoie rien. Elle utilise les fonctions extraire_bloc(), moyenne_couleurs() et dessiner_carre() définies précédemment.
fonction pixeliser(nom_fichier, taille)
image ← charger_image(nom_fichier)
nb_blocs_vertical ← len(image) // taille # nombre de blocs sur la hauteur
nb_blocs_horizontal ← len(image[0]) // taille # nombre de blocs sur la largeur
Pour i de 0 à nb_blocs_vertical - 1 faire
Pour j de 0 à nb_blocs_horizontal - 1 faire
ligne ← i * taille # coin haut-gauche du bloc
colonne ← j * taille
bloc ← extraire_bloc(image, ligne, colonne, taille)
couleur ← moyenne_couleurs(bloc)
dessiner_carre(image, ligne, colonne, taille, couleur)
sauvegarder_image(image, "pixel_" + nom_fichier)
Exemple : l'appel pixeliser("perroquet.jpg", 20) crée le fichier pixel_perroquet.jpg dans le même dossier.
La démarche de projet est toujours sensiblement la même, quel que soit le projet :
Étape 1 : Répartition des tâches principales = qui fait quoi ?
Répartissez-vous les différentes fonctions extraire_bloc(), moyenne_couleurs(), dessiner_carre(), dessiner_cercle().
Étape 2 : Travail individuel pour implémenter (= programmer) la (ou les) fonction(s) qui vous est (sont) attribuée(s). Voici les étapes à suivre pour implémenter une fonction :
mini ou fabriquez-en d'autres !)
⚙️ Cette phase de tests/corrections est tout à fait normale et importante ! Il faudra la présenter dans le compte-rendu (quel(s) étai(en)t le(s) problème(s) ? quelle(s) solution(s) avez-vous trouvée(s) ? ...) --> donc il faut garder une trace de tout cela !
🆘 Demandez si besoin de l'aide à votre professeur sur l'écriture des fonctions, mais seulement après avoir réfléchi suffisamment de temps et testé plusieurs choses.
Étape 3 : Écriture de la fonction pixeliser : traduire l'algorithme précédent en Python
pixeliser peut tout à fait être écrite sans que les fonctions principales ne soient programmées ! (évidemment, elle ne fonctionnera pas dans ce cas, mais on peut déjà l'écrire)Étape 4 : Tests de la fonction pixeliser
Étape 5 : Correction et amélioration de la fonction pixeliser (et éventuellement des fonctions principales) pour qu'il n'y ait plus de problèmes.
Une fois le programme fonctionnel, vous pouvez l'améliorer. Par exemple :
forme à la fonction pixeliser pour choisir entre un rendu en carrés et un rendu en cercles (avec la fonction dessiner_cercle) ;niveau_gris(couleur) qui renvoie la moyenne entière des trois composantes du tuple couleur ;caractere(niveau, palette) qui renvoie le caractère de la chaîne palette (par exemple "@%#*+=-:. ") correspondant au niveau de gris niveau (0 → premier caractère, 255 → dernier caractère) ;ascii_art(nom_fichier, taille) qui affiche l'image sous forme de texte. Attention : un caractère est environ deux fois plus haut que large, il faut en tenir compte pour ne pas obtenir une image déformée !Germain BECKER & Sébastien POINT, Lycée Mounier, ANGERS 