L'image en ISN, ICN

Module PIL - Stéganographie

Plus tard ! 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).

Un tour de passe-passe

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 :

  1. La première sera obtenue à partir de l'image initiale en remplaçant le triplet (r, g, b) de chaque pixel par ((r//16)*16, (g//16)*16, (b//16) * 16).
  2. La seconde sera obtenue à partir de l'image initiale en remplaçant le triplet (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.

  • Le code
  • Les deux images obtenues
  • Explication

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
image un image deux
Si vous ne savez pas expliquer ce tour de passe-passe, traitez l'exercice suivant.

Stéganographie

Masque d'entiers

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


		
		

Décalage

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


					
					

Masque des bits de poids faibles sur une image

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 ?

Cacher une image dans une autre

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.

  • Masque des bits de poids faible
  • Cacher une image dans une autre
  • Dévoiler l'image masquée
  • Commentaire

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.

Explication brève

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.

Stéganographie, bis

chat messager

Votre mission est maintenant de :

  1. Écrire un script qui permette de cacher un message dans une image (il est possible de télécharger l'image ci-contre en cliquant dessus).
  2. Écrire un script permettant de récupérer le message dans l'image.

Pour simplifier, on fait les choix suivants :

  • Les caractères du message ont un code unicode compris entre 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).
  • La longueur totale du message est d'au plus 255 caractères (pour faciliter l'inscription de cette longueur au début du message, mais on peut facilement permettre des messages plus longs).
  • Cacher, un code possible
  • Récupérer
  • Format
  • Mini-projet

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 :

chat messager

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.

Cacher un message dans une image

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.

Attention

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

Objectifs

  1. Écrire un programme permettant de cacher un message dans l'image.
  2. Écrire un programme permettant à un tiers de récupérer le message (qui s'affichera lors de l'exécution du programme).

Méthode employée pour cacher le message

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.

Indications et précisions

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 :