|
| 1 | +/** |
| 2 | + * file: Peripath_Detection_Demo.groovy |
| 3 | + * author: Tiago Ferreira |
| 4 | + * version: 2026.04.09 |
| 5 | + * info: Demonstrates the PeripathDetector workflow: automated detection of |
| 6 | + * intensity maxima (spines, varicosities, puncta) around traced paths. |
| 7 | + * |
| 8 | + * The script loads a demo image of a cultured hippocampal neuron with |
| 9 | + * synaptic markers, auto-traces a dendrite, fits radii, runs detection, |
| 10 | + * and then shows how the annular search region (torus mask) can be used |
| 11 | + * to extract the peri-neuronal signal for further analysis. |
| 12 | + * |
| 13 | + * @see https://imagej.net/plugins/snt/spines-varicosities |
| 14 | + * @see https://imagej.net/plugins/snt/scripting |
| 15 | + * @see https://javadoc.scijava.org/SNT/ |
| 16 | + * @see sc.fiji.snt.analysis.PeripathDetector |
| 17 | + */ |
| 18 | + |
| 19 | +#@ SNTService snt |
| 20 | + |
| 21 | +// Ensure SNT is up-to-date and is not busy with another operation |
| 22 | +snt.requireVersion("5.0.7") |
| 23 | +if (snt.isActive() && snt.getUI() && !snt.getUI().isReady()) { |
| 24 | + print("Please complete current operation before running this script!") |
| 25 | + return |
| 26 | +} |
| 27 | + |
| 28 | +// 1) Load a demo image. We'll use CIL:810: it is a cultured hippocampal |
| 29 | +// neuron with 3 channels: Ch1: N-caherin; Ch2: V-glut; Ch3: NMDAR. We'll |
| 30 | +// detect V-glut puncta (Ch2) around a traced dendrite traced from Ch1 |
| 31 | +imp = snt.demoImage("cil810") |
| 32 | +detectionChannel = 2 // V-glut channel |
| 33 | + |
| 34 | +// 2) Trace detection neurite. This demo image has no pre-existing |
| 35 | +// reconstructions, so we'll auto-trace a path between two manually chosen |
| 36 | +// points on a primary dendrite |
| 37 | +plugin = snt.initialize(imp, snt.isActive()) // image to be traced, boolean flag for GUI display |
| 38 | +startPoint = SNTPoint.of(58.986, 39.211, 0.0) |
| 39 | +endPoint = SNTPoint.of(0.339, 17.967, 0.0) |
| 40 | +path = plugin.autoTrace(startPoint, endPoint, null) |
| 41 | +println("Traced path: ${path.getName()} (${path.size()} nodes)") |
| 42 | + |
| 43 | +// 3) Fit radii. PeripathDetector uses per-node radii to define the search |
| 44 | +// annulus. Without fitted radii, a fallback (2x voxel size) is used for all |
| 45 | +// nodes, which may not reflect the actual neurite thickness. path.fitRadii() |
| 46 | +// fits circular cross-sections to the image signal at each node, computing |
| 47 | +// locally adaptive radii, equivalent to the GUI's "Fit Path" action |
| 48 | +path.fitRadii(imp) |
| 49 | +println("Mean fitted radius: ${sprintf('%.2f', path.getMeanRadius())} ${path.getCalibration().unit}") |
| 50 | + |
| 51 | +// 4) Configure the detector. The detector searches for intensity maxima in |
| 52 | +// an annular band around each path node. The annulus is defined by inner/ |
| 53 | +// outer radius settings: |
| 54 | +// |
| 55 | +// innerRadiusMultiplier: where the search starts, as a fraction of the node's |
| 56 | +// fitted radius. 1.0 = at the neurite edge; < 1.0 = includes the membrane; |
| 57 | +// > 1.0 = starts outside the neurite |
| 58 | +// |
| 59 | +// outerRadiusMultiplier: where the search ends, as a multiple of the node's |
| 60 | +// radius. 2.0 = searches up to 2x the neurite thickness |
| 61 | +// |
| 62 | +// prominence: noise tolerance for MaximumFinder (grayscale units). A maximum |
| 63 | +// must protrude above its surrounding saddle by at least this value. |
| 64 | +// Higher = fewer, more confident detections. A good starting point may be |
| 65 | +// ~5% of the channel's dynamic range. It can be previewed using IJ's built-in |
| 66 | +// command |
| 67 | +// |
| 68 | +// mergingDistance: nearby detections within this distance (physical units) are |
| 69 | +// merged, keeping the brightest. Set to -1 for auto (= outer radius) |
| 70 | +cfg = new PeripathDetector.Config() |
| 71 | + .innerRadiusMultiplier(.70) // start within the neurite |
| 72 | + .outerRadiusMultiplier(2.5) // search up to 2.5x neurite thickness |
| 73 | + .prominence(50.0) // moderate noise tolerance |
| 74 | + .mergingDistance(-1) // auto |
| 75 | + .assignToNearestPath(true) |
| 76 | + |
| 77 | +// 5) Run detection |
| 78 | +detectionImg = ImgUtils.getCtSlice3d(imp, detectionChannel, 1) |
| 79 | +paths = [path] |
| 80 | +detections = PeripathDetector.detect(paths, detectionImg, cfg) |
| 81 | +println("Detected ${detections.size()} maxima in channel ${detectionChannel}") |
| 82 | + |
| 83 | +// 6) Display detections as ROIs. RoiConverter.toPointRoi() creates one |
| 84 | +// multi-point ROI per path, with per-point Z positions and the path's |
| 85 | +// name/color for easier identification |
| 86 | +rm = RoiManager.getInstance2() |
| 87 | +if (rm == null) { |
| 88 | + rm = new RoiManager() |
| 89 | +} |
| 90 | +detections.groupBy { |
| 91 | + it.path |
| 92 | +}.each { |
| 93 | + p, dets -> name = "${p.getName()} (${dets.size()} maxima)" |
| 94 | + rm.addRoi(RoiConverter.toPointRoi(dets, imp, name, p.getColor())) |
| 95 | +} |
| 96 | +rm.runCommand("Sort") |
| 97 | +rm.runCommand("Show All") |
| 98 | + |
| 99 | +// 7) Generate and display the torus mask, a binary image representing the |
| 100 | +// annular search region. |
| 101 | +// NB: This mask is an approximation for visualization and segmentation. The |
| 102 | +// detection itself operates on per-node cross-section profiles, _not_ this |
| 103 | +// mask. For 2D images, each node contributes a perpendicular line segment; |
| 104 | +// small gaps between nodes are expected from digitization. |
| 105 | +maskImp = ImpUtils.create("Torus Mask", imp.getWidth(), imp.getHeight(), imp.getNSlices(), 8) // title, dimensions, and bit-depth |
| 106 | +maskImp.setCalibration(imp.getCalibration()) |
| 107 | +PeripathDetector.createTorusMask(paths, maskImp, cfg, 1) // set masked pixels to 1 |
| 108 | + |
| 109 | +// 8) Use the mask to extract peri-neuronal signal. We'll multiply the |
| 110 | +// detection channel by the normalized mask (0/1) to isolate only the |
| 111 | +// signal within the torus region. This is useful for measuring intensities, |
| 112 | +// computing integrated density, or feeding into downstream analysis |
| 113 | +// (e.g., colocalization, thresholding) |
| 114 | +chImp = ImpUtils.getChannel(imp, detectionChannel) |
| 115 | +extracted = ImageCalculator.run(chImp, maskImp, "multiply create 32-bit") |
| 116 | +extracted.setTitle("Ch${detectionChannel} Signal in Torus") |
| 117 | +extracted.show() |
| 118 | + |
| 119 | +println("Done. ${detections.size()} maxima detected. ROIs, torus mask, and extracted signal displayed.") |
| 120 | + |
| 121 | + |
| 122 | +// Imports below: |
| 123 | +import ij.IJ |
| 124 | +import ij.plugin.ImageCalculator |
| 125 | +import ij.plugin.frame.RoiManager |
| 126 | +import sc.fiji.snt.analysis.PeripathDetector |
| 127 | +import sc.fiji.snt.analysis.RoiConverter |
| 128 | +import sc.fiji.snt.util.ImgUtils |
| 129 | +import sc.fiji.snt.util.ImpUtils |
| 130 | +import sc.fiji.snt.util.SNTPoint |
0 commit comments