lundi 3 décembre 2012

Pseudocolorisation en javascript

Dans le dernier billet, j'ai parlé de pseudo-colorisation. L'exemple proposé était écrit en C, avec OpenCV.
Je vais maintenant vous détailler une solution pour faire de même en javascript, dans un navigateur.

(Si l'explication est trop longue, vous pouvez aller directement à la démonstration, à la fin de ce billet).
Pour résumer :

1) charger une image dans un <canvas>
2) charger une colormap dans un <canvas>
3) appliquer l'algorithme de recolorisation (rechercher la valeur de l'image en niveaux de gris dans la colormap)



Javascript & l'objet <canvas>

En "HTML5", quoi que veuille réellement impliquer le nom aujourd'hui, si vous avez un navigateur récent (Opéra, Chrome, Firefox, Safari...), vous pouvez travailler avec un élément bien particulier : le <canvas>.
Un canvas 2D contient tous les pixels d'une image rastérisée (bitmap), tout en étant un élément d'une page web. Le bénéfice d'un <canvas> sur un <img> vient de la possibilité d'accéder à tous les pixels qui composent le canvas (l'élément <img> est lui strictement passif, il ne sert qu'à afficher une image).

A sa création, le Javascript était lent. Maintenant, les navigateurs grand public utilisent tous, sous une forme ou l'autre, différentes améliorations et accélérations (machine virtuelle de type JIT, fonctions inline, etc...). Cela permet d'effectuer des calculs uniquement possibles sur machine fixe jusque là.

Le code javascript présenté ici va implémenter l'algorithme codé avec OpenCV dans le billet précédent.

Représentation en mémoire des images couleurs


Vous avez besoin du tableau de pixels de l'image et du tableau de pixels en provenance de la colormap  (une unique colonne de 256 pixels de haut).


Ensuite, il vous suffit d'itérer sur tous les pixels de l'image d'origine, récupérer la luminosité des pixels, chercher dans la colormap le triplet R,G,B associé à cette valeur, et l'écrire au final dans le tableau d'origine (vous pouvez trouver un schéma pour illustrer ce fonctionnement dans le billet précédent : http://podeplace.blogspot.fr/2012/11/pseudocouleurs-avec-opencv.html)

Image Couleur vue par OpenCV : 3 plans de couleurs (Rouge, Vert, Bleu)
OpenCV travaille avec deux concepts d'image : couleur et niveaux de gris. Une image en niveaux de gris utilise un seul plan de couleur (l'intensité du gris), mais les images couleurs ont trois plans de couleurs (rouge, vert, bleu). Ainsi, la bibliothèque peut gérer la mémoire plus efficacement.




Image en niveaux de gris : OpenCV ne voit qu'un seul plan de couleur en mémoire

Par contre,  l'élément <canvas> ne connaît qu'un seul type d'image : 4 plans de couleurs - rouge, vert, bleu, alpha (pour la transparence).
Cela signifie que pour travailler avec. une "vraie" image en noir et blanc, nous devons stocker la valeur en niveaux de gris dans un des plans de couleurs, et la recopier dans les deux autres canaux (en laissant le canal de transparence complètement opaque).


Image en mémoire pour un <canvas> : 4 pixels de couleurs consécutifs (Rouge, Vert, Bleu, Alpha) pour chaque pixel coloré
Nous ne pouvons pas charger une image JPEG ou PNG directement dans le canvas, car nous ne savons pas vraiment quels sont les choix effectués par le logiciel qui a servi a sauvegarder ces images. On ne sait pas si le logiciel a choisi de ne stocker qu'un seul plan de couleur (en niveaux de gris) ou plusieurs plans (même pour une image noire & blanche !). De plus, certains formats de fichiers n'autorisent pas le choix du nombre de plans de couleurs lors de l'enregistrement.
Dès lors, nous devons partir d'un canvas dans lequel une image a été enregistrée, et convertir ce ces couleurs en niveaux de gris, au sein de l'objet <canvas>.

Conversion d'un canvas couleurs en niveau de gris

Mélange RGB vers Gris par simple moyenne


Le processus de conversion d'une couleur RGB peut être aussi simple que prendre la moyenne des 3 canaux de couleurs, divisée par 3. Dans ce cas, le niveau de gris obtenu n'est pas la meilleure approximation possible du gris correspondant à la couleur d'origine. L’œil humain est plus sensible au vert, et moins sensible au bleu ( ceci étant du aux fonctionnement des cellules de la rétine),. Nous pouvons alors attribuer un poids à chaque canal, de manière à ce qu'il contribue plus ou moins au. niveau de gris final.

