Les programmes ci-dessous ne sont pas à réaliser le jour de la formation (sauf s'il vous reste du temps bien évidemment). Ils sont là pour vous donner d'autres idées d'exercices (ou vous entraîner si besoin).
Module PIL - Stéganographie
Les programmes ci-dessous ne sont pas à réaliser le jour de la formation (sauf s'il vous reste du temps bien évidemment). Ils sont là pour vous donner d'autres idées d'exercices (ou vous entraîner si besoin).
Télécharger l'image ci-contre (au format .png
) puis la sauvegarder
dans le répertoire courant.
A partir de cette image, créer deux nouvelles images :
r
, g
, b
) de chaque pixel par
((r//16)*16
, (g//16)*16
, (b//16) * 16
).r
, g
, b
) de chaque pixel par
((r%16)*16
, (g%16)*16
, (b%16) * 16
).Visualiser les images obtenues puis expliquer comment on a réalisé ce tour de passe-passe.
Dans le code pour construire les deux nouvelles images,
on utilise la méthode .getdata()
qui permet de
récupérer sous forme de liste l'intégralité des triplets
des composantes couleurs des pixels de l'image d'origine :
##----- Importation des Modules -----##
from PIL import Image
##----- Définition des Fonctions -----##
def magie1(n):
return (n//16)*16
def magie2(n) :
return (n%16)*16
##----- Informations sur l'image d'origine -----##
imageFusion = Image.open('fusion.png')
##----- Conception des nouvelles images -----##
imageUn = Image.new('RGB', imageFusion.size)
imageDeux = Image.new('RGB', imageFusion.size)
##----- Stockage dans une liste de l'intégralité des composantes couleurs -----##
listePixelsFusion = list(imageFusion.getdata())
listePixelsUn = []
listePixelsDeux = []
##----- Insertion dans les nouvelles images après modification -----##
for (r, g, b) in listePixelsFusion :
listePixelsUn.append( (magie1(r) ,magie1(g) ,magie1(b)) )
listePixelsDeux.append( (magie2(r) ,magie2(g) ,magie2(b)) )
imageUn.putdata(listePixelsUn)
imageDeux.putdata(listePixelsDeux)
##-----Finalisation-----##
imageUn.save('Un.png')
imageDeux.save('Deux.png')
Les deux images obtenues :
Image Un | Image Deux |
---|---|
Exécuter le programme suivant.
Modifier la valeur de n
(valeurs entre
0
et 255
) et la valeur du
masque binaire (entre 0b00000000
et
0b11111111
).
Tester le programme ci-dessous.
Modifier les valeurs de n
et de dec
pour observer les effets du décalage. Tester également en
remplaçant <<
par >>
.
Télécharger le logo ISN-IREM
en cliquant sur
l'image ci-contre.
Faire subir à chaque composante RGB des pixels un masquage
n ↦ n & 0b11110000
puis afficher
l'image obtenue.
Obtient-on un résultat analogue avec le masquage
n ↦ n & 0b00001111
?
A l'aide de ce qui précède, imaginer une opération permettant
de cacher le logo Python à
l'intérieur de l'image représentant le logo ISN-IREM
(ces deux images ont les mêmes dimensions). Dans le
résultat final, on devra voir le logo ISN-IREM, le logo
Python étant caché.
Écrire ensuite une fonction permettant de récupérer l'image cachée.
On définit la fonction masque()
à
l'aide de la première activité.
En masquant les bits de poids faibles, on décale les couleurs de chaque pixel mais en gardant à peu près les couleurs de départ.
##----- Importation des Modules -----##
from PIL import Image
##----- Définition des Fonctions -----##
def masque(n):
return n & 0b11110000 # équivaut à return (n//16)*16
##----- Informations sur l'image d'origine -----##
im = Image.open('logoIsnIrem.png')
l, h = im.size
##----- Conception de la nouvelle image -----##
im_masque = Image.new('RGB', (l, h))
for x in range(l):
for y in range(h):
r, v, b = im.getpixel((x, y))
im_masque.putpixel((x,y), (masque(r), masque(v), masque(b)))
##-----Finalisation-----##
im_masque.save('logoIsnIremPoidsForts.png')
im_masque.show()
L'essentiel de l'information définissant une image se trouve
dans les bits de poids forts des composantes
RGB
.
L'idée est donc de remplacer les 4 bits de poids faible du logo ISN-IREM par les 4 bits de poids fort du logo Python (image à cacher). Suivant les couleurs initiales des deux images, on pourra retrouver une trace visible de l'image cachée dans le résultat (c'est le cas ici).
##----- Importation des Modules -----##
from PIL import Image
##----- Définition des Fonctions -----##
def masque(n):
"""Masquage des pixels de poids faible."""
return n & 0b11110000
def decale(n, dec=4):
"""Remplace les pixels de poids faible par les pixels de poids fort."""
return n >> dec # n >> 4 = n//16
##----- Informations sur les images d'origine -----##
im1 = Image.open('logoIsnIrem.png') # image qui restera visible
im2 = Image.open('logoPython.png') # image à cacher
l, h = im1.size
##----- Conception de la nouvelle image -----##
im_resultat = Image.new('RGB', (l, h))
for x in range(l):
for y in range(h):
r1, v1, b1 = im1.getpixel((x, y))
r1, v1, b1 = masque(r1), masque(v1), masque(b1)
r2, v2, b2 = im2.getpixel((x, y))
r2, v2, b2 = decale(r2), decale(v2), decale(b2)
im_resultat.putpixel((x,y), (r1+r2, v1+v2, b1+b2))
# r1 + r2 = les 4 bits de poids fort de r1initial à gauche,
# les 4 bits de poids forts de r2initial à droite
##-----Finalisation-----##
im_resultat.save('melange.png')
im_resultat.show()
Un second code pour créer l'image fusion des deux images initiales :
from PIL import Image
def fusionne(n, m) :
return (n//16)*16 + (m//16)
# ou return (n & 0b11110000) | (m >> 4)
# ouverture de l'image qui restera apparente :
imageVisible = Image.open('NomImageApparente.png')
# ouverture de l'image à cacher dans l'autre (mêmes dimensions que la précédente) :
imageACacher = Image.open('NomImageACacher.png')
# création d'une image de mêmes dimensions :
imageFusion = Image.new('RGB', imageVisible.size)
listePixelsVisible = list(imageVisible.getdata())
listePixelsACacher = list(imageACacher.getdata())
listePixelsFusion = []
for i, (r, g, b) in enumerate(listePixelsVisible) :
r0, g0, b0 = listePixelsACacher[i]
listePixelsFusion.append( (fusionne(r,r0) ,fusionne(g,g0) ,fusionne(b,b0)) )
imageFusion.putdata(listePixelsFusion)
# on lance une visualisation :
imageFusion.show()
# sauvegarde de l'image :
imageFusion.save('fusion.png')
On retrouve l'image masquée en récupérant ses bits de poids fort. On peut utiliser le code ci-dessous ou celui de l'exercice 1.
##----- Importation des Modules -----##
from PIL import Image
##----- Définition des Fonctions -----##
def demasque(n, dec=4):
"""On décale vers les bits de poids fort et on ne garde que 8 bits."""
return (n << dec) & 0b11110000
##----- Informations sur l'image d'origine -----##
im = Image.open('melange.png')
l, h = im.size
##----- Conception de la nouvelle image -----##
im_sortie = Image.new('RGB', (l, h))
for x in range(l):
for y in range(h):
r, v, b = im.getpixel((x, y))
im_sortie.putpixel((x,y), (demasque(r), demasque(v), demasque(b)))
##-----Finalisation-----##
im_sortie.save('demelange.png')
im_sortie.show()
Essayer maintenant le même travail avec des images au format .jpg
.
Vous constaterez que le dévoilement de l'image cachée se passe beaucoup plus mal...
Analyser cela.
Le format .jpg
est un format de compression avec perte d'information,
le format .png
est sans perte d'information. La fabrication en
.jpg
de l'image mélangeant les deux sources va avoir pour effet de
perdre de l'information, ce qui ne permettra pas la récupération des images de
départ.
Votre mission est maintenant de :
Pour simplifier, on fait les choix suivants :
0
et 255
(pour que le code des caractères à cacher soit de
longueur fixe, mais on aurait pu permettre des longueurs supérieures en
fixant cette longueur fixe à une valeur plus grande).255
caractères
(pour faciliter l'inscription de cette longueur au début du message,
mais on peut facilement permettre des messages plus longs).
from PIL import Image
imageSource = Image.open('chat.png')
listePixels = list(imageSource.getdata())
imageBut = Image.new(imageSource.mode, imageSource.size)
def caractereToDigit(caractere):
"""
Transforme caractere en une liste de digits correspondant
au code unicode du caractère (on se limitera aux caractères
dont le code est entre 0 et 255).
Exemple : unicode de a = 97, écriture binaire de 97 = 1100001,
la fonction retourne la liste de longueur 8 [0,1,1,0,0,0,0,1].
"""
assert( 0 <= ord(caractere) < 256 )
chaine = bin(ord(caractere))[2:]
lg = len(chaine)
while lg < 8 :
chaine = '0' + chaine
lg += 1
liste = []
for b in chaine:
liste.append(int(b))
return liste
def messageToListe(message):
""" transforme le message en une liste de digits,
chaque caractère (code supposé entre 0 et 255) étant
transformé en son code unicode sur une longueur 8. """
liste = []
for caractere in message :
liste.extend(caractereToDigit(caractere))
return liste
def substitueBit(entierSource, digit):
""" entierSource : entier entre 0 et 255.
digit : 0 ou 1
change le bit de poids faible de entierSource en le remplaçant par digit."""
return (entierSource & 254) | digit # ou (entierSource//2)*2 + digit
def cacheMessage(message, liste):
""" liste est une liste de triplets (r, g, b) d'entiers entre 0 et 255.
message sera caché dans les triplets à raison d'un bit par triplet
(chaque bit de poids faible de la composante b
est remplacé par un bit du code unicode d'un caractère du message).
On commence par le triplet (r, g, b) d'indice 1, le triplet d'indice 0
sera réservé au stockage de la longueur du message.
"""
listeDigits = messageToListe(message)
for i, digit in enumerate(listeDigits):
liste[i+1] = ( liste[i+1][0], liste[i+1][1], substitueBit(liste[i+1][2] , digit) )
def cacheLongueur(message,liste):
""" Pour simplifier, on suppose que message a au plus 255 caractères
(après tout, c'est plus que la longueur d'un tweet...).
On inscrira cette longueur à la place de la composante b du premier pixel
de l'image.
liste est une liste de triplets (r, g, b). On remplace le b du premier élément
par la longueur de message.
"""
lg = len(message)
liste[0] = (liste[0][0], liste[0][1], lg)
# on cache un message dans l'image :
message = "Je ne donne jamais mon avis. Car lorsqu'il est donné, je ne l'ai plus."
cacheLongueur(message,listePixels)
cacheMessage(message, listePixels)
imageBut.putdata(listePixels)
imageBut.save('chatMessager.png', format='PNG')
Vous pouvez constater que le message n'altère pas visiblement l'image :
On récupère maintenant le message :
from PIL import Image
imageSource = Image.open('chatMessager.png')
listePixels = list(imageSource.getdata())
def recupereLongueur(liste):
"""
Récupère la longueur du message stocké dans la composante b
de l'élément d'indice 0 de liste."""
return liste[0][2]
def recupereMessage(liste):
""" récupère le message dans liste. """
lg = recupereLongueur(liste)
lg *= 8
message = ""
lettre = ""
for i in range(1, lg+2) :
if len(lettre) < 8 :
lettre = lettre + str(liste[i][2]%2)
else :
lettre = int(lettre,2)
message = message + chr(lettre)
lettre = str(liste[i][2]%2)
return message
# on récupère le message :
message = recupereMessage(listePixels)
print(message)
Comme dans l'exercice précédent, on fera attention au format.
On a utilisé le format .png
. Le format .jpg
ne fonctionnerait pas.
En effet, au moment où l'on crée le fichier chatMessager.jpg
,
il y a détérioration de l'information (il y a perte d'information
dans l'algorithme de compression au format .jpg
).
On ne récupèrera donc pas le message initial.
Cet exercice a été donné en mini-projet à des élèves. Il nécessite d'avoir travaillé aussi sur les images au format Portable Bitmap.
Nous avons vu comment cacher une image dans une image (en exercice dans le cours, l'exercice 1 de cette page avait été proposé aux élèves).
Vous allez ici cacher un texte dans une image.
Pour cela, nous utiliserons une image au format .pgm ascii
:
zebre.pgm.
Nous n'utiliserons pas ici le module PIL
mais ce que nous
avons vu lors de la manipulation de fichiers images.
Rappel : vous pouvez ouvrir le fichier zebre.pgm
avec un simple
éditeur de texte. Cela vous sera utile pour le programme demandé.
Tous les n
pixels, on remplacera le code du pixel par le code
ascii d'un caractère du message. L'entier n
sera un paramètre
de votre programme : sa valeur devra être définie en fonction de la longueur
du message, de façon à ce que la distance entre deux caractères consécutifs
dans l'image soit la plus grande possible.
Pour simplifier le travail, votre message ne comportera pas d'accent. Limitez vous aux caractères codés en ascii.
Les fonctions ord() et chr() du langage Python vous seront utiles.
La valeur de n
(ou l'écart entre deux caractères) sera
transmise à l'utilisateur devant récupérer votre message. Vous pouvez aussi
tenter d'améliorer cela en cachant également cette valeur de n
dans le fichier image et en prévoyant de la faire lire par le programme de
récupération du message.
Le message utilisé sera un message relativement court : vous choisirez une longueur maximale (que vous chercherez à justifier par le résultat de vos expériences) et vous ferez contrôler par le programme que le message à cacher ne dépasse pas cette longueur maximale.
Vous trouverez ci-dessous des liens vers deux "copies" d'élèves :