Introduction into Developing Plugins

QR for this page

From Fiji

Jump to: navigation, search

Plugins

Spring cleaning

Recently Johannes Schindelin wrote an email about Spring cleaning:

Since the repository at https://github.com/fiji/fiji has grown to an unhealthy two hundred megabyte or so -- way too much to allow new contributors to clone and build and contribute -- it was time to split out all the individual projects we accumulated over the years.

Now there are a hundred new repositories in https://github.com/fiji/, allowing for easy, independent development of the individual parts of which the base version of Fiji consists.

It is now really dirt-easy to follow http://try.github.io/ for about fifteen minutes and then dive right away into developing Fiji components.

The idea of splitting the repositories out was to make them projects in their own right, that do not require a full Fiji to work. Then we finally have consistent versions for both users *and* developers.

Each and every Fiji project has not only a GitHub repository, but also a Jenkins job. For example, bUnwarpJ lives in https://github.com/fiji/bUnwarpJ and is built by http://jenkins.imagej.net/view/Fiji/job/bUnwarpJ.

The purpose of the Jenkins job is not only to make sure that everything builds fine, but it also deploys the .jar file to http://maven.imagej.net/.

Once you are ready to upload a new version, just change the -SNAPSHOT version in the pom.xml to a non-SNAPSHOT version and push. Jenkins will then build and deploy it. If everything worked as expected, the pom.xml in fiji.git needs to be changed to reflect the new deployed version.

The long-term plan is to trigger that automatically, as well as an upload of the respective .jar file.

Remember, the deployed artifacts (i.e. the .jar files "uploaded" to the Maven repository) can be used *very* easily with every major development environment such as Eclipse, Netbeans or IntelliJ.

The standardized Maven way -- while it did force a few changes onto the Fiji projects, especially in development style -- brings so many goodies with it, such as attached Javadocs and sources.

Even better: those artifacts can be used as-are by KNIME and CellProfiler, and others!

After having Jenkins build and deploy a non-SNAPSHOT version, the version in the pom.xml should be bumped to the next SNAPSHOT version.

Curtis hinted at our very concrete plans to streamline that process by building better tools (in the "ImgLib split?" thread). This will help, for the benefit of making all of our projects much more accessible (and thereby reusable!) to other projects.

Time permitting, I will add a screencast that demonstrates how to contribute to an existing Fiji plugin using Eclipse which now is truly fun. (Hope a Netbeans version is added also)

You still can compile using the command line. If you want the .jar files to go into your fiji clone, just call

       mvn -Dimagej.app.directory=/path/to/fiji 

(where /path/to/fiji is obviously to be replaced with the real path to fiji/).

You can use the command-line above, or for even easier testing, fiji.Debug.run() of fiji-lib as described here: http://fiji.sc/Debugging#Debugging_plugins_in_an_IDE_.28Netbeans.2C_IntelliJ.2C_Eclipse.2C_etc.29

Note that the fiji.Debug method gives you many advantages over all other methods.

What are plugins (in terms of files)?

  • Plugins are the most convenient way to extend ImageJ/Fiji
  • A plugin consists of one or more Java classes living inside a .jar file in plugins/ (exception: a single .class file)
  • All plugin .jar (or .class) files must contain an underscore in their name
  • Plugins can use 3rd party libraries; in Fiji, store them in the jars/ directory (in ImageJ you have to put them into the plugins/ directory, and to avoid funny menu entries in the Plugins menu, you should rename them if their name contains an underscore)

Updating plugins

  • After storing the .jar file(s) into the plugins/ (or jars/) directory, call Help>Refresh Menus or restart Fiji
  • If the respective plugin is in-use, you might get funny results when refreshing the menus, due to limitations in Sun Java's handling of .jar files.

What are plugins (in terms of menu entries)?

  • If the .jar file contains a file called plugins.config, it determines what menu items are provided by the plugin
  • If the .jar file does not contain a file called plugins.config, all contained classes containing an underscore in their name are added to the Plugins menu