Vous pouvez trouver en ligne plusieurs méthodes pour définir ce mélange des trois valeurs RGB. Par exemple, avec le format PAL/NTSC (utilisé par la télévision analogique), le niveau de luminance est défini comme ceci :
Mélange RGB vers Gris (plus proche de la perception humaine)

lum = 0.299 * rouge + 0.587 * vert + 0.114 * bleu

La luminance est définie dans la norme CIE 1931 comme suit :

Y = 0.2126 * rouge + 0.7152 * vert + 0.0722 * bleu


(Les  valeurs utilisées par OpenCV sont Rouge : 0.212671, Vert : 0.715160, et Bleu : 0.072169).

Comme pour d'autres exemples accessibles sur internet, j'ai utilisé les valeurs suivantes :

 luminosité = 0.34 * rouge + 0.5 * vert + 0.16 * bleu 

Tous ces modèles prennent en compte la sensibilité de l’œil humain pour le vert.

Différence entre gris obtenu par moyenne et gris obtenu par le modèle physiologique précédent.
La différence entre les valeurs de gris calculée par les deux méthodes est subtile mais suffisante pour être remarquée.

L'algorithme en javascript


Dans notre exemple, nous pouvons transformer l'image en niveaux de gris au chargement de la page, et travailler ensuite avec cette image modifiée pour lui appliquer notre colormap.

Quand vous travaillez avec un canvas cvs et son contexte associé ctx, vous pouvez récupérer un tableau d'informations spécifiques à cette image via


 var myImageData = ctx.getImageData(0, 0, cvs.width, cvs.height);
 

Cette variable contient un tableau de données , contenant les pixels de couleurs, dans l'ordre suivant : [rouge, vert, bleu, alpha, rouge, vert, bleu, alpha,...].

En javascript, pour éviter de traverser le DOM, qui est une opération lente (lors de l'accès de myImageData.data, vous pouvez détacher le tableau de pixels en tant que nouvelle variable :


 var dataSrc = myImageData.data;
 


et accéder ce qui est contenu dans dataSrc, pour un accès plus rapide.

Ce tableau étant à une dimension, vous pouvez accéder aux différents pixels du tableau avec une boucle comme celle-ci :


for(var y = 0; y < height; y++){         
    for(var x = 0; x < width; x++){
        index = (x + y * width) * 4;
        dataSrc[index+0] = ROUGE;
        dataSrc[index+1] = VERT;
        dataSrc[index+2] = BLEU;

        //dataSrc[index+3] = ALPHA;  //pas de modifications
    }
}

Ici, pas besoin de changer la valeur alpha. Le  *4  permet d'atteindre le pixel suivant en mémoire (car il faut sauter dessus des 4 valeurs RGBA).

Si vous voulez appliquer une conversion en niveaux de gris sur cette image, vous pouvez maintenant écrire la nouvelle valeur de gris dans les canaux rouge, vert et bleu, pour des raisons d'affichage (attention, l'image sera 3 fois plus lumineuse que la vraie conversion RGB->Gris, bien que ce ne soit le cas que lors de l'affichage sur un écran d'ordinateur).



Ne vous inquiétez pas, quand vous allez travailler avec ces pixels à l'étape suivante, pour appliquer la colormap, vous allez utiliser la valeur de gris d'un seul des canaux, puisque c'est la même valeur sur les 3.
Si nous avions choisi de ne stocker la valeur de gris que dans un seul canal, le rouge par exemple, l'image affichée serait seulement en teintes de rouge (et cela peut paraître un peu étrange de considérer une image intégralement en niveau de rouge, vert ou bleu comme une image "en niveaux de gris").


Ce genre d'accès aux pixels est la base de toutes les opérations et filtres sur les images en javascript (et plus généralement dans tous les langages de programmation quand on traite avec des images).

Quand toutes les modifications sont effectuées, n'oubliez pas d'écrire ces pixels à nouveau dans l'image d'origine !


 myImageData.data = dataRes; //ré-attacher la variable
 ctx.putImageData(myImageData,0,0); //l'écrire dans le canvas

Dans l'exemple fourni, nous chargeons une image JPEG classique, la transformons en niveaux de gris au clic d'un bouton, et la recolorisons quand l'utilisateur clique sur une des colormaps. Vous devez recharger la page si vous voulez utiliser une autre couleur, vous redéclencher la conversion couleurs -> niveaux de gris d'origine. Si vous ne le faites pas, vous allez appliquer différentes colormaps successives sur une image déjà modifiée, ce qui va amener une saturation colorée de l'image à la fin.

Démonstration javascript :



Lien pour le projet sur GitHub : https://github.com/Pseudopode/javascript_pseudocolors et l'archive .zip du projet.

1 commentaire: