TrakEM2 Scripting

Revision as of 09:13, 16 October 2010 by Albertcardona (talk | contribs) (Obtaining the X,Y,Z coordinates of all nodes in a Tree)

Examples in Jython.

Open the "Plugins - Scripting - Jython Interpreter" (see Scripting Help) and make sure there is a TrakEM2 project open, with a display open. Then type or paste the examples below.

Introduction to scripting TrakEM2

Some basics:

  • The canvas into which images are dragged and visualized is part of a Display object. The latter has methods to access its Selection, as well as the Layer and LayerSet that the Display is viewing.
  • The Layer contains 2D objects like Patch (each Patch wraps an image) and DLabel (floating text).
  • The LayerSet contains 3D objects like AreaList, Pipe, Polyline, Ball, Dissector, Treeline and Stack (the latter wraps an ij.ImagePlus that contains an ij.ImageStack).

Both Layer and LayerSet are in a way containers. The LayerSet contains as well a list of Layer. The Display merely views the data in a LayerSet, one Layer at a time.

See a TrakEM2 class diagram for a complete list.

See also the complete TrakEM2 API documentation

To run a script, follow isntructions as indicated in the Scripting Help.

Get the instance of a selected image

>>> p = Display.getFront().getActive()
>>> print p
090504_0314_ex0768.mrc z=0.0 #67398


Obtain the ImagePlus of a selected image

>>> p = Display.getFront().getActive()
>>> imp = p.getImagePlus()
>>> print imp.width, imp.height
2048 2048


Access the Layer and Selection of a Display

The 'front' is the last activated display window. If there's only one display window, then that is 'front'. To access the front display, we call static function getFront() in namespace Display:

>>> front = Display.getFront()
>>> layer = front.getLayer()
>>> layer_set = front.getLayerSet()
>>> sel = front.getSelection()
>>> print sel.getSelected().size()
10
>>> print sel.isEmpty()
0

In Jython, 1 is True and 0 is False

The most interesting data members of a Display, as seen above, are mainly the Layer and the Selection.

Lock all selected objects

for d in Display.getFront().getSelected():
  d.setLocked(True)

Obtain a collection of selected images

The Selection object of a Display can return a number of collections with any selected objects in it, for example of type Patch (those that wrap an image). All you need to do is to call getSelected with the name of the class to filter for:

for d in Display.getFront().getSelected(Patch):
  print d.title


Setting and getting member objects in jython

In Jython as in Python, member objects have automatically get and set functions.

For example, altough a Displayable has a private String title member, this is valid python code for getting and setting the title of a Displayable like a Patch:

>>> p = Display.getFront().getActive()
>>> print p.title
090504_0314_ex0768.mrc

Above, a Patch takes as title the name of the file containing the ImagePlus, by default. Let's change the title to something else:

>>> p = Display.getFront().getActive()
>>> p.title = "A new name for this Patch"
>>> print p.title
A new name for this Patch

The properties of a Displayable: title, color, visibility, locked, alpha, affine transform, dimensions and bounds

Let's set a few values:

>>> p = Display.getFront().getActive()
>>> p.title = "Test image"
>>> p.alpha = 0.4
>>> p.visible = True
>>> p.locked = False
>>> from java.awt import Color
>>> p.color = Color.blue

Tell all displays to update the canvas, so we see the changes:

>>> Display.repaint()


Let's read a few values:

>>> p.getAffineTransform()
AffineTransform[[1.0, 0.0, 474.0], [0.0, 1.0, 567.0]]
>>> print p.getBoundingBox()
java.awt.Rectangle[x=474,y=567,width=2048,height=2048]

The affine transform cannot be set, because it's a final member. But itself the value may be edited via setAffineTransform:

>>> from java.awt.geom import AffineTransform
>>> aff = AffineTransform()
>>> aff.scale(2.0, 2.0)
>>> p.setAffineTransform(aff)
>>> p.updateBucket()

Be careful: java's AffineTranform does concatenations and not pre-concatenations (order matters in matrix multiplication).

In most occasions, what you want can be accomplished with preTransform, such as translating an image:

