Je présente ici comment écrire un plugin pour ImageJ avec Clojure. L’exemple est tiré de Digital Image Processing: An Algorithmic Introduction Using Java: l’inversion d’une image (page 32).
Il s’agit d’inverser tous les pixels d’une image codée en niveaux de gris, sur 8 bits, transformant ainsi une image en son négatif. Puisqu’un pixel est codé sur 8 bits, la valeur maximale possible est 255. Il faut donc transformer tout les pixels v de l’image en 255-v.
Je commence par présenter le code Java, décrivant ainsi les éléments nécessaires à un plugin ImageJ. Puis plusieurs versions Clojure sont données. La dernière version est aussi rapide que le code Java, et pourtant plus réutilisable.
Version Java
Il faut une correspondance entre le nom du plugin et le fichier contenant son code. De plus ce nom doit comporter un _ (underscore). Le code source Java est ici placé dans un fichier invert_java.java, le nom du plugin est donc obligatoirement invert_java :
// fichier invert_java.java import ij.ImagePlus; import ij.plugin.filter.PlugInFilter; import ij.process.ImageProcessor; public class invert_java implements PlugInFilter { public int setup (String arg, ImagePlus im) { return DOES_8G; } public void run (ImageProcessor ip) { int w = ip.getWidth(); int h = ip.getHeight(); for (int u = 0; u < w; u++) { for (int v = 0; v < h; v++) { int p = ip.getPixel(u, v); ip.putPixel(u, v, 255 - p); } } } }
Le plugin est un PlugInFilter car il doit être appliqué sur une image existante. Il faut écrire deux méthodes : setup() et run().
La méthode setup() est appelée par ImageJ pour obtenir des informations sur le plugin. Dans cet exemple, la méthode renvoie la valeur DOES_8G pour indiquer que le plugin ne gère que les images en niveaux de gris sur 8 bits.
La méthode run() effectue le travail, recevant un ImageProcessor (ici ip) contenant l’image à traiter. Le traitement consiste à parcourir l’image (deux boucles). Pour chaque position (u, v), la valeur du pixel est lue avec la méthode getPixel(), puis modifiée avec putPixel().
La compilation produit un fichier invert_java.class à placer dans le répertoire plugins d’ImageJ. Si un autre répertoire est choisi, il faut alors l’indiquer au moment de lancer ImageJ:
fredn:> javac -cp /home/fredn/ImageJ/ij.jar invert_java.java
fredn:> java -Dplugins.dir=/home/fredn/ij-plugins\
-cp /home/fredn/ImageJ/ij.jar ij.ImageJ
L’inversion d’une image 1376×1035 prend environ 0,01s, avec environ 140 million de pixels traités par seconde.
Versions Clojure
Le code équivalent en Clojure est le suivant, à placer dans un fichier src/invert_clj.clj (pour quelques détails sur la production d’une classe et l’organisation des fichiers voir ce billet).
;; fichier src/invert_clj.clj (ns invert_clj (:gen-class :implements [ij.plugin.filter.PlugInFilter])) (defn -setup [this arg im] ij.plugin.filter.PlugInFilter/DOES_8G) (defn -run [this ip] (let [w (.getWidth ip) h (.getHeight ip)] (doseq [u (range w) v (range h)] (.putPixel ip u v (- 255 (.getPixel ip u v))))))
La méthode run s’appuie ici sur la macro doseq qui permet de parcourir des séquences. Ainsi pour chaque valeur de u prise dans la séquence (0, 1, 2, ..., w-1) et pour chaque valeur de v prise dans la séquence (0, 1, 2, ..., h-1), la ligne (.putPixel ip u v (- 255 (.getPixel ip u v))) est exécutée.
La compilation produit un ensemble de fichier .class placés dans le répertoire classes.
fredn:> java -cp /home/fredn/ImageJ/ij.jar\
:/home/fredn/.libjar/clojure.jar:./src:./classes\
clojure.main -e "(compile 'invert_clj)"
invert_clj
fredn:> ls classes
invert_clj.class invert_clj$_run__9.class
invert_clj__init.class invert_clj$_setup__6.class
invert_clj$loading__6309__auto____4.class
Ces fichiers .class sont à utiliser exactement comme les fichiers .class produits précédemment avec javac.
fredn:> cp classes/* /home/fredn/ij-plugins/Crestic/ fredn:> java -Dplugins.dir=/home/fredn/ij-plugins\ -cp /home/fredn/.libjar/clojure.jar:\ /home/fredn/ImageJ/ij.jar:\ /home/fredn/ij-plugins/Crestic\ ij.ImageJ
Attention, lors du lancement d’ImageJ, il faut impérativement que le répertoire contentant les fichiers *.class soient dans le classpath pour que Clojure soit à même de les exploiter. Il faut également ajouter clojure.jar.
L’inversion d’image image 1376×1035 prend environ 20s, soit environ 73000 pixel/seconde. Ce qui est beaucoup plus lent que la version Java. En fait ce premier code est concis, mais absolument pas optimisé. Il est possible d’ajouter des déclarations et de transformer l’utilisation de doseq en deux boucles imbriquées:
(defn -run [this #^ij.process.ImageProcessor ip] (let [w (int (.getWidth ip)) h (int (.getHeight ip))] (loop [u (int 0)] (when (< u w) (loop [v (int 0)] (when (< v h) (.putPixel ip u v (unchecked-subtract 255 (.getPixel ip u v))) (recur (unchecked-inc v)))) (recur (unchecked-inc u))))))
La fonction unchecked-subtract remplace l’opération -. La fonction unchecked-subtract se comporte comme l’opérateur - en Java, alors que la fonction - en Clojure est une fonction très générale, donc plus lente. De même, l’appel de inc est transformée en appel de unchecked-inc. Avec ces modifications, l’inversion prend environ 0,012s, soit environ 120 million pixel/seconde. Le temps de traitement est ainsi parfaitement comparable que celui du code Java.
Pour terminer, le parcours des pixels d’une image est un idiome qui revient très fréquement. Il est donc judicieux de le placer dans une macro:
(defmacro loop-on-ip [ip u v & body] `(let [w# (int (.getWidth ~ip)) h# (int (.getHeight ~ip))] (loop [~u (int 0)] (when (< ~u w#) (loop [~v (int 0)] (when (< ~v h#) ~@body (recur (unchecked-inc ~v)))) (recur (unchecked-inc ~u)))))) (defn -run [this #^ij.process.ImageProcessor ip] (loop-on-ip ip u v (.putPixel ip u v (unchecked-subtract 255 (.getPixel ip u v)))))
Ainsi, l’utilisation des deux loop n’apparait plus et le code de la méthode run est parfaitement clair et concis. Le temps de traitement est identique à la version précédente : environ 120 million pixel/seconde. Le code est également plus réutilisable que la version Java avec le placement du parcours des pixels dans une macro.
FMN.
- Une sélection (automatique) de billets similaires :
- Utiliser ImageJ dans un notebook Sage, un exemple d'appel de Java depuis Python
- Représentation d'images (Clojure et Java), première partie.
- Représentation d'images (Clojure et Java) #2
- Image numérique et tableau : est-ce identique ?
- Fonction avec méta-données (Clojure). Représentation d'images #3
Tags: clojure, exemple, image, imagej, java, niveaux de gris, programmation, traitement d'image, tutoriel
You can also use the let tric so no reflection is used when calling putPixel
(ns invert_clj (:gen-class :implements [ij.plugin.filter.PlugInFilter])) (defn -setup [this arg im] ij.plugin.filter.PlugInFilter/DOES_8G) (defn -run [this #^ij.process.ImageProcessor ip] (let [w (.getWidth ip) h (.getHeight ip)] (doseq [u (range w) v (range h)] (let [u1 (int u) v1 (int v)] (.putPixel ip u1 v1 (int (- 255 (.getPixel ip u1 v1))))))))Thanks for the suggestion. The processing time with this version is 0.165s, ~10 million pixel/second, a good improvement. What is captivating with Clojure is that you can start with a version and granularly improve it.