Difference between revisions of "Jython Scripting"

(Subtract the minimal value to an image)
m (Error handling with try / except / finally)
Line 582: Line 582:
 
  Unexpected error:  (<type 'exceptions.NameError'>, NameError("name 'x' is not defined",), <traceback object at 0x2>)
 
  Unexpected error:  (<type 'exceptions.NameError'>, NameError("name 'x' is not defined",), <traceback object at 0x2>)
  
 +
 +
To ensure that you see the stack trace, print it to the ImageJ log window instead of stdout (whathever the latter may be):
 +
 +
<source lang="python">
 +
    IJ.log(str(sys.exc_info()))
 +
</source>
 
[[Category:Scripting]]
 
[[Category:Scripting]]
 
[[Category:Jython]]
 
[[Category:Jython]]

Revision as of 04:43, 22 March 2010

The Jython interpreter plugin

The interpreter provides a screen and a prompt. Type any jython code on the prompt to interact with ImageJ.

Launch it from plugins - Scripting - Jython Interpreter. See Scripting Help for all keybindings, and also Scripting comparisons.

Within the interpreter, all ImageJ, java.lang.* and TrakEM2 classes are automatically imported. So creating new images and manipulating them is very straighforward.


Language basics

  • Any text after a # is commented out.
  • There are no line terminators (such as ';' in other languages), neither curly braces to define code blocks.
  • Indentation defines code blocks.
  • Functions are defined with def, and classes with class.
  • Functions are objects, and thus storable in variables.
  • Jython (and python in general) accepts a mixture of procedural and object-oriented code.
  • Jython currently implements the Python language at its 2.5 version. All documentation for python 2.5 applies to Jython bundled with Fiji (with the remarks listed later).


Workflow for creating Jython scripts

The recommended setup is the following:

  • Edit a file in your favorite text editor, and save it with an underscore in the name and a .py extension anywhere under ImageJ plugins folder.
  • Run Plugins - Scripting - Refresh Jython scripts only the very first time after newly creating the file under any folder or subfolder of ImageJ's plugins folder. A menu item will appear with its name, from which it can be run.
  • Keep editing (and saving) the file from your editor. Just select the menu item to execute it over and over. Or use the "Find..." command window to launch it easily (keybinding 'l').

The next time Fiji is run, automatic commands in macros/StartupMacros.txt will setup all your scripts in the Plugins menu.

Some limitations of jython

Though jython tries to be as close as possible as python, there are some differences you may experience during scripting.

  • Float "special numbers" such as NaN and Inf are not handled.

For instance,

 a = float('nan') 

will create the correct float number in python, but will throw an exception in jython.

Instead, to create a NaN in jython, use:

>>> a = Double.NaN
>>> print a
NaN 

To test if a number is NaN:

>>> if Double.isNaN(a):
        print "a is NaN!"
a is NaN! 
  • Some existing python modules can't be imported in jython.

This is for instance the case of the module numpy, which would have been really convenient for analysing data and results.