>>> from java.awt.geom import AffineTransform
>>> aff = AffineTransform()
>>> aff.translate(300, -400)
>>> p.preTransform(aff, True)

More convenient are the methods scale, translate, rotate and particularly preTransform, for the manipulation of a Displayable's affine transform (see AffineTransform) and that of its linked Displayables (any transform propagates to the linked ones).

If you change the affine transform of a Displayable directly (by calling getAffineTransform() and then manipulating it), keep in mind that you will most likely screw up the internal cached maps for fast location of the Displayable object. To solve that, be sure to call updateBucket() on the affected Displayable object.

Manipulating Displayable objects

Resetting the affine transform of all images in a Layer

Suppose you open the project and find that the images of a Layer have non-rigid affine transforms, and you'd like to remove the non-rigid part. A reasonable approach is to reset their affine transforms to identity, and then translate them to approximately where they used to be (based on their bounding box):

layer = Display.getFront().getLayer()

# Get all selected images
# patches = Display.getFront().getSelection().getSelected(Patch)

# Get all images in the current layer
patches = layer.getDisplayables(Patch)

for patch in patches:
  bounds = patch.getBoundingBox()
  patch.getAffineTransform().setToIdentity()
  patch.translate(bounds.x, bounds.y, False)

Display.repaint()

Save the above into a file named "reset_affine_transforms.py" under plugins directory or subdirectory to run it directly from the menus, or copy-paste it into the Jython Interpreter.

See also: the different methods for manipulating the affine transform of a Displayable object like a Patch.

And a WARNING: if you modify the AffineTransform of a Patch and don't call then any of the Displayable methods for doing so as well (like we did above: the script calls "Displayable.translate"), then you must update the bucket yourself:

patch.updateBucket()

The bucket is the region of the 2D world where the Patch lives. Picture the world as a checkerboard, where a given image, wrapped in a Patch object, belongs to each of the square that it intersects. Failing to update the bucket will result in improper canvas repaints--the Patch cannot be found.

Adding images

Adding a single image to a layer shown in an open display

# Obtain a pointer to the frontmost open display:
front = Display.getFront()
# Open an image
filepath = "/path/to/image.tif"
imp = IJ.openImage(filepath)
# Create a new Patch, which wraps an image
patch = Patch(front.project, imp.title, 0, 0, imp)
patch.project.loader.addedPatchFrom(filepath, patch)
# Add it to a layer
front.layer.add(patch)


Copying images between two open projects

The script checks that at least two displays are open, and that they belong to two different projects. Then offers a dialog to choose the direction of copying, and finally copies all images, or all visible or selected images, from one project to the other:

# Albert Cardona 20100201
# Script to copy all images, all visible images, or all selected images
# from a source layer to a target layer.
# To run the script, put it under Fiji plugins folder or subfolder and call "Plugins - Scripting - Update Fiji"
# and make sure you have at least two projects open, each with at least one display open.
# 
# Written for Natalya at Graham Knott's group, EPFL

from ini.trakem2.display import *
from ij.gui import GenericDialog
from ij import IJ
from array import array


def run():
	# Check precondition: at least some displays open
	displays = Display.getDisplays()
	if displays.isEmpty():
		IJ.showMessage("Could not find any TrakEM2 displays open!")
		return
	# Check precondition: at least two displays from two different projects 
	projects = {}
	for display in displays:
		projects[display.project] = display
	if len(projects) < 2:
		IJ.showMessage("You need at least two projects with at least one display open for each!")
		return
	# Show choices
	gd = GenericDialog("Copying images between projects")
	titles = array(String, [display.getFrame().getTitle() for display in displays])
	gd.addChoice("Source layer:", titles, titles[0])
	gd.addChoice("Target layer:", titles, titles[1])
	choices = ["All images", "All visible images", "All selected images"]
	gd.addChoice("Copy:", choices, choices[0])
	gd.showDialog()
	if gd.wasCanceled():
		return
	source = displays[gd.getNextChoiceIndex()]
	target = displays[gd.getNextChoiceIndex()]
	if source == target:
		IJ.showMessage("You must choose different source and target layers!")
		return
	copy_mode = gd.getNextChoiceIndex()
	patches = None
	if 0 == copy_mode:
		patches = source.getLayer().getDisplayables(Patch)
	elif 1 == copy_mode:
		patches = source.getLayer().getDisplayables(Patch, True)
	else:
		patches = source.getSelection().getSelected(Patch)
	if 0 == len(patches):
		IJ.showMessage("No images to copy with option: " + choices[copy_mode])
		return
	# Copy images
	for patch in patches:
		p = patch.clone(target.project, False)
		target.getLayer().add(p)
	target.getLayerSet().enlargeToFit(patches)
	IJ.showStatus("Done copying images between layers.")

