Aller au contenu

TP — Images et matrices de pixels

Travaux pratiques · MPSI (tronc commun) · 2021-2022 · Python

Cours : Boucles imbriquées et dimensionnalité

Dans ce TP, on va manipuler des images sous forme de matrices de pixels. Pour cela, on commence par définir des fonctions qui nous seront utiles dans la suite. Évaluez la cellule suivante !

Noir et blanc

Une image en noir et blanc est une matrice de 0 ou de 1. C'est-à-dire un tableau dont les éléments sont des tableaux de 0 et de 1 représentant chaque ligne de l'image, de haut en bas.

Ainsi pour faire un échiquier, on peut écrire :

def echiquier(w,h):
    m = []
    for j in range(h):
        l = [] # la jeme ligne
        for i in range(w):
            l.append( (i+j)%2 )
        m.append(l)
    return m
affiche_image_nb(echiquier(10,20))

Échiquier 10 × 20

affiche_image_nb(racoon_nb)

Raton laveur en noir et blanc

Les deux fonctions suivantes sont pratiques. taille renvoie la taille (largeur, hauteur) d'une image et image_nb_vide renvoie une image avec uniquement des 0.

def taille(m):
    return (len(m[0]), len(m))
def image_nb_vide(w,h):
    m = []
    for j in range(h):
        l = []
        for i in range(w):
            l.append(0)
        m.append(l)
    return m

On peut alors réécrire la fonction précédente de manière plus naturelle.

Attention pour accéder au pixel de coordonnées \((i,j)\) comme il est sur la \(j\)-ème ligne et la \(i\)-ème colonne, il faut écrire m[j][i].

m = image_nb_vide(20,10)
x1, x2, y = 3, 8, 5
for x in range(x1, x2+1):
    m[y][x] = 1
affiche_image_nb(m)

Segment horizontal

>>> list(range(3,8))
[3, 4, 5, 6, 7]

Les résultats attendus pour les trois fonctions suivantes sont

Résultat attendu pour ligne_horiz Résultat attendu pour ligne_vert Résultat attendu pour rectangle

Question 1

def ligne_horiz(m, x1, x2, y):
    '''Trace une ligne horizontale de pixels à 1
    allant de (x1,y) à (x2,y)'''
Corrigé
def ligne_horiz(m, x1, x2, y):
    '''Trace une ligne horizontale de pixels à 1
    allant de (x1,y) à (x2,y)'''
    for x in range(x1, x2+1):
        m[y][x] = 1

Tests :

m = image_nb_vide(15,10)
ligne_horiz(m, 3, 12, 3)
ligne_horiz(m, 5, 10, 6)
affiche_image_nb(m)
m
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]

Question 2

def ligne_vert(m, x, y1, y2):
    '''Trace une ligne verticale de pixels à 1
    allant de (x,y1) à (x,y2)'''
Corrigé
def ligne_vert(m, x, y1, y2):
    '''Trace une ligne verticale de pixels à 1
    allant de (x,y1) à (x,y2)'''
    for y in range(y1, y2+1):
        m[y][x] = 1

Tests :

m = image_nb_vide(15,10)
ligne_vert(m, 3, 2, 8)
affiche_image_nb(m)

Question 3

def rectangle(m, x1, y1, x2, y2):
    '''Trace un rectangle de (x1,y1) à (x2,y2)'''
Corrigé
def rectangle(m, x1, y1, x2, y2):
    '''Trace un rectangle de (x1,y1) à (x2,y2)'''
    ligne_horiz(m, x1, x2, y1)
    ligne_horiz(m, x1, x2, y2)
    ligne_vert(m, x1, y1, y2)
    ligne_vert(m, x2, y1, y2)

Tests :

m = image_nb_vide(15,10)
rectangle(m, 3, 2, 12, 8)
affiche_image_nb(m)

Pour la prochaine fonction vous devez créer une image de dimension \((2n+1, 2n+1)\) présentant des carrés concentriques comme ici

Carrés concentriques

On va commencer par faire une fonction qui étant donné une image de dimension \((2n+1) \times (2n+1)\) et un entier \(i \le n\) va afficher le carré centré dont le coin supérieur gauche est \((i, i)\).

Question 4

