Matrices de pixels et images
Cours complet · informatique (tronc commun des prépas scientifiques), chapitre 8 · prépas scientifiques, tronc commun
Travailler ce chapitre sur Adloun Exercices corrigés de ce chapitre
<i class="fa-solid fa-compass mr-2" style="color:#9A563B"></i>8.1 Introduction et motivation
Une photographie numérique n'est rien d'autre qu'un immense tableau de nombres : chaque case — un pixel — contient l'intensité lumineuse d'un point de l'image. Faire pivoter la photo, l'éclaircir, la flouter, détecter les contours d'un visage : toutes ces opérations, qui semblent relever de la magie des logiciels de retouche, sont des parcours de tableaux à deux dimensions — des doubles boucles du chapitre 3, appliquées à un objet qu'on peut voir.
C'est ce qui fait des images le terrain d'entraînement parfait pour les tableaux bidimensionnels, dont la manipulation est l'objectif technique du chapitre : création (et son piège fameux), parcours par lignes et par colonnes, transformations d'indices (rotations, symétries), changements d'échelle, et l'opération reine du traitement d'image — la convolution, où chaque pixel est recalculé à partir de ses voisins. Le bénéfice pédagogique est immédiat : une erreur d'indice, invisible dans un tableau de nombres, saute aux yeux sur une image — l'image penche, se déchire ou se brouille. Le débogage devient littéralement visuel.
8.2 Tableaux à deux dimensions
8.2.1 Listes de listes
En Python, un tableau à deux dimensions de lignes et colonnes se représente par une liste de listes de longueur : M[i] est la ligne , et M[i][j] l'élément en ligne , colonne — avec et .
M = [[1, 2, 3],
[4, 5, 6]] # n = 2 lignes, p = 3 colonnes
M[1][2] # 6 : ligne 1, colonne 2
len(M) # n = 2 (nombre de lignes)
len(M[0]) # p = 3 (nombre de colonnes)
Convention à fixer une fois pour toutes : premier indice ligne, second colonne — l'ordre des mathématiques (matrices), qui n'est pas celui des coordonnées géométriques : la ligne croît vers le bas.
Pour créer une matrice nulle de , l'écriture suivante est un bogue célèbre :
M = [[0] * p] * n # FAUX : n fois LA MÊME ligne !
M[0][0] = 1
# M == [[1, 0, 0], [1, 0, 0]] : TOUTES les lignes ont changé
[ligne] n ne copie pas la ligne : il répète fois la même liste — modifier une case les modifie « toutes » (il n'y en a qu'une). La forme correcte crée une ligne neuve* à chaque tour :
M = [[0] * p for _ in range(n)] # n lignes indépendantes
([0] p reste licite : les entiers ne sont pas modifiables, partager des est sans danger — c'est le partage de listes* qui mord.)
8.2.2 Les deux parcours
Méthode : Parcours ligne par ligne, colonne par colonne
for i in range(n): # ligne par ligne (l'ordre naturel)
for j in range(p):
... M[i][j] ...
for j in range(p): # colonne par colonne
for i in range(n):
... M[i][j] ...
Les deux visitent les cases — coût , le « linéaire » des images (on dit aussi : linéaire en le nombre de pixels). Le choix de l'ordre suit le problème : sommes par ligne premier parcours ; sommes par colonne second.
def somme_matrice(M: list) -> float:
s = 0
for ligne in M:
for x in ligne:
s += x
return s
def maximum_matrice(M: list) -> float:
"""Précondition : M non vide."""
m = M[0][0]
for ligne in M:
for x in ligne:
if x > m:
m = x
return m
Quand les indices ne servent pas, le parcours par valeurs à deux étages est le plus lisible — exactement la règle du chapitre 2, doublée.
8.3 Les images comme matrices
Une image en niveaux de gris de hauteur et largeur est une matrice d'entiers entre et : est le noir, le blanc, les valeurs intermédiaires des gris de plus en plus clairs. (Une image en couleurs superpose trois telles matrices — rouge, vert, bleu ; le chapitre travaille en gris, tout s'y transpose canal par canal.)
Deux retouches en un parcours — on produit une nouvelle image, sans modifier l'originale :
def negatif(img: list) -> list:
"""Renvoie le négatif : chaque pixel v devient 255 - v."""
n, p = len(img), len(img[0])
return [[255 - img[i][j] for j in range(p)] for i in range(n)]
def seuillage(img: list, s: int) -> list:
"""Image en noir et blanc pur : blanc si v >= s, noir sinon."""
n, p = len(img), len(img[0])
return [[255 if img[i][j] >= s else 0 for j in range(p)] for i in range(n)]
La compréhension imbriquée [[... for j ...] for i ...] construit la matrice résultat ligne par ligne — c'est l'idiome de tout le chapitre. Ces opérations point à point (le pixel résultat ne dépend que du pixel source) coûtent et ne posent aucun problème d'indices ; les ennuis commencent quand les pixels bougent.
Pour expérimenter sur de vraies images, plt.imshow(img, cmap="gray") (matplotlib, chapitre 4) affiche une matrice comme une image — l'aller-retour entre la matrice de nombres et son rendu visuel est le meilleur outil de validation du chapitre : un bogue d'indices se voit.
8.4 Transformations géométriques
8.4.1 Symétries et rotation : des affaires d'indices
Le miroir gauche-droite renverse chaque ligne : le pixel de l'image résultat vient de :
def miroir(img: list) -> list:
n, p = len(img), len(img[0])
return [[img[i][p - 1 - j] for j in range(p)] for i in range(n)]
Le réflexe de validation : miroir(miroir(img)) == img — une involution, testable par assertion sur des matrices aléatoires (chapitre 1, test de propriété).
Faire pivoter l'image de degrés dans le sens horaire : l'image résultat a lignes et colonnes — les dimensions s'échangent — et son pixel vient du pixel de la source :
def rotation_horaire(img: list) -> list:
n, p = len(img), len(img[0])
return [[img[n - 1 - j][i] for j in range(n)] for i in range(p)]
assert rotation_horaire([[1, 2, 3],
[4, 5, 6]]) == [[4, 1],
[5, 2],
[6, 3]]
D'où sort la formule ? La colonne de gauche de la source (, lue de bas en haut) devient la ligne du haut du résultat. Plutôt que de la deviner, on la dérive : la rotation horaire est la composée « transposer puis miroir », et l'on vérifie sur la petite matrice ci-dessus, dont on suit un pixel à la trace. Quatre rotations doivent rendre l'image initiale — second test de propriété.
Méthode : Écrire une transformation d'indices sans se tromper
- Écrire la correspondance dans le bon sens : pour chaque pixel du résultat, de quel pixel de la source vient-il ? (Le sens inverse — « où va chaque pixel source ? » — produit des images à trous dès que la transformation n'est pas bijective.)
- Déterminer les dimensions du résultat avant d'écrire la boucle (la rotation les échange !).
- Valider sur une matrice non symétrique (jamais une carrée constante, qui masque tout), puis par les propriétés : involution, quatre quarts de tour, dimensions.
8.4.2 Réduction et agrandissement
Réduire d'un facteur : chaque pixel du résultat résume un bloc de la source par sa moyenne — moins brutal que de ne garder qu'un pixel sur (le « sous-échantillonnage », qui jette de l'information et crénelle les contours) :
def reduction(img: list, k: int) -> list:
"""Réduit d'un facteur k. Précondition : k divise les deux dimensions."""
n, p = len(img), len(img[0])
res = [[0] * (p // k) for _ in range(n // k)]
for i in range(n // k):
for j in range(p // k):
s = 0
for a in range(k): # moyenne du bloc k x k
for b in range(k):
s += img[k * i + a][k * j + b]
res[i][j] = s // (k * k)
return res
Quatre boucles, mais chaque pixel source n'est lu qu'une fois : coût , linéaire en pixels — compter les lectures, pas les for (chapitre 3).
Agrandir d'un facteur : chaque pixel source devient un bloc . La formule tient en une ligne — le pixel du résultat vient de :
def agrandissement(img: list, k: int) -> list:
n, p = len(img), len(img[0])
return [[img[i // k][j // k] for j in range(k * p)] for i in range(k * n)]
L'image agrandie a un aspect « pixelisé » : la réplication n'invente aucune information. Les agrandissements lisses des logiciels interpolent entre pixels voisins — une moyenne pondérée, cousine de la convolution qui vient.
8.5 La convolution
8.5.1 Recalculer chaque pixel d'après ses voisins
Soit une matrice de coefficients (le noyau). La convolution de l'image par recalcule chaque pixel comme combinaison de ses voisins :
def convolution(img: list, K: list) -> list:
"""Convolution par un noyau 3x3 ; les bords sont laissés tels quels."""
n, p = len(img), len(img[0])
res = [ligne[:] for ligne in img] # copie (les bords y survivent)
for i in range(1, n - 1):
for j in range(1, p - 1):
s = 0
for a in range(-1, 2):
for b in range(-1, 2):
s += K[1 + a][1 + b] * img[i + a][j + b]
res[i][j] = min(255, max(0, int(s))) # ramener dans [0, 255]
return res
Deux précautions de contrat : les pixels du bord n'ont pas voisins — ici on les laisse inchangés (d'autres conventions existent : les ignorer, refléter l'image… l'énoncé doit trancher) ; et le résultat est écrêté dans , car une combinaison peut sortir de l'intervalle.
La convolution lit img et écrit dans res : deux matrices distinctes. Écrire directement dans l'image lue serait un bogue sournois — les pixels déjà recalculés contamineraient le calcul de leurs voisins, et le résultat dépendrait de l'ordre de parcours. C'est le même phénomène que la copie préalable des tests du chapitre 3 : quand on transforme, on sépare la source du résultat.
8.5.2 Les noyaux classiques
FLOU = [[1/9, 1/9, 1/9], # moyenneur : chaque pixel devient la
[1/9, 1/9, 1/9], # moyenne de son voisinage -> adoucit
[1/9, 1/9, 1/9]]
CONTOURS = [[0, -1, 0], # laplacien : sensible aux VARIATIONS ;
[-1, 4, -1], # nul sur les zones unies, fort sur les bords
[0, -1, 0]]
NETTETE = [[0, -1, 0], # identité + laplacien :
[-1, 5, -1], # accentue les transitions
[0, -1, 0]]
Lecture des deux premiers : le moyenneur remplace chaque pixel par la moyenne de son voisinage — les détails fins (et le bruit) s'estompent, c'est le flou. Le laplacien a des coefficients de somme nulle : sur une zone unie, la combinaison s'annule (noir) ; sur un contour — là où l'intensité saute — elle s'emballe : l'image résultat ne montre que les bords. La détection de contours, première étape de toute vision par ordinateur, tient dans ces neuf coefficients.
Complexité : Convolution
Pour un noyau : multiplications par pixel, soit . Pour une image ( mégapixels) et : environ opérations — la seconde sur une machine ordinaire, et la raison pour laquelle le traitement d'image réel s'écrit avec des bibliothèques optimisées (numpy) dont les boucles sont compilées : l'algorithme est celui qu'on vient d'écrire, seule l'intendance change.
<i class="fa-solid fa-dumbbell mr-2" style="color:#2E7559"></i>8.6 Exercices résolus
Niveau (Application directe du cours)
Prédire ce qu'affiche le programme suivant, expliquer, corriger :
grille = [[0] * 3] * 2
grille[0][1] = 7
print(grille)
compteur = [[0, 0], [0, 0]]
ligne = compteur[0]
ligne[0] = 5
print(compteur)
Démonstration (Solution)
Première partie : [[0, 7, 0], [0, 7, 0]] — les deux lignes sont la même liste, modifier l'une « modifie l'autre ». Correction : [[0] 3 for _ in range(2)]. Seconde partie : [[5, 0], [0, 0]] — ligne n'est pas une copie mais un alias de la première ligne : toute affectation nom = liste partage, ne copie pas. Pour copier une ligne : ligne = list(compteur[0]) (ou compteur[0][:]). (Les deux pièges sont le même phénomène — plusieurs noms pour une seule liste — vu à la création puis à l'affectation ; le diagnostic en une question : « combien de listes distinctes ai-je réellement créées ? ».)*
Pour une matrice de notes (élèves en lignes, devoirs en colonnes), écrire moyennes_lignes(M) et moyennes_colonnes(M), et donner leur coût.
Démonstration (Solution)
def moyennes_lignes(M: list) -> list:
return [sum(ligne) / len(ligne) for ligne in M]
def moyennes_colonnes(M: list) -> list:
n, p = len(M), len(M[0])
return [sum(M[i][j] for i in range(n)) / n for j in range(p)]
M = [[10, 14], [12, 18]]
assert moyennes_lignes(M) == [12.0, 15.0] # par élève
assert moyennes_colonnes(M) == [11.0, 16.0] # par devoir
Chaque fonction lit chaque case une fois : . La version colonnes illustre le parcours « externe, interne » du cours — ou, en une ligne, la somme sur à fixé. (Élèves/devoirs, villes/mois, pixels : la matrice est la structure des données croisées, et les deux familles de moyennes sont ses deux lectures.)
Écrire eclaircir(img, delta) qui ajoute delta à chaque pixel en écrêtant dans , et montrer sur un exemple pourquoi l'écrêtage n'est pas optionnel — puis vérifier que eclaircir(eclaircir(img, 30), -30) ne redonne pas toujours l'image initiale.
Démonstration (Solution)
def eclaircir(img: list, delta: int) -> list:
n, p = len(img), len(img[0])
return [[min(255, max(0, img[i][j] + delta)) for j in range(p)]
for i in range(n)]
img = [[250, 100]]
assert eclaircir(img, 30) == [[255, 130]] # 280 aurait débordé !
aller_retour = eclaircir(eclaircir(img, 30), -30)
assert aller_retour == [[225, 100]] # 250 est PERDU : 255 - 30 = 225
Sans écrêtage, le pixel sortirait du format — affiché n'importe comment, ou pire, stocké modulo comme : un point brûlé devenu noir. Et l'aller-retour montre que l'écrêtage détruit de l'information : tous les pixels au-dessus de sont devenus , indiscernables — l'éclaircissement n'est pas inversible. (Une transformation d'image n'est pas une bijection en général ; quand on retouche, on garde l'originale — version photographique de « copier avant de muter ».)
Niveau (Application avec raisonnement intermédiaire)
Écrire transpose(M) (), puis exprimer rotation_horaire et rotation_antihoraire comme composées de transpose et miroir. Vérifier par test de propriété que quatre rotations horaires rendent l'image initiale.
Démonstration (Solution)
def transpose(M: list) -> list:
n, p = len(M), len(M[0])
return [[M[i][j] for i in range(n)] for j in range(p)]
def rotation_horaire(img: list) -> list:
return miroir(transpose(img)) # transposer PUIS miroir g-d
def rotation_antihoraire(img: list) -> list:
return transpose(miroir(img)) # miroir PUIS transposer
import random
for essai in range(200):
n, p = random.randint(1, 6), random.randint(1, 6)
img = [[random.randint(0, 255) for _ in range(p)] for _ in range(n)]
r = rotation_horaire(img)
assert len(r) == p and len(r[0]) == n # dimensions échangées
q = rotation_horaire(rotation_horaire(rotation_horaire(r)))
assert q == img # quatre quarts = identité
Vérifions la composée sur un pixel : transpose envoie la source en ; le miroir gauche-droite envoie en — donc le pixel résultat vient de : c'est exactement la formule directe du cours. (Composer deux transformations simples et sûres plutôt qu'inventer une formule d'indices : moins rapide d'un facteur deux, mais beaucoup plus difficile à rater — un arbitrage légitime tant que le profil de coût reste .)
Écrire cherche_motif(motif, img) qui renvoie la position du coin supérieur gauche de la première occurrence exacte de la matrice motif dans img, ou None — la généralisation 2D de la recherche de facteur du chapitre 3. Donner le coût.
Démonstration (Solution)
def cherche_motif(motif: list, img: list):
m, q = len(motif), len(motif[0])
n, p = len(img), len(img[0])
for i in range(n - m + 1):
for j in range(p - q + 1):
# le motif coïncide-t-il en (i, j) ?
ok = True
for a in range(m):
for b in range(q):
if img[i + a][j + b] != motif[a][b]:
ok = False
break
if not ok:
break
if ok:
return (i, j)
return None
Coût : positions, au plus comparaisons chacune — , le produit des tailles, fidèle au du facteur 1D. Et comme en 1D, l'échec est en pratique très précoce (le premier pixel discordant suffit), mais le pire cas — image unie, motif presque uni — paie le prix entier. (Les bornes n - m + 1 et p - q + 1 sont les mêmes « positions de départ » que le chapitre 3, dans chaque dimension ; ce balayage par fenêtre est aussi la structure de la convolution — la vision par ordinateur cherche ses motifs exactement ainsi, en remplaçant l'égalité stricte par un score de ressemblance.)
Sans machine, décrire l'effet de chacun des noyaux suivants, puis vérifier sur une image-test :
Démonstration (Solution)
est le noyau identité : la somme se réduit à — l'image est inchangée (le test de base de toute implémentation de convolution !). prélève le voisin de droite : — l'image entière se décale d'un pixel vers la gauche. est le filtre de Sobel (vertical) : différence pondérée entre colonnes droite et gauche — nul sur les zones unies, maximal sur les contours verticaux (un mur clair sur fond sombre) ; les contours horizontaux, eux, lui échappent (la somme par colonne est équilibrée) ; son transposé les détecterait. (Lire un noyau : où sont les coefficients positifs et négatifs, leur somme est-elle nulle — détecteur de variations — ou égale à un — moyenne conservée ? Deux questions qui prédisent l'essentiel de l'effet, à vérifier ensuite sur l'image-test : une matrice avec un bord franc.)
Écrire histogramme_image(img) (les effectifs des niveaux — chapitre 2 !), puis etire_contraste(img) qui étire linéairement les niveaux pour que le minimum de l'image devienne et son maximum . Quel est l'effet sur une image « grisâtre » dont tous les pixels sont entre et ?
Démonstration (Solution)
def histogramme_image(img: list) -> list:
h = [0] * 256
for ligne in img:
for v in ligne:
h[v] += 1
return h
def etire_contraste(img: list) -> list:
"""Précondition : l'image n'est pas uniforme (min < max)."""
mini = min(min(ligne) for ligne in img)
maxi = max(max(ligne) for ligne in img)
n, p = len(img), len(img[0])
return [[(img[i][j] - mini) * 255 // (maxi - mini) for j in range(p)]
for i in range(n)]
Sur l'image grisâtre, les niveaux – sont envoyés affinement sur – : l'écart entre deux gris voisins est multiplié par — les détails noyés dans la brume deviennent visibles. L'histogramme avant (un pic étroit autour de ) et après (étalé sur toute la plage) rend l'effet lisible sans même afficher l'image. La précondition exclut l'image uniforme (division par zéro — et qu'attendre d'un contraste sans contraste ?). (Le comptage du chapitre 2 — ici en tableau plutôt qu'en dictionnaire, les clés étant les entiers – : quand les clés sont de petits entiers denses, le tableau est le dictionnaire du pauvre, et le plus rapide.)
Niveau (Raisonnement subtil ou plusieurs étapes)
Appliquer le flou moyenneur fois de suite à une image-test contenant un point blanc unique sur fond noir. Décrire l'évolution, conjecturer la limite, et expliquer le lien avec la réduction-agrandissement : pourquoi flouter puis réduire donne-t-il une meilleure miniature que réduire directement ?
Démonstration (Solution)
img = [[0] * 21 for _ in range(21)]
img[10][10] = 255 # un point blanc au centre
for _ in range(10):
img = convolution(img, FLOU)
Après un flou, le point devient un carré d'intensité ; après deux, une tache aux coins plus sombres ; au fil des itérations, la tache s'élargit et prend un profil en cloche — l'intensité se répartit comme les sommes de dés du chapitre 4, et pour la même raison : chaque passage moyenne des contributions voisines, et la répétition de moyennes fabrique une cloche (le cours de probabilités donnera son nom à ce phénomène). La limite, à image infinie, est un étalement uniforme vers ; sur l'image bornée avec nos bords figés, une quasi-uniformité.
Pour la miniature : réduire directement d'un facteur par sous-échantillonnage ne lit qu'un pixel sur — un détail fin (une ligne d'un pixel) peut tomber entre les mailles et disparaître ou produire des escaliers (le crénelage). Flouter d'abord répartit l'information de chaque détail sur un voisinage de la taille de la maille : le pixel conservé « contient » alors un résumé de son bloc — c'est d'ailleurs exactement ce que fait la réduction par moyenne du cours, qui combine les deux étapes en une. (Règle des photographes numériques : on lisse à l'échelle de ce qu'on va jeter. La version théorique — le théorème d'échantillonnage — attendra ; l'intuition expérimentale est déjà la bonne.)
Le flou moyenneur peut se calculer en deux passes : une moyenne horizontale sur pixels, puis une moyenne verticale sur pixels. Montrer l'équivalence, écrire la version deux-passes, et comparer les coûts ( contre ).
Démonstration (Solution)
Équivalence : la moyenne sur le bloc s'écrit
— la somme double se factorise : moyenner d'abord chaque ligne par fenêtres de (la parenthèse), puis moyenner verticalement ces moyennes. Le noyau moyenneur est dit séparable (produit d'un noyau colonne par un noyau ligne).
def flou_separable(img: list, k: int) -> list:
n, p = len(img), len(img[0])
r = k // 2
# passe 1 : moyennes horizontales (copie : les bords y survivent)
h = [ligne.copy() for ligne in img]
for i in range(n):
for j in range(r, p - r):
h[i][j] = sum(img[i][j + b] for b in range(-r, r + 1)) // k
# passe 2 : moyennes verticales sur h (même convention de bords)
res = [ligne.copy() for ligne in img]
for i in range(r, n - r):
for j in range(r, p - r):
res[i][j] = sum(h[i + a][j] for a in range(-r, r + 1)) // k
return res
(Les divisions entières successives peuvent différer de la division par d'une unité : l'égalité avec la version directe est à un près.)
Coûts : la version directe fait lectures par pixel ; les deux passes en font — gain d'un facteur , soit pour un flou large (très utilisés en pratique). (Et l'on peut faire mieux encore : la somme d'une fenêtre glissante se met à jour en — ajouter l'entrant, retirer le sortant, comme la moyenne glissante du chapitre 4 — ramenant le flou à , indépendant de . Trois implémentations, trois coûts, un seul résultat : l'algorithmique du traitement d'image est une affaire de factorisations.)
On veut livrer le module du chapitre (negatif, miroir, rotation, reduction, convolution). Construire le banc de validation complet : tests unitaires sur petites matrices calculées à la main, tests de propriétés (involutions, composition des rotations, dimensions, conservation de la somme par le flou hors écrêtage), et un garde-fou commun à toutes les fonctions — l'image source ne doit jamais être modifiée.
Démonstration (Solution)
import random
def image_aleatoire():
n, p = random.randint(1, 8), random.randint(1, 8)
return [[random.randint(0, 255) for _ in range(p)] for _ in range(n)]
def copie(img):
return [ligne[:] for ligne in img]
# 1. unitaires : la matrice 2x3 témoin, tout à la main
T = [[1, 2, 3], [4, 5, 6]]
assert negatif(T) == [[254, 253, 252], [251, 250, 249]]
assert miroir(T) == [[3, 2, 1], [6, 5, 4]]
assert rotation_horaire(T) == [[4, 1], [5, 2], [6, 3]]
assert reduction([[1, 3], [5, 7]], 2) == [[4]]
# 2. propriétés, sur 500 images aléatoires
random.seed(8)
for _ in range(500):
img = image_aleatoire()
avant = copie(img)
assert miroir(miroir(img)) == img # involution
assert negatif(negatif(img)) == img
r = rotation_horaire(img)
assert (len(r), len(r[0])) == (len(img[0]), len(img)) # dimensions
assert rotation_horaire(rotation_horaire(
rotation_horaire(rotation_horaire(img)))) == img # 4 quarts
convolution(img, FLOU)
assert img == avant # 3. source intacte
Le garde-fou final (3) est testé après chaque fonction : c'est lui qui attrape les [[0]p]n et les écritures dans la source au lieu de la copie — les deux bogues structurels du chapitre, indétectables sur le seul résultat. La conservation de la somme par le flou (somme des coefficients ) se teste hors bords et hors écrêtage, sur des images aux valeurs centrales : chaque propriété s'accompagne de son domaine de validité, comme toute spécification. (Ce banc tient en trente lignes et rejoue en une seconde : c'est lui, plus que la relecture, qui autorise à modifier une fonction six mois plus tard — la définition même d'un livrable, au sens des compétences « mettre en œuvre » et « justifier » du programme.)
- Tableaux 2D : listes de listes,
M[i][j]ligne , colonne ; création[[0] p for _ in range(n)]— jamais[[0] p] * n( alias d'une même ligne) ; affecter ne copie pas (alias), copier une ligne :ligne[:]; deux parcours ( externe ou externe), coût . - Images : matrices d'entiers (noir) à (blanc) ; opérations point à point (négatif, seuillage, éclaircissement écrêté — l'écrêtage détruit de l'information : garder l'originale) ; histogramme en tableau de cases.
- Transformations d'indices : raisonner « le pixel du résultat vient de… » ; dimensions du résultat d'abord (la rotation les échange) ; rotation horaire transposée puis miroir, source ; valider sur une non symétrique propriétés (involutions, quarts identité).
- Échelles : réduction par moyenne de blocs (un pixel résume — supérieur au sous-échantillonnage, qui crénelle), agrandissement par réplication (source ) ; coût — compter les lectures, pas les boucles.
- Convolution () : chaque pixel recombiné depuis ses voisins par le noyau ; toujours écrire dans une matrice neuve ; bords clause de contrat ; écrêter dans . Noyaux : somme moyenne conservée (flou) ; somme détecteur de variations (laplacien, Sobel : contours) ; identité test de base ; séparable deux passes, .
- Validation visuelle et programmée : un bogue d'indices se voit (
imshow) ; banc complet unitaires sur témoin propriétés aléatoires garde-fou « source jamais modifiée ».
8.7 Exercices d'entraînement
Cette banque d'exercices, classée par thème, couvre l'intégralité du chapitre. La numérotation prolonge celle des dix exercices résolus. Légende : application directe, raisonnement intermédiaire, approfondissement ; le symbole signale un classique incontournable.
A. Tableaux 2D
- () Écrire
matrice_identite(n),table_multiplication(n)() etest_symetrique(M). - () Écrire
diagonale(M),antidiagonale(M)ettrace(M)pour une matrice carrée — attention aux indices de l'antidiagonale. - ( ) Écrire
somme_voisins(M, i, j): la somme des cases adjacentes (jusqu'à ) de , robuste aux bords et aux coins — la brique de la convolution et du jeu de la vie. - () Un carré magique : matrice dont toutes les lignes, colonnes et les deux diagonales ont la même somme. Écrire le vérificateur, le tester sur le carré classique.
- () Le jeu de la vie de Conway : à chaque génération, une cellule vivante survit avec ou voisines vivantes, une morte naît avec exactement . Écrire
generation(M)(nouvelle matrice !), faire évoluer un « planeur » sur générations.
B. Transformations
- () Écrire
miroir_vertical(img)(haut-bas) de deux façons : par formule d'indices, et parimg[::-1]avec copie des lignes — vérifier l'égalité des résultats. - () Écrire
rotation_180(img)de trois façons (formule directe ; deux quarts de tour ; deux miroirs) et tester leur égalité sur des images aléatoires. - ( ) Écrire
recadre(img, i0, j0, h, l)(extraire le rectangle de coin , hauteur , largeur ) avec validation des préconditions par assertions. - () Écrire
incruste(fond, motif, i0, j0)qui copiemotifdansfonden position donnée (nouvelle image), puis vérifiercherche_motif(motif, incruste(...)) == (i0, j0)— deux fonctions du chapitre se testent l'une l'autre. - () L'agrandissement bilinéaire d'un facteur : les pixels intermédiaires sont des moyennes de leurs voisins (milieu d'arête : moyenne de ; centre de carré : moyenne de ). L'implémenter et comparer visuellement à la réplication.
C. Convolutions et filtres
- () Vérifier sur une image aléatoire que la convolution par le noyau identité rend l'image inchangée (hors bords) — le test de base.
- () Construire le noyau qui décale l'image d'un pixel vers le bas, et le vérifier.
- ( ) Appliquer Sobel vertical et Sobel horizontal (son transposé) à une image de damier : prédire puis observer quels bords chacun détecte ; combiner les deux par (écrêtée) pour un détecteur omnidirectionnel.
- () Le filtre médian (chaque pixel remplacé par la médiane de ses voisins — chapitre 4) : l'implémenter et comparer au moyenneur sur une image bruitée de « poivre et sel » (pixels aléatoires à ou ) — lequel préserve les contours ?
- () Mesurer le temps de
convolutionsur des images de tailles croissantes et vérifier la linéarité en ; estimer le temps pour une image . - () Implémenter le flou par fenêtre glissante en indépendant de (exercice résolu 9, version sommes glissantes) et vérifier l'égalité avec la version directe, hors bords et à une unité près (divisions entières obligent).
D. Études
- () Générer et afficher (
imshow) : un dégradé horizontal, un damier , des cercles concentriques ( modulo , replié) — trois images de test pour tous les filtres du chapitre. - ( ) Le négatif, le miroir et la rotation commutent-ils avec le flou ? Pour chaque paire, tester l'égalité des deux ordres sur images aléatoires, et expliquer les écarts observés (rôle des bords).
- () Compression naïve par blocs : remplacer chaque bloc par sa moyenne, mesurer l'erreur (écart moyen par pixel) et le facteur de compression ; tracer l'erreur en fonction de la taille de bloc — le compromis taille-fidélité, ancêtre conceptuel de JPEG.
- ( ) Étude complète « chaîne de traitement » : charger une image (ou la générer), corriger le contraste, débruiter (médian), détecter les contours (Sobel combiné), seuiller, et produire la planche des cinq étapes côte à côte — avec le banc de validation de l'exercice résolu 10 rejoué après chaque fonction. Rédiger le rapport selon les six compétences du programme.