Skip to content

Commit 81abdf4

Browse files
committed
Make skeletonizerCmd work in stream mode.
See #314
1 parent 5d28c95 commit 81abdf4

3 files changed

Lines changed: 122 additions & 47 deletions

File tree

src/main/java/sc/fiji/snt/SNT.java

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,22 @@ public boolean isMaterializedCrop() {
899899
return ImpUtils.isMaterializedCrop(xy);
900900
}
901901

902+
/**
903+
* @return the world-space (calibrated) extent of the currently materialized crop (see
904+
* {@link #materializeDisplayCanvas(BoundingBox)}), or {@code null} if none is active. Useful for callers
905+
* that want to restrict an operation to the region actually materialized.
906+
*/
907+
public BoundingBox getMaterializedCropWorldBounds() {
908+
if (!isMaterializedCrop()) return null;
909+
final Calibration cal = getCalibration();
910+
final PointInCanvas off = activeCanvasPixelOffset;
911+
final BoundingBox box = new BoundingBox();
912+
box.setSpacing(cal.pixelWidth, cal.pixelHeight, cal.pixelDepth, cal.getUnit());
913+
box.setOrigin(new PointInImage(-off.x * cal.pixelWidth, -off.y * cal.pixelHeight, -off.z * cal.pixelDepth));
914+
box.setDimensions(width, height, depth);
915+
return box;
916+
}
917+
902918
/*
903919
* Reverts a materialized crop back to a plain Stream-mode session: restores ctSlice3d to the original
904920
* streamedSourceData cached by materializeDisplayCanvas(BoundingBox), resets every Path's canvasOffset back to 0
@@ -2522,7 +2538,37 @@ public synchronized ImagePlus makePathVolume(final Collection<Path> paths, final
25222538
for (int i = 0; i < depth; ++i)
25232539
snapshot_data[i] = new short[width * height];
25242540

2525-
pathAndFillManager.setPathPointsInVolume(paths, snapshot_data, (labelsImage) ? (short)-1 : (short) 255, width);
2541+
// setPathPointsInVolume() rasterizes into this session's raw voxel-index grid via each Path's
2542+
// own canvasOffset and its own spacing (see Path#getXUnscaled/Y/Z: node.x/x_spacing + canvasOffset.x).
2543+
// Interactively-traced Paths are kept in sync with activeCanvasPixelOffset (see syncActivePathCanvasState(),
2544+
// AbstractBigViewer#finishPath()), and installMaterializedCrop() also re-stamps every already-loaded Path's
2545+
// spacing from the crop's own calibration - but Paths added in bulk (addTree()/addTrees(), SWC/graph import,
2546+
// loading a .traces file) are never re-stamped either way, so they can still be carrying canvasOffset's class
2547+
// default of (0,0,0) and/or whatever spacing they had at import.
2548+
// Apply this session's live activeCanvasPixelOffset/calibration just for this rasterization call
2549+
// (picks up a materialized crop's own crop-relative offset/calibration automatically, since both are
2550+
// read live), then restore each Path's original canvasOffset/spacing immediately after - same
2551+
// save/apply/restore pattern Tree#getSkeletonInternal() already uses for, so paths keep rendering exactly as
2552+
// before everywhere else (viewers, Path Manager, etc.).
2553+
final Calibration liveCal = getCalibration();
2554+
final List<PointInCanvas> originalOffsets = new ArrayList<>(paths.size());
2555+
final List<Calibration> originalSpacings = new ArrayList<>(paths.size());
2556+
for (final Path p : paths) {
2557+
originalOffsets.add(p.getCanvasOffset());
2558+
originalSpacings.add(p.getCalibration());
2559+
p.setCanvasOffset(activeCanvasPixelOffset);
2560+
p.setSpacing(liveCal);
2561+
}
2562+
try {
2563+
pathAndFillManager.setPathPointsInVolume(paths, snapshot_data, (labelsImage) ? (short)-1 : (short) 255, width);
2564+
} finally {
2565+
int i = 0;
2566+
for (final Path p : paths) {
2567+
p.setCanvasOffset(originalOffsets.get(i));
2568+
p.setSpacing(originalSpacings.get(i));
2569+
i++;
2570+
}
2571+
}
25262572

25272573
final ImageStack newStack = new ImageStack(width, height);
25282574

src/main/java/sc/fiji/snt/gui/cmds/CommonDynamicCmd.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,12 @@ protected void resetUI(final boolean validateDimensions, final int state) {
204204
if (validateDimensions && !isCanceled())
205205
ui.runCommand("validateImgDimensions");
206206
}
207+
if (snt != null && snt.isStreamMode() && ui != null) {
208+
final sc.fiji.snt.viewer.AbstractBigViewer viewer = ui.getActiveBigViewer();
209+
if (viewer != null) {
210+
viewer.updateStatus("", 0, 0);
211+
}
212+
}
207213
statusService.clearStatus();
208214
}
209215

src/main/java/sc/fiji/snt/plugin/SkeletonizerCmd.java

Lines changed: 69 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -23,53 +23,53 @@
2323
package sc.fiji.snt.plugin;
2424

2525
import java.awt.image.IndexColorModel;
26+
import java.util.Collection;
2627
import java.util.HashMap;
28+
import java.util.List;
2729
import java.util.Map;
30+
import java.util.stream.Collectors;
2831

2932
import net.imagej.ImageJ;
3033

3134
import org.scijava.command.Command;
35+
import org.scijava.module.MutableModuleItem;
3236
import org.scijava.plugin.Parameter;
3337
import org.scijava.plugin.Plugin;
34-
import org.scijava.ui.DialogPrompt;
35-
import org.scijava.ui.DialogPrompt.Result;
36-
import org.scijava.ui.UIService;
37-
import org.scijava.widget.ChoiceWidget;
3838

3939
import ij.ImagePlus;
4040
import ij.ImageStack;
4141
import ij.gui.Roi;
4242
import ij.plugin.LutLoader;
4343
import ij.process.ImageProcessor;
44+
import org.scijava.widget.ChoiceWidget;
4445
import sc.fiji.analyzeSkeleton.AnalyzeSkeleton_;
4546
import sc.fiji.skeletonize3D.Skeletonize3D_;
46-
import sc.fiji.snt.SNT;
47-
import sc.fiji.snt.SNTService;
47+
import sc.fiji.snt.Path;
4848
import sc.fiji.snt.Tree;
49+
import sc.fiji.snt.gui.cmds.CommonDynamicCmd;
50+
import sc.fiji.snt.util.BoundingBox;
4951
import sc.fiji.snt.util.ImpUtils;
5052

5153
/**
5254
* Convenience command for converting Paths into skeleton images
5355
*
5456
* @author Tiago Ferreira
5557
*/
56-
@Plugin(type = Command.class, label = "Convert Paths to Topographic Skeletons")
57-
public class SkeletonizerCmd implements Command {
58+
@Plugin(type = Command.class, initializer = "init", label = "Convert Paths to Topographic Skeletons")
59+
public class SkeletonizerCmd extends CommonDynamicCmd {
5860

5961
static { net.imagej.patcher.LegacyInjector.preinit(); } // required for _every_ class that imports ij. classes
6062

61-
@Parameter
62-
private UIService uiService;
63-
64-
@Parameter
65-
private SNTService sntService;
63+
private static final String NO_RESTRICTION = "None (Convert complete paths)";
64+
private static final String MATERIALIZED_REGION_RESTRICTION = "Convert only paths within the materialized region";
65+
private static final String ROI_RESTRICTION = "Convert only path segments contained by ROI";
6666

6767
@Parameter(required = false, label = "Output", style = ChoiceWidget.RADIO_BUTTON_VERTICAL_STYLE, choices = {
6868
"Binary (all paths have the same intensity)", "Labels (each path has an unique intensity)" })
6969
private String imgChoice;
7070

71-
@Parameter(required = false, label = "Roi filtering", style = ChoiceWidget.RADIO_BUTTON_VERTICAL_STYLE, choices = {
72-
"None (Convert complete paths)", "Convert only path segments contained by ROI" })
71+
@Parameter(required = false, label = "Roi filtering", style = ChoiceWidget.RADIO_BUTTON_VERTICAL_STYLE,
72+
choices = { NO_RESTRICTION, ROI_RESTRICTION })
7373
private String roiChoice;
7474

7575
@Parameter(required = false, label = "Run \"Analyze Skeleton\" after conversion")
@@ -78,7 +78,23 @@ public class SkeletonizerCmd implements Command {
7878
@Parameter(required = true)
7979
private Tree tree;
8080

81-
private SNT plugin;
81+
82+
@SuppressWarnings("unused")
83+
private void init() {
84+
super.init(true);
85+
if (snt != null && snt.isStreamMode()) {
86+
if (snt.isMaterializedCrop()) {
87+
// BDV/BVV have no ROI concept, but a materialized crop (see SNT#isMaterializedCrop()) defines an
88+
// analogous region to restrict to
89+
final MutableModuleItem<String> mi = getInfo().getMutableInput("roiChoice", String.class);
90+
mi.setChoices(List.of(NO_RESTRICTION, MATERIALIZED_REGION_RESTRICTION));
91+
roiChoice = MATERIALIZED_REGION_RESTRICTION;
92+
} else {
93+
resolveInput("roiChoice"); // No crop materialized: nothing to restrict to
94+
roiChoice = NO_RESTRICTION;
95+
}
96+
}
97+
}
8298

8399
/*
84100
* (non-Javadoc)
@@ -92,37 +108,58 @@ public void run() {
92108
error("No Paths to convert.");
93109
return;
94110
}
95-
plugin = sntService.getInstance();
96-
if (plugin == null) {
111+
if (snt == null) {
97112
error("No active instance of SNT was found.");
98113
return;
99114
}
100115

101-
final ImagePlus imp = plugin.getImagePlus();
102-
final boolean displayCanvas = !plugin.accessToValidImageData();
103-
final boolean twoDdisplayCanvas = imp != null && imp.getNSlices() == 1 && tree.is3D();
104-
final boolean useNewImage = displayCanvas || twoDdisplayCanvas;
116+
final ImagePlus imp = snt.getImagePlus();
117+
final boolean displayCanvas = !snt.accessToValidImageData();
118+
final boolean twoDDisplayCanvas = imp != null && imp.getNSlices() == 1 && tree.is3D();
119+
final boolean useNewImage = displayCanvas || twoDDisplayCanvas;
120+
121+
// Stream mode's analog of classic mode's ROI restriction below: skeletonization is already spatially confined
122+
// to a materialized crop (SNT#makePathVolume() rasterizes into the crop's own, small array; nodes outside it
123+
// are silently dropped), but every Path in the tree would still be walked to discover that, wasting Bresenham3D
124+
// computation. Pre-filter to just the whole paths that intersect the crop's world bounds instead
125+
final boolean restrictToMaterializedRegion = !useNewImage && snt.isStreamMode() && snt.isMaterializedCrop()
126+
&& MATERIALIZED_REGION_RESTRICTION.equals(roiChoice);
105127

106128
final Roi roi = (imp == null) ? null : imp.getRoi();
107-
boolean restrictByRoi = !roiChoice.startsWith("None");
129+
boolean restrictByRoi = !restrictToMaterializedRegion && !roiChoice.startsWith("None");
108130
final boolean validAreaRoi = (roi == null || !roi.isArea());
109131
if (restrictByRoi && validAreaRoi) {
110-
if (!getConfirmation(
132+
if (!getConfirmationEdtSafe(
111133
"ROI filtering requested but no area ROI was found.\n" +
112134
"Proceed without ROI filtering?", "Proceed Without ROI Filtering?"))
113135
return;
114136
restrictByRoi = false;
115137
}
116138

117-
plugin.showStatus(0, 0, "Converting paths to skeletons...");
139+
Collection<Path> pathsToConvert = tree.list();
140+
if (restrictToMaterializedRegion) {
141+
final BoundingBox cropBounds = snt.getMaterializedCropWorldBounds();
142+
if (cropBounds != null) {
143+
pathsToConvert = tree.list().stream()
144+
.filter(p -> p.getNodes().stream().anyMatch(cropBounds::contains))
145+
.collect(Collectors.toList());
146+
if (pathsToConvert.isEmpty()) {
147+
error("No paths intersect the materialized region.");
148+
return;
149+
}
150+
}
151+
}
152+
153+
setStatus("Converting paths to skeletons...");
118154
final boolean asLabelsImage = imgChoice.startsWith("Labels");
119155
try {
120-
final ImagePlus imagePlus = (useNewImage) ? tree.getSkeleton((asLabelsImage) ? -1 : 255) : plugin.makePathVolume(tree.list(), asLabelsImage);
156+
final ImagePlus imagePlus = (useNewImage)
157+
? tree.getSkeleton((asLabelsImage) ? -1 : 255)
158+
: snt.makePathVolume(pathsToConvert, asLabelsImage);
121159
if (asLabelsImage) {
122160
final IndexColorModel model = LutLoader.getLut("glasbey_on_dark");
123-
if (model != null)
124-
imp.getProcessor().setColorModel(model);
125-
imagePlus.setDisplayRange(0, tree.size());
161+
if (model != null) imagePlus.getProcessor().setColorModel(model);
162+
imagePlus.setDisplayRange(0, pathsToConvert.size());
126163
} else {
127164
ImpUtils.convertTo8bit(imagePlus);
128165
}
@@ -143,32 +180,18 @@ public void run() {
143180
analyzer.setup("", imagePlus);
144181
analyzer.run(imagePlus.getProcessor());
145182
}
146-
147183
imagePlus.show();
148184
}
149185
catch (final OutOfMemoryError error) {
150186
final String msg = "Out of Memory: There is not enough RAM to perform skeletonization under "
151-
+ "current options. Please allocate more memory to IJ, downsample the reconstruction, "
187+
+ "current options. Please allocate more memory to Fiji, downsample the paths, "
152188
+ " or consider skeletonization through API scripting";
153189
error(msg);
190+
} finally {
191+
resetUI();
154192
}
155193
}
156194

157-
private boolean getConfirmation(final String msg, final String title) {
158-
final Result res = uiService.getDefaultUI().dialogPrompt(msg, title,
159-
DialogPrompt.MessageType.QUESTION_MESSAGE,
160-
DialogPrompt.OptionType.YES_NO_OPTION).prompt();
161-
return Result.YES_OPTION.equals(res);
162-
}
163-
164-
private void error(final String msg) {
165-
// With HTML errors, uiService will not use the java.awt legacy messages
166-
// that do not scale in hiDPI
167-
uiService.getDefaultUI().dialogPrompt("<HTML>" + msg, "Error",
168-
DialogPrompt.MessageType.ERROR_MESSAGE,
169-
DialogPrompt.OptionType.DEFAULT_OPTION).prompt();
170-
}
171-
172195
/* IDE debug method **/
173196
public static void main(final String[] args) {
174197
final ImageJ ij = new ImageJ();

0 commit comments

Comments
 (0)