Thursday, January 13, 2011

Images in a jiffy: speeding up Jiffle with Janino

Jiffle is a scripting language for raster algebra dreamed up by myself and Andrea Antonello. You can use it to create, combine and analyse raster (pixel) images. It's based on the r.mapcalc language for map calculations in the GRASS GIS program.

The idea of Jiffle is to let you concentrate on the interesting bits rather than having to write the incredibly tedious boiler plate code that is normally required to work with image data using Java. The image here of a simple trigonometric function was created with this script...
    xc = width() / 2
    yc = height() / 2
    dx = (x()-xc)/xc
    dy = (y()-yc)/yc
    d = sqrt(dx^2 + dy^2)
    outImg = sin(8 * PI * d)

The Jiffle parser, developed using ANTLR, converts a script like the one above into a tree representation of the expressions (Abstract Syntax Tree or AST). Up until now, running the script involved walking this tree multiple times (once per destination image pixel) and executing the expression like an interpreter. While this worked, it was much too slow to process large images.

Enter Janino, which advertises itself (quite accurately) as a "super-small, super-fast Java™ compiler". One of things you can do with Janino is to compile source and load the resulting bytecode in-memory. To apply this to Jiffle, I first replaced the ANTLR grammar for the run-time tree walker with one that translates an AST into a Java method body. For example, the script above is translated to this...

double xc=_width / 2.0;
double yc=_height / 2.0;
double dx=(_x - xc) / xc;
double dy=(_y - yc) / yc;
double d=Math.sqrt(Math.pow(dx, 2.0) + Math.pow(dy, 2.0));
writeToImage("out", _x, _y, _band, Math.sin(25.132741228718345 * d));

Next, I created a new interface JiffleRuntime and a template for an implementing class that looks like this...

import jaitools.jiffle.runtime.JiffleRuntime;

import java.awt.image.RenderedImage;
import java.awt.image.WritableRenderedImage;
import java.util.HashMap;
import java.util.Map;
import javax.media.jai.iterator.RandomIter;
import javax.media.jai.iterator.RandomIterFactory;
import javax.media.jai.iterator.WritableRandomIter;

public class JiffleRuntimeImpl implements JiffleRuntime {

/*
* Note not using generics here because they are not
* supported by Janino.
*/
private Map images = new HashMap();
private Map readers = new HashMap();
private Map writers = new HashMap();

private double _width;
private double _height;

public void evaluate(int _x, int _y, int _band) {
// COMPILER_BREAK

throw new UnsupportedOperationException("Method body to be provided by Jiffle compiler");

// COMPILER_RESUME
}

public double readFromImage(String imageName, int x, int y, int band) {
RandomIter iter = (RandomIter) readers.get(imageName);
return iter.getSampleDouble(x, y, band);
}

public void writeToImage(String imageName, int x, int y, int band, double value) {
WritableRandomIter iter = (WritableRandomIter) writers.get(imageName);
iter.setSample(x, y, band, value);
}

public void setDestinationImage(String imageName, WritableRenderedImage image) {
images.put(imageName, image);

if (images.size() == 1) {
_width = image.getWidth();
_height = image.getHeight();
}

writers.put(imageName, RandomIterFactory.createWritable(image, null));
}

public void setSourceImage(String imageName, RenderedImage image) {
images.put(imageName, image);
readers.put(imageName, RandomIterFactory.create(image, null));
}

}

The Jiffle compiler inserts the generated statements into the evaluate method of this class...

public void evaluate(int _x, int _y, int _band) {
double xc=_width / 2.0;
double yc=_height / 2.0;
double dx=(_x - xc) / xc;
double dy=(_y - yc) / yc;
double d=Math.sqrt(Math.pow(dx, 2.0) + Math.pow(dy, 2.0));
writeToImage("out", _x, _y, _band, Math.sin(25.132741228718345 * d));
}

Now we have the run-time class source as a String in memory, we compile it with Janino's SimpleCompiler class and retrieve the resulting executable bytecode as shown here...

SimpleCompiler compiler = new SimpleCompiler();
compiler.cook(runtimeSource);
Class clazz = compiler.getClassLoader().loadClass(PACKAGE_NAME + "." + RUNTIME_CLASS_NAME);
runtimeInstance = (JiffleRuntime) clazz.newInstance();

And voila ! The Jiffle script has now been compiled into a form that will run at the speed of the JVM, hundreds of time faster than the original tree walking approach.

Janino doesn't handle generic collections or var-arg method calls so some work-arounds were required in the parser grammars and Jiffle's run-time support classes to deal with this. For example, instead of generating this source...

double foo = JiffleFunctions.median(x1, x2, x3, x4, x5);

We change the median method to expect a Double array and have the compiler generate this...

double foo = JiffleFunctions.median(new Double[]{x1, x2, x3, x4, x5});

3 comments:

  1. Oh man, I am so looking forward to try that out inside JGrass!! Is this in the current snapshot or are you guys planning a release?

    ReplyDelete
  2. Yep - JGrass will be smokin :)

    Once I get the event handling side of things in sync with the new runtime model I'll create a snapshot. Should be the next couple of days (fingers crossed).

    ReplyDelete
  3. Janino now supports VARARGS and generics (partially) - you may want to take a look.

    ReplyDelete