This notebook is part of the PyImageJ Tutorial Series, and assumes familiarity with the ImageJ API. Dedicated tutorials for ImageJ can be found here.S

11 Working with the original ImageJ

So far, we have limited ourselves to the world of ImageJ2. But PyImageJ also includes support for the original ImageJ.

You can tell whether a Java class is part of ImageJ or ImageJ2 by its package prefix:

Project

Prefix

Example class

ImageJ

ij

ij.ImagePlus

ImageJ2

net.imagej

net.imagej.Dataset

The original ImageJ API is simpler than that of ImageJ2 in many ways. However, the design of ImageJ imposes some important restrictions.

11.1 Original ImageJ in headless mode

The original ImageJ is a fundamentally GUI-driven application. Some of its features do not work in headless mode, which (as explained in Using PyImageJ without a screen) is PyImageJ’s default mode. In particular, you cannot use RoiManager or WindowManager headless. PyImageJ makes a best effort to warn you if you attempt to perform such unsupported operations. For further details, see the section “The original ImageJ API is limited in headless mode” of the PyImageJ Troubleshooting guide.

import imagej

# initialize ImageJ
ij = imagej.init(mode='headless')
print(f"ImageJ version: {ij.getVersion()}")
ImageJ version: 2.14.0/1.54f

Note that mode='headless' isn’t necessary to specify here. PyImageJ’s default mode is headless, thus ij = imagej.init() is equivalent to specifying the headless mode.

Accessing ImageJ resources that do not function headless will produce a warning about limited functionality.

ij.WindowManager.getCurrentImage()
Operating in headless mode - the original ImageJ will have limited functionality.
Operating in headless mode - the WindowManager class will not be fully functional.

11.2 One ImageJ-enabled gateway at a time

ImageJ was not designed to run multiple simultaneous instances in the same JVM, whereas ImageJ2 supports multiple gateways at once.

def legacy_status(gateway):
    print(f" legacy service: {gateway.legacy}")
    print(f"  legacy active? {gateway.legacy and gateway.legacy.isActive()}")
    print(f"ImageJ2 version: {gateway.getVersion()}")

another_ij = imagej.init()
print("[ij - the original gateway]")
legacy_status(ij)
print("\n[another_ij - a second gateway we constructed just now]")
legacy_status(another_ij)
Operating in headless mode - the original ImageJ will have limited functionality.
[ij - the original gateway]
 legacy service: net.imagej.legacy.LegacyService [priority = 1.0]
  legacy active? True
ImageJ2 version: 2.14.0/1.54f

[another_ij - a second gateway we constructed just now]
 legacy service: net.imagej.legacy.LegacyService [priority = 1.0]
  legacy active? False
ImageJ2 version: 2.14.0/Inactive

11.3 Converting images to ImagePlus

The ij.py.to_imageplus(image) function converts an image to an ij.ImagePlus.

# load data and conver to ImagePlus
dataset = ij.io().open('sample-data/test_still.tif')
imp = ij.py.to_imageplus(dataset)

# print types
print(f"dataset type: {type(dataset)}")
print(f"imp type: {type(imp)}")

# show imp
ij.py.show(imp)
dataset type: <java class 'net.imagej.DefaultDataset'>
imp type: <java class 'ij.ImagePlus'>
_images/3c229b73635dae0d5ce02ad5a4eed2d781ce436c285591134f6e83811f356272.png

In interactive mode, you can display an ImagePlus using ij.ui().show(imp) as described in 06 Working with Images, or by calling imp.show().

But beware: if you then close the image window, ImageJ will dispose the ImagePlus, leaving it in an unusable state.

Note that PyImageJ does not implement dtype, slicing, or element access for ImagePlus objects yet. See imagej/pyimagej#194 for details.

11.4 Converting ImagePlus to other image formats

If you have an ImagePlus, but would like something else:

  • ij.py.to_dataset(imp) will convert it to a net.imagej.Dataset.

  • ij.py.from_java(imp) will convert it to an xarray.

imp2ds = ij.py.to_dataset(imp)
xr = ij.py.from_java(imp)

Now we need to be careful here as Python and ImageJ/ImgLib2 have different dimension order preferences. Let’s print out the dimensions and shape of imp2ds and xr.

print(f"imp2ds dims: {imp2ds.dims}\nimp2ds shape: {imp2ds.shape}")
print(f"xr dims: {xr.dims}\nxr shape: {xr.shape}")
imp2ds dims: ('X', 'Y')
imp2ds shape: (300, 300)
xr dims: ('row', 'col')
xr shape: (300, 300)

