Fit paths with simple neurite tracer API

@tferr I am having some trouble figuring out how to use the Fit Paths option in simple neurite tracer from a script.

I have a set of images and associated .traces files (100+) from a while back and I would like to apply the ‘Fit Paths’ option on the already traced paths and save the results.
The script would:

  1. open the image and associated traces file
  2. select all paths
  3. run Fit Paths… with diameter = X, optimization options 1) and 2) and replace nodes option.
  4. save traces

I am looking through the traces javadocs and I assume PathFitter (https://javadoc.scijava.org/Fiji/tracing/PathFitter.html) is the class I am looking for, but I am getting lost when trying to use it from a python script (for example, how to set the diameter and optimization options before running the .call() method).

Any guidance or tips would be much appreciated!

These are great questions! There are two ways of doing so:

Option 1: Call the Path Manager’s command

# @SNTService snt_service

from sc.fiji.snt import PathManagerUI
pm = snt_service.getUI().getPathManager()
pm.runCommand("Fit Paths...")

Option 2: Call PathFitter directly on every path:

# @SNTService snt_service

from sc.fiji.snt import PathFitter
snt = snt_service.getPlugin()
for path in snt_service.getPaths():
    pf = PathFitter(snt, path)
    pf.setScope(PathFitter.RADII_AND_MIDPOINTS)
    pf.setMaxRadius(PathFitter.DEFAULT_MAX_RADIUS)
    #pf.setReplaceNodes(False)
    pf.call()
    print("Fitting ", str(path), ": ", str(pf.getSucceeded()))

if snt_service.getUI():
    snt_service.getUI().getPathManager().update()

Option 1) is effectively the same as running the command manually. Option 2) is more flexible, and suitable for running headless. As is it will be slower because fits are performed in sequence: you would need to split the loop across threads. (NB: the reason you need to instantiate a PathFitter for every Path is parallelization, so that each instance can run on a separated thread).

It is not your fault. the javadocs on the scijava portal are awfully outdated. This is because they are built against the old code of Simple Neurite Tracer, not the new version served by the NeuroAnatomy update site. We’ll solve all this, as soon as we release SNT.

2 Likes

@tferr this is a big help, thank you for getting back to me so quickly. I just had a chance to play with this a bit more over the weekend and I made some progress by looking at your tips and referring directly to the SNT source code (the one on the Fiji Github repo) but I have a few more questions.

I am planning to do this headless-ly without opening the UI and associated dialogues at all. So I used your latter example and have the following to load the traces file and image and fit the paths:

from ij import ImagePlus
from sc.fiji.snt import SNT
from sc.fiji.snt import PathAndFillManager
from sc.fiji.snt import PathFitter

def fit_path(img, path_id):
    print("called with path {}".format(path_id))
    pf = PathFitter(img, path_id)
    pf.setScope(4)
    pf.setMaxRadius(10)
    pf.call()
    print(pf.getSucceeded())
    # return the PathFitter instance in case I need it to update the pfm path passed in.
    return pf 

img = ImagePlus(image_test) # some image I already traced
pfm = PathAndFillManager()
pfm.setHeadless(True)
pfm.load(traces_file_test) # load the associated traces file associated with that image

for path in pfm.getPaths():
    pm_temp = fit_path(img, path)
    #.... do something here to update the path on the main list...
#finally, save the csv properties file for all the traces (pfm.exportToCSV())
# and save the new traces file with the fit paths
  1. This seems to be fitting the paths without errors, but is it updating the paths in the PathAndFillManager() instance in place? In your example you used snt_service.getUI().getPathManager().update() after looping through all the paths, which seems to update everything at the end, is that correct? If so, how do you do it without opening the UI version of the plugin?
  2. Do I need to assign spatial settings headlessly or are they read from the traces file? I tried to pass the ImagePlus to .assignSpatialSettings() but it is protected and I can’t see it being called anywhere in PathAndFillManager.
  3. Once I have updated and processed the paths, how do i use the protected method PathAndFillManager.writeXML() to save a new traces file? I haven’t tried it yet, but it looks like PathAndFillManager.exportToCSV() is public so I shouldn’t have a problem accessing that.

Thanks again for your help, I appreciate it.