A plugins.config file looks like this:

# Comments (such as title, author, etc)
#
# The other lines have this format:
#  Menu, “Menu Item”, ClassName
# Example:

Plugins>Analyze, “Plot”, fiji.Plot

A class can be reused for multiple menu entries, by passing an optional argument in the plugins.config file:

# Example how to reuse a Java class

Help, “Bug report”, fiji.Send(“bug”)
Help, “Contact”, fiji.Send(“contact”)

What are plugins (in terms of Java code)?

There are two different types of plugins:

  • Plugins which operate on one image
  • All other plugins (including plugins which require more than one input images, or image format loaders)

A filter plugin looks like this:

public class My_Plugin implements PlugInFilter {
    public int setup(String arg, ImagePlus image) {
        return DOES_ALL;
    }
    public void run(ImageProcessor ip) {
        // Here is the action
    }
}

A general plugin looks like this:

public class My_Plugin implements PlugIn {
    public void run(String arg) {
        // Here is the action
    }
}


Note: it is of course possible to implement a filter plugin using the PlugIn interface, but ImageJ will perform more convenience functions if you use the PlugInFilter interface, such as verifying that there is an image and that it is of the correct type, and error handling.

Compile & Run

  • Fiji has a menu entry Plugins>Compile & Run... which you can use to compile & run a plugin without restarting Fiji
  • In Fiji, you can put .java files into the plugins/ directory, and they will be compiled & run transparently (i.e. the user will hardly notice a difference between such plugins and precompiled ones)

Limitations

  • Plugins can only implement menu entries (in particular, they cannot provide tools in the toolbar)
  • Some functions which are easy to call via macros are not available via the public Java API (e.g. Image>Stacks>Plot Z-axis profile...)
  • It is often quicker to write macros

Rapid prototyping with the Script Editor

There a few good reasons why you should try the Script Editor for rapid prototyping of your plugins or scripts:

  • Supports Jython, JRuby, Javascript, Clojure, Java, BeanShell, and ImageJ's Macro Language
  • Syntax highlighting
  • Compile and Run without restarting Fiji (mini-IDE)
  • Export scripts/plugins as .jar files
  • You can compile & run Java classes in the Script Editor which implement neither a plugin nor a plugin filter, but which have a static main() method
  • Provides code templates
  • Convenience functions, (add import, open JavaDoc, for given class, etc)

Quick Start

To plunge into writing plugins, make sure that there is an active image (e.g. a sample image), start the Script Editor (File>New>Script), and select the Process Pixels menu item from the Templates>Java menu. Then, run the plugin with Run>Run.

ImageJ's API

The source of the various Fiji-related projects is spread over several source code repositories, and so is their API documentation. An overview of all the javadoc resources can be found on this page with javadoc links.

The class IJ

The class ij.IJ is a convenience class with many static functions. Two of them are particularly useful for debugging:

// output into the Log window
IJ.log(“Hello, World!”);

// Show a message window
IJ.showMessage(“Hello, World!”);


The class ImageJ

The class ij.ImageJ implements the main window of ImageJ / Fiji, and you can access it via ij.IJ's static method getInstance():

// check if ImageJ is used interactively
if (IJ.getInstance() != null)
    IJ.showMessage(“Interactive!”);


Typically, all you do with that instance is to test whether ImageJ is used as a library (in which case the instance is null).

The class WindowManager

Use the class ij.WindowManager to access the ImageJ windows / images:

// how many windows / images are active?
IJ.log(“There are “
    + WindowManager.getNonImageWindows().length
    + “ windows and “
    + WindowManager.getImageCount()
    + “ images!”);


When implementing a filter plugin, you usually do not need to access WindowManager directly.

The hierarchy of the classes representing an image

All images are represented as instances of ij.ImagePlus. This class wraps an ij.ImageStack of slices. Slices are data-type dependent instances of ij.process.ImageProcessor: ij.process.ByteProcessor, ij.process.ShortProcessor, ij.process.FloatProcessor, and ij.process.ColorProcessor. Or graphically:

