A minimal ImageJ Plugin in Clojure: image inversion

Posted by fmn on January 15, 2010 at 1:02 pm.

I show in this post how to write an ImageJ plugin with Clojure. This example is taken from Digital Image Processing: An Algorithmic Introduction Using Java: an image inversion (page 32).

The goal is to invert all the pixels of a 8-bit grayscale image, turning an image into its negative. As a pixel value is coded with 8 bits, the higher possible value is 255. The operation is thus to transform each pixel value v into 255-v.

I first present the plugin in Java, with a description of the essentials elements of an ImageJ plugin. Then, i give several Clojure versions. The last is as fast as the Java one, but more reusable.

Java version

First, a strict correspondence is needed between the plugin name and the file containing the code. Moreover, this name must includes an _ (underscore). The Java code is here write in the file invert_java.java, the plugin name must be invert_java.

// file 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); } } } }

The plugin is a PlugInFilter because it will be applied to an existing image. Two methods are needed: setup() and run().

The setup() method is called by ImageJ to obtain informations on the plugin. In this example, the method returns DOES_8G, indicating that the plugin only deals with 8-bit grayscale images.

The run() method do the job, receiving an ImageProcessor (here ip) containing the image to process. The processing consists here in an image scan (with two loops). For each position (u, v), the pixel value is read with getPixel() and modified with putPixel().

The compilation produces a file invert_java.class to be installed in the ImageJ plugins dir. Another dir can be given in the java command line:

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
Inverting a 1376x1035 image takes about 0.01s, with approximately 140 million pixels processed per second.

Clojure versions

The Clojure code is the following, to be placed in a file src/invert_clj.clj (for more details on a .class generation from Clojure, see this post).

;; file 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))))))

The run method rests on the doseq macro. doseq allows side-effects execution for values taken in sequences. Thus for each u taken in the sequence (0, 1, 2, ..., w-1) and for each v in (0, 1, 2, ..., h-1), the line (.putPixel ip u v (- 255 (.getPixel ip u v))) is executed.

The compilation produces a bunch of .class files in the classes dir:

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
These .class files must be considered exactly as .class files produced with 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
Beware, when launching ImageJ, you must imperatively have in the classpath the dir containing the .class files. Clojure needs this. You must also add clojure.jar.

The 1376x1035 image inversion takes roughly 20s, with about 73000 pixel/second. This is very slow compared to the Java version. Actually, this first Clojure version is concise, but not optimized. It is possible to add type hints and to transform the doseq use in two real loops:

(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))))))
Moreover, the function - is replaced by unchecked-subtract, which is like the - operator in Java. In Clojure - is a very general function, thus slower. The inc (increment) call is replaced by unchecked-inc for the same reasons. With these modifications, the inversion take 0.12s, about 120 million pixel/second. This time is very comparable to the Java one.

But Clojure allows some elegance. Using two loops in order to scan an image is a very frequent pattern in image processing. It is thus wise to put it in a 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)))))

This way, the two imbricated loops no more appear. The run method is more concise and clear (thus less error-prone). The processing time is identical to the previous version (about 120 million pixel/second). This code is even more reusable as the Java version, by the isolation of the image scanning pattern in a macro.

FMN.

3 Comments

  • Godbe says:

    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))))))))

  • fmn says:

    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.

  • hamza says:

    Je veux apprendre à reconnaître des formes géométriques dans le Image. Grâce à la technologie imageJ avec java et merci

Trackbacks / Pingbacks

Leave a Reply