@Nick_George, Sorry for the delay. I’ve been traveling.
PathAndFillManager is a bit low level for this kind of task (reason why those methods are not public).
Scripting becomes more friendly if you use SNTService and/or SNT directly. Here is an example that uses SNTService to initialize SNT in “headless” mode using a specified image, before performing the fit. Also, when working without a GUI, the best way to ensure that the fitted “flavor” of Path is being used is to use PathFitter.setReplaceNodes() :

#@SNTService sntService

from sc.fiji.snt import (Path, PathFitter, Tree)

#NB: These could also be retrieved using @parameters
img_path = "/path/to/image.tif";  # path to image
tracing_path = "/path/to/reconstruction.file"; # .traces, swc, .json

# Initialize SNT using the specified image
snt = sntService.initialize(img_path, False)  # Initialize with GUI?

# Load and retrieve paths. Alternative one could also use
## loaded_tree = Tree(tracing_path)
## sntService.loadTree(loaded_tree)
sntService.loadTracings(tracing_path)
loaded_tree = sntService.getTree(False)  # Selected paths only?

# Perform the fit
for p in loaded_tree.list():
    pf = PathFitter(snt, p)
    pf.setScope(PathFitter.RADII) # PathFitter.MIDPOINTS or PathFitter.RADII_AND_MIDPOINTS
    pf.setReplaceNodes(True)
    pf.call()
    print("Fitting ", str(p), ": ", str(pf.getSucceeded()))

# Save result
sntService.save(tracing_path.replace(".", "_fitted_")+".traces")
1 Like

@tferr thanks for following up. This looks like just what I need and I’ll give it a shot this weekend and report back.
Can you point me to documentation (if any exists yet) for the SNT’s ImageJ Ops @SNTService? If there isn’t documentation wherever it is in the code would be helpful as well.

Also- I’ll be doing a lot of Jython scripting with SNT in the future, I’d be happy to help document features and provide examples if that would be helpful.

@Nick_George,

The API documentation is currently at http://fiji.github.io/SNT/ (it will be at https://javadoc.scijava.org/ once SNT is released)

That would be AWESOME!! Currently all the documentation has been written by @arshadic and me. https://imagej.net/SNT:_Scripting would be the place for it. Some things could also go in the FAQs. Do let us know how it goes!

1 Like

@tferr Your suggestion worked great! I edited it a bit so I could get at the PathAndFillManager.exportToCSV() function. The current script for fitting a single image single threaded (slightly modified from your example) is below:

# @SNTService sntService
# script to fit paths from previous tracing session programatically via Jython and SNT
# single threaded fitting and slow. Will refactor to multithreaded in future....
# see: https://forum.image.sc/t/fit-paths-with-simple-neurite-tracer-api/30139/

# test traces file
traces_test = "path/to/traces_file.traces"
# test img file
image_test = "path/to/image_file.tif"


import os
from java.io import File
from sc.fiji.snt import Path
from sc.fiji.snt import PathFitter
from sc.fiji.snt import PathAndFillManager
from sc.fiji.snt import Tree

print("starting")


snt = sntService.initialize(image_test, False) # No GUI
sntService.loadTracings(traces_test)
loaded_tree = sntService.getTree(False)

pfm = PathAndFillManager() # for later quick export. 
pfm.addTree(loaded_tree) # add tree to manager instance

for p in loaded_tree.list():
    pf = PathFitter(snt, p)
    pf.setMaxRadius(10) # option for path fitting. 
    pf.setScope(PathFitter.RADII_AND_MIDPOINTS) # another option for path fitting. 
    pf.setReplaceNodes(True)
    pf.call()
    print("fitting {}  : {}".format(p, str(pf.getSucceeded())))

new_traces_fname = os.path.join(os.path.dirname(traces_test), "new_traces.traces")
new_properties_fname = os.path.join(os.path.dirname(traces_test), "new_properties.csv")
print("saving csv properties: {}".format(new_properties_fname))
pfm.exportToCSV(File(new_properties_fname)) # export quick measurements to csv, using Java IO file. 
print("saving traces properties: {}".format(new_traces_fname))
sntService.save(new_traces_fname) # save traces file for csv
print("done")

I will work on making it multithreaded and will update the code via this gist if anyone is interested.
I also noticed that every time I run FitPaths (manually with the same traces and image file) I get slightly different results in the exportToCSV() csv file every time. Is this expected behavior? If not I will make a reproducible example.
Thanks for your help!