def carres_concentriques(n):
    m = image_nb_vide(2*n+1,2*n+1)
    return m
Corrigé
def carres_concentriques(n):
    m = image_nb_vide(2*n+1,2*n+1)
    for i in range(n+1):
        if i % 2 == 1:
            rectangle(m, i, i, 2*n-i, 2*n-i)
    return m

Tests :

affiche_image_nb(carres_concentriques(20))

On vous demande maintenant de faire une fonction qui inverse une image en changeant tous les 0 en 1 et tous les 1 en 0.

ATTENTION Il faudra renvoyer une nouvelle image et non modifier l'image passée en entrée. C'est important : il en va de la survie des ratons-laveurs !

>>> m = carres_concentriques(5)
>>> m
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
 [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0],
 [0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0],
 [0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0],
 [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0],
 [0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0],
 [0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0],
 [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0],
 [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]

Question 5

def inverse_nb(m):
    w, h = taille(m)
    m2 = image_nb_vide(w, h)
    return m2
Corrigé
def inverse_nb(m):
    w, h = taille(m)
    m2 = image_nb_vide(w, h)
    for i in range(w):
        for j in range(h):
            m2[j][i] = 1  - m[j][i]
    return m2

Tests :

affiche_image_nb(inverse_nb(carres_concentriques(10)))
affiche_image_nb(inverse_nb(racoon_nb))

Carrés concentriques inversés

On va maintenant écrire des fonctions opérant des symétries et des rotations d'image. Les résultats attendus sont

Symétrie verticale

Symétrie horizontale

Rotation d'un quart de tour

Question 6

def sym_vert(m):
    '''Effectue une symétrie verticale de l'image, c'est-à-dire que le haut devient le bas
    et réciproquement'''
    w, h = taille(m)
    m2 = image_nb_vide(w, h)
    return m2
Corrigé
def sym_vert(m):
    '''Effectue une symétrie verticale de l'image, c'est-à-dire que le haut devient le bas
    et réciproquement'''
    w, h = taille(m)
    m2 = image_nb_vide(w, h)
    for i in range(w):
        for j in range(h):
            m2[h-j-1][i] = m[j][i]
    return m2

Tests :

affiche_image_nb(sym_vert(racoon_nb))

Question 7

def sym_horiz(m):
    '''Effectue une symétrie horizontale de l'image, c'est-à-dire que la gauche devient la droite
    et réciproquement'''
    w, h = taille(m)
    m2 = image_nb_vide(w, h)
    return m2
Corrigé
def sym_horiz(m):
    '''Effectue une symétrie horizontale de l'image, c'est-à-dire que la gauche devient la droite
    et réciproquement'''
    w, h = taille(m)
    m2 = image_nb_vide(w, h)
    for i in range(w):
        for j in range(h):
            m2[j][w-i-1] = m[j][i]
    return m2

Tests :

affiche_image_nb(sym_horiz(racoon_nb))

Question 8

def rot90(m):
    '''Effectue une rotation de l'image d'un quart de tour dans le sens trigonométrique'''
    w, h = taille(m)
    m2 = image_nb_vide(h, w)
    return sym_vert(m2)
Corrigé
def rot90(m):
    '''Effectue une rotation de l'image d'un quart de tour dans le sens trigonométrique'''
    w, h = taille(m)
    m2 = image_nb_vide(h, w)
    for i in range(w):
        for j in range(h):
            m2[i][j] = m[j][i]
    return sym_vert(m2)

Tests :

affiche_image_nb(rot90(racoon_nb))

Niveaux de gris

Les images en niveaux de gris sont des matrices d'entiers où la couleur va de 0 (noir) à 255 (blanc). Ainsi 128 sera un gris médian.

def degradé(w,h):
    m = []
    for j in range(h):
        l = []
        for i in range(w):
            l.append( (i+j)%256 )
        m.append(l)
    return m
affiche_image(degradé(200,200))

Dégradé de gris

affiche_image(racoon_g)

Raton laveur en niveaux de gris

Normalement, si vous avez fait ce qu'il faut vos fonctions de symétries et de rotations continuent de fonctionner. On pourra même directement utiliser image_nb_vide car 0 est noir ici aussi.

affiche_image(rot90(racoon_g))

