Adloun

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

Définition 8.1Tableau bidimensionnel

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.

AttentionLe piège de la création

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.

Exemple 8.2Premières fonctions sur matrices

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

Définition 8.3Image en niveaux de gris

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.)

Exemple 8.4Négatif et seuillage

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.

iRemarque

Pour expérimenter sur de vraies images, plt.imshow(img, cmap=&quot;gray&quot;) (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

Exemple 8.5Symétrie horizontale (miroir)

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é).

Exemple 8.6Rotation d'un quart de tour

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

Exemple 8.7Réduction par moyenne de blocs

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).

Exemple 8.8Agrandissement par réplication

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

Définition 8.9Convolution par un noyau

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.

Important

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

Exemple 8.10Flou, contours, netteté

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)

Exercice 1 : Le piège de la création, vu de près

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 ? ».)*

Exercice 2 : Statistiques par ligne et par colonne

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.)

Exercice 3 : Éclaircir sans déborder

É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)

Exercice 4 : La transposée et les quatre rotations

É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 .)

Exercice 5 : Chercher un motif dans une image

É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.)

Exercice 6 : Lire un noyau, prédire son effet

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.)

Exercice 7 : L'histogramme d'une image et l'amélioration du contraste

É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)

Exercice 8 : Le flou répété, expérience et conjecture

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.)

Exercice 9 : Convolution séparable — économiser un facteur

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.)

Exercice 10 : Valider une bibliothèque d'images

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.)

Synthèse du chapitre (à retenir)
  • 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

B. Transformations

C. Convolutions et filtres

D. Études

Continuer sur Adloun : animation, QCM, fiches, exercices