Here we can see that imp2ds and xr actually have inverted dimension orders to each other (note that row is the y-axis and col is the x-axis). For more on dimension order in PyImageJ infomration see 06 Working with Images.

11.5 Keeping image data in sync with ij.py.sync_image(imp)

PyImageJ is built on imglyb which allows direct wrapping of NumPy array data into special RandomAccessibleInterval structures, avoiding the duplication of data. However, when working with an ImagePlus in PyImageJ it is important to understand that the original ImageJ has logic that can cause unexpected behavior when modifying the pixel values of the image (whether by direct modification or through a plugin). Let’s look at an example of this in action.

We’ll start by loading a new image and converting it to an ImagePlus:

import skimage, xarray

skin = xarray.DataArray(skimage.data.skin(), name='skin', dims=('row', 'col', 'ch'))
skimp = ij.py.to_imageplus(skin)

def print_3x3_corner():
    print(f"skin: {[int(skin[y,x,0]) for x in range(3) for y in range(3)]}")
    print(f"skimp: {[skimp.getPixel(x,y)[0] for x in range(3) for y in range(3)]}")
print_3x3_corner()

ij.py.show(skin)
skin: [219, 217, 211, 219, 217, 211, 220, 215, 211]
skimp: [219, 217, 211, 219, 217, 211, 220, 215, 211]
_images/4ab0781de0a7dea139b977812e2d2b08f12e9f7a2ca6e0b1ce40eafab86cccb0.png

To modify the values of our image, we can use the ImageProcessor from our ImagePlus by calling the getProcessor() method. ImageProcessor has a number of methods available for modifying pixel values; in this case let’s use the simple set(double) method to set all pixels to 17 (a number unlikely to come up otherwise by chance).

skimp.getProcessor().set(17)
assert skimp.getPixel(0,0)[0] == 17

Now let’s compare the pixel values of the ImagePlus and the original.

print_3x3_corner()
ij.py.show(skin)
skin: [219, 217, 211, 219, 217, 211, 220, 215, 211]
skimp: [17, 17, 17, 17, 17, 17, 17, 17, 17]
_images/4ab0781de0a7dea139b977812e2d2b08f12e9f7a2ca6e0b1ce40eafab86cccb0.png

Is this what you expected? As you can see, the ImagePlus is aware of the updated pixel values, but that change hasn’t propagated through to the backing NumPy array. For this reason, we created the ij.py.sync_image helper method, to sync ImagePlus changes back to ImageJ2 and Python.

# sync the ImagePlus to update the backing NumPy array
ij.py.sync_image(skimp)

Now that skimp has been synced, let’s view the image again.

print_3x3_corner()
ij.py.show(skin)
skin: [17, 17, 17, 17, 17, 17, 17, 17, 17]
skimp: [17, 17, 17, 17, 17, 17, 17, 17, 17]
_images/a5ae09d4b884bdf23433a22d36c912032fa868016f32103f9a826498246d1727.png

Red channel is effectively gone because it’s 17s all the way down now.

This step is necessary because the original ImageJ makes a copy of image data from ImgLib2 (and therefore Python) on a plane-by-plane basis, and modifications happen directly on that copied data, rather than on the original data.

11.6 Invoking ImageJ Plugins

The ij.py.run_plugin function lets you run ImageJ plugins from Python.

Running ImageJ plugins from script languages is typically done via ij.IJ.run(imp, plugin, args), where imp is the ImagePlus to make active during plugin execution, plugin is the label in the menus (as recorded by the Macro Recorder), and args is a single space-separated string of arguments, e.g. "path=[/data/image.tif] gamma=2 overwrite".

To make this more Pythonic, PyImageJ provides a helper function ij.py.run_plugin(plugin, args={}, imp=None) where args is a dict of variables, and imp is an optional ImagePlus; if no imp is given, the ij.IJ.run(plugin, args) method without ImagePlus argument is used instead. For example: ij.py.run_plugin(plugin, {'path': '/data/image.tif', 'gamma': 2, 'overwrite': True}).

Let’s try it. First we create a new ImagePlus:

import skimage
mitepy = skimage.data.human_mitosis()
miteimp = ij.py.to_imageplus(mitepy)
ij.py.show(miteimp)
_images/3a29fe8c4ddb272d4b28c6215c8bb4781613e0cf4c53320acef7d907674d5649.png

Now let’s blur the image using the “Gaussian Blur…” plugin, which modifies the image in-place.

