EX 01

Passage en niveaux de gris

Transformer une image couleur (triplets RGB) en matrice d'entiers [0..255]

Théorie

Une image couleur est une matrice de triplets [R, G, B] où chaque composante est dans [0..255]. Convertir en niveaux de gris consiste à remplacer chaque triplet par un seul entier représentant la luminosité.

  • Q1 — Moyenne simple : les 3 canaux ont le même poids → g = (R + G + B) // 3
  • Q2 — Luminance pondérée : l'œil humain est bien plus sensible au vert qu'au bleu → g = int(0.2126·R + 0.7152·G + 0.0722·B)
g = 0.2126·R + 0.7152·G + 0.0722·B  ← Standard ITU-R BT.709

La formule Q2 donne un résultat visuellement plus fidèle à ce que l'œil perçoit réellement comme "luminosité".

Syntaxe Python utile
Déballage de tuple dans une compréhension
for (r, g, b) in row:
    ...
Compréhension imbriquée (ligne × pixel)
[[expr for pix in row]
 for row in img]
Division entière (floor)
(r + g + b) // 3
Conversion float → int
int(0.2126*r + 0.7152*g
    + 0.0722*b)
Correction
Q1 — Moyenne simple
grayscale.py
def to_grayscale(img):
    """
    Convertit une image couleur en niveaux de gris (moyenne simple).
    Paramètre : img — matrice de triplets [R, G, B]
    Retour    : matrice d'entiers dans [0..255]
    """
    # Pour chaque ligne, pour chaque pixel (r,g,b), calculer la moyenne
    return [[(r + g + b) // 3
              for (r, g, b) in row]
             for row in img]

# ── Test ──────────────────────────────────────
img = [[[120, 60, 30], [255, 255, 255]]]
print(to_grayscale(img))  # [[70, 255]]
Q2 — Luminance pondérée
grayscale.py
def to_grayscale2(img):
    """
    Convertit une image couleur en niveaux de gris (luminance pondérée).
    Formule : g = int(0.2126*R + 0.7152*G + 0.0722*B)
    Paramètre : img — matrice de triplets [R, G, B]
    Retour    : matrice d'entiers dans [0..255]
    """
    # Coefficients ITU-R BT.709 — standard de la télévision haute définition
    return [[int(0.2126*r + 0.7152*g + 0.0722*b)
              for (r, g, b) in row]
             for row in img]
Q2 produit un résultat visuellement meilleur — un pixel vert pur restera plus lumineux, un pixel bleu pur apparaîtra plus sombre, ce qui correspond à ce que l'œil perçoit réellement.
Tester sur une vraie image

Les fonctions attendent une liste de listes Python. Une image réelle se charge via matplotlib ou PIL — il faut la convertir en liste, appliquer la fonction, puis afficher le résultat.

Le pipeline complet est le suivant :

  • Charger l'image → obtenir un tableau NumPy de forme (h, w, 3)
  • Convertir en liste Python avec .tolist()
  • Appliquer to_grayscale() ou to_grayscale2()
  • Afficher le résultat avec imshow(..., cmap='gray')
Pipeline complet — matplotlib
test_grayscale.py
import matplotlib.pyplot as plt
import matplotlib.image as mpimg

# ── 1. Charger l'image (tableau NumPy de forme h×w×3) ──────────────
img_np = mpimg.imread('mon_image.jpg')

# ── 2. Convertir en liste Python (liste de listes de triplets) ──────
img = img_np.tolist()

# ── 3. Appliquer les deux fonctions ────────────────────────────────
gray1 = to_grayscale(img)   # moyenne simple
gray2 = to_grayscale2(img)  # luminance pondérée

# ── 4. Afficher les trois images côte à côte ───────────────────────
fig, axes = plt.subplots(1, 3, figsize=(15, 5))

axes[0].imshow(img_np)
axes[0].set_title('Originale (RGB)')
axes[0].axis('off')

axes[1].imshow(gray1, cmap='gray')
axes[1].set_title('Moyenne simple')
axes[1].axis('off')

axes[2].imshow(gray2, cmap='gray')
axes[2].set_title('Luminance (Q2)')
axes[2].axis('off')

plt.tight_layout()
plt.show()
Si l'image est un JPEG, les valeurs sont des entiers dans [0..255]. Si c'est un PNG, elles peuvent être des flottants dans [0.0..1.0] — multiplier par 255 dans ce cas : int(val * 255) dans les fonctions, ou faire (img_np * 255).astype(int).tolist() avant de convertir.
Alternative — PIL / Pillow
test_pil.py
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt

