Images et PIL

Symétrie .

L'image à charger ici est une photo au format jpg.

L'objectif est de retourner la photo, c'est à dire de passer de la première image à la seconde.

panda retourné

Une résolution :


# import d'un module de manipulation
# d'images :
from PIL import Image

# ouverture de l'image initiale :
imageSource=Image.open('ptpanda.jpg')
# récupération de ses dimensions :
largeur, hauteur=imageSource.size
# création d'une image de mêmes dimensions :
imageBut=Image.new('RGB',(largeur,hauteur))

# parcours des pixels :
for y in range(hauteur) :
	for x in range(largeur) :
		# récupération de la couleur du pixel (x,y) :
		p=imageSource.getpixel((x,y))
		# coordonnées pour ce pixel dans l'image finale :
		imageBut.putpixel((x,-y+hauteur-1),p)
# sauvegarde de l'image créée :
imageBut.save('sym_panda.jpg')
# on lance une visualisation :
imageBut.show()

Fonction prédéfinie

Il existe une fonction du module PIL automatisant cette tâche.


from PIL import Image

# ouverture de l'image initiale :
imageSource=Image.open('ptpanda.jpg')
# rotation :
imageSource=imageSource.rotate(180)
# sauvegarde du résultat sous un nouveau nom :
imageSource.save('pandateteenbas.jpg')
# on lance une visualisation :
imageSource.show()

Tête en bas.

L'image à charger ici est une photo au format png.

L'objectif est de remettre la tête d'Angelina dans le bon sens.

L'utilisation de rotate est interdite. On travaillera pixel par pixel.

Il suffit de reprendre le code de l'exercice précédent en changeant le nom du fichier.


from PIL import Image
 

# ouverture image   :
imageSource=Image.open("ange.png") 
#   largeur et   hauteur en pixels de l'image
largeur,hauteur=imageSource.size 
 
imageBut=Image.new("RGB",(largeur,hauteur)) 



# pour chaque ligne :
for y in range(hauteur):
    #pour chaque colonne :
    for x in range(largeur):
        # code du pixel  
        p=imageSource.getpixel((x,y))
        # création du pixel correspondant dans la nv image :
        imageBut.putpixel((x,-y+hauteur-1),p)


# sauvegarde de l'image créée :
imageBut.save("sym_axe.jpg")
# on montre l'image :
imageBut.show()

Fonction prédéfinie

Avec rotate :


from PIL import Image

# ouverture de l'image initiale :
imageSource=Image.open('ange.png')
# rotation :
imageSource=imageSource.rotate(180)
# sauvegarde du résultat sous un nouveau nom :
imageSource.save('tetehaute.png')
# on lance une visualisation :
imageSource.show()

Relief.

La recherche d'algorithmes sur les images est encore aujourd'hui en plein essor avec certaines applications que vous connaissez : reconnaissance des visages par les appareils photo, de façon plus générale reconnaissance de formes, reconnaissance de mots,...

L'étude de ces algorithmes s'appuient pour la plupart sur des notions mathématiques qui dépassent le niveau de la terminale. Toutefois l'expression de l'algorithme lui-même peut être parfois extrêmement simple.

Nous allons ici mettre en oeuvre un algorithme simple de "mise en relief".

L'image à charger ici est une image au format pgm : chaque pixel est, dans ce format, codé par un seul entier, entre 0 et 255, cet entier correspond à un 'niveau' de gris.

  1. Les entiers a,b,d,f,h,i étant compris entre 0 et 255, montrer que l'entier q=128+(-2a-b-d+f+h+2i)//8 est un entier compris entre 0 et 255.
  2. Programmer la transformation suivante de l'image :
    1. On ne traitera pas les pixels de la ligne du haut, de la ligne du bas, de la colonne gauche, de la colonne droite (on les passera éventuellement en noir).
    2. Tout autre pixel sera codé par un niveau de gris calculé à partir des niveaux de gris de ses voisins suivant la formule q=128+(-2a-b-d+f+h+2i)//8. Dans cette fomule le pixel prenant la valeur q est le pixel central e et ses voisins sont définis par le schéma suivant :
      abc
      def
      ghi

Encadrement de q=128+(-2a-b-d+f+h+2i)//8

