Architecture des ordinateurs TP-30

by Joseph Razik, last modified on 2024-04-07

10   Watermarking d'image

10.1   Objectif

L'objectif de cet exercice de travaux pratiques est de faire quelques manipulations de base sur une image : sa création, l'addition d'images, l'insertion d'informations. Pour cela il faudra travailler avec les constituants de base d'une image numérique : le pixel. Pour visualiser les images générées, elles seront sauvegardée au format ASCII PBM (Portable Bitmap format) et binaire PGM (Portable Greymap format), les autres manipulations se feront au niveau de l'octet ou du bit.

10.2   Création d'une image

Une image numérique est définie par un ensemble de valeurs codant des information sur la couleur des points qui la composent. Par l’échantillonnage ce nombre de points est limité sur l'axe vertical et l'axe horizontal, et par la quantification la précision de la valeur codée est également limitée.

L'objectif de cette question est de créer une image à partir de zéro pour représenter quelque chose et la sauvegarder dans un format qui permettra de la visualiser.

Prise en main

Une image peut être modélisée par un tableau dont les lignes et colonnes représentent l’échantillonnage vertical et horizontal de l'image. À l'intersection d'une ligne et d'une colonne se trouve la valeur d'un pixel qui représente sa couleur.

Pour visualiser l'image que nous allons créer, nous allons suivre une famille de formats reconnus par la plupart des logiciels et qui reste assez simple : le format PBM ASCII et PGM binaire.