Image Class Hierarchy.png

Example usage:

// get the current image
ImagePlus image = WindowManager.getCurrentImage();

// get the current slice
ImageProcessor ip = image.getProcessor();
 
// duplicate the slice
ImageProcessor ip2 = ip.duplicate();


Beyond 3D: Hyperstacks

In ImageJ, you can represent more than 3 dimensions in an image: X, Y, Z, channels, frames (time). Internally, these 5-dimensional images are still represented as stacks of images (essentially, a one-dimensional array of ImageProcessor instances). The ImagePlus class knows how to transform (channel, z-slice, frame) triplets into the corresponding index in the ImageStack, though:

// get the n'th slice (1 <= n <= N!)
ImageStack stack = image.getStack();
int size = stack.getSize();
ImageProcessor ip = stack.getProcessor(size);

// get the ImageProcessor for a given
// (channel, slice, frame) triple
int index = image.getStackIndex(channel, slice, frame);
ImageProcessor ip = stack.getProcessor(index);


Note: for historical reasons, slice indices (and channel and frame indices as well) start at 1. This is in contrast, e.g. to the x, y coordinates, which start at 0 (as one might be used to from other computer languages except BASIC, Pascal and Matlab).

Working with the pixels' values

The subclasses of the ImageProcessor class implement 2-dimensional images for specific data types (8-bit, 16-bit, 32-bit floating point, and RGB color). Let's start with the grayscale ones:

// get one pixel's value (slow)
float value = ip.getf(0, 0);


This would get you the value of the top left pixel of an object ip of type ImageProcessor, as a 32-bit floating point value.

Since the original data type might be 8-bit, that operation can require a cast (type conversion), which can be quite costly when done very often. Therefore, if you know the data type of your images, you can write more efficient (but data type depednent) code:

// get all type-specific pixels (fast)
// in this example, a ByteProcessor
byte[] pixels = (byte[])ip.getPixels();
int w = ip.getWidth(), h = ip.getHeight();
for (int j = 0; j < h; j++)
    for (int i = 0; i < w; i++) {
        // Java has no unsigned 8-bit data type, so we need to perform Boolean arithmetics
        int value = pixels[i + w * j] & 0xff;
        ...
    }


Note: The previous example assumes that your images are 8-bit (unsigned, i.e. values between 0 and 255) images. Since Java has no data type for unsigned 8-bit integers, we have to use the & 0xff dance (a Boolean AND operation) to make sure that the value is treated as unsigned integer.

Accessing the pixels' values gets trickier when it comes to RGB images. These use the native data type int (32-bit signed integer) to encode 3 color channels à 8-bit, packed into the lower 24 bits (note that ImageJ might store things in the upper 8 bits, so you cannot assume them to be 0). Therefore, the getf() method of the ImageProcessor class does not make sense on color images. You have to access the pixels like this:

// get all pixels of a ColorProcessor
int[] pixels = (int[])ip.getPixels();
int w = ip.getWidth(), h = ip.getHeight();
for (int j = 0; j < h; j++)
    for (int i = 0; i < w; i++) {
        int value = pixels[i + w * j];
        // value is a bit-packed RGB value
        int red = value & 0xff;
        int green = (value >> 8) & 0xff;
        int blue = (value >> 16) & 0xff;
}


Making new images

To make a new image - be it 2, 3, 4 or 5 dimensional - you have to create instances of ImageProcessor first. Example:

ImageProcessor gradient(double angle, int w, int h) {
    float c = (float)Math.cos(angle);
    float s = (float)Math.sin(angle);
    float[] p = new float[w * h];
    for (int j = 0; j < h; j++)
        for (int i = 0; i < w; i++)
            p[i + w *j] = (i – w / 2) * c + (j – h / 2) * s;
    return new FloatProcessor(w, h, p, null);
}


This example implements a method that shows a gradient along a given angle. You can use this method to build a 3-dimensional image:

