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 :

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

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 :

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

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.