Le format image PNM + PAM / PNM + PAM easy graphic format

  Pour cette info en français, voir www.jchr.be/python/pnm-pam.htm

PNM (Portable aNy Map) is a graphic file format by Jef Poskanzer (b. 1958) in the eighties.

Such graphic files can have more specific extensions:

  • .pbm (Portable BitMap) for black/white graphics files
  • .pgm (Portable GrayMap) for grays graphics files (it can be more than 50)
  • .ppm (Portable PixMap) for coloured graphic files

It's no compressed format, and even worse (a single pixel can be encoded '243 222 123 '!), but very easy to create. A PNM begins with a tag ('P1' to 'P6'), a width, a height and (for gray files and coloured maps) a hightest value; after comes the data.

The P1, P2 and P3 data are ascii-coded and space-separated. A comment is possible between an # and an end-of-line before the dimensions; the P4, P5 and P6 data are binary-coded, with no separation.

P1: black&white ascii-coded image

A PNM Black/White file begins with 'P1 width height ' (width and height are pixel values), followed by '0' (white) or '1' (black), and - if you want - a space between each pixel.

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
"P1 6 4  111111100001100001111111 " is an equivalent (a last dummy pixels seems to be necessary).

Note: this graphic is a 6x4 pixels images, you'll have to zoom it.

A 1000x1000 pixels will be 1 or 2MB large, but it's possible to shrink it by 8 (each pixel is a bit) with the P4 binary format (see below).

P2: gray ascii-coded image

These files begins with 'P2' tag, a width and a height, followed by the highest value (until 255). The data are also ascii-typed and must have space-separations. In the following sample the values go from 0 (black) to 6 (white)

P2 4 4 6
6 5 4 3
5 4 3 2
4 3 2 1
3 2 1 0
"P2 4 4 6  6 5 4 3 5 4 3 2 4 3 2 1 3 2 1 0 " is an equivalent.

P3: coloured ascii-coded image

A coloured graphic ASCII begins with a 'P3' tag, follow by a width, a height and a maximal value. The datas are a sequence of three values which are the red, green and blue values:

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

The number of spaces betwen each value doesn't have any importance. Red, green and blue are only coded with 0 or 255: in this example, it's possible to code in an easier way, setting the maximal value to 1 (before the '#'):

P3 # header (comment between '#' and carriage-return)
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
"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 " is an equivalent

Note: you'll have to zoom this image to see these nine pixels!

P4, P5 et P6: binary formats

P4, P5 et P6 tags in the beginning of a PNM file are respectivily for black/white, gray scaled and coloured graphics where values are binary coded:

P4: black & white binary-coded:

P4 16 16
...byte-coded data

...without space nor carriage-return ('\n','\r') 

For a P4-PNM file, the first eight pixels must be translated into a value between 0 ('00000000') and 255 ('11111111').

For instance, WBBWWBBW is encoded through the binary number '01100110' = 102, (the 'f' character).

You can translate the binary number with int(number,2), and transform it into a character with char():

char(int("01100110",2)) 

The file will be:

"P4 8 4
ffff"

