Description et génération des images PNM et PAM
PNM (Portable aNy Map) est un format de fichier «image» inventé par Jef Poskanzer dans les années 80. Les formats P5 (nuances de gris, données binaires) et P6 (couleurs, données binaires) sont reconnus par l'interface graphique Tkinter.
Cette page a été adaptée à python3 (fonction bytes() pour les tableaux d'octets), python2 n'étant officiellement plus supporté depuis le premier janvier 2020.
Bien que pouvant porter l'extension générique .pnm, trois autres sont plus spécifiques :
- .pbm (Portable BitMap) pour l'encodage des fichiers «noir et blanc», P1 pour des données en ASCII, P4 en format binaire.
- .pgm (Portable GrayMap) pour l'encodage des fichiers en nuances de gris, P2 pour des données en ASCII, P5 en format binaire.
- .ppm (Portable PixMap) pour l'encodage des fichiers en couleurs, P3 pour des données en ASCII, P6 en format binaire.
- .pam (Portable Arbitrary Map ou Portable Any Map) est une extension de ce format, admettant la transparence et permettant le codage de chaque composante des couleurs sur 65536 nuances, avec P7 pour début d'entête. Il n'est pas dit que tous les systèmes d'exploitation le reconnaissent.
Les lignes des fichiers ASCII ne peuvent normalement comporter plus de 70 caractères, probablement en raison de la norme RFC822 concernant les courriers électroniques (P1, P2 et P3 ont été créés pour cela). Cette limitation ne semble pas gêner les systèmes actuels.
Il ne s'agit en aucun cas de formats compressés, mais ces images sont faciles à générer. Un fichier PNM comprend successivement une signature pour le type de l'image (P1 à P6), les éventuels commentaires (une ou plusieurs ligne(s) commençant par le croisillon #), largeur et hauteur séparées par une espace, la valeur maximale pour les images en valeur de gris ou en couleur, une espace ou une fin de ligne avant la suite des valeurs (ou de triplets de valeurs) pour chaque pixel :
P3 # ceci n'est que le début d'un fichier # au format PNM en couleurs et en ASCII 55 46 4 0 1 3 2 4 3 1 4 0 4 1 3 2 3 1 4 2 1 4 3 2 …
En GNU/Linux Debian GNOME ou Mate-Desktop, les 6 formats PNM sont lisibles par le système.
P1 : Noir/Blanc, fichier ASCII ↑
Les fichiers images en noir et blanc comprennent la signature P1, la largeur et la hauteur de l'image exprimées en pixels, puis une suite de valeurs pour chaque pixels exprimée en caractères ASCII (0 pour le blanc ou 1 pour le noir). L'exemple suivant a été organisé pour être facilement lisible par un humain :
P1 6 4 1 1 1 1 1 1 1 0 0 0 0 1 1 0 0 0 0 1 1 1 1 1 1 1
L'image fait 6 pixels de large et 4 de hauteur : il faura l'agrandir pour la visualiser. Il faut deux caractères (0 ou 1 et l'espace). Une image de 1000x1000 pixels générera un fichier d'un peu plus de deux mégaoctets. Le fichier ci-dessus aurait pu être écrit :
P1 6 4 111111100001100001111111.
Le script qui suit génère l'image d'un cercle noir sur fond blanc, à partir du rayon désiré exprimé en pixel. Il teste les pixels un à un pour savoir s'ils sont situés à la distance "rayon" du point central : racine carrée de la somme des carrés de la différence des abscisses et des ordonnées avec le point central. Un fichier rond-diametre.pnm (cela aurait pu être .pbm) est généré.
#! /usr/bin/python3 rayon =int(input("rayon: ")) diametre =rayon*2+1 diam =str(diametre) # chaîne reprenant l'information sur le diamètre fichier ="P1 %s %s\n" %(diam,diam) # entête de fichier for i in range(diametre) : for j in range(diametre) : if round((abs((i-rayon) **2) +abs((j -rayon) **2)) **.5)==rayon : fichier +="1" else : fichier +="0" fichier +="\n" open("rond"+diam+".pnm","w").write(fichier)
P2 : images en nuances de gris, fichier ASCII ↑
Les fichiers d'image en valeurs de gris commencent par la signature P2, suivie de la largeur et la hauteur de l'image exprimées en pixels puis de la valeur maximale (jusqu'à 255) et enfin de la valeur de chaque pixel en ASCII. Les valeurs pour cet exemple vont de 0 (noir) à 15 (blanc), avec les 14 nuances de gris intermédiaires.
P2 9 8 15 15 14 13 12 11 10 9 8 7 14 13 12 11 10 9 8 7 6 13 12 11 10 9 8 7 6 5 12 11 10 9 8 7 6 5 4 11 10 9 8 7 6 5 4 3 10 9 8 7 6 5 4 3 2 9 8 7 6 5 4 3 2 1 8 7 6 5 4 3 2 1 0
Notes
- contrairement au format noir et blanc, le 0 code ici pour le noir.
- il n'est nullement obligatoire de disposer les valeurs dans un rectangle de 9x8
- le nombre d'espaces entre chaque valeur n'a pas d'importance, mais il est impossible de les supprimer.
P3 : images en couleurs, fichier ASCII ↑
Les fichiers d'image en couleurs commencent par la signature P3, suivie de la largeur et la hauteur de l'image exprimées en pixels puis de la valeur maximale (jusqu'à 255), chaque pixel étant codé par trois valeurs représentant les lumières fondamentales : rouge, vert et bleu, chacune de 0 (rien) à la valeur maximale précisée :
P3 3 3 255 0 0 0 255 255 255 0 0 0 255 255 0 255 0 255 0 255 255 255 0 0 0 255 0 0 0 255
Le nombre d'espaces entre chaque valeur n'a pas d'importance, mais il est impossible de les supprimer pour les fichiers d'images en couleur. Néanmoins, pour cet exemple à deux valeurs 0 ou 255, la limitation des couleurs fondamentales aux valeurs 0 et 1 (1 en fin de première ligne) est plus économique, le fichier affichant une image strictement équivalente à la précédente :
P3 3 3 1 0 0 0 1 1 1 0 0 0 1 1 0 1 0 1 0 1 1 1 0 0 0 1 0 0 0 1
Rappelons que chaque pixel de couleur est défini par trois valeurs.
P4 : images en noir et blanc, données brutes ↑
Les fichiers commençant par la signature P4 concernent les images en noir et blanc, où huit pixels sont codés sur un octet, inscrit en binaire (pas sous forme de chiffres de 0 à 9) dans le fichier.
P4 16 16 …données sous forme d'octets (0 à 255), sans espace ni retour à la ligne
Huit premiers pixels blanc-noir-noir-blanc-blanc-noir-noir-blanc donne le nombre binaire 01100110, soit l'octet 102, qui correspond au caractère f.
P4 8 1 f
Note : le format P1 est plus pratique pour coder une image PNM/PBM en noir et blanc, mais il est 8 fois plus lourd (un octet par pixel). En P4, certaines valeurs ne correspondent pas à des caractères imprimables ; en python2, on utilisait chr() pour former la chaîne des données brutes. En python3, on ne peut plus utiliser les chaînes, codées en UTF-8. Il faut passer par un nouvel objet, le bytearray, et sauvegarder en fichier binaire (voir exemple au point suivant).
P5 : images en nuances de gris, données brutes ↑
Les fichiers commençant par les signatures P5 concernent les images en nuances de gris, chaque octet codant un pixel, ce qui est finalement plus pratique que le format P4. Une image en noir et blanc produira un fichier P5 huit fois plus lourd qu'en P4, mais cela ne devrait plus être le cas une fois converti en PNG (convert).
P5 # le '255' final est la valeur maximale : 20 20 255 … et un octet par pixel
Contrairement aux formats noir/blanc, 0 est l'absence de lumière, le noir, et 255 est le blanc. Il peut donc y avoir 254 nuances de gris entre 0 (le noir) et 255 (le blanc). Ces octets s'inscrivent avec python3 dans une liste que l'on transformera en bytearray. N'oubliez pas un saut de ligne ou une espace entre l'en-tête (première ligne) et les données. Un exemple:
#! /usr/bin/python3 import random entete= b"P5 30 20 255 " # l'espace finale (ou fin de paragraphe "\n") est nécessaire liste=[] # les octets s'ajouteront dans une liste… for i in range(30 *20) : liste +=[random.randrange(256)] # transformation de la liste en 'bytes' et ajout à entete : fichier =entete +bytes(liste) with open("essai.pgm","wb") as fd : # inscrit les données 'bytes' df.write(fichier)
Ce script génère une image que l'interface graphique tkinter associée à python peut afficher.
P6 : images en couleurs, données brutes ↑
Les fichiers commençant par les signatures P6 concernent les images en couleurs, chaque pixel étant codé par trois octets, contenant respectivement les quantités de rouge, vert et bleu.
P6 # 255 est pour ce fichier la valeur maximale pour les trois couleurs, suivie d'une espace : 20 20 255 … et trois octets par pixel
0 est l'absence de couleur, et 255 respectivement le rouge, le vert et le bleu. Entre les deux, 254 nuances de rouge, de vert ou de bleu. En python2, ces octets s'inscrivaient dans une chaîne avec chr(valeur) : en python3, le plus simple est d'inscrire les valeurs numériques (0 à 255) dans une liste puis la transformer en objet 'bytes' sauvegardé comme fichier binaire. N'oubliez pas un saut de ligne ou une espace entre l'en-tête et les données. Un petit exemple :
#! /usr/bin/python3 import random entete= b"P6 30 20 255 " # inscrire dans un objet 'bytes' liste=[] for i in range(30 *20) : liste+=[200 +random.randrange(40)] liste+=[0 +random.randrange(40)] liste+=[50 +random.randrange(40)] fichier =entete +bytes(liste) # transformation en bytearray et ajout à entete with open("essai.ppm", "wb") as fd : # écriture d'un fichier binaire fd.write(fichier)
Ce script génère une image formée de pixels colorés au hasard, autour de la couleur (220, 20, 70), que Tkinter peut charger et afficher.
P7 : Portable Arbitrary Map (PAM) ↑
Infos anglophones sur cette page
Un format récapitule les trois formats précédents (P4, P5 et P6), en ajoutant deux fonctionnalités nouvelles :
- la possibilité de la transparence
- possibilité de 65536 nuances pour chaque composante
Il est (encore?) assez peu reconnu par les applications graphiques, mais il est possible de visualiser une image PAM avec ImageMagic et la transformer en PNG (format universel admettant la transparence). Si ImageMagic est présent sur votre système UNIX, la commande convert peut convertir une image PAM :
convert votreimage.pam votreimage.png
L'en-tête est plus long, plus souple et plus précis :
P7
# premier endroit possible pour un commentaire (croisillon nécessaire)
WIDTH 443
HEIGHT 87
DEPTH 4
MAXVAL 255
TUPLTYPE RGB_ALPHA
# dernier endroit possible pour un commentaire, nécessairement avant ENDHDR (croisillon nécessaire)
ENDHDR
…données au format binaire…
Les paramètres du format de l'image sont à écrire, ligne après ligne, entre P7 et ENDHDR
- les entiers après WIDTH et HEIGHT signalent la largeur et la hauteur en pixels de l'image
- DEPTH définit la profondeur, le nombre de composantes par pixel : 1 octet pour les nuances de gris ou le noir et blanc, 3 pour la couleur, éventuellement +1 pour la transparence
- MAXVAL définit la valeur maximale des données pour la proportion de la composante (rouge, vert, bleu ou transparence). Si MAXVAL vaut de 256 à 65535, chaque composante est inscrite sur deux octets, en commençant par le plus significatif : 2 pour N/B ou nuances de gris, 6 pour la couleur, et encore 2 si l'on veut de la transparence
- TUPLTYPE donne le type de l'image : BLACKANDWHITE, GRAYSCALE ou RGB, éventuellement suivi de _ALPHA qui code le degré de transparence de chaque pixel : 0 est le plus transparent, la valeur définie par MAXVAL est la plus couvrante.
- ne pas oublier de terminer la définition des paramètres avec ENDHDR et de passer à la ligne suivante
- décrire chaque pixel de gauche à droite (rouge-vert-bleu-alpha), de la première à la dernière ligne, en n'utilisant que des valeurs de 0 à la valeur de MAXVAL. Si MAXVAL est supérieur à 255, deux octets sont utilisés par couleur fondamentale (éventuellement ALPHA), octet fort avant octet faible.
Note 2016.05.29
Format expérimenté avec RGB_ALPHA, GRAYSCALE_ALPHA et BLACKANDWHITE_ALPHA, tous trois MAXVAL 255.
Le format noir et blanc est paradoxal : BLACKANDWHITE (avec ou sans _ALPHA) ne peut être écrit de façon économe avec 8 pixels par octet comme le permet le format P4, ce qui revient à une organisation des données selon le format GRAYSCALE_ALPHA avec les deux seules valeurs 0 (noir) et celle définie par MAXVAL (blanc).
Par ailleurs, pour une image en noir et blanc avec des nuances dans la transparence, il faut utiliser GRAYSCALE_ALPHA, avec par exemple MAXVAL à 255, en codant le noir avec 0 et le blanc avec la valeur de MAXVAL.