run()

To create a script with the above code, copy paste it into a file with an underscore in its name and extension ".py". Then place it in Fiji's plugins folder or subfolder thereof. Finally, restart Fiji or just call "Plugins - Scripting - Refresh Jython Scripts".


Concatenating multiple project XML files by copying all their layers

# Albert Cardona 2010-06-30 for JC Rah
# Takes a list of project XML files
# and grabs all layers in order
# and clones each and all its images
# and then adds it to a newly created project named "all_layers.xml"


from ini.trakem2 import Project
from ini.trakem2.display import Patch
from ini.trakem2.utils import Utils
from ij import IJ


source_dir = "/path/to/projects/" # MUST have ending slash
project_paths = ["project1.xml", "project2.xml", "project3.xml"]

# folder to save the target project at
target_folder = source_dir

def merge_layers():
  # Create a new project target_folder as the storage folder:
  target = Project.newFSProject("blank", None, target_folder)
  # Save it there as "all_layers.xml" so we can call "save()" on it later
  target.saveAs(target_folder + "all_layers.xml", True)
  targetlayerset = target.getRootLayerSet()
  z = 0
  # For each project to concatenate, open it, and:
  for path in project_paths:
    IJ.log("Processing project " + path)
    project = Project.openFSProject(source_dir + path, False)
    rectangle = project.getRootLayerSet().get2DBounds()
    # For each layer in the project, create a new layer "targetlayer" to host a copy of its images:
    for layer in project.getRootLayerSet().getLayers():
      targetlayer = targetlayerset.getLayer(z, 1, True)
      z += 1
      # Add to the new layer copies of each image
      for ob in layer.getDisplayables():
        targetlayer.add(ob.clone(target, False)) # clone in the context of the target project
    project.getLoader().setChanged(False) # avoid dialog at closing
    project.destroy()
    targetlayerset.setMinimumDimensions()
  # Regenerate all image mipmaps
  futures = []
  for patch in targetlayerset.getDisplayables(Patch):
    futures.append(patch.updateMipMaps())
  Utils.wait(futures)
  target.save() # to validate mipmaps
  #target.destroy()  # comment out to close it
  IJ.log("Done!")

# Invoke the function!
merge_layers()

Measure

Measure the minimal distance from each ball to a surface defined by a profile list

Suppose for example that, using a Ball object, you have clicked on each vesicle of a synaptic terminal. And that, using a profile list, you have traced the surface area of a synapse.

3D view of a synaptic surface and its vesicles
Synaptic vesicle measurements of the minimal distance from each vesicle to the synaptic surface

Using the following script, we generate a surface from the profile list, and then measure, for each synaptic vesicle, its minimal distance to the synaptic surface.

The results are finally listed in a results table, from which column-ordered data may be exported for further processing in a spreadsheet.

# Albert Cardona 20100201
# Select a Ball and a Profile, and list the minimal distances of each ball
# to the nearest vertex of the mesh created by the profile list to which
# the profile belongs.
# 
# As asked by Graham Knott and Natalya, from EPFL


from ini.trakem2.display import Display, Ball, Profile
from ini.trakem2.utils import M, Utils
from ij import IJ
from ij.measure import ResultsTable
from ij.gui import GenericDialog
from java.util import HashSet


