ImageJ plugins 2
(This is a followup to ImageJ plugins)
Let us say that each filter will have two constructors. Filter() will be used by the menu and macro systems exclusively. It should pop up a dialog and fetch values from the user. The macro system takes the first word of each field in the dialog and tries to fill in the values given as arguments. We’ll end up breaking this somewhat later, but this is fine by me. The macro language is an abomination.
The other constructor takes all the necessary parameters as arguments: Filter(param1, param2, ...). It should be used when writing Java, and doesn’t pop up a dialog.
The end goal is to be able to write code like:
ImagePlus im = ...;
ImagePlus im2 = (new Filter(param1, param2)).filter(im);
We can actually abstract essentially all this behavior into a superclass, which I’ll call Filter. The programmer should only specify the required parameters, a setup method, and a run method. Actually, he’ll have to define a Filter(param1, param2, ...) constructor as well, but I don’t see a way to escape this in Java.
Filter() reflects upon the object that calls it (so it we don’t override it, it gets continuously inherited), and finds all that object’s fields. How do we tell it which fields should be parameters and what kind they are? Annotations!
Annotations are a new features from Java 5, which let you attach a few bits of data to objects, namely primitive types, arrays of primitive types, and enums. This is frustratingly slim, but Java was designed to be a straightjacket, not a set of wings.
We define annotations for each of our acceptable parameter types: numbers, strings, boolean values, binary masks, and kernels. The numbers, strings, and booleans are self explanatory. Binary masks are rectangular arrays of booleans; kernels are rectangular arrays of floats. We’ll want them for doing convolutions and morphology.
The annotation definition looks something like this:
@Retention(RetentionPolicy.RUNTIME)
public @interface BooleanPlugInParameter {
String label();
boolean value();
}
If Java had real polymorphism, we could use a single annotation, but that would be too easy. We’re stuck with this.
So we iterate through the list of all an object’s fields and add fields to a dialog:
GenericDialog gd = new GenericDialog("Arguments to " + this.getClass().toString());
for (Field f : this.getClass().getDeclaredFields()) {
if (f.getAnnotation(BooleanPlugInParameter.class) != null) {
// Java doesn't let me define variables in the comparisons of if statements
BooleanPlugInParameter bann = f.getAnnotation(BooleanPlugInParameter.class);
gd.addCheckbox(bann.label(), bann.value());
} else if (f.getAnnotation(NumericPlugInParameter.class) != null) {
// similar stuff
} // and similar stuff for each annotation
}
Then we display the dialog, get the responses back, and iterate through the fields again to put the data in the right places. This takes care of the macro language and menu use.
We can put several utility methods for the Java programmer into Filter as well:
- ImagePlus applyFilter(ImagePlus imp)
- Takes an ImagePlus, applies the filtering operation in place (defined by the class’s setup and run methods, and returns a reference to the same ImagePlus. The code is modified from ImageJ's IJ.runPlugInFilter method.
- ImagePlus duplicate(ImagePlus imp)
- Make a deep copy of imp. This perhaps needn't be carried around by every Filter, but it's really nice to have. The code is a hacked up version of what's in ImageJ's Duplicator class.
- ImagePlus filterDuplicate(ImagePlus imp)
- Just the composition of the previous two: apply this filter to a duplicate of imp.
Here's an example plugin using this structure. It's purpose should be pretty obvious.
public class AddN extends Filter {
@NumericPlugInParameter(label="Value to add", value=0) Integer n;
public AddN(Integer n) { this.n = n; }
public int setup(String cmd, ImagePlus imp) {
return DOES_16 + DOES_STACK;
// Yes, this framework automatically handles stacks right!
}
public void run(ImageProcessor ip) {
short[] pix = ip.getPixels();
for (int i = 0; i < pix.length; i++) pix[i] += n;
}
}
This is the beginning though. We can subclass Filter to create more specialized frameworks: LocalFilter to automatically handle all filters requiring neighborhoods; Convolver, whose purpose is obvious; BinaryFilter which takes two images and performs binary operations between them. More on these possibilities later.
I am going to finish cleaning up the basic classes and make a release of it soon. Probably next week, since I actually want to put documentation in.
madhadron :: Apr.25.2007 :: Uncategorized :: Comments Off