args = {"sigma": 10}
ij.py.run_plugin("Gaussian Blur...", args, imp=miteimp)
ij.py.sync_image(miteimp) # see section 11.5 for details
ij.py.show(miteimp)
Operating in headless mode - the IJ class will not be fully functional.
_images/28204ccdd763bd12c823818d5c86a921fd8a82e86289451645bc73be3a3955ea.png

11.7 Running ImageJ macros

The ij.py.run_macro function lets you run ImageJ macros from Python.

Before you get too excited, there are some things you should know:

  • The macro language does not support the complete ImageJ API, only a set of built-in functions, although there are ways to work around this.

  • Macros are executed by a custom interpreter that is buggier and less well tested than other script languages (Groovy, JRuby, Jython, etc.).

  • Macros are not intended to run concurrently, meaning you should only run one macro at a time.

  • Macros only support three data types: numbers, strings, and simple arrays. Images are passed via numerical IDs.

  • Macros rely on the active image—i.e., the image window currently displayed in the foreground—which is tricky to get right while in headless mode.

💡 See the “Scripting with ImageJ” living workshop for a primer on ImageJ macro programming.

All of that said, macros are popular because they are simpler and easier to learn than other script languages. Let’s look at using a macro that clears an image’s background and center crops it.

background_clear_and_crop = """
setBatchMode(true);

// Compute image background.
original = getImageID();
run("Duplicate...", " ");
run("Median...", "radius=6");
setAutoThreshold("Li dark");
run("Create Selection");

// Clear background of the original image.
selectImage(original);
run("Restore Selection");
setBackgroundColor(0, 0, 0);
run("Clear Outside");

// Crop to center portion of the image.
x = getWidth() / 4
y = getHeight() / 4
makeRectangle(x, y, x*2, y*2);
run("Crop");
rename(getTitle() + "-cropped")
"""

💡 A great tool for building up macros is the Macro Recorder(see 08 Discover and run ImageJ commands). It can also generate SciJava script code by changing the recorded language to JavaScript. Another powerful tool is the SciJava Script Editor, which has an autocomplete feature useful for exploring available functions.

To run this script all we have to do is call ij.py.run_macro with this background_crop_and_clear macro. But first we need to set the image to be processed as the active image. To do that, you must convert the desired image into an ImagePlus and then call show() on it. You can do this as a succinct one-liner: ij.py.to_imageplus(image).show(). Even in headless mode, the show() call is a clue to ImageJ to register and activate this image.

Now we are ready to use this macro on the skimage coins sample image.

import skimage

coins = skimage.data.coins()
ij.py.show(coins)
_images/1386b1d0e8d60a504b24a354687cf8c54dfc71357264ccf0ac3b982a427a1ad2.png

Next we need to convert the image to ImagePlus, set the title and activate with the show() call.

coins_plus = ij.py.to_imageplus(coins)
coins_plus.setTitle("coins")
coins_plus.show()
assert ij.WindowManager.getImage("coins") is not None

Now that coins has been converted into an ImagePlus and activated as the active image, we can now use the macro.

ij.py.run_macro(background_clear_and_crop)
<java object 'org.scijava.script.ScriptModule'>

This example macro does not have any explicitly output script parameters. Thus, in order to obtain the result image we will need to ask for the image from the WindowManager. First let’s see what windows are open now:

for imp_id in ij.WindowManager.getIDList():
    print(ij.WindowManager.getImage(imp_id))
img["coins-cropped" (-6), 8-bit, 192x152x1x1x1]
img["coins-1" (-7), 8-bit, 384x303x1x1x1]

Here we can see we have two images open. The first one is the original coin image (“coins-1”) and the second one is the cropped coins image (“coins-cropped”). Let’s get the cropped image:

coins_cropped = ij.WindowManager.getImage("coins-cropped")
print(f"coins_cropped dims: {coins_cropped.dims}\ncoins_cropped shape: {coins_cropped.shape}")
coins_cropped dims: ('X', 'Y')
coins_cropped shape: (192, 152)

Now let’s see it!

ij.py.show(coins_cropped)
_images/1386b1d0e8d60a504b24a354687cf8c54dfc71357264ccf0ac3b982a427a1ad2.png

Unfortunately, there is a bug here with the crop funcion when changing the ImagePlus dimensions where some synching fails to occur. One way around this, is to duplicate the image first.

ij.py.show(coins_cropped.duplicate())
_images/1058169c3dfa5548b87215f1deb7b8ed0b8be9bf0180b6a5cba2c506c6fe8fe0.png