Rotation de l'image en niveaux de gris

Résultat attendu pour la fonction suivante :

Niveaux de gris inversés

Question 9

def inverse_g(m):
    '''Inverse le niveau de gris.'''
    w, h = taille(m)
    m2 = image_nb_vide(w, h)
    return m2
Corrigé
def inverse_g(m):
    '''Inverse le niveau de gris.'''
    w, h = taille(m)
    m2 = image_nb_vide(w, h)
    for i in range(w):
        for j in range(h):
            m2[j][i] = 255 - m[j][i]
    return m2

Le corrigé d'origine calculait 256 - x, qui envoie le noir (0) sur 256, hors de l'intervalle des niveaux de gris \([0, 255]\).

Tests :

affiche_image(inverse_g(racoon_g))

Pour faire un effet de flou, on peut remplacer chaque valeur par la moyenne des 9 valeurs constituées par le pixel et ses 8 voisins immédiats dans la grille, en ignorant les bords. On vous demande de faire cette fonction. Le résultat attendu est

Image floutée

On pourra laisser ainsi les bords en noir.

Question 10

def flou(m):
    w, h = taille(m)
    m2 = image_nb_vide(w, h)
    return m2
Corrigé
def flou(m):
    w, h = taille(m)
    m2 = image_nb_vide(w, h)
    for i in range(1, w-1):
        for j in range(1, h-1):
            s = 0
            for di in range(i-1,i+2):
                for dj in range(j-1,j+2):
                    s += m[dj][di]
            m2[j][i] = s // 9
    return m2

Tests :

affiche_image(flou(racoon_g))

Pour la fonction suivante, on suppose que l'image a des dimensions toutes paires \((2p, 2q)\), on peut donc regrouper les pixels en carrés 2x2 ainsi :

a11 b11 a12 b12 ... a1p b1p
c11 d11 c12 d12 ... c1p d1p
...
aq1 bq1 ...         aqp bqp
cq1 dq1 ...         cqp dqp

et on vous demande de les réorganiser ainsi :

a11 a12 ... a1p b11 b12 ... b1p
...
aq1 aq2 ... aqp bq1 bq2 ... bqp
c11 c12 ... c1p d11 d12 ... d1p
...
cq1 cq2 ... cqp dq1 dq2 ... dqp

En quelque sorte, on veut l'image des pixels en haut à gauche, l'image des pixels en haut à droite, ...

Le résultat attendu ici est

Résultat de split4

Question 11

def split4(m):
    w, h = taille(m)
    m2 = image_nb_vide(w, h)
    return m2
Corrigé
def split4(m):
    w, h = taille(m)
    m2 = image_nb_vide(w, h)
    p = w//2
    q = h//2
    for i in range(w):
        for j in range(h):
            mi = i % 2
            mj = j % 2
            m2[j//2 + mj * q][i//2 + mi * p] = m[j][i]
    return m2

Tests :

affiche_image(split4(racoon_carre))

Dans la cellule suivante on effectue une boucle d'affichage en itérant la fonction précédente sur racoon_carre : racoon_carre, split4(racoon_carre), split4(split4(racoon_carre)), ...

Vous pouvez expliquer ?

m = racoon_carre
for i in range(10):
    # Enlevez le commentaire suivant, il empeche de sauvegarder le notebook...
    # affiche_image(m)
    m = split4(m)

Images en couleur

On peut également faire des images en couleurs en rajoutant une dimension : chaque pixel n'est plus un entier, mais une liste ou un tuple à trois éléments [r,g,b] donnant les quantités, de 0 à 255, de chaque composante rouge, verte ou bleue.

def degradé_couleur(w,h):
    m = []
    for j in range(h):
        l = []
        for i in range(w):
            l.append( [ i%256, (256-j)%256, (i+j)%256 ] )
        m.append(l)
    return m
affiche_image(degradé_couleur(300,200))

Dégradé en couleur

On donne aussi une image pour faire les tests appelée racoon.

affiche_image(racoon)

Raton laveur en couleur

Reprendre les fonctions précédentes dans le cas de la couleur !

Bonus

Allez chercher la définition de la fractale de Mandelbrot et faites un calcul de cette fractale dans notre format d'image.