# Charger et convertir en liste Python
img = list([list(row) for row in np.array(Image.open('mon_image.jpg'))])

# Appliquer et afficher
gray = to_grayscale2(img)
plt.imshow(gray, cmap='gray')
plt.axis('off')
plt.show()
EX 02

Filtrage des couleurs

Isoler un seul canal RGB, mettre les deux autres à zéro

Théorie

Une image RGB stocke 3 canaux indépendants par pixel. Filtrer une couleur signifie conserver uniquement un canal et forcer les deux autres à 0.

  • Filtre rouge : (120, 80, 200) → (120, 0, 0)
  • Filtre vert : (120, 80, 200) → (0, 80, 0)
  • Filtre bleu : (120, 80, 200) → (0, 0, 200)

La version générique utilise un index canal ∈ {0, 1, 2} pour sélectionner dynamiquement quel canal conserver.

Syntaxe Python utile
Reconstruire un triplet avec un seul canal
[r, 0, 0]   # rouge
[0, g, 0]   # vert
[0, 0, b]   # bleu
Version générique avec ternaire
[p[canal] if i==canal
 else 0
 for i in range(3)]
Correction
Q1 — Trois filtres explicites
filtres.py
def rouge(img):
    """Filtre uniquement le canal rouge. G=0, B=0."""
    return [[[r, 0, 0] for [r, g, b] in row] for row in img]

def vert(img):
    """Filtre uniquement le canal vert. R=0, B=0."""
    return [[[0, g, 0] for [r, g, b] in row] for row in img]

def bleu(img):
    """Filtre uniquement le canal bleu. R=0, G=0."""
    return [[[0, 0, b] for [r, g, b] in row] for row in img]

# Test : rouge([[255, 100, 50]]) → [[255, 0, 0]]
Bonus — Filtre générique
filtres.py
def filtre(img, canal):
    """
    Filtre générique : conserve le canal d'index `canal`.
    canal : 0 = rouge, 1 = vert, 2 = bleu
    """
    # Pour chaque pixel p, garder p[canal] à sa position, 0 ailleurs
    return [[[p[canal] if i == canal else 0
               for i in range(3)]
              for p in row]
             for row in img]

# rouge = filtre(img, 0)  |  vert = filtre(img, 1)  |  bleu = filtre(img, 2)
EX 03

Réorganisation des pixels

Une fonction générique pilote toutes les transformations géométriques

Théorie

Toutes les transformations géométriques partagent la même idée fondamentale : d'où vient le pixel (i, j) du résultat dans l'image source ?

  • Symétrie horizontale (flip vertical) : ligne i ← ligne n-1-i, colonnes inchangées
  • Symétrie verticale (flip horizontal) : colonne j ← colonne m-1-j, lignes inchangées
  • Rotation 90° horaire : résultat de dimensions m×n. Pixel (i,j) du résultat ← pixel (n-1-j, i) de la source
  • Rognage : pixel (i,j) du résultat ← pixel (i0+i, j0+j) de la source

cree_image(img, n, m, f) abstrait tout cela : on passe une fonction de correspondance f(i,j) → (i', j') et elle construit la nouvelle image.

résultat[i][j] = source[ f(i,j)[0] ][ f(i,j)[1] ]
Syntaxe Python utile
Lambda — fonction anonyme
f = lambda i, j: (n-1-i, j)
Accéder au résultat de f(i,j)
img[f(i,j)[0]][f(i,j)[1]]
Lever une exception
raise ValueError("message")
Dimensions d'une image
n = len(img)
m = len(img[0])
Correction
Q1 — cree_image (fonction de base)
geometrie.py
def cree_image(img, n, m, f):
    """
    Crée une nouvelle image n×m en réorganisant les pixels de img.
    f : (i,j) → (i',j') indique d'où vient chaque pixel du résultat.
    """
    return [[img[f(i,j)[0]][f(i,j)[1]]
              for j in range(m)]
             for i in range(n)]
Q2 — Symétrie horizontale
geometrie.py
def symetrie_horizontale(img):
    """Retourne l'image verticalement (haut ↔ bas)."""
    n, m = len(img), len(img[0])
    # Ligne i du résultat ← ligne n-1-i de la source
    return cree_image(img, n, m, lambda i, j: (n-1-i, j))