Chacun des entiers étant compris entre 0 et 255, 2a+b+d est compris entre 0 et 4*255 = 1020. De même f+h+2i est compris entre 0 et 1020. L'entier (-2a-b-d) étant compris entre -1020 et 0 et l'entier (f+h+2i) étant compris entre 0 et 1020, leur somme est comprise entre -1020 et 1020. On a donc : \[ -128 \times 8+4 \leqslant -2a-b-d+f+h+2i \leqslant 127\times 8 + 4 \] D'où : \[ -128 \leqslant (-2a-b-d+f+h+2i)//8 \leqslant 127 \] (rappel : la double barre // correspond à la division entière).
Et \[ 0 \leqslant (-2a-b-d+f+h+2i)//8 + 128 \leqslant 255 \] Ceux qui ne maîtrisent pas la division euclidienne pourront aussi par exemple tester le programme suivant :

L = []

for k in range(-1020, 1021) :
    L.append(k//8)
    
print(L)

Ou :


# L, liste qui contiendra les résultats
# de k//8+128 avec k entre -1020 et +1020 :
L = [] 
for k in range(-1020, 1021) :
    L.append(k//8 + 128)
    
    
# on transforme L en ensemble pour supprimer les répétitions
L =set(L) 
# on ordonne L
L = list(L)
L.sort()

# on compare L à la liste des entiers 0,1,2,..., 255
print( L == list(range(0,256)) )

Programmation


from PIL import Image
 
    
imageSource=Image.open("lpa.pgm") 
largeur,hauteur=imageSource.size 
imageBut=Image.new("L",(largeur,hauteur))

# traitement des bords :
for x in range(largeur):
    imageBut.putpixel((x,0),0)
    imageBut.putpixel((x,hauteur-1),0)
for y in range(hauteur):
    imageBut.putpixel((0,y),0)
    imageBut.putpixel((largeur-1,y),0)

 
# pour chaque ligne :
for y in range(1,hauteur-1):
    #pour chaque colonne :
    for x in range(1,largeur-1):
        # code du pixel (niveau de gris)
        a=imageSource.getpixel((x-1,y-1))
        b=imageSource.getpixel((x,y-1))
        d=imageSource.getpixel((x-1,y))
        f=imageSource.getpixel((x+1,y))
        h=imageSource.getpixel((x,y+1))
        i=imageSource.getpixel((x+1,y+1))
        q=-2*a-b-d+f+h+2*i
        q=q//8
        q+=128
        imageBut.putpixel((x,y),q)

 

# sauvegarde de l'image créée :
imageBut.save("convol.pgm")
# on montre l'image :
imageBut.show()

On obtient l'image à charger ici.

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 (entre 0b00000000 et 0b11111111).


n = 145 # entier entre 0 et 255
masque = 0b11110000 # entier en binaire, 8 bits

m = n & masque

print('n en décimal : {}, et en binaire : {}.'.format(n, bin(n)) )
print('masque en décimal : {}, et en binaire : {}.'.format(masque, bin(masque)) )
print('m en décimal : {}, et en binaire : {}.'.format(m, bin(m)) )

Expliquer ensuite pourquoi on parle ici de masquage.

Décalage.

Tester le programme ci-dessous, modifier les valeurs de n, essayer pour dec d'autres valeurs que sa valeur par défaut.


def decale(n, dec=4) :
	return n>>dec
	
n=52
print('n en décimal : {}, et en binaire : {}.'.format(n ,bin(n)) )
m=decale(n)
print('m en décimal : {}, et en binaire : {}'.format(m,bin(m)) )

Vous testerez également l'effet d'un décalage obtenu avec <<.

Masque des bits de poids faibles sur une image.

  1. Charger l'image de chat de ce lien.
  2. Faites subir à chaque entier des codes RGB des pixels de l'image un masquage n ↦ n & 0b11110000.
  3. Observations ? Explications ?
  4. Et 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 une image à l'intérieur d'une autre.

Vous écrirez également le programme permettant de récupérer l'image cachée.

Vous utiliserez l'image du chat précédente, et l'image (de mêmes largeur et hauteur) suivante. Dans le résultat final, on devra voir l'image des tortues, l'image du chat étant cachée dans l'image des tortues.

Le masquage

Le résultat de l'opération n & masque s'écrit en binaire avec des chiffres 0 aux rangs correspondant à des 0 sur le masque et avec les mêmes chiffres (0 ou 1) que l'écriture binaire de n aux rangs correspondants à des 1 sur le masque.

Exemple (a,b,c,d,e,f,g,h étant les 0 et 1 de l'écriture binaire de n) :

masquage
Ecriture binaire de n abcdefgh
masque en binaire10110001
n & masque en binairea0cd000h

L'opération consiste ainsi à masquer la valeur de certains bits de l'entier de départ en les mettant systématiquement à 0.

Décalage.

n >> 4 décale les bits de l'écriture binaire de n vers la droite, ce qui revient à dire que l'on efface les 4 bits de poids faibles. Le bit de poids 4 devient donc le bit de poids 0 dans le résultat, le bit de poids 5 devient le bit de poids 1...

Le masquage des codes RGB d'une image

On masque chaque entier des composantes RGB par le programme suivant :


from PIL import Image


def masque(n) :
	return n & 0b11110000

# ouverture de l'image initiale :
imageSource=Image.open('cat.png')
# récupération de ses dimensions :
largeur, hauteur=imageSource.size
# création d'une image de mêmes dimensions :
imageBut=Image.new('RGB',(largeur,hauteur))

# parcours des pixels :
for y in range(hauteur) :
	for x in range(largeur) :
		# récupération de la couleur du pixel (x,y) :
		r,v,b = imageSource.getpixel((x,y))
		# dans l'image finale :
		imageBut.putpixel( (x,y),(masque(r),masque(v), masque(b)) )
# sauvegarde de l'image créée :
imageBut.save('catuni.png')
# on lance une visualisation :
imageBut.show()

L'image obtenue au final est encore largement reconnaissable. On a toutefois une perte de qualité visible sur certaines zones.

En masquant les bits de poids faibles, on modifie les composantes mais de manière peu prononcée : on décale ainsi les couleurs de chaque pixel mais en gardant à peu près les couleurs de départ. L'opération de masquage aura pour effet de masquer les différences de couleurs entre couleurs relativement peu éloignées.

Par contre si l'on remplace la fonction masque par la suivante :


 def masque(n) :
	return n & 0b00001111
 

on aura cette fois une perte complète des informations de départ puisqu'on ramène toutes les composantes entre 0b0000 et 0b1111 (c'est à dire entre 0 et 15), masquant cette fois les différences entre des couleurs très éloignées.

Cacher une image dans une autre.

Ce que nous ont appris les manipulations précédentes, c'est que l'essentiel de l'information définissant l'image se trouve dans les bits de poids forts des composantes RGB.

L'idée est donc de récupérer les 4 bits de poids forts des composantes de l'image à cacher et de remplacer les 4 bits de poids faibles de l'image qui restera apparente par ces quatre bits.

Cacher une image avec les bits
composante R sur l'image du chat a7a6 a5a4 a3a2 a1a0
composante R sur l'image des tortues b7b6 b5b4 b3b2 b1b0
composante R sur l'image des tortues cachant le chat b7b6 b5b4 a7a6 a5a4
composante R sur l'image d'un chat cachant les tortues a7a6 a5a4 b7b6 b5b4

Programme pour cacher le chat dans l'image des tortues :


from PIL import Image

def masquelow(n) :
	return  n & 0b11110000 
	
def decale(n, dec=4) :
	return  n>>dec  
	
# ouverture des images de départ :
chat=Image.open('cat.png')
tortue=Image.open('turtle.png')

# récupération de leurs dimensions communes :
largeur, hauteur=chat.size

# variable pour image  tortue cachant chat  :
tortuechat=Image.new('RGB',(largeur,hauteur))


# parcours des pixels :
for y in range(hauteur) :
	for x in range(largeur) :
		# récupération de la couleur du pixel (x,y) :
		rc, vc, bc = chat.getpixel((x,y))
		rc, vc, bc = decale(rc), decale(vc), decale(bc)
		rt, vt, bt = tortue.getpixel((x,y))
		rt, vt, bt = masquelow(rt), masquelow(vt), masquelow(bt)
		# dans l'image finale :
		tortuechat.putpixel( (x,y),(rc+rt, vc+vt, bc+bt) )
# sauvegarde de l'image créée :
tortuechat.save('ghostcat.png')
# on lance une visualisation :
tortuechat.show()

Code de récupération du chat :


from PIL import Image
	
def decale(n, dec=4) :
	"""on décale cette fois vers la gauche.
	Et on ne garde que 8 bits."""
	return  (n << dec) & 0b11110000 
	

# ouverture de l'image  "truquée" :
tortuechat=Image.open('ghostcat.png')
 

# dimensions communes :
largeur, hauteur=tortuechat.size


# variable pour image  de récupération du chat  :
newcat=Image.new('RGB',(largeur,hauteur))


# parcours des pixels :
for y in range(hauteur) :
	for x in range(largeur) :
		# récupération de la couleur du pixel (x,y) :
		r,v,b = tortuechat.getpixel((x,y))
		r,v,b = decale(r),decale(v),decale(b)
		# dans l'image finale :
		newcat.putpixel((x,y),(r,v,b))
# sauvegarde de l'image créée :
newcat.save('resurrection.png')
# on lance une visualisation :
newcat.show()

Réduire.

L'image à charger ici est une image au format jpeg. Cette image étant 'lourde' (trop lourde par exemple pour l'usage usuel sur une page web), on souhaite la réduire.

Il existe bien sûr des logiciels se chargeant de cela. Mais l'objectif est ici de le faire en agissant directement sur les pixels.

Pour cela, on se propose de mettre en oeuvre l'idée suivante :

  • On regroupe les pixels de l'image par carrés de quatre pixels (carrés de côté 2),
  • on remplace ces 4 pixels par un seul, dont les composantes rouge, vert, bleu sont les moyennes des composantes correspondantes des 4 pixels initiaux.

Mettre en oeuvre cette idée.

  • On fera en sorte que le facteur de réduction (fr=2 dans l'explication ci-dessus : on divise largeur et hauteur par fr) puisse être facilement modifié.
  • Si la largeur (resp. hauteur) de l'image initiale n'est pas un multiple de fr, on pourra 'oublier' les dernières colonnes et les dernières lignes de pixels pour simplifier le programme (par exemple avec fr=4, et une largeur=4k+3, on peut ne pas traiter les 3 dernières colonnes de pixels).

Un programme possible :


# import d'un module de manipulation
# d'images :
from PIL import Image

# ouverture de l'image initiale :
source=Image.open('tronctortues.JPG')
# récupération de ses dimensions :
largeur, hauteur=source.size
# facteur de réduction :
fr=4
# image réduite :
nvlg=largeur//fr
nvht=hauteur//fr
but=Image.new('RGB',(nvlg,nvht))

# parcours des pixels 
# on néglige éventuellement des colonnes à droite
# et des lignes en bas
for y in range(0,nvht*fr,fr) :
	for x in range(0,nvlg*fr,fr) :
		# récupération des couleurs initiales  et moyenne
		rouge=sum([source.getpixel((a,b))[0] for a in range(x,x+fr) for b in range(y,y+fr)])//(fr*fr)
		vert=sum([source.getpixel((a,b))[1] for a in range(x,x+fr) for b in range(y,y+fr)])//(fr*fr)
		bleu=sum([source.getpixel((a,b))[2] for a in range(x,x+fr) for b in range(y,y+fr)])//(fr*fr)
		# coordonnées pour ce pixel dans l'image finale :
		but.putpixel((x//fr,y//fr),(rouge,vert,bleu))
# sauvegarde de l'image créée :
but.save('reduc.jpg')
# on lance une visualisation :
but.show()

Prenez le temps de vérifier sur l'image finale que largeur et hauteur ont été divisées par fr.

Fonction prédéfinie

Il existe une fonction du module PIL automatisant cette tâche de réduction.

Si l'on veut par exemple réduire notre image de tortues aux dimensions (324, 216) :


from PIL import Image

# ouverture de l'image initiale :
imageSource=Image.open('tronctortues.jpg')
# réduction :
imageSource=imageSource.resize((324,216))
# sauvegarde du résultat sous un nouveau nom :
imageSource.save('petitestortues.jpg')
# on lance une visualisation :
imageSource.show()