def run():
	sel = Display.getSelected()
	# Check conditions: one Ball and one Profile only must be selected
	if sel is None or sel.isEmpty():
		IJ.log("Please select a Ball and a Profile!")
		return
	c = [ob.getClass() for ob in sel]
	if Ball in c and Profile in c and 2 == sel.size():
		pass
	else:
		IJ.log("Please select just one Ball and one Profile")
		return
	obs = {}
	for ob in sel:
		obs[ob.getClass()] = ob
	balls = obs[Ball].getWorldBalls()
	profile = obs[Profile]
	profiles = []
	# Gather triangles from profile mesh (into a HashSet to remove the many duplicate vertices)
	verts = HashSet(Profile.generateTriangles(profile.project.findProjectThing(profile).getParent(), 1))
	# Prepare a results table
	rt = ResultsTable()
	rt.setPrecision(2)
	rt.setHeading(0, "Index")
	rt.setHeading(1, "Min distance to surface")
	# Fill data rows
	unit = profile.layer.parent.calibration.unit
	i = 0
	count = len(balls)
	for i in range(count):
		rt.incrementCounter();
		rt.addLabel("units", unit)
		rt.addValue(0, i)
		b = balls[i]
		# For each ball, measure the minimal distance to any of the triangle vertices.
		rt.addValue(1, Math.sqrt(reduce(Math.min, [M.distanceSq(b[0], b[1], b[2], vert.x, vert.y, vert.z) for vert in verts])))
		Utils.showProgress(float(i)/count)
	rt.show("Distances from ball to profile list surface")
	# Reset progress bar
	Utils.showProgress(1)

run()

Interacting with Treeline, AreaTree and Connector

All three types: "treeline", "areatree", and "connector" are expressed by homonimous classes that inherit from the abstract class ini.trakem2.display.Tree.

A Tree is a Displayable and hence presents properties such as title, alpha, color, locked, visible ... which are accessible with their homonimous set and get methods (e.g. setAlpha(0.8f);, getAlpha(); etc.)

The Tree consists of a root Node and public methods to access it and modify it.

The root Node gives access to the rest of the nodes of the Tree. From the canvas, a user would push 'r' on a selected Treeline, AreaTree or Connector to bring the field of view to where the root node is. From code, we would call:

# Acquire a reference the selected object in the Display
t = Display.getFront().getActive()
# If t is not a Tree, the following will fail:
root = t.getRoot()

Now that we have a reference to the root Node, we'll ask it to give us the entire collection of subtree nodes: all nodes in the Tree:

nodes = root.getSubtreeNodes()

The NodeCollection is lazy and doesn't do caching. If you are planning on calling size() on it, and then iterating its nodes, you would end up iterating the whole sequence twice. So let's start by duplicating it:

nodes = [nd for nd in nodes]

Each Node has:

  1. X, Y coordinates, relative to the local coordinate system of the Tree that contains the Node.
  2. A reference to a layer (get it with nd.getLayer()). The Layer has a getZ() method to get the Z coordinate (in pixels).
  3. A data field, which can be a radius or a java.awt.geom.Area (see below).

Each Node contains a getData() public method to acquire whatever it is that it has:

  • Treeline and Connector: its nodes getData() return a radius. The default value is zero.
  • AreaTree: its nodes getData() return a java.awt.geom.Area instance, or null if none yet assigned to it.

Obtaining the X,Y,Z coordinates of all nodes in a Tree

Here is how to iterate over all the node's X,Y,Z positions, in world coordinates:

from ini.trakem2.display import Display
from jarray import array

def getNodeCoordinates(tree):
  """ Returns a map of Node instances vs. their X,Y,Z world coordinates. """
  root = tree.getRoot()
  if root is None:
    return {}
  calibration = tree.getLayerSet().getCalibration()
  affine = tree.getAffineTransform()
  coords = {}
  #
  for nd in root.getSubtreeNodes():
    fp = array([nd.getX(), nd.getY()], 'f')
    affine.transform(fp, 0, fp, 0, 1)
    x = fp[0] * calibration.pixelWidth
    y = fp[1] * calibration.pixelHeight
    z = nd.getLayer().getZ() * calibration.pixelWidth   # a TrakEM2 oddity
    # data may be a radius or a java.awt.geom.Area 
    coords[nd] = [x, y, z]
  #
  return coords