Comme pour la plupart des formats, le fichier se compose d'un entête définissant les paramètres de l'image et d'un corps qui correspond aux valeurs de couleur des pixels. La particularité des formats PBM ASCII et PGM binaire est que son entête est décrit en mode texte et le contenu sera écrit en mode texte ou en mode binaire. Voici ci-après la description de ce format, chaque ligne étant séparée de la suivante par un caractère de retour à la ligne \n. (cf. la page Wikipédia du format PBM) :

  • un nombre magique (magic number) composé de 2 caractères : P1 pour le format PBM ASCII et P5 pour le format PGM binaire,
  • la largeur et la hauteur de l'image, séparée par un caractère d'espacement,
  • des commentaires éventuels sur des lignes commençant par le caractère #.
  • la valeur maximal de couleur dans le cas du format PGM (cette ligne n'existe pas pour le format PBM).

Suivent ensuite les valeurs de chaque pixel, en commençant par le coin en haut à gauche de l'image et avec un parcours de la gauche vers la droite de la première ligne, puis de la gauche vers la droite de la seconde ligne etc.

Dans cette première partie, nous supposerons que notre image est bicolore en noir et blanc et que nous la coderons au format PBM ASCII. Attention, contrairement à la plupart des conventions, dans le format PBM, la valeur 1 correspond au noir et la valeur 0 correspond au blanc.

Voici un exemple d'un fichier au format PBM ASCII pour le chiffre 1 :

P1
# Un exemple pour le chiffe "1"
7 9
0 0 0 0 0 0 0
0 0 0 1 0 0 0
0 0 1 1 0 0 0
0 1 0 1 0 0 0
0 0 0 1 0 0 0
0 0 0 1 0 0 0
0 0 0 1 0 0 0
0 1 1 1 1 1 0
0 0 0 0 0 0 0
  1. Créez un tableau à deux dimensions correspondant à une image complètement noire de dimension 120 lignes et 160 colonnes.

  2. Créez une fonction save_pbm(nom_fichier, tab_image) qui va sauvegarder dans un fichier au format PBM ASCII le tableau correspondant à l'image donnée en paramètre.

  3. Sauver votre image noire de la première question au format PBM ASCII avec le nom image_vide.pbm et visualisez la pour vérifier que le résultat correspond à ce qui est attendu.

    image1

  4. Créez une fonction carre_blanc(tab_image) qui insère dans l'image donnée en paramètre (donc le tableau) un carré blanc plein de 10x10 pixels dans le coin en bas à gauche, à 5 pixels des bords. Enregistrez l'image obtenue par l'appel de cette fonction sur votre image noire des questions précédentes sous le nom image_carre_blanc.pbm. Quelle est la particularité du repère de l'image ?

    image2

Signature

Dans cette partie nous allons utiliser une évolution du format précédent, le format PGM (Portable Graymap format), dans lequel la définition d'une couleur se fait sur un éventail de niveaux de luminosités, variant de 0 pour le noir jusqu'à la valeur maximale pour le blanc (classiquement la valeur 255). Pour indiquer le nombre de niveaux de luminosité (dits niveaux de gris), la valeur maximale de niveaux est ajoutée à l'entête, juste après les dimensions de l'image.

Afin de compresser un peu le fichier, nous utiliserons la version binaire du format et plus celle totalement ASCII : l'entête reste en ASCII mais les données sont encodées sur 1 octet non signé. En effet, avec un octet on peut coder les 256 valeurs de niveaux de gris or, pour le cas d'une image blanche cela prendrait sinon 3 fois plus de place mémoire (un octet par chiffre et non par valeur).

Afin de distinguer ce format du précédent le magic number n'est plus le même, c'est désormais la valeur `P5.

Voici le nouvel entête pour le chiffre 1 précédent mais au format PGM binaire :

P5
# Un exemple pour le chiffe "1"
7 9
255

et les données des pixels en hexadécimal :

00000000000000000000FF0000000000FFFF00000000FF00FF000000000000FF000000000000FF000000000000FF00000000FFFFFFFFFF0000000000000000

Dans cette question vous allez créer une image contenant votre signature qui servira de calque pour l'intégrer plus tard dans d'autres images. Pour cela vous allez définir quelques fonctions intermédiaires.

  1. Créez une fonction save_pgm(nom_fichier, tab_image) qui va sauvegarder dans un fichier au format PGM binaire le tableau correspondant à l'image donnée en paramètre.

  2. Créez une fonction dessine_rectangle(tab_image, coin_sg_ligne, coin_sg_colonne, largeur, hauteur, epaisseur) qui prend en paramètre la position en ligne et colonne du coin supérieur gauche du rectangle, sa largeur et sa hauteur ainsi que l'épaisseur du trait de bordure, et qui dessine un rectangle creux de bordure blanche dans l'image donnée en paramètre. Note : la hauteur (et la largeur) du rectangle inclus les bordures et l'intérieur du rectangle n'est pas modifié (on ne dessine que la bordure). Testez et visualisez le résultat de votre fonction en l'enregistrant dans le fichier image_rectangle.pgm.

    Exemple :

    dessine_rectangle(tab_image, 10, 10, 40, 20, 2)
    

    image3

  3. Créez les fonctions dessine_lettre_E(tab_image, coin_sg_ligne, coin_sg_colonne) qui dessineront dans l'image donnée en paramètre les lettres de vos initiales en blanc, une fonction par lettre, avec comme paramètre la position du coin supérieur gauche du rectangle englobant le dessin de la lettre (l'exemple ici est donné pour la lettre E). Il n'est pas utile de se lancer dans un pixelart complexe au commencement. Attention à bien conserver la valeurs des pixels qui ne sont pas le blanc de la lettre. Testez et visualisez le résultat de vos fonctions en les enregistrant par exemple dans les fichiers image_lettre_E.pgm etc.

    Exemple :

    dessine_lettre_E(tab_image, 20, 20)
    

    image4

  4. Créez une fonction dessine_signature(tab_image) qui place dans le coin inférieur droit, à 5 pixels de distance des bords, un rectangle de bordure blanche de 2 pixels avec à l'intérieur vos initiales en blanc. Testez et visualisez le résultat de votre fonction en l'enregistrant dans le fichier image_signature.pgm.

    Exemple:

    image5

10.3   Watermarking

Le principe du watermarking d'une image est le marquage de la-dite image en y insérant un texte ou un logo indiquant soit l'auteur soit la source de l'image pour essayer de contrôler l'exploitation sauvage de l'image originale.

Nous supposerons que les images que nous allons manipuler sont toujours en 256 niveaux de gris. L'exemple support utilisé dans cette question et la suivante sera le fichier image_source.raw qui contient la suite des octets non signés correspondant à une image de taille 160x120 pixels (160 colonnes, 120 lignes) en 256 niveaux de gris.

  1. Écrivez la fonction charger_image(nom, largeur, hauteur) qui va lire le fichier donné en paramètre et convertir son contenu en un tableau à deux dimension d'entiers. Les deux autres paramètres permettent d'indiquer les dimensions de l'image, ces valeurs n'étant pas inscrites dans le fichier. Enregistrez le résultat de votre fonction dans le fichier image_source.pgm et visualisez-la.

  2. Écrivez la fonction marquer_image(nom) qui va retourner un tableau correspondant à l'image dont le nom est passé en paramètre mais où votre signature a été appliquée. Vérifiez ensuite le marquage de celle-ci en l'enregistrant dans le fichier image_marquee.pgm.

    image6

10.5   Position sensibles

Dans une image en niveau de gris et encore plus en couleurs, la modification de la valeur du bit de poids faible n'a que peu d'incidence sur le rendu visuel de l'image. Or, ce n'est clairement pas le cas pour la modification du bit de poids fort. Pour s'en rendre compte, vous allez écrire des fonctions qui modifient ces bits. La façon de modifier ces bits passe par ce que l'on appelle le masquage binaire en utilisant les opérateurs binaires et et ou qui se traduisent en Python par les symboles & et | respectivement, par exemple l'opération /C = A et B/ s'écrira C = A & B en Python.

  1. Quel masque binaire et quel opérateur faut-il utiliser pour mettre le bit i d'une valeur à 0 ?
  2. Quel masque binaire et quel opérateur faut-il utiliser pour mettre le bit i d'une valeur à 1 ?
  3. Écrivez la fonction tous_0(image) qui va retourner une copie de l'image dans laquelle le bit de poids faible de chaque octet a été mis à 0. Attention de bien retourner une image et de ne pas modifier l'image originale. Enregistrez le résultat dans le fichier de nom image_tous_0.pgm et visualisez le résultat obtenu à partir du fichier exemple précédent et comparez avec l'image originale.
  4. De même, écrivez la fonction tous_1(image) qui va retourner une copie de l'image dans laquelle le bit de poids faible de chaque octet a été mis à 1. Enregistrez le résultat dans le fichier de nom image_tous_1.pgm et visualisez le résultat obtenu à partir du fichier exemple précédent et comparez avec l'image originale.
  5. Afin de comparez avec le choix du bit de poids fort, écrivez la fonction tous_fort_0(image) qui va retourner une copie de l'image dans laquelle le bit de poids fort de chaque octet a été mis à 0. Enregistrez le résultat dans le fichier de nom image_tous_fort_0.pgm et visualisez le résultat obtenu à partir du fichier exemple précédent et comparez avec l'image originale.
  6. Faites de même avec la fonction tous_fort_1(image) qui va retourner une copie de l'image dans laquelle le bit de poids fort de chaque octet a été mis à 1. Enregistrez le résultat dans le fichier de nom image_tous_fort_1.pgm et visualisez le résultat obtenu à partir du fichier exemple précédent et comparez avec l'image originale.

10.6   Décomposition binaire

Les chaînes de caractères sont stockées dans l'ordinateur en suivant un certain encodage (utf-8 par exemple) qui fait le lien entre la graphie associée au caractère et le caractère lui-même. En utf-8, les points de code associés aux caractères sont stockés sur 1 à 4 octets, c'est-à-dire entre 8 et 32 bits.

L'idée dans cet exercice va être de décomposer les octets d'une chaîne de caractères en leur succession de bits, puis de placer chacun de ces bits dans le bit de poids faible des octets d'une image.

  1. Écrivez la fonction str2bits(message) qui retourne la liste des bits composant les octets du message encodé en utf-8. Exemple :

    >>> str2bits('12')
    [0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0]
    
  2. Pour une image en niveaux de gris de dimension 160x120, combien peut-on utiliser de bits et d'octets pour cacher un message si on utilise que le bit de poids faible ?

  3. Écrivez la fonction cache_message(message, image) qui va retourner une nouvelle image dans laquelle on va modifier la valeurs des bits de poids faible des octets de l'image pour qu'ils soient égaux à la suite de bits correspondant au message encodé. Attention ! Pour indiquer qu'un message est contenu dans l'image, un marqueur de début et fin de message sera utilisé : la séquence hexadécimale FFFE. Testez et visualisez le résultat de votre fonction en l'enregistrant dans le fichier image_message_cache.pgm et comparer avec l'image originale.

    >>> image_cache = cache_message('coucou', tab_image)
    >>> save_pgm('image_message_cache.pgm', image_cache)
    
  4. Écrivez maintenant la fonction inverse lire_message(tab_image) qui va extraire un message caché et l'afficher, s'il l'image donné en paramètre en comporte un. Attention, ici c'est l'image sous forme de tableau des valeurs des pixels, pas le fichier d'origine au format PGM avec son entête.

    >>> lire_message(image_cache)
    'coucou'
    
  5. Finalement, écrivez la fonction lire_message_fichier(nom_fichier) qui va lire un fichier image au format PGM binaire et en extraire le message caché afin de l'afficher, si l'image en comporte un.

    >>> lire_message_fichier('image_message_cache.pgm')
    'coucou'