Skip to content

Commit 03c8df7

Browse files
committed
Fix: Path detectors outputting locations w/ wrong offsets in stream mode
While at it improve command prompts: Only compute detections if all criteria are met
1 parent 3ddd34c commit 03c8df7

3 files changed

Lines changed: 337 additions & 233 deletions

File tree

‎src/main/java/sc/fiji/snt/gui/cmds/AlongPathDetectorCmd.java‎

Lines changed: 98 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,10 @@ public class AlongPathDetectorCmd extends CommonDynamicCmd {
9191
+ "These locations may have naturally enlarged radii and may produce false positives.")
9292
private boolean excludeJunctions = true;
9393

94-
@Parameter(label = "Output", choices = {"ROIs", "Bookmarked locations"})
94+
private static final String OUTPUT_BOOKMARKS = "Bookmarked locations";
95+
private static final String OUTPUT_ROIS = "ROIs";
96+
97+
@Parameter(label = "Output", choices = {OUTPUT_ROIS, OUTPUT_BOOKMARKS})
9598
private String outputChoice;
9699

97100
@Parameter(label = "Paths", required = false, persist = false)
@@ -106,6 +109,7 @@ private void init() {
106109
if (paths == null || paths.isEmpty()) {
107110
paths = snt.getUI().getPathManager().getSelectedPaths(true);
108111
}
112+
if (noPathsError()) return;
109113

110114
// If selected paths have no radii but their fitted flavors do have radii, use those instead
111115
paths = paths.stream()
@@ -140,12 +144,30 @@ private void init() {
140144
mItem.setMaximumValue(0.0);
141145
resolveInput("channel");
142146
}
147+
148+
final MutableModuleItem<String> outputChoiceItem = getInfo().getMutableInput("outputChoice", String.class);
149+
// ROI output needs a classic canvas to attach the PointRoi t
150+
if (snt.getImagePlus() == null)
151+
outputChoiceItem.setChoices(List.of(OUTPUT_BOOKMARKS));
143152
}
144153