# Obtain the tree selected in the canvas:
tree = Display.getFront().getActive()

# Print all its node coordinates:
for node, coord in getNodeCoordinates(tree).iteritems():
  x, y, z = coord
  print "Coords for node", node, " : ", x, y, z

Sorting nodes by their tags

from ini.trakem2.display import Display

def sortNodesByTags(tree):
  table = {}
  root = tree.getRoot()
  if root is None:
    return table # empty
  #
  for nd in tree.getRoot().getSubtreeNodes():
    tags = nd.getTags()
    if tags is None:
      continue
    for tag in tags:
      tagged = table[tag]
      if tagged is None:
        tagged = []
        table[tag] = tagged
      tagged.append(nd)
  #
  return table

# Obtain the currently selected Tree in the canvas:
tree = Display.getFront().getActive()

# Print the number of nodes that have any given tag:
for tag, tagged in sortNodeByTags(tree).iteritems():
  print "Nodes for tag '" + str(tag) + "':", len(tagged)


Compute the betweenness centrality of every node

The centrality is the measure of how important is a node in tree, according to how many times any possible pair of nodes is linked by a path that passes through that node.

The method we use is Ulrik Brande's fast algorithm for computing betweenness centrality (see the paper).

The method computeCentrality() of class Tree returns as a Map of Node instance vs. its centrality value:

from ini.trakem2.display import Display

# Obtain the currently selected Tree in the canvas:
tree = Display.getFront().getActive()

# Compute betweenness centrality
bc = tree.computeCentrality()   # a java.util.Map

# Print the value for each node
for e in bc.entrySet():
  print e.getKey(), "=>", e.getValue()


We may then use the centrality to colorize the tree with a heat map: the higher the centrality value, the more intense the yellow color; the lower, the more intense the blue color:

from ini.trakem2.display import Display
from java.awt import Color

def computeColor(centrality, highest):
  red = centrality / float(highest)
  blue = 1 - red
  return Color(red, red, blue)

# Obtain the currently selected Tree in the canvas:
tree = Display.getFront().getActive()

# Compute betweenness centrality
bc = tree.computeCentrality()   # a java.util.Map

# Find out the maximum centrality value, to scale:
maximum = reduce(max, bc.values())

# Colorize each node according to its centrality
for e in bc.entrySet():
  node = e.getKey()
  centrality = e.getValue()
  node.setColor(computeColor(centrality, maximum))

# Update display
Display.repaint()

# Show the tree in the 3D Viewer
Display3D.show(tree.getProject().findProjectThing(tree))

Compute the degree of every node

The degree of a node is the number of parent nodes that separate it from the root node. It's a built-in function in Tree (and also in Node):

In the following example, we colorize the tree based on the degree of the node: the closer to the root, the hotest:

from ini.trakem2.display import Display
from java.awt import Color

def computeColor(degree, highest):
  blue = degree / float(highest)
  red = 1 - blue
  return Color(red, red, blue)

# Obtain the currently selected Tree in the canvas:
tree = Display.getFront().getActive()

# Compute betweenness centrality
degrees = tree.computeAllDegrees()   # a java.util.Map

# Find out the maximum degree value, to scale:
maximum = reduce(max, degrees.values())

# Colorize each node according to its degree:
for e in degrees.entrySet():
  node = e.getKey()
  degree = e.getValue()
  node.setColor(computeColor(degree, maximum))

# Update display
Display.repaint()

# Show the tree in the 3D Viewer
Display3D.show(tree.getProject().findProjectThing(tree))

Find branch nodes or end nodes

The Tree class offers methods to obtain the list of all branch points, end points, or both:

from ini.trakem2.display import Display

# Obtain the currently selected treeline or areatree or connector:
tree = Display.getFront().getActive()

# A collection of all end nodes (not lazy):
endNodes = tree.getEndNodes()

# A lazy collection of all branch nodes:
branchNodes = tree.getBranchNodes()

# A lazy collection of both all end nodes and all branch nodes:
endOrBranchNodes = tree.getBranchAndEndNodes()

Remember that these lazy collections are non-caching. If you call size() on it, it will traverse the whole tree of nodes just to find out how many nodes of that kind exist.