// make a stack of gradients
int w = 512, h = 512;
ImageStack stack = new ImageStack(w, h);
for (int i = 0; i < 180; i++)
    stack.addSlice(“”, gradient(i / 180f * 2 * Math.PI, w, h));
ImagePlus image = new ImagePlus(“stack”, stack);
// you do not need to show intermediate images
image.show();


Informing the user about the progress

This code snippet shows you how to update the progress bar and the status text:

// show a progress bar
for (int i = 0; i < 100; i++) {
    // do something
    IJ.showProgress(i + 1, 100);
}

// show something in the status bar
IJ.showStatus(“Hello, world!”);


Note: Calling IJ.showProgress(n, n); will hide the progress bar; Therefore, it makes sense to update the progress bar at the end of a loop iteration, so that after the last iteration, the progress bar is hidden.

Frequently used operators

The ImageProcessor class has a few methods such as smooth(), sharpen(), findEdges(), etc

Tip: use the Script Editor's functions in the Tools menu:

  • Open Help for Class... (opens the JavaDoc for a class in a browser),
  • Open .java file for class... (requires the respective source files to be present in the Fiji directory, such as after Downloading and Building Fiji From Source, or
  • Open .java file for menu item... (also needs the source files).

Plots

You can show a plot window very easily using the Plot class:

void plot(double[] values) {
    double[] x = new double[values.length];
    for (int i = 0; i < x.length; i++)
        x[i] = i;
    Plot plot = new Plot(“Plot window”, “x”, “values”, x, values);
    plot.show();
}


It is almost as easy to put multiple plots into one window:

void plot(double[] values, double[] values2) {
    double[] x = new double[values.length];
    for (int i = 0; i < x.length; i++)
        x[i] = i;
    Plot plot = new Plot(“Plot window”, “x”, “values”, x, values);
    plot.setColor(Color.RED);
    plot.draw();
    plot.addPoints(x, values2, Plot.LINE);
    plot.show();
}


To update the contents of a plot window, remember the return value of plot.show() which is a PlotWindow, and use its drawPlot() method:

void plot(double[] values) {
    ...
    PlotWindow plotWindow = plot.show();
    ...
    Plot plot = new Plot("Plot window", "x", "values", x, values);
    plotWindow.drawPlot(plot);
}


The results table

Whenever your plugin quantifies things in the images, you might want to output the values in a results table:

ResultsTable rt = Analyzer.getResultsTable();
if (rt == null) {
    rt = new ResultsTable();
    Analyzer.setResultsTable(rt);
}
for (int i = 1; i <= 10; i++) {
    rt.incrementCounter();
    rt.addValue(“i”, i);
    rt.addValue(“log”, Math.log(i));
}
rt.show(“Results”);


Regions of interest

You can access the ROIs in the following fashion:

// testing ROI type
    if (roi != null && roi.getType() == Roi.POLYGON)
        IJ.log(“This is a polygon!”);
    showCoordinates((PolygonRoi)roi);

...

// get ROI coordinates
void showCoordinates(PolygonRoi polygon) {
    PolygonRoi polygon = (PolygonRoi)roi;
    int[] x = polygon.getXCoordinates();
    int[] y = polygon.getYCoordinates();
    Rectangle bounds = polygon.getBounds();
    for (int i = 0; i < x.length; i++)
        // x, y are relative to the bounds' origin
        IJ.log(“point ” + i + “: “ + (x[i] + bounds.x) + (y[i] + bounds.y));
}


Note: If the image has no ROI set, then getRoi() will return null, so you must check whether roi != null before accessing fields or methods on the object.

Of course, you can also set ROIs programmatically:

// rectangular ROI
Roi roi = new Roi(10, 10, 90, 90);
image.setRoi(roi);

// oval ROI
Roi roi = new OvalRoi(10, 10, 90, 90);
image.setRoi(roi);


Further tips

Please see also the developers tips how to use ImageJ's API effectively.