But see these java numerical libraries: http://math.nist.gov/javanumerics/#libraries , of which:

  • JaMa (Java Matrix Package)
  • Java3D (particularly its vecmath package provides general matrix and vector classes (GMatrix, GVector).

... are already included in Fiji.

Jython tutorials for ImageJ

Defining variables: obtaining the current image

imp = IJ.getImage()

Which is the same as:

imp = WindowManager.getCurrentImage()

Since calling the above is long and tedious, one can declare a variable that points to the above static methods:

c = WindowManager.getCurrentImage

Above note the lack of parentheses.

To execute the function, just use parentheses on it:

 imp = c()

The above gets the value of c, which is the method named getCurrentImage in class WindowManager, and executes it, storing its returned object in imp.


Manipulating pixels

Creating a grayscale ramp image

First create an image and obtain its pixels:

imp = ImagePlus("my new image", FloatProcessor(512, 512))
pix = imp.getProcessor().getPixels()

The length of an array:

n_pixels = len(pix)

Then loop to modify them:

# catch width
w = imp.getWidth()
 
# create a ramp gradient from left to right
for i in range(len(pix)):
   pix[i] = i % w
 
# adjust min and max, since we know them
imp.getProcessor().setMinAndMax(0, w-1)

... and show the new image:

imp.show()


Creating a random 8-bit image

First import necessary packages: Random, from standard java util library, and jarray, the Jython module for native java arrays:

from java.awt import Random
from jarray import zeros

Then create the array and fill it with random bytes:

width = 512
height = 512
 
pix = zeros(width * height, 'b')
Random().nextBytes(pix)

(See the jarray documentation where the 'b'-byte, 'd'-double, etc. are explained.)

Now make a new IndexColorModel (that's what ImageJ's ij.process.LUT class is) for 8-bit images:

channel = zeros(256, 'b')
for i in range(256):
    channel[i] = (i -128) 
cm = LUT(channel, channel, channel)

... and compose a ByteProcessor from the pixels, and assign it to an ImagePlus:

imp = ImagePlus("Random", ByteProcessor(width, height, pix, cm)
imp.show()

Creating a random image, the easy way

All the above can be summarized like the following:

from java.util import Random
imp = IJ.createImage("A Random Image", "8-bit", 512, 512, 1)
Random().nextBytes(imp.getProcessor().getPixels())
imp.show()

Running a watershed plugin on an image

# 1 - Obtain an image
blobs = IJ.openImage("http://rsb.info.nih.gov/ij/images/blobs.gif")
# Make a copy with the same properties as blobs image:
imp = blobs.createImagePlus()
ip = blobs.getProcessor().duplicate()
imp.setProcessor("blobs copy", ip)

# 2 - Apply a threshold: only zeros and ones
# Set the desired threshold range: keep from 0 to 74
ip.setThreshold(147, 147, ImageProcessor.NO_LUT_UPDATE)
# Call the Thresholder to convert the image to a mask
IJ.run(imp, "Convert to Mask", "")

# 3 - Apply watershed
# Create and run new EDM object, which is an Euclidean Distance Map (EDM)
# and run the watershed on the ImageProcessor:
EDM().toWatershed(ip)

# 4 - Show the watersheded image:
imp.show()

The EDM plugin that contains the watershed could have been indirectly applied to the currently active image, which is not recommended:

imp = IJ.getImage()  # the current image
imp.getProcessor().setThreshold(174, 174, ImageProcessor.NO_LUT_UPDATE)
IJ.run(imp, "Convert to Mask", "")
IJ.run(imp, "Watershed", "")

If you had called show() on the image at any early stage, just update the screen with:

imp.updateAndDraw()


... and counting particles, and measuring their areas

Continuing from the imp above, that contains the now watersheded "blobs" sample image:

# Create a table to store the results
table = ResultsTable()
# Create a hidden ROI manager, to store a ROI for each blob or cell
roim = RoiManager(True)
# Create a ParticleAnalyzer, with arguments:
# 1. options (could be SHOW_ROI_MASKS, SHOW_OUTLINES, SHOW_MASKS, SHOW_NONE, ADD_TO_MANAGER, and others; combined with bitwise-or)
# 2. measurement options (see [http://rsb.info.nih.gov/ij/developer/api/ij/measure/Measurements.html Measurements])
# 3. a ResultsTable to store the measurements
# 4. The minimum size of a particle to consider for measurement
# 5. The maximum size (idem)
# 6. The minimum circularity of a particle
# 7. The maximum circularity
pa = ParticleAnalyzer(ParticleAnalyzer.ADD_TO_MANAGER, Measurements.AREA, table, 0, Double.POSITIVE_INFINITY, 0.0, 1.0)
pa.setHideOutputImage(True)

if pa.analyze(imp):
  print "All ok"
else:
  print "There was a problem in analyzing", blobs

# The measured areas are listed in the first column of the results table, as a float array:
areas = table.getColumn(0)

To print out the area measurement of each:

>>> for area in areas: print area
76.0
185.0
658.0
434.0
...


Now, we want to measure the intensity of each particle. To do so, we'll retrieve the ROI from the ROIManager, set them one at a time on the original (non-watershed, non-thresholded) image stored in the variable blobs, and measure:

# Create a new list to store the mean intensity values of each blob:
means = []

for roi in RoiManager.getInstance().getRoisAsArray():
  blobs.setRoi(roi)
  stats = blobs.getStatistics(Measurements.MEAN)
  means.append(stats.mean)

Finally read out the measured mean intensity value of each blob, along with its area:

for area, mean in zip(areas, means):
  print area, mean
6.0 191.47368421052633
185.0 179.2864864864865
658.0 205.61702127659575
434.0 217.32718894009216
477.0 212.1425576519916
...

Creating an image from a text file

A data file containing rows with 4 columns:

...
399 23 30 10.12
400 23 30 12.34
...

... where the columns are X, Y, Z and value, for every pixel in the image. We assume we know the width and height of the image. From this sort of data, we create an image, read out all lines and parse the numbers:

width = 512
height = 512
stack = ImageStack(width, height)

file = open("/home/albert/Desktop/data.txt", "r")

try:
  fp = FloatProcessor(width, height)
  pix = fp.getPixels()
  cz = 0
  # Add as the first slice:
  stack.addSlice(str(cz), fp)
  # Iterate over all lines in the text file:
  for line in file.readlines():
    x, y, z, value = line.split(" ")
    x = int(x)
    y = int(y)
    z = int(z)
    value = float(value)
    # Advance one slice if the Z changed:
    if z != cz:
      # Next slice
      fp = FloatProcessor(width, height)
      pix = fp.getPixels()
      stack.addSlice(str(cz), fp)
      cz += 1
    # Assign the value:
    pix[y * width + x] = value
  # Prepare and show a new image:  
  imp = ImagePlus("parsed", stack)
  imp.show()
# Ensure closing the file handle even if an error is thrown:
finally:
  file.close()


Obtain/View histogram and measurements from an image

The easiest way is to grab an image and call an ImageJ command to show its histogram:

imp = IJ.openImage("http://rsb.info.nih.gov/ij/images/blobs.gif")
IJ.run(imp, "Histogram", "")

How ImageJ does it, internally, has to do with the ImageStatisics class:

stats = imp.getStatistics()
print stats.histogram
array('i',[0, 0, 0, 0, 0, 0, 0, 0, 53, 0, 0, 0, 0, 0, 0, 0, 304,
           0, 0, 0, 0, 0, 0, 0, 1209, 0, 0, 0, 0, 0, 0, 0, 3511, 0,
           0, 0, 0, 0, 0, 0, 7731, 0, 0, 0, 0, 0, 0, 0, 10396, 0, 0,
           0, 0, 0, 0, 0, 7456, 0, 0, 0, 0, 0, 0, 0, 3829, 0, 0, 0,
           0, 0, 0, 0, 1992, 0, 0, 0, 0, 0, 0, 0, 1394, 0, 0, 0, 0,
           0, 0, 0, 1158, 0, 0, 0, 0, 0, 0, 0, 1022, 0, 0, 0, 0, 0,
           0, 0, 984, 0, 0, 0, 0, 0, 0, 0, 902, 0, 0, 0, 0, 0, 0,
           0, 840, 0, 0, 0, 0, 0, 0, 0, 830, 0, 0, 0, 0, 0, 0, 0,
           926, 0, 0, 0, 0, 0, 0, 0, 835, 0, 0, 0, 0, 0, 0, 0, 901,
           0, 0, 0, 0, 0, 0, 0, 1025, 0, 0, 0, 0, 0, 0, 0, 1180, 0,
           0, 0, 0, 0, 0, 0, 1209, 0, 0, 0, 0, 0, 0, 0, 1614, 0, 0,
           0, 0, 0, 0, 0, 1609, 0, 0, 0, 0, 0, 0, 0, 2220, 0, 0, 0,
           0, 0, 0, 0, 2037, 0, 0, 0, 0, 0, 0, 0, 2373, 0, 0, 0, 0,
           0, 0, 0, 1568, 0, 0, 0, 0, 0, 0, 0, 1778, 0, 0, 0, 0, 0,
           0, 0, 774, 0, 0, 0, 0, 0, 0, 0, 1364, 0, 0, 0, 0, 0, 0, 0])


The histogram, area and mean are computed by default. Other values like the median need to be specified.

To calculate other parameters, specify them by bitwise-or composition (see flags in Measurements):

stats = imp.getStatistics(Measurements.MEAN | Measurements.MEDIAN | Measurements.AREA)
print "mean:", stats.mean, "median:", stats.median, "area:", stats.area
mean: 103.26857775590551 median: 64.0 area: 65024.


If we set a ROI to the image, then we are measuring only for the inside of the ROI. Here we set an oval ROI of radius 25 pixels, centered:

radius = 25
roi = OvalRoi(imp.width/2 - radius, imp.height/2 -radius, radius*2, radius*2)
imp.setRoi(roi)
stats = imp.getStatistics(Measurements.MEAN | Measurements.MEDIAN | Measurements.AREA)
print "mean:", stats.mean, "median:", stats.median, "area:", stats.area
mean: 104.96356275303644 median: 64.0 area: 1976.0


To display the histogram window ourselves, we may use the HistogramWindow class:

hwin = HistogramWindow(imp)

... of which we may grab the image (the plot itself) and save it:

plotimage = hwin.getImagePlus()
IJ.save(plotimage, "/path/to/our/folder/plot.tif")

Removing bleeding from one channel to another

The technique to use is to divide one channel by the other: the channel to denoise divided by the channel that bled through.

The relatively high-level way to do it is to split the channels and call the ImageCalculator with a "Divide" argument:

# 1 - Obtain an RGB image stack
imp = WindowManager.getCurrentImage()
if imp.getType() != ImagePlus.COLOR_RGB:
  IJ.showMessage("The active image is not RGB!")
  raise RuntimeException("The active image is not RGB!")

if 1 == imp.getNSlices():
  IJ.showMessage("Not a stack!")
  raise RuntimeException("Not a stack!")

# 2 - Split channels
red_stack = ImageStack(imp.width, imp.height)
green_stack = ImageStack(imp.width, imp.height)

# 3 - Iterate all slices -- notice slices are 1<=i<=size
for i in range(1, imp.getNSlices()+1):
  slice = stack.getProcessor(i)
  red_stack.addSlice(str(i), slice.toFloat(0, None))
  green_stack.addSlice(str(i), slice.toFloat(1, None))

# 4 - Apply "divide" via ImageCalculator to the red_stack, which is a new 32-bit stack
# Don't use the parameters "create" or "float" or "32" in the parameters string
# of the calc.calculate call--then the result of the operation would be
# in a new stack that opens beyond our control. Without them, results are
# applied to the red_stack
calc = ImageCalculator()
calc.calculate("Divide stack", ImagePlus("red", red_stack), ImagePlus("green", green_stack))

# 5 - Compose a new color stack
new_stack = ImageStack(imp.width, imp.height)
for i in range(1, imp.getNSlices()+1):
  cp = stack.getProcessor(i).duplicate()
  cp.setPixels(0, red_stack.getProcessor(i))
  new_stack.addSlice(stack.getSliceLabel(i), cp)

# 6 - Show the new image
ImagePlus("Normalized " + imp.title, new_stack).show()


Alternatively and as an example of direct pixel manipulation, we'll iterate all slices of the image stack, divide the red channel by the green channel, and compose a new stack:

# 1 - Obtain an RGB image stack
imp = WindowManager.getCurrentImage()
if imp.getType() != ImagePlus.COLOR_RGB:
  IJ.showMessage("The active image is not RGB!")
  raise RuntimeException("The active image is not RGB!")

if 1 == imp.getNSlices():
  IJ.showMessage("Not a stack!")
  raise RuntimeException("Not a stack!")

stack = imp.getStack()

# 2 - Create a new stack to store the result
new_stack = ImageStack(imp.width, imp.height)

# 3 - Iterate all slices -- notice slices are 1<=i<=size
for i in range(1, imp.getNSlices()+1):
  # Get the slice i
  slice = stack.getProcessor(i)
  # Get two new FloatProcessor with the green and red channel data in them
  red = slice.toFloat(0, None)
  green = slice.toFloat(1, None)
  pix_red = red.getPixels()
  pix_green = green.getPixels()
  # Create a new FloatProcessor for the normalized result
  new_red = FloatProcessor(imp.width, imp.height)
  pix_new_red = new_red.getPixels()
  # Iterate and set all normalized pixels
  for k in range(len(pix_red)):
    if 0 != pix_green[k]:
      pix_new_red[k] = pix_red[k] / pix_green[k]
  # Create a ColorProcessor that has the normalized red and the same green and blue channels
  cp = slice.duplicate()
  cp.setPixels(0, new_red)   # at channel 0, the red
  # Store the normalized slice in the new stack, copying the same slice label
  new_stack.addSlice(stack.getSliceLabel(i), cp)

# 4 - Show the normalized stack
new_imp = ImagePlus("Normalized " + imp.title, new_stack)
new_imp.show()

Notice that this second approach is much slower: accessing every pixel from jython has a high cost. If you would like to do very fast pixel-level manipulations, use java or Clojure.


Subtract the minimal value to an image

Which is to say, translate the histogram so that the lowest value is at zero.

# Obtain current image and its pixels
imp = IJ.getImage()
pix = imp.getProcessor().convertToFloat().getPixels()

# find out the minimal pixel value
min = reduce(Math.min, pix)

# create a new pixel array with the minimal value subtracted
pix2 = map(lambda x: x - min, pix)

ImagePlus("min subtracted", FloatProcessor(imp.width, imp.height, pix2, None)).show()

Notice we used:

  • The reduce function to obtain a single value from a list of values (the pixel array) by applying a function to every pair of consecutive values (in this case, the Math.min).
  • lambda, which is used to declare an anonymous function that takes one argument.
  • The map function, which runs a function given as argument to every element of a list (here, every pixel) and returns a new list with all the results.

Tips and Tricks

Getting a list of all members in one package

You can use the Python function dir(<package>) to see the contents of a package:

import ij
print dir(ij)

Specifying the encoding of the source

When your source code contains non-ASCII characters (such as umlauts), Jython will complain with a SyntaxError: Non-ASCII character in file '<iostream>', but no encoding declared.

You can fix this issue by putting the line

# -*- coding: iso-8859-15 -*-

as first line into your source code (or if it starts with #!/usr/bin/python, as second line), as suggested here. You might need to replace the string iso-8859-15 by something like utf-8 if your source code is encoded in UTF-8.

Error handling with try / except / finally

See complete documentation at: jython book chapter 6.

x = 10
y = 0

try:
     z = x / y
except NameError, e1:
     print "A variable is not defined!", e1
except ZeroDivisionError, e2:
     print "Dividing by zero doesn't make any sense! Error:", e2
finally:
     print "This line will always print no matter what error occurs."

Which prints:

Dividing by zero doesn't make any sense! Error: integer division or modulo by zero
This line will always print no matter what errors occurs

To catch any kind of errors, use sys.exc_info:

import sys

try:
  z = x / z
except:
  print "Error: ", sys.exc_info()

Which prints:

Unexpected error:  (<type 'exceptions.NameError'>, NameError("name 'x' is not defined",), <traceback object at 0x2>)


To ensure that you see the stack trace, print it to the ImageJ log window instead of stdout (whathever the latter may be):

    IJ.log(str(sys.exc_info()))

Jython for plugins

Using a jython script as a plugin

The simplest way is to place the jython script file into fiji/plugins/ folder or a subfolder, and it will appear in the menus after running "Plugins>Scripting>Refresh Jython Scripts" or "Help>Update Menus", or on restarting Fiji.

Distributing jython scripts in a .jar file

PLEASE NOTE: there is no need to do the following. See entry above.

The whole idea is to be able to distribute an entire collection of scripts in a single .jar file, for best convenience.

In this example, we create two jython scripts that we want to distribute in a .jar file as plugins:

The printer.py script:

IJ.log("Print this to the log window")

... and the create_new_image.py script:

ip = ByteProcessor(400, 400)
imp = ImagePlus("New", ip)
ip.setRoi(OvalRoi(100, 100, 200, 200))
ip.setValue(255)
ip.fill(ip.getMask())
imp.show()

Place both scripts under a folder named scripts/ .

You will need a tiny .java file specifying a launcher PlugIn, such as:

package my;
import ij.plugin.PlugIn;
import Jython.Refresh_Jython_Scripts;

public class Jython_Launcher implements PlugIn {
    public void run(String arg) {
        new Refresh_Jython_Scripts().runScript(getClass().getResourceAsStream(arg));
    }
}

Notice we place the above file under directory my/, packaged.

To compile it:

$ javac -classpath .:ij.jar:../jars/fiji-scripting.jar:../plugins/Jython_Interpreter.jar my/Jython_Launcher.java 

(check that the path to the three jars that you need is correct!)


Then we define the plugins.config file:

Plugins>My Scripts, "Print to log window", my.Jython_Launcher("/scripts/printer.py")
Plugins>My Scripts, "Create image with a white circle", my.Jython_Launcher("/scripts/create_new_image.py")

Finally, we put all files in a .jar file:

$ jar cf my_jython_scripts.jar plugins.config my/Jython_Launcher.class scripts/*py

Then, drop the jar file into fiji/plugins/ folder and run "Help - Update Menus", or restart fiji. Your scripts will appear under Plugins - My Scripts.

For clarity, this is a summary of the files in the folder:

my/Jython_Launcher.java
my/Jython_Launcher.class
scripts/printer.py
scripts/create_new_image.py
plugins.config


Notice, though, that you don't need to do the .jar packaging at all. Just place the python scripts directly under fiji/plugins/My Scripts/ and they will appear in the menus as regular plugins.


Jython examples in Fiji

See also