Programmation objet et géométrie/Objets en Python sous Gimp/Programmation JavaScript sous ImageJ

ImageJ est, lui aussi, un logiciel de traitement d'images, dans lequel les images sont des tableaux de pixels. Mais il est écrit en Java, donc multiplateforme, et possède des fonctionnalités de statistiques très poussées, ce qui renforce son utilité en mathématiques. Et, outre un langage de macros proche de Java (donc objet), son interface de scripts est Rhino qui permet donc de le programmer en JavaScript...

JavaScript sans image

modifier

Pour afficher un résultat (par exemple le contenu d'une variable), on écrit print (en minuscules) suivi, entre parenthèses, du nom de la variable à afficher. Par exemple, on souhaite trouver une expression donnant la somme des carrés des entiers jusqu'à n, en fonction de n. Pour cela, on peut profiter de l'outil d'interpolation d'ImageJ, et créer un tableau en JavaScript, contenant une dizaine de valeurs de cette somme. Plus précisément, ImageJ a besoin de deux tableaux, l'un pour les valeurs de x, l'autre pour les valeurs de y. Voici le script (la variable s contient les valeurs successives de la somme):

var s=0;
var xp=new Array();
var yp=new Array();
for(var i=1;i<=10;i++){
	s+=i*i;
	xp[i]=i;
	yp[i]=s;
	print(xp[i]+"    "+yp[i]);
}

Remarque: Bien qu'il ait l'air itératif, le script ci-dessus est basé sur la récursivité, puisqu'il utilise le fait que   (ligne 5).

Sous ImageJ, on doit ouvrir une console par Plugins>New>JavaScript puis y taper le script ci-dessus. Ensuite le raccourci clavier Control+R lance le script, ce qui a pour effet d'ouvrir une fenêtre dans laquelle se trouve un tableau, donnant en première colonne les entiers successifs, et en deuxième colonne les valeurs successives de la suite. Ce tableau est exactement ce dont l'outil Curve Fitting d'ImageJ a besoin. Il suffit alors de copier ce tableau (Control+A puis Control+C) puis de le coller dans l'outil en question (accessible par Analyse>Tools>Curve Fitting qui crée un tableau où il faut remplacer les données actuelles par celles créées par JavaScript). Après choix du polynôme de degré 4 (tâtonnements précédents ou intuition), on obtient ceci:

 

L'image porte assez d'informations pour qu'on voie que l'expression est exacte (parce que le carré du coefficient de régression est égal à 1), polynomiale (on l'a choisie telle), de degré 3, et ayant x comme facteur. Il ne reste alors plus qu'à reconnaître des fractions dans les coefficients, puis à effectuer la factorisation, par exemple avec un logiciel de calcul formel, mais ceci est une autre histoire...

création d'images en JavaScript

modifier

Lignes de niveau

modifier

L'objet le plus important, c'est ImageJ lui-même, appelé IJ en JavaScript. Pour dessiner, il faut un chevalet, en l'occurence une image blanche, où on va placer des pixels, qui sera en couleurs (RGB) et fera 400 pixels par 400. La méthode createImage de l'objet IJ s'en charge. On peut en fait considérer l'objet dessin ci-dessous comme une instance de la classe IJ. Mais ce n'est pas fini! Il faut ensuite créer un objet (instance de la classe image processor) qui hérite des filtres d'ImageJ. Ce qui se fait en invoquant la méthode getProcessor de l'objet dessin (qui hérite cette méthode d'ImageJ).

Enfin pour que l'image apparaisse à l'écran, il faut la mettre à jour (transférer son contenu dans une partie de la mémoire qui soit visible à l'écran), avec dessin.show() qui est une méthode que l'objet dessin a héritée de sa classe mère (IJ).

Fonction de deux variables

modifier

Puisqu'on travaille dans des coordonnées entières (numéros des pixels), on va représenter par ses lignes de niveau, la fonction pgcd(x,y) qui est elle-même entière. Dans la boucle sur l'abscisse i, on a placé un rectangle (méthode showProgressBar de l'objet IJ, affiché en bas à droite dans la console ImageJ) qui affiche la progression du programme, ce qui permet de voir quelle partie de l'image occupe le plus de temps de calcul. En mode RGB, les couleurs sont indiquées par des entiers longs, dont le reste modulo 256 indique la quantité de bleu. Donc une valeur nulle représente du noir, une valeur 255 du bleu clair, une valeur 255*256 du vert clair, et une valeur 255*256*256 du rouge clair. En multipliant le pgcd par 256*256, on obtient une valeur de rouge d'autant plus claire que le pgcd est élevé. Mais lorsque celui-ci dépasse 255, il est de nouveau représenté par une valeur sombre (visible en bas de la diagonale ci-dessous). Le script pour représenter en rouge les pgcd des nombres de 0 à 399 est ici:

function pgcd(a,b){//on crée une instruction "pgcd" en JavaScript
	var x=a, y=b;
	while(y>0){//implémentation itérative
		t=x%y;
		x=y;
		y=t;
	}
	return(x);
}
var N=400;//taille de l'image
dessin = IJ.createImage("pgcd", "RGB", N, N, 1);//instanciation de la classe "image"
ip = dessin.getProcessor();//ip est un "processeur d'images"
for(var i=0;i<N;i++){
	IJ.showProgress(i, N-1);//affichage de l'état du programme
	for(var j=0;j<N;j++){
		c=pgcd(i,j)*256*256;//niveau de rouge
		ip.putPixel(i, j, c);//pour le pgcd
	}
}
dessin.show();//mise à jour de l'image

Comme il y a beaucoup de nombres premiers entre eux, l'image obtenue est sombre:

 

Représentation dans l'espace

modifier

ImageJ permet aussi de convertir une surface donnée par ses lignes de niveau comme ci-dessus, en une représentation graphique sous forme d'une surface dans l'espace. De même que ci-dessus, on va donc créer une image où les niveaux de gris représentent une fonction   et la faire convertir par ImageJ en dessin de la surface correspondante, vue en perspective. La fonction   considérée est la somme des distances entre le point de coordonnées (i,j) et les points de coordonnées respectives A(200;50), B(100;350) et C(350;350) (Attention: L'axe des y est orienté vers le bas). On sait depuis Fermat et Torricelli que cette fonction atteint son minimum en un point appelé point de Fermat-Torricelli du triangle dont les sommets sont donnés ci-dessus.

Comme on fait de la géométrie, il est assez efficace de créer des objets point et triangle qui vont considérablement simplifier le code JavaScript (avec des méthodes adéquates) qui suivra. Tout d'abord, l'objet point ne possède que deux propriétés, ses coordonnées, et une méthode, son affichage. On a besoin d'une fonction distance (entre deux points) et d'une procédure triangle (ayant pour variables ses trois sommets):

function point(x,y){//objet point
	this.x=x;
	this.y=y;//les coordonnées sont des propriétés du point
	this.plot=function(proc){//l'affichage est une méthode du point
		proc.putPixel(x,y,255*256+255*256*256);
	}
}

function distance(a,b){//a et b sont deux points
	return Math.sqrt(Math.pow(a.x-b.x,2)+Math.pow(a.y-b.y,2));
}

function triangle(a,b,c,proc){//a, b et c sont les sommets du triangle, 
                              // proc est là où on le dessine
	proc.moveTo(a.x,a.y);// on part du premier sommet
	proc.lineTo(b.x,b.y);// tracé du côté [ab]
	proc.lineTo(c.x,c.y);// puis du côté [bc]
	proc.lineTo(a.x,a.y);// et du côté [ca]
}

On note A.x l'abscisse de A (x est une propriété de l'objet A). La création de l'image se fait comme ci-dessus, avec la méthode createImage de l'objet IJ. On obtient alors un objet plan qui est une image (le chevalet initialement blanc). La méthode getProcessor permet alors d'avoir les filtres de cet objet (flou, Voronoi etc.). Ce processeur ne servira ici que pour ses méthodes de tracé de pixel et de segments (le triangle):

var N=400;//taille de l'image
plan=IJ.createImage("Fermat", "16-bit", N, N, 1);//création de l'image en noir et blanc
ip = plan.getProcessor();//les filtres du plan dans ip

A=new point(N/2,N/8);
B=new point(N/4,7*N/8);
C=new point(7*N/8,7*N/8);//les sommets du triangle sont des points

for(var i=0;i<N;i++){
	IJ.showProgress(i, N-1);//on regarde où on en est
	for(var j=0;j<N;j++){
		P=new point(i,j);
		d=distance(P,A)+distance(P,B)+distance(P,C);
		c=Math.round(d*64-32*256);//le niveau de blanc est la somme des distances 
		ip.putPixel(i, j, c);//normalisée empiriquement
	}
}
triangle(A,B,C,ip);//dessin du triangle
plan.show();//affichage une seule fois, pour gagner du temps

L'exécution du script produit alors l'image suivante:

 

Comme c'est une image en noir et blanc, ImageJ permet automatiquement de tracer la surface, avec Analyse>Surface Plot:

 

Dessin de fractales

modifier

Tout ce qui précède peut servir à construire des images fractales en JavaScript:

Sierpinski

modifier

Version itérative

modifier

Le triangle de Sierpinski, version IFS, est placé dans un objet appelé flocon, qui est une instance de la classe image:

flocon = IJ.createImage("Sierpinski", "RGB", 400, 400, 1);
ip = flocon.getProcessor();//cet objet contient les filtres
var x=Math.random()*400;
var y=Math.random()*400;
var xx, yy, c;
for(var i=0;i<100000;i++){
	de=Math.ceil(Math.random()*3);
	switch(de){
	case 1: {
		xx=(x+200)/2;
		yy=(y+50)/2;
		c=255*256*256;//rouge
		break;
		}
	case 2: {
		xx=(x+350)/2;
		yy=(y+350)/2;
		c=255*256;//vert
		break;
		}
	case 3: {
		xx=(x+50)/2;
		yy=(y+350)/2;
		c=255;//bleu
		break;
		}
	}
	x=xx;
	y=yy;
	ip.putPixel(Math.round(x), Math.round(y), c);
}
flocon.show();//affichage du flocon une fois calculé

Le fichier obtenu est celui-ci (en quelques secondes):

 


Version récursive

modifier

On peut aussi utiliser l'objet triangle utilisé ci-dessus pour dessiner le triangle de Sierpinski comme un polygone. Seulement on aura besoin non pas de la distance entre deux points, mais de leur milieu (un point):

function point(x,y){//objet point
	this.x=x;
	this.y=y;//les coordonnées sont des propriétés du point
	this.plot=function(proc){//l'affichage est une méthode du point
		proc.putPixel(x,y,255*256+255*256*256);
	}
}

function milieu(a,b){//a et b sont deux points
	return new point((a.x+b.x)/2,(a.y+b.y)/2);
}

function triangle(a,b,c,proc){//a, b et c sont les sommets du triangle, 
                              // proc est là où on le dessine
	proc.moveTo(a.x,a.y);// on part du premier sommet
	proc.lineTo(b.x,b.y);// tracé du côté [ab]
	proc.lineTo(c.x,c.y);// puis du côté [bc]
	proc.lineTo(a.x,a.y);// et du côté [ca]
}

Avec ces objets, la définition récursive du polygone de Sierpinski est assez courte:

function sierpinski(a,b,c,proc,n){//définition récursive du triangle de S.
	if(n<1){//ordre nul: on ne dessine que le triangle
		triangle(a,b,c,proc);
	} else {
		sierpinski(a,milieu(a,b),milieu(a,c),proc,n-1);
		sierpinski(milieu(a,b),b,milieu(b,c),proc,n-1);
		sierpinski(milieu(a,c),milieu(b,c),c,proc,n-1);
	} 	
}

Pour obtenir le dessin, il suffit d'appeler la fonction sierpinski ci-dessus avec les trois points A, B et C et l'ordre 6 (à l'ordre 0, seul un triangle est tracé; en fait à l'ordre   il y a exactement   triangles):

var N=400;//taille de l'image
img=IJ.createImage("SierpinskiTrianglesImageJ", "8-bit", N, N, 1);
construction = img.getProcessor();
A=new point(N/2,N/8);
B=new point(N/4,7*N/8);
C=new point(7*N/8,7*N/8);
sierpinski(A,B,C,construction,6);
img.show();

L'image obtenue est incontestablement un triangle de Sierpinski:

 


Comme le triangle ci-dessus est en noir et blanc, on peut utiliser la méthode statistique Fractal Box Count d'ImageJ pour estimer sa dimension fractale. Le diagnostic d'ImageJ est alors celui-ci:

 

La valeur exacte   est légèrement supérieure à celle estimée par ImageJ. Ce biais est sans doute dù à ce que les détails les plus fins se fondent dans les pixels. Refaire le tout avec une plus grande résolution est également laissé en exercice.

Une double boucle parcourant tous les pixels permet de dessiner un ensemble de Julia par balayage (méthode moins rapide que l'itération inverse mais plus colorée). Le script permettant de dessiner l'ensemble de Julia de   ci-dessous (x allant de -1 à 1 et y aussi) est laissé en exercice.

 

En voyant cette image, comment ne pas avoir le coup de foudre pour les ensembles de Julia?

Mandelbrot

modifier

La même méthode permet de dessiner l'ensemble de Mandelbrot en donnant à chaque pixel une couleur proportionnelle au nombre d'itérations qu'il a fallu pour que le terme de la suite  ,   sorte du disque de centre l'origine et de rayon 2. Comme précédemment, on crée une image cardio qui est un objet, une instance de la classe IJ, et on y place l'un après l'autre des pixels dont la couleur est calculée dans une boucle:


var N=400;//taille de l'image
cardio=IJ.createImage("Mandelbrot", "RGB", N, N, 1);//l'image est en couleurs et fait N fois N pixels
ip = cardio.getProcessor();//l'objet "ip" est un retoucheur d'image
for(var i=0;i<N;i++){
	IJ.showProgress(i, N-1);//on regarde le café chauffer
	for(var j=0;j<N;j++){
		x0=(i-3*N/4)/N*4;//x va de -3 à 1
		y0=(j-N/2)/N*4;//y va de -2 à 2: image carrée
		x=x0;
		y=y0;//le premier terme de la suite est c=x0+iy0
		var k=0; var m=0;//k mesure le temps d'échappement
		while(k<256 && m<4){//soit le nombre d'itérations pour avoir m>4
			xx=x*x-y*y+x0;//partie réelle de z*z+c
			yy=2*x*y+y0;//partie imaginaire de z*z+c
			m=xx*xx+yy*yy;//m est le carré du module
			x=xx;
			y=yy;
			k++;//on passe au terme suivant de la suite
		}
		ip.putPixel(i, j, 8*k);//le bleu est d'autant plus intense qu'on diverge lentement
	}
}
cardio.show();//affichage de la cardioïde à la fin

En observant la barre de progrès, on constate que la partie gauche de l'ensemble de Mandelbrot est beaucoup plus rapide à calculer que la partie droite. Ce phénomène illustre la notion de vitesse de divergence.