Un corrigé pédagogique pour comprendre et justifier vos réponses. Les conseils de rédaction ne constituent pas un barème officiel détaillé.
Application
Compression RLE des images en niveaux de gris
Une image en niveaux de gris associe à chaque pixel un entier entre 0 (noir) et 255 (blanc). On lit les pixels ligne par ligne ; la largeur suffit à retrouver les lignes. La figure 1 contient quatre lignes de cinq pixels :
| Colonne 0 | Colonne 1 | Colonne 2 | Colonne 3 | Colonne 4 |
|---|---|---|---|---|
| 0 | 128 | 128 | 255 | 64 |
| 255 | 128 | 128 | 255 | 255 |
| 128 | 128 | 0 | 0 | 64 |
| 255 | 0 | 0 | 0 | 0 |
Le codage RLE remplace chaque suite de valeurs identiques par le couple (nombre de répétitions, valeur), puis aplatit les couples. Par exemple, [4,4,4,0,5,5] devient [3,4,1,0,2,5]. Les aplats d’un dessin peuvent ainsi être compressés. Le décodage doit restituer exactement chaque pixel, dans le même ordre. La bibliothèque Pillow est requise pour les manipulations d’images.
Le dossier comporte le sujet et les fichiers listés dans les téléchargements. Les codes de départ sont reproduits ci-dessous. Travaillez sur une copie. Les appels professeur signalés dans les questions servent à présenter votre démarche ; vous pouvez aussi demander de l’aide en cas de difficulté.
Code fourni : rle.py
from PIL import Image
def codage_rle(liste_octets):
"""Renvoie une liste d'octets obtenue par compression RLE"""
liste_rle = []
i = 0
while i < len(liste_octets):
valeur = liste_octets[i]
occ = 1
while i + occ < len(liste_octets) and liste_octets[i + occ] == valeur:
occ += 1
liste_rle.append(occ)
liste_rle.append(valeur)
i += occ
return liste_rle
def decodage_rle(liste_rle):
"""Renvoie la liste d'octets obtenue à partir de la liste liste_rle obtenue
par compression RLE"""
pass # A VOUS D'ÉCRIRE LE CODE LA FONCTION
def test_codage():
assert codage_rle([255, 255, 0, 255, 255, 255]) == [2, 255, 1, 0, 3, 255]
assert decodage_rle([2, 255, 1, 0, 3, 255]) == [255, 255, 0, 255, 255, 255]
def enregistrer_octets(nom_fichier, liste_octets):
"""Enregistre une liste de valeurs numériques entre 0 et 255 dans un
le fichier nom_fichier. Si une valeur est plus grande que 255 on considère
que c'est 255. De même pour les valeur plus petite que 0."""
# Le fichier est ouvert en mode binaire pour pouvoir écrire sans restriction toute valeur
# entre 0 et 255
with open(nom_fichier, "wb") as fichier:
# Pour convertir une liste de valeur entre 0 et 255 en une liste d'octets qui peuvent
# être écrits dans le fichier, on utilise `bytes`
fichier.write(bytes([max(0, min(255, b)) for b in liste_octets]))
def charger_octets(nom_fichier):
"""Renvoie la liste des octets présents dans le fichier nom_fichier"""
with open(nom_fichier, "rb") as fichier:
liste_octets = list(fichier.read())
return liste_octets
def enregistrer_image(nom_image, largeur, liste_niveaux):
"""Enregistre un fichier image nom_image de la largeur donnée et dont les
valeurs de niveaux de gris des pixels sont celles de la liste
liste_niveaux"""
hauteur = len(liste_niveaux) // largeur
im = Image.frombytes("L", (largeur, hauteur), bytes(liste_niveaux))
im.save(nom_image)
def charger_image(nom_image):
"""Étant donné une image nom_image, renvoie un couple (largeur, liste_niveaux) où
largeur est la largeur de l'image et liste_niveaux est la liste des valeurs de niveaux
de gris de l'image ligne par ligne"""
image = Image.open(nom_image).convert("L")
return (image.width, list(image.tobytes()))
#############################################################################
# Fonction nécessaire pour les tests de la question 3 #
#############################################################################
def encoder_decoder_image(nom_image):
"""Fonction de test permettant d'encoder puis décoder une image avec un
codage RLE. Le fichier rle est nommé nom_image.rle et le fichier decodé
est nom_image.dec.png"""
largeur, niveaux = charger_image(nom_image)
enregistrer_octets(nom_image + ".rle", codage_rle(niveaux))
niveaux_dec = charger_octets(nom_image + ".rle")
enregistrer_image(nom_image + ".dec.png", largeur,
decodage_rle(niveaux_dec))
Question 1
#Déterminer si la liste obtenue par codage RLE est forcément de longueur inférieure ou égale à la liste de départ.
Indice
Une image qui alterne noir et blanc comporte-t-elle de longues répétitions ?
Comprendre la correction
Non. La liste [0,255] devient [1,0,1,255] : deux valeurs deviennent quatre. Pour n pixels répartis en r suites maximales, le codage occupe 2 r valeurs. Il raccourcit la liste si 2 r < n, soit une longueur moyenne des suites supérieure à 2. Dans le pire cas, chaque pixel diffère du précédent et la taille double. Il faut donc examiner le contenu, pas seulement la taille de l’image.
Question 2
#En étudiant bien la fonction codage_rle qui réalise le codage, écrire le corps de la fonction decodage_rle qui réalise le décodage d’une liste. Des tests sont fournis dans la fonction test_codage, on pourra les compléter.
PythonRedonner tous ses pixels à l’imageÉcrivez votre solution et mettez-la à l’épreuve
Écrivez decodage_rle(liste_rle). Chaque couple aplati contient un nombre de répétitions puis une intensité. La liste est valide et peut être vide. Renvoyez une nouvelle liste de pixels sans modifier l’entrée. Ici, nous testons le décodage en mémoire, sans Pillow ni fichier.
def decodage_rle(liste_rle):
# À vous de jouer
pass
Les cas de test proposés :
- Un aplat gris : Le premier élément est une quantité, le second une intensité.
- Trois aplats successifs : Le changement de couleur ne doit ni perdre ni ajouter un pixel.
- Liste vide : Aucun couple signifie aucun pixel : une nouvelle liste vide est attendue.
- Noir et blanc alternés : Une intensité nulle reste un pixel ; elle ne doit pas être traitée comme une quantité.
- Deux couples de même couleur : Le découpage des longues suites peut produire des couples voisins identiques.
- Préserver la liste encodée : Le décodage construit le résultat sans détruire le code RLE.
Indice 1
Parcourir la liste avec un pas de 2.
Indice 2
Un couple représente plusieurs pixels, pas un seul.
Comprendre la correction
def decodage_rle(liste_rle):
resultat = []
for i in range(0, len(liste_rle), 2):
compte = liste_rle[i]
valeur = liste_rle[i + 1]
for _ in range(compte):
resultat.append(valeur)
return resultatLes indices 0,2,4… désignent les comptes. La valeur associée se situe juste après. La boucle intérieure recopie cette valeur autant de fois qu’indiqué. La liste vide produit naturellement une liste vide. On suppose ici une liste RLE valide, de longueur paire, comme le prévoit le contrat. Appeler test_codage(), puis vérifier plusieurs cas :
assert decodage_rle([]) == []
assert decodage_rle([1, 0]) == [0]
assert decodage_rle([3, 255, 2, 0]) == [255, 255, 255, 0, 0]
for image in ([], [0], [4, 4, 4, 0, 5, 5], [0, 255] * 10):
assert decodage_rle(codage_rle(image)) == imageSuivons le décodage de [3, 4, 1, 0, 2, 5]. Au départ, le résultat est vide. Le premier couple ajoute trois fois 4, le deuxième ajoute une fois 0, le troisième ajoute deux fois 5. Après chaque couple, le résultat contient exactement les pixels décrits par les couples déjà lus. C’est cette propriété qui permet d’expliquer pourquoi la concaténation finale restitue toute l’image.
Une assertion d’aller-retour doit porter sur la liste entière. Comparer seulement les longueurs laisserait passer une image dont les couleurs ont été changées ; comparer seulement la première valeur laisserait passer un décalage des lignes. L’égalité de listes contrôle longueur, ordre et valeurs. Elle n’explique toutefois pas seule pourquoi le programme convient à toute image : il faut aussi justifier la lecture par couples.
Question 3
#Pour tester le codage sur une image, utiliser la fonction fournie encoder_decoder_image sur les images bac_nsi_32.png et bac_nsi_256.png et observer la différence de comportement.
Indice
Lire la transformation max(0, min(255, b)) dans enregistrer_octets.
Comprendre la correction
encoder_decoder_image("bac_nsi_32.png")
encoder_decoder_image("bac_nsi_256.png")| Fichier réel | Pixels initiaux | Plus longue suite | Pixels décodés après écrêtage |
|---|---|---|---|
bac_nsi_32.png | 1024 | 131 | 1024 |
bac_nsi_256.png | 65536 | 5842 | 51494 |
La petite image est restituée sans perte. La grande est corrompue : enregistrer_octets remplace toute valeur supérieure à 255 par 255, y compris les comptes RLE. Des répétitions sont donc perdues. Le tableau ci-dessus provient des fichiers fournis : ce défaut se produit lors de l’écriture des octets, alors que l’encodage et le décodage en mémoire peuvent réussir. enregistrer_image calcule ensuite la hauteur par division entière, qui ne répare pas la perte de pixels. L’affichage exact peut dépendre de Pillow ; la perte de données, elle, se démontre par ces longueurs.
Question 4
#Le problème précédent est lié au fait que sur des grandes images, il est possible d’avoir plus de 255 pixels de la même couleur. Proposer une démarche de résolution de ce problème qui modifie les fonctions d’encodage et de décodage, puis l’implémenter.
PythonFaire tenir 600 pixels identiques dans des octetsÉcrivez votre solution et mettez-la à l’épreuve
Modifiez codage_rle : les intensités sont comprises entre 0 et 255 et chaque compteur doit tenir dans un octet, donc entre 1 et 255. Découpez chaque suite en blocs de 255 au maximum, sans perdre de pixels. Le décodeur précédent reste compatible. Cet atelier teste la stratégie par découpage, sans écriture de fichier.
def codage_rle(liste_octets):
# À vous de jouer
pass
Les cas de test proposés :
- 600 pixels noirs : 255 + 255 + 90 restitue les 600 pixels tout en respectant la capacité de chaque octet.
- Exactement 255 : La borne 255 est autorisée ; aucun bloc vide ne doit suivre.
- Un pixel au-delà de la borne : Le 256e pixel doit démarrer un nouveau couple, même sans changement de couleur.
- Changement après une longue suite : Le curseur avance du nombre réellement encodé, puis respecte le changement de couleur.
- Vide et pixels isolés : Le format reste le même sur les petits cas.
- Sans modification de l’image : Deux blocs pleins suffisent, sans altérer l’image source.
Indice
Plusieurs couples successifs peuvent-ils désigner la même couleur ?
Comprendre la correction
Une solution simple conserve le format de couples mais découpe chaque suite en blocs d’au plus 255 pixels. Une suite de 600 zéros devient [255,0,255,0,90,0]. Le décodeur de la question 2 accepte déjà des couples consécutifs de même valeur : il reste compatible et n’a pas besoin d’une modification algorithmique. Il suffit donc de remplacer l’encodeur ; cette absence de changement du décodage doit être explicitement justifiée, même si l’énoncé invite à modifier les deux fonctions.
def codage_rle(liste_octets):
resultat = []
i = 0
while i < len(liste_octets):
valeur = liste_octets[i]
compte = 1
while (i + compte < len(liste_octets)
and liste_octets[i + compte] == valeur
and compte < 255):
compte += 1
resultat.extend([compte, valeur])
i += compte
return resultatdef decodage_rle(liste_rle):
resultat = []
for i in range(0, len(liste_rle), 2):
compte = liste_rle[i]
valeur = liste_rle[i + 1]
for _ in range(compte):
resultat.append(valeur)
return resultatLa garde compte <255 impose la capacité d’un octet sans perdre de pixel. L’indice i avance du nombre réellement encodé, donc les pixels restants sont traités à l’itération suivante. Aucun compte nul n’est émis. Relancer les deux images et vérifier l’égalité pixel par pixel, pas seulement la ressemblance visuelle.
for n in [0, 1, 254, 255, 256, 510, 511, 600]:
image = [42] * n
rle = codage_rle(image)
assert all(0 <= v <= 255 for v in rle)
assert decodage_rle(list(bytes(rle))) == image
for nom in ["bac_nsi_32.png", "bac_nsi_256.png"]:
encoder_decoder_image(nom)
assert charger_image(nom) == charger_image(nom + ".dec.png")Le cas frontière le plus instructif est une suite de 256 pixels. La première itération encode 255 pixels et place i sur le dernier pixel restant. La suivante émet un couple de compte 1. Le décodeur lit donc 255 + 1 pixels de même couleur, ce qui reconstitue exactement l’aplat. Pour 510 pixels, deux couples de compte 255 suffisent : aucun bloc final vide ne doit apparaître. Ces deux cas mettent à l’épreuve deux frontières différentes.
Où disparaissent les pixels ?Un atelier pour expérimenter
Choisissez une réponse puis confrontez votre intuition à l’explication. Vous pouvez modifier vos choix.
Lire les réponses et leurs explications
- Quel format représente 600 pixels gris 42 sans dépasser un octet ?
[255,42,255,42,90,42]. Les comptes totalisent 600 et tiennent tous sur un octet.
- Pourquoi le décodeur initial proposé reste-t-il utilisable ?
Chaque couple se décode indépendamment.. Deux blocs de même gris restituent exactement leur concaténation.
Appuyez votre explication sur les valeurs et les conditions exactes du programme.
Compresser, perdre ou conserver une suite ?Un atelier pour expérimenter
Modifiez la longueur d’un aplat et le format. Comparez le compte stocké et le nombre de pixels restitués après enregistrement sur des octets.
Lire le résultat de l’expérience initiale
Tous les pixels sont conservés.
Chaque ligne est un couple stocké sur deux octets. Le décodage additionne les comptes ; le gris reste42.
| Bloc | Compte | Gris |
|---|---|---|
| 1 | 255 | 42 |
| 2 | 255 | 42 |
| 3 | 90 | 42 |
Le test en mémoire ne suffit pas : le format de stockage impose lui aussi des contraintes.
Du sujet à la méthode
Votre prochaine séance de révision
- Toujours tester le cycle complet : données, codage, octets, décodage.
- Une compression sans perte doit conserver toutes les valeurs, même lorsque le fichier devient plus gros.
Retrouver ces notions dans d’autres sujets
Toutes les annales de NSI · Le guide pour préparer le bac NSI 2027
Énoncé : sujet 26_BCG_NSI_1 (PDF). Corrigé et explications pédagogiques proposés par Sofien.