Q3 — Symétrie verticale
geometrie.py
def symetrie_verticale(img):
    """Retourne l'image horizontalement (gauche ↔ droite)."""
    n, m = len(img), len(img[0])
    # Colonne j du résultat ← colonne m-1-j de la source
    return cree_image(img, n, m, lambda i, j: (i, m-1-j))
Q4 — Rotation 90° sens horaire
geometrie.py
def rotation(img):
    """Rotation 90° sens horaire. Dimensions résultat : m×n."""
    n, m = len(img), len(img[0])
    # Résultat : m lignes × n colonnes (dimensions inversées !)
    # Pixel (i,j) du résultat ← pixel (n-1-j, i) de la source
    return cree_image(img, m, n, lambda i, j: (n-1-j, i))
Les dimensions du résultat sont inversées : une image n×m devient m×n. Ne pas oublier de passer m, n (et non n, m) à cree_image — c'est l'erreur classique sur cette question.

Q5 — Rognage
geometrie.py
def rognage(img, i0, j0, nbl, nbc):
    """
    Extrait une sous-image de dimensions nbl×nbc.
    Hypothèses : i0 >= 0, j0 >= 0, nbl > 0, nbc > 0
                 i0 + nbl <= len(img), j0 + nbc <= len(img[0])
    """
    # Vérification des hypothèses — lever une exception si violation
    if i0 < 0 or j0 < 0 or nbl <= 0 or nbc <= 0:
        raise ValueError("Paramètres invalides")
    if i0 + nbl > len(img) or j0 + nbc > len(img[0]):
        raise ValueError("Dépassement des bords de l'image")
    # Pixel (i,j) du résultat ← pixel (i0+i, j0+j) de la source
    return cree_image(img, nbl, nbc, lambda i, j: (i0+i, j0+j))
EX 04

Transformation du photomaton

Disperse les pixels en 4 copies réduites arrangées en grille 2×2

Théorie

Le photomaton prend une image h×l (h et l pairs) et produit une image de même taille contenant 4 copies réduites de moitié, disposées en grille 2×2.

Le principe : les pixels aux positions paires/impaires (lignes et colonnes) sont séparés dans les 4 quadrants.

  • Source (2i, 2j) → résultat haut-gauche (i, j)
  • Source (2i, 2j+1) → résultat haut-droite (i, j + l//2)
  • Source (2i+1, 2j) → résultat bas-gauche (i + h//2, j)
  • Source (2i+1, 2j+1) → résultat bas-droite (i + h//2, j + l//2)

Appliquée de façon répétée, la transformation finit par revenir à l'image originale — c'est une permutation réversible de pixels, ce qui la rapproche conceptuellement d'un chiffrement par transposition.

Syntaxe Python utile
Créer une matrice vide h×l
result = [[None] * l
           for _ in range(h)]
Tester la parité
h % 2 == 0   # True si pair
Division entière pour les quadrants
h // 2  # nb de lignes/quadrant
l // 2  # nb de colonnes/quadrant
Boucle sur les indices des quadrants
for i in range(h//2):
  for j in range(l//2):
    ...
Correction
Q1 — Photomaton
photomaton.py
def photomaton(img):
    """
    Calcule la transformation du photomaton de img.
    Hypothèse : len(img) et len(img[0]) doivent être pairs.
    Retour : nouvelle matrice de même taille que img.
    """
    h = len(img)
    l = len(img[0])

    # Vérifier que les dimensions sont bien paires
    if h % 2 != 0 or l % 2 != 0:
        raise ValueError("Les dimensions doivent être paires")

    # Créer une matrice résultat vide de même taille
    result = [[None] * l for _ in range(h)]

    # Remplir les 4 quadrants simultanément
    for i in range(h // 2):
        for j in range(l // 2):
            result[i][j]                   = img[2*i  ][2*j  ]  # haut-gauche
            result[i][j + l//2]           = img[2*i  ][2*j+1]  # haut-droite
            result[i + h//2][j]           = img[2*i+1][2*j  ]  # bas-gauche
            result[i + h//2][j + l//2]   = img[2*i+1][2*j+1]  # bas-droite

    return result

# Test minimal (image 4×4 pour voir l'effet)
# Appliquer photomaton() plusieurs fois ramène à l'image originale.
Le photomaton est sa propre inverse après un certain nombre d'itérations — appliqué répétitivement, il finit par revenir à l'image originale. C'est pour ça qu'il est lié conceptuellement à la stéganographie et aux chiffrements réversibles par transposition.