145-
@Override
146-
public void run() {
154+
private boolean noPathsError() {
147155
if (paths == null || paths.isEmpty()) {
148156
error("No paths selected for analysis.");
157+
return true;
158+
}
159+
return false;
160+
}
161+
162+
@Override
163+
public void run() {
164+
if (noPathsError()) return;
165+
166+
// Short-circuit ROI output as early as possible, before running
167+
final boolean roiOutput = !(ui != null && OUTPUT_BOOKMARKS.equals(outputChoice));
168+
if (roiOutput && (snt == null || snt.getImagePlus() == null)) {
169+
error(String.format("ROI output requires a %s. Use 'Bookmarked locations' output instead.",
170+
(snt != null && snt.isStreamMode() ? "materialized crop" : "valid image")));
149171
return;
150172
}
151173

@@ -230,16 +252,18 @@ public void run() {
230252
SNTUtils.log("AlongPathDetectorCmd: " + cfg);
231253

232254
// Run detection
233-
final List<Detection> results;
234-
if (intensityImg != null) {
235-
// Temporarily stamp each path's canvasOffset/spacing from the live session state, so
236-
// AlongPathDetector's on-skeleton intensity sampling reads the correct voxel from
237-
// intensityImg. Same save/apply/restore idiom as SNT#makePathVolume(): bulk-added paths
238-
// (addTree()/SWC import/.traces loading) never get canvasOffset/spacing stamped otherwise,
239-
// and intensityImg is always the CURRENTLY ACTIVE grid (getLoadedData()/getCtSlice3d()),
240-
// matching snt.getActiveCanvasPixelOffset() (crop-relative while a materialized crop is active)
241-
final List<PointInCanvas> originalOffsets = new ArrayList<>(pathsToProcess.size());
242-
final List<ij.measure.Calibration> originalSpacings = new ArrayList<>(pathsToProcess.size());
255+
// Temporarily stamp each path's canvasOffset/spacing from the live session state. This is needed
256+
// regardless of whether intensity filtering is active: it is not just AlongPathDetector's own
257+
// on-skeleton sampling that depends on it, but also Detection#xyzct()/RoiConverter's world->pixel
258+
// conversion for bookmark/ROI output further below. Same save/apply/restore idiom as
259+
// SNT#makePathVolume(): bulk-added paths (addTree()/SWC import/.traces loading) never get
260+
// canvasOffset/spacing stamped otherwise (see PathAndFillManager#assignSpatialSettings()'s NB),
261+
// so they default to (0,0,0) - wrong whenever the source has a non-zero world-origin offset
262+
// (Stream mode). snt can be null when this command runs headless against Paths that already
263+
// carry their own correct canvasOffset; leave those untouched.
264+
final List<PointInCanvas> originalOffsets = (snt == null) ? null : new ArrayList<>(pathsToProcess.size());
265+
final List<ij.measure.Calibration> originalSpacings = (snt == null) ? null : new ArrayList<>(pathsToProcess.size());
266+
if (snt != null) {
243267
final PointInCanvas liveOffset = snt.getActiveCanvasPixelOffset();
244268
final ij.measure.Calibration liveCal = snt.getCalibration();
245269
for (final Path p : pathsToProcess) {
@@ -248,73 +272,74 @@ public void run() {
248272
p.setCanvasOffset(liveOffset);
249273
p.setSpacing(liveCal);
250274
}
251-
try {
252-
results = AlongPathDetector.detect(pathsToProcess, intensityImg, cfg);
253-
} finally {
275+
}
276+
try {
277+
final List<Detection> results = AlongPathDetector.detect(pathsToProcess, intensityImg, cfg);
278+
279+
if (results.isEmpty()) {
280+
error("No swellings detected with current parameters.");
281+
return;
282+
}
283+
284+
SNTUtils.log("Detected " + results.size() + " swellings");
285+
286+
// Group results by path, resolving fitted paths back to their
287+
// unfitted originals so that labels and counts match PathManagerUI
288+
final Map<Path, List<Detection>> resultsByPath = results.stream()
289+
.collect(Collectors.groupingBy(v ->
290+
v.path.isFittedVersionOfAnotherPath()
291+
? v.path.getUnfitted() : v.path
292+
));
293+
294+
// Update spine/varicosity counts on the (unfitted) paths
295+
resultsByPath.forEach((p, detections) -> p.setSpineOrVaricosityCount(
296+
p.getSpineOrVaricosityCount() + detections.size()));
297+
298+
if (ui != null)
299+
ui.getPathManager().applyDefaultTags("No. of Spine/Varicosity Markers");
300+
301+
if (ui != null && OUTPUT_BOOKMARKS.equals(outputChoice)) {
302+
303+
// NB: Detection#xyzct()/RoiConverter (ROI branch below) read each Path's CURRENT
304+
// canvasOffset/calibration to convert back to pixel space, so this must run while paths
305+
// are still stamped with the live offset set above - see the identical NB in
306+
// PeripathDetectorCmd#run()
307+
resultsByPath.forEach((path, detections) -> {
308+
final List<double[]> locs = detections.stream()
309+
.map(Detection::xyzct)
310+
.collect(Collectors.toList());
311+
ui.getBookmarkManager().add(path.getName() + " Swelling ", locs, path.getColor());
312+
});
313+
resetUI();
314+
ui.selectTab("Bookmarks");
315+
ui.showStatus(results.size() + " swellings added to Bookmark Manager.", true);
316+
317+
} else {
318+
// imp/roiOutput already validated up front, before detection ran - see the guard at
319+
// the top of run()
320+
final ImagePlus imp = snt.getImagePlus();
321+
RoiManager rm = RoiManager.getInstance2();
322+
if (rm == null) rm = new RoiManager();
323+
for (final Map.Entry<Path, List<Detection>> entry : resultsByPath.entrySet()) {
324+
final Path path = entry.getKey();
325+
final String name = path.getName() + " (" + entry.getValue().size() + " swellings)";
326+
rm.addRoi(RoiConverter.toPointRoi(entry.getValue(), imp, name, path.getColor()));
327+
}
328+
resetUI();
329+
rm.runCommand("sort");
330+
rm.runCommand("show all");
331+
}
332+
} finally {
333+
// Restore each Path's original canvasOffset/spacing only now that all output (bookmarks/
334+
// ROIs) has been built from the live-offset-stamped state above
335+
if (snt != null) {
254336
int i = 0;
255337
for (final Path p : pathsToProcess) {
256338
p.setCanvasOffset(originalOffsets.get(i));
257339
p.setSpacing(originalSpacings.get(i));
258340
i++;
259341
}
260342
}
261-
} else {
262-
results = AlongPathDetector.detect(pathsToProcess, intensityImg, cfg);
263-
}
264-
265-
if (results.isEmpty()) {
266-
error("No swellings detected with current parameters.");
267-
return;
268-
}
269-
270-
SNTUtils.log("Detected " + results.size() + " swellings");
271-
272-
// Group results by path, resolving fitted paths back to their
273-
// unfitted originals so that labels and counts match PathManagerUI
274-
final Map<Path, List<Detection>> resultsByPath = results.stream()
275-
.collect(Collectors.groupingBy(v ->
276-
v.path.isFittedVersionOfAnotherPath()
277-
? v.path.getUnfitted() : v.path
278-
));
279-
280-
// Update spine/varicosity counts on the (unfitted) paths
281-
resultsByPath.forEach((p, detections) -> p.setSpineOrVaricosityCount(
282-
p.getSpineOrVaricosityCount() + detections.size()));
283-
284-
if (ui != null)
285-
ui.getPathManager().applyDefaultTags("No. of Spine/Varicosity Markers");
286-
287-
if (ui != null && outputChoice.toLowerCase().contains("bookmark")) {
288-
289-
resultsByPath.forEach((path, detections) -> {
290-
final List<double[]> locs = detections.stream()
291-
.map(Detection::xyzct)
292-
.collect(Collectors.toList());
293-
ui.getBookmarkManager().add(path.getName() + " Swelling ", locs, path.getColor());
294-
});
295-
resetUI();
296-
ui.selectTab("Bookmarks");
297-
ui.showStatus(results.size() + " swellings added to Bookmark Manager.", true);
298-
299-
} else {
300-
// Stream mode without a materialized crop: no classic canvas for RoiManager/PointRoi to
301-
// attach to (unlike detection itself above, this really has no Stream-mode equivalent)
302-
final ImagePlus imp = (snt != null) ? snt.getImagePlus() : null;
303-
if (imp == null) {
304-
error(String.format("ROI output requires a %s. Use 'Bookmarked locations' output instead.",
305-
(snt != null && snt.isStreamMode() ? "materialized crop" : "valid image")));
306-
return;
307-
}
308-
RoiManager rm = RoiManager.getInstance2();
309-
if (rm == null) rm = new RoiManager();
310-
for (final Map.Entry<Path, List<Detection>> entry : resultsByPath.entrySet()) {
311-
final Path path = entry.getKey();
312-
final String name = path.getName() + " (" + entry.getValue().size() + " swellings)";
313-
rm.addRoi(RoiConverter.toPointRoi(entry.getValue(), imp, name, path.getColor()));
314-
}
315-
resetUI();
316-
rm.runCommand("sort");
317-
rm.runCommand("show all");
318343
}
319344
}
320345
}

0 commit comments

Comments
 (0)