Some values cannot be written with ASCII-character. You can use chr(int('001100110',2) in a routine.

The best should be adding a line in your script with a command which transforms a PNM-file into PNG-one with os.system("convert image.pnm image.png") from the ImageMagick package (Unix only?).

P5 gray binary-coded image: each pixels is coded by a byte.

P6 colour binary-coded image: each pixel is coded by three bytes (red/green/blue)

NEW! (2016.05.29)

P7: Portable Arbitrary Map (PAM)

P4, P5 or P6 images can be written in a P7 new format, adding transparency or a 2 byte colour definition (256->65536 values) if wanted. ImageMagic can read and convert a PAM image (easy to generate) into PNG (universal format with transparency). If ImageMagic is installed on a UNIX system, you convert it with an easy command:

convert votreimage.pam votreimage.png

The header is a little longer, more versatile and more precise:

P7
# first comments after P7
WIDTH 443
HEIGHT 87
DEPTH 4
MAXVAL 255
TUPLTYPE RGB_ALPHA
# the last comment before ENDHDR
ENDHDR
...binary data byte, from 0 to 255...

  • Each parameter must be on a single line, between P7 and ENDHDR
  • DEPTH is the number of bytes per pixel: 1 for gray or B/W, 3 for colour, 1 more for transparency
  • MAXVAL can be up to 65535; if more than 255, DEPTH must be 2, 6 or 4, 8 if transparency - each canal is written with 2 bytes
  • TUPLTYPE is the image type: BLACKANDWHITE, GRAYSCALE or RGB, you'll have to add _ALPHA for transparency (0 is completely transparent, MAXVAL is completely coloured)
  • Parameters definition ends with ENDHDR and a linefeed
  • data definition of colour pixels is RVBA or RRVVBBAA

 

 

How to generate a P1-file

This script generates a black circle on a white background (you give the radius). It tests the pixels one by one: are they at the good distance from the center (x°,y°)? Pythagore answers with r²=(x-x°)²+(y-y°)². A file 'circle'+diameter+'.pnm' is created.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#! /usr/bin/python -Qnew 
 
import os
 
r=input("radius: ") # asking for a radius
d=r*2+1 # image width and height 
diam=str(d) # string  for header
fichier="P1 %s %s " %(diam,diam) # header 
 
for i in range(d): # rough way to produce a circle
  for j in range(d): 
    if round((abs((i-r)**2)+abs((j-r)**2))**.5)==r: 
      fichier+="1" 
    else: 
      fichier+="0" 
  fichier+="\n" # a last dummy pixel seems to be necessary 
 
handle=open("circle"+diam+".pnm","w") 
handle.write(fichier) 
handle.close() 
 
 
try:
   os.system("convert rond"+diam+".pnm rond"+diam+".png")
except:
   print "Sorry! no ImageMagick package found!"

Cycles sur trois niveaux / Three level cycles

Voici la suite de la page spirographe, à laquelle un niveau a été ajouté. Mon avatar a été réalisé avec cette application. Comme je suis paresseux, le paramétrage est déterminé par une fonction random.

En python 2, il faut préciser -Qnew pour la division 'réelle; 'print' n'est pas encore la fonction print() 

Here comes a tail for spirographe: a third level is added. My avatar comes from this script. As I'm rather lazy, the params are randomly produced, but comments are few by few translated.

Python 2 requires '-Qnew' for 'real' division, and 'print' is a statement, not yet the 'print()' python3 function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#! /usr/bin/python -Qnew
# -*- coding: utf-8 -*-
import os,time,math,random
 
t0=time.time()
print "\n Initialisation"
 
# radius length (px) for cycles k1, k2, k3 
k1=100+random.randrange(100)
k2=50+random.randrange(50)
k3=30+random.randrange(30)
 
# image dimension 
centre=k1+k2+k3+5
cote=centre*2
 
# multiplicity of secondary and tertiary cycles
 
r2=random.randrange(10)+3
r3=random.randrange(14)+6
 
 
# clockwise secondary cycle if s2==-1
s2=random.randrange(2)*2-1
if s2==1:
  ss2="p";
else:
  ss2="m"
 
# clockwise tertiary cycle if s3==-1
s3=random.randrange(2)*2-1 # 1 ou -1 
if s3==1:
  ss3="p";
else:
  ss3="m"
 
nom="spiro-%d-%d%s%d-%d%s%d" %(k1,k2,ss2,r2,k3,ss3,r3) # filename with params 
entete="P1 %s %s " %(cote,cote) # header for an ascii-coded black and white PNM file
 
 
matrice=['0']*cote*cote # white pixels matrix
 
t1=time.time()
print "  ",t1-t0,"\n\n Matrix filling",nom+".pnm"
 
for i in range(8000):
  angle=i*math.pi/4000
  x=int(round(centre+math.cos(angle)*k1+math.cos(angle*r2)*k2+math.cos(angle*r3)*k3))
  y=int(round(centre+math.sin(angle)*k1+math.sin(angle*r2)*k2*s2+math.sin(angle*r3)*k3*s3))
  matrice[y*cote+x]="1"        # black pixel 
  matrice[y*cote+x+1]="1"      # right pixel 
  matrice[y*cote+cote+x]="1"   # bottom pixel (a line down) 
  matrice[y*cote+cote+x+1]="1" # bottom-right pixel 
 
fichier=entete+"".join(matrice)
 
han=open(nom+".pnm","w")
han.write(fichier)
han.close()
 
t2=time.time()
print "  ",t2-t1,"\n\n File creation",nom+".trace.pnm"
 
for i in range(1000): # dotted for primary and secondary cycles
  angle=i*math.pi/500
  x=int(round(centre+math.cos(angle)*k1+math.cos(angle*r2)*k2))
  y=int(round(centre+math.sin(angle)*k1+math.sin(angle*r2)*k2*s2))
  matrice[y*cote+x]="1"      # setting black pixel 
 
fichier=entete+"".join(matrice)
 
han=open(nom+".trace.pnm","w")
han.write(fichier)
han.close()
  
t3=time.time()
print "  ",t3-t2,"\n\n  ",t3-t0,"pour le temps global"
 
print
 
# 'eom' must be changed by into your favorite image-viewer 
os.system("eom "+nom+".pnm &")
 
q=raw_input(" Conserver cette image (O/N): ")
if q=="N" or q=="n":
  os.system("rm "+nom+".pnm") # rm delete a file (UNIX) 
  os.system("rm "+nom+".trace.pnm")
  print "   Not saved image\n"
else:
  print "   Saved image\n"

On a common laptop (Celeron 2core 1,8GHz on GNU/Linux Debian Wheezy), less than .2sec:

  • primary cycle: 140 pixels, counterclockwise
  • secondary cycle: 69 pixels, 4 coils clockwise
  • cycle tertiaire: 33pixels, 16 coils, counterclockwise

...with the dotted primary+secondary cycle:

 

  Some more images

 

 

 

  Funny, isn't it?

 

 

 

Spirographe

Il ne s'agira pas vraiment de la simulation d'un spirographe, qui utilise des hypotrochoïdes (Wikipedia dixit). Deux cycles sont composés: un point tourne autour d'un centre qui tourne lui-même, de façon plus lente, autour d'un point fixe comme la lune tourne autour de la terre, qui tourne autour du soleil.

Attention: j'utilise encore python 2.x, qui doit se lancer avec -Qnew pour disposer de la division 'réelle'. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#! /usr/bin/python -Qnew
# -*- coding: utf-8 -*-
import math
 
gr=150 # grand rayon
pr=50  # petit rayon
 
# rapport du cycle secondaire. Attention:
# si signe==1, le nombre de spires= n-1
# si signe==-1, le nombre de spires= n+1 
rapport=7
signe=1; s="p"
 
# decommenter la ligne suivante pour inverser le sens de la courbe secondaire
#signe=-1; s="m"
 
centre=gr+pr+10 # eloignement maximal du centre + marge de 10 pixels
cote=centre*2  # image side 
 
matrice=['0']*cote*cote # pixels matrix 
 
for i in range(4000): # courbe resultant de la revolution de cercles autour d'un cercle 
  angle=i*math.pi/2000 # le cercle fait 2PI, chaque pas 2PI/10000, donc PI/5000
  x=int(round(centre+math.cos(angle)*gr+math.cos(angle*rapport)*pr))
  y=int(round(centre+math.sin(angle)*gr+math.sin(angle*rapport)*pr*signe))
  matrice[y*cote+x]="1" # chaque pixel '1' est noir
 
nom="spiro-%d-%d%s%d" %(gr,pr,s,rapport)
 
entete="P1 %s %s " %(cote,cote) # entete de fichier PNM noir/blanc au format ASCII
fichier=entete+"".join(matrice) # transforme la liste ['0','0','1','0'...] en chaîne '0010...'
 
han=open(nom+".pnm","w")
han.write(fichier)
han.close()
 
# Compl´etion de l'image avec le cercle de base en pointill´e
 
for i in range(200): # tour trigonometrique plus rapide
  angle=i*math.pi/100
  x=int(round(centre+math.cos(angle)*gr))
  y=int(round(centre+math.sin(angle)*gr))
  matrice[y*cote+x]="1"
 
fichier=entete+"".join(matrice)
 
han=open(nom+".trace.pnm","w")
han.write(fichier)
han.close()

 

Petit paradoxe:alors que le rapport entre le cycle secondaire est de 7 par rapport au cycle central, on obtient seulement six spires. C'est parce qu'il faut décompter le tour du cycle central sur lui-même.

En décommentant la ligne 15, l'orientation des spires est inversée. Et ici, le nombre de spires est supérieur au rapport 7.

  Note: j'ai placé l'image contenant le tracé en pointillé du cycle central. Une suite est prévue!!!