bunex-industries

Picture in Picture

(Rien à voir avec le pip !)

Voici une expérience intrigante (et utile ?) consistant à dissimuler une image numérique à l'intérieur d'une autre. Il s'agit d'un exemple de stéganographie. Le thème est intéressant, j'ai relevé cette affirmation dans la page, elle me laisse songeur... :

"Actuellement, à la vue de la quantité du flot continu d'informations qui inonde nos sociétés modernes, il est mathématiquement impossible d'empêcher l'utilisation de la stéganographie qui a l'avantage de pouvoir revêtir d'innombrables formes cumulatives."

Je présente ici une façon de procéder pour mixer deux images et, bien sûr, l'opérations inverse consistant à séparer de nouveau les deux images à partir de la vue mixée.

Généralités sur les pixels et le codage des couleurs

Afin de présenter ces méthodes, voici une rapide description de la manière dont les pixels sont stockés dans un fichier image ordinaire (RVB, 8bits/couche) :

En base 10, le système de numération auquel nous sommes habitués, nous parlons d'unité, dizaines, centaines, milliers. Le chiffre le plus à gauche d'un nombre est celui qui "pèse le plus lourd" alors que celui le plus à droite ne contribue que modestement au nombre..

En binaire, dans un octet (8 bits), par exemple 10010111, le premier bit à gauche représente le "bit de poids fort" (ou MSB : most significant bit) alors que le dernier à droite représente le "bit de poids faible" (LSB : least significant bit). La notion de "poids" doit s'entendre comme la mesure de l'influence du bit sur la valeur finale. Le LSB ne fera varier cette valeur finale que d'une unité alors que le MSB fera fortement varier cette valeur (en ajoutant 128 si le bit est à 1).

On peut convertir un nombre binaire en base 10 comme ceci, avec b7 = bit de poids fort et b0 = bit de poids faible :

$$ Valeur_{base10}= 128 \times b_7 + 64 \times b_6 + 32 \times b_5 + 16 \times b_4 + 8 \times b_3 + 4 \times b_2 + 2 \times b_1 + 1 \times b_0 $$

Avec (quand tous les bits sont à 1) :

$$ 128+64+32+16+8+4+2+1 = 255 $$

L'opération inverse est un peu plus compliquée (avec v le nombre entier, "floor" la partie entière et "mod 2" l'opération modulo 2 sur le nombre entier obtenu) :

$$ b_7 = floor(v/128) \bmod{2} \\ b_6 = floor(v/64) \bmod{2} \\ b_5 = floor(v/32) \bmod{2} \\ b_4 = floor(v/16) \bmod{2} \\ b_3 = floor(v/8) \bmod{2} \\ b_2 = floor(v/4) \bmod{2} \\ b_1 = floor(v/2) \bmod{2} \\ b_0 = floor(v/1) \bmod{2} $$

À titre d'exemple, voici comment sera codé un pixel marron :

un pixel marron
231 189 27
1
b7 (MSB)
1
b6
1
b5
0
b4
0
b3
1
b2
1
b1
1
b0 (LSB)
1
b7 (MSB)
0
b6
1
b5
1
b4
1
b3
1
b2
0
b1
1
b0 (LSB)
0
b7 (MSB)
0
b6
0
b5
1
b4
1
b3
0
b2
1
b1
1
b0 (LSB)

Toutes les images ne sont pas de ce type et il existe plusieurs manières de "ranger" ces données de pixels en mémoire. Je soulignerai simplement quelques points :

Méthode de mixage

Idéalement, les deux images doivent avoir la même dimension au pixel près. Dans l'exemple ci-dessous, pas de souci, les images chargées seront automatiquement retaillées.

L'idée générale est la suivante : remplacer les bits de poids faibles des pixels de l'image publique par les bits de poids forts des pixels de l'image privée. En effet, ôter les bits de poids faible d'une image va entraîner une perte de précision dans les nuances et créer des dégradés "en escalier" mais l'image restera très lisible.

Il faut savoir que l'oeil est plus sensible aux variations de luminosité qu'aux variations de teinte. De plus, il est capable de distinguer au plus 200 niveaux de luminosité différents. Cela explique pourquoi 256 niveaux suffisent pour nous donner l'impression d'une variation continue de gris.

Vous pouvez observer ci-après la dégradation progressive de l'image lorsqu'on ôte de plus en plus de bits de poids faibles (de gauche à droite : originale 8 bits, image 5 bits, 4,3,2 et 1 bit). Notez qu'avec seulement 4 ou 5 bits, l'image reste encore très similaire à l'originale (et avec 6 ou 7 bits, la différence est quasi-indiscernable) :

L'image tolère donc bien de se passer de l'information contenue dans les bits de poids faibles. Nous allons utiliser ces bits pour y stocker les pixels de notre image secrète. Nous remplacerons les 2 bits de poids faibles de l'image publique par les 2 bits de poids fort de l'image privée. L'image publique n'en souffrira presque pas et nous pourrons stocker discrètement une seconde image (au prix d'une forte dégradation de celle-ci).

Cette opération s'effectue pixel par pixel, composante par composante.

Dans l'exemple interactif plus bas, j'ai légèrement amélioré cette méthode. les 2 bits de poids faibles de chaque pixel et pour chaque composante RVB sont ôtés. Nous avons donc en tout 6 bits libres par pixel. En acceptant de sacrifier la couleur de notre image privée et de ne la dissimiler qu'en N&B, ces 6 bits seront plus que suffisants pour conserver une bonne qualité d'image. Ainsi nous rangerons les bits comme ceci :

Nous avions 3 composantes de 8 bits = 24 bits par pixel, nous avons désormais 3 composante RVB de 6 bits + 1 composante N&B de 6 bit = 24 bits par pixel. Le décodage consistera à extraire les 2 bits de poids faible de chaque couche de l'image mixée et de ré-assembler nos pixels 6 bits N&B.

Exemple interactif

On choisira comme image publique, une vue riche de détails et en couleurs (on évitera les larges zones d'aplats).





À gauche : l'image mixée (on ne distingue pas l'image dissimulée). À droite : l'image dissimulée, extraite de l'image de gauche.