If you want to sort out all nodes in one pass, query the number of children that each node has:

* if 0, it's an end node
* if 1, it's a slab node
* if more than 1, it's a branch node
from ini.trakem2.display import Display

# Obtain the currently selected treeline or areatree or connector:
tree = Display.getFront().getActive()

endNodes = []
branchNodes = []
rest = []

for nd in tree.getRoot().getSubtreeNodes():
  count = nd.getChildrenCount()
  if 1 == count:
    rest.append(nd)
  elif 0 == count:
    endNodes.append(nd)
  else:
    branchNodes.append(nd)

print "Found:"
print "end nodes:", len(endNodes)
print "branch nodes:", len(branchNodes)
print "slab nodes:", len(rest)


Find out at which nodes the tree is connected to other trees, via Connector

The idea here is to iterate all nodes of a tree, and determine, for each node, whether it is enclosed by the origin point of a Connector instance. Then, we query that connector for its target objects. In the end, we obtain a table of nodes vs. lists of objects that node is connected to:

from ini.trakem2.display import Display, Connector
from jarray import array
from java.awt.geom import Area
 
# Obtain the currently selected treeline or areatree:
tree = Display.getFront().getActive()
affine = tree.getAffineTransform()
layerset = tree.getLayerSet()
 
# Maps of nd vs list of trees:
outgoing = {}   # e.g. presynaptic to some trees
 
for nd in tree.getRoot().getSubtreeNodes():
  # Obtain the node position in world coordinates 
  fp = array([nd.getX(), nd.getY()], 'f')
  affine.transform(fp, 0, fp, 0, 1)
  x = int(fp[0])
  y = int(fp[1])
  # Query the LayerSet for Connector objects that intersect it
  cs = layerset.findZDisplayables(Connector, nd.getLayer(), x, y, False)
  if cs.isEmpty():
    continue
  # Else, get the target Tree instances that each connector links to:
  targets = []
  area = Area(Rectangle(x, y, 1, 1))
  for connector in cs:
    if connector.intersectsOrigin(area):
      for target in connector.getTargets(Tree):
        target.append(target)      
  if len(targets) > 0:
    outgoing[nd] = targets
 
# print the map of nodes and the number of trees each connects to:
for node, targets in outgoing.iteritems():
  print node, " connects to", len(targets)


Similarly, we could compute the incomming connections. There is a convenience method findConnectors() in class Tree to return two lists: that of the outgoing and that of the incomming Connector instances. From these, one can easily get the connectivity graph, which you may also get by right-clicking on a Display and going for "Export - Connectivity graph...".

Enrich the GUI of TrakEM

Add an extra tab to a Display

TrakEM API is accessible at all times. Here is an example that adds a new tab to the display. The new tab consists of a JPanel with a single button in it.

Notice that Jython lets you define the methods of event listeners as additional arguments to the constructor. So the JButton gets an actionPerformed method (from the ActionListener interface) just by referencing a declared method.

# Albert Cardona 2010-03-19 at EMBL
# Specially demo'ed for Larry Lindsey

def doSomething(evt):
  IJ.showMessage("Button pushed!")

def addReconstructToolkit(display):
  frame = display.getFrame()
  split = frame.getContentPane().getComponent(0)
  left = split.getComponent(0)
  tabs = left.getComponent(2)
  # Check that it's not there already
  title = "Reconstruct toolbar"
  for i in range(tabs.getTabCount()):
    if tabs.getTitleAt(i) == title:
      IJ.showMessage("Reconstruct toolbar already in this Display!")
      return
  # Otherwise, add it new:
  from javax.swing import JPanel, JButton
  pane = JPanel()
  b = JButton("Push it", actionPerformed=doSomething)
  pane.add(b)
  tabs.add(title, pane)


front = Display.getFront()
if front is not None:
  addReconstructToolkit(front)
else:
  IJ.showMessage("Open a display first!")

See also

TrakEM2 tutorials

Jython scripting

Jython scripts for TrakEM2

All the following are included in Fiji's plugins/Examples/TrakEM2_Example_Scripts/ folder: