diff --git a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/AnalyseSkeletonWrapper.java b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/AnalyseSkeletonWrapper.java index 9af0efa2..ae08137a 100644 --- a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/AnalyseSkeletonWrapper.java +++ b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/AnalyseSkeletonWrapper.java @@ -98,61 +98,167 @@ public class AnalyseSkeletonWrapper extends BoneJCommand { LegacyInjector.preinit(); } + /** The binary 3D image to skeletonize and analyze. */ @Parameter(label = "Input image", validater = "validateImage", persist = false) private Dataset inputDataset; + /** + * Cycle pruning method for the skeleton graph. + *
+ * Each foreground voxel is labeled with its topological role in the + * skeleton graph: + *
+ *+ * Source: {@code AnalyzeSkeleton_.getResultImage(false)} which returns + * the internal {@code taggedImage} stack. + *
+ *+ * Null if {@link #displaySkeletons} is false. + *
*/ - @Parameter(type = ItemIO.OUTPUT, label = "Branch information") - private DefaultGenericTable verboseTable; + @Parameter(type = ItemIO.OUTPUT, label = "Tagged topology skeleton") + private Dataset taggedImage; /** - * The labelled skeletons image, null if user didn't check the - * {@link #displaySkeletons} option. + * Tree-labeled skeleton image (float32). + *+ * Each foreground voxel is assigned the integer ID of the skeleton tree + * (connected component) it belongs to. Background is 0. + *
+ *+ * Stored as float32 (not uint8) because the number of distinct trees + * can exceed 255. Float can exactly represent integer IDs up to + * 2⁴² without loss of precision. + *
+ *+ * Source: {@code AnalyzeSkeleton_.getLabeledSkeletons()} which returns + * the internal {@code labeledSkeletons} stack. + *
+ *+ * Naming note: This corresponds to the field called + * {@code labeledSkeletons} inside {@code AnalyzeSkeleton_}. It is + * not the same as the tagged image (confusingly, the original + * wrapper named its output {@code labelledSkeleton} but it actually held + * the tagged image). This field is now renamed to avoid that ambiguity. + *
+ *+ * Null if {@link #displaySkeletons} is false. + *
*/ - @Parameter(type = ItemIO.OUTPUT) - private Dataset labelledSkeleton; + @Parameter(type = ItemIO.OUTPUT, label = "Tree-labeled skeleton") + private Dataset treeLabeledImage; /** - * The shortest paths image, null if user didn't check the - * {@link #displaySkeletons} option. + * Longest-shortest-path image (8-bit). + *+ * A copy of the input skeleton stack where voxels lying on the + * longest shortest path (graph diameter) of each tree are overwritten + * with value {@code 96} ({@link AnalyzeSkeleton_#SHORTEST_PATH SHORTEST_PATH}). + * All other skeleton voxels retain their original intensity. + *
+ *+ * Source: {@code AnalyzeSkeleton_.getResultImage(true)} which returns + * the internal {@code shortPathImage} stack. + *
+ *+ * Null if {@link #calculateShortestPaths} is false. + *
*/ - @Parameter(type = ItemIO.OUTPUT) + @Parameter(type = ItemIO.OUTPUT, label = "Shortest paths") private Dataset shortestPaths; + /** + * Per-branch detail table (only populated if {@link #verbose} is true). + *+ * Columns: {@code # Skeleton}, {@code # Branch}, {@code Branch length}, + * {@code V1 x}, {@code V1 y}, {@code V1 z}, {@code V2 x}, {@code V2 y}, + * {@code V2 z}, {@code Euclidean distance}, {@code running average length}, + * {@code average intensity (inner 3rd)}, {@code average intensity}. + *
+ *+ * Null if {@link #verbose} is false. + *
+ */ + @Parameter(type = ItemIO.OUTPUT, label = "Branch information") + private DefaultGenericTable verboseTable; + @Parameter private UIService uiService; @@ -170,7 +276,7 @@ public class AnalyseSkeletonWrapper extends BoneJCommand { @Parameter private StatusService statusService; - + @Parameter private LegacyService legacyService; @@ -187,12 +293,12 @@ public void run() { } } - ImagePlus imp = convertService.convert(inputDataset, ImagePlus.class); - - if (imp == null) { - logService.error("Connectivity: Failed to convert Dataset to ImagePlus."); - return; - } + ImagePlus imp = convertService.convert(inputDataset, ImagePlus.class); + + if (imp == null) { + logService.error("AnalyseSkeleton: Failed to convert Dataset to ImagePlus."); + return; + } statusService.showStatus("Analyse skeleton: skeletonising"); final ImagePlus skeleton = skeletonise(imp); @@ -211,18 +317,34 @@ public void run() { } showResults(results); showAdditionalResults(results); + if (displaySkeletons) { - final ImageStack labelledStack = analyzeSkeleton_.getResultImage(false); - ImagePlus labelSkeleton = new ImagePlus(imp.getTitle() + - "-labelled-skeletons", labelledStack); - labelSkeleton.setCalibration(imp.getCalibration()); - labelledSkeleton = convertService.convert(labelSkeleton, Dataset.class); - labelSkeleton.close(); - labelledSkeleton.getImgPlus().setColorTable(ColorTables.FIRE, 0); - //if there is a UI, display the dataset - if (uiService != null && uiService.isVisible()) - uiService.show(labelledSkeleton); + // --- Tree-labeled image (float32, IDs 1, 2, 3...) --- + final ImageStack treeLabeledStack = analyzeSkeleton_.getLabeledSkeletons(); + if (treeLabeledStack != null) { + ImagePlus treeLabeledImp = new ImagePlus(imp.getTitle() + + "-tree-labels", treeLabeledStack); + treeLabeledImp.setCalibration(imp.getCalibration()); + treeLabeledImage = convertService.convert(treeLabeledImp, Dataset.class); + treeLabeledImp.close(); + treeLabeledImage.getImgPlus().setColorTable(ColorTables.FIRE, 0); + if (uiService != null && uiService.isVisible()) + uiService.show(treeLabeledImage); + } + + // --- Tagged topology image (8-bit, values 0, 30, 70, 127) --- + final ImageStack taggedStack = analyzeSkeleton_.getResultImage(false); + ImagePlus taggedSkeleton = new ImagePlus(imp.getTitle() + + "-tagged-skeletons", taggedStack); + taggedSkeleton.setCalibration(imp.getCalibration()); + taggedImage = convertService.convert(taggedSkeleton, Dataset.class); + taggedSkeleton.close(); + taggedImage.getImgPlus().setColorTable(ColorTables.FIRE, 0); + if (uiService != null && uiService.isVisible()) + uiService.show(taggedImage); + if (calculateShortestPaths) { + // --- Shortest path image (8-bit, path voxels = 96) --- final ImageStack stack = analyzeSkeleton_.getResultImage(true); final String title = imp.getShortTitle() + "-shortest-paths"; ImagePlus shortPaths = new ImagePlus(title, stack); @@ -230,13 +352,11 @@ public void run() { shortestPaths = convertService.convert(shortPaths, Dataset.class); shortPaths.close(); shortestPaths.getImgPlus().setColorTable(ColorTables.FIRE, 0); - //if there is a UI, display the dataset - if (uiService != null && uiService.isVisible()) - uiService.show(shortestPaths); + if (uiService != null && uiService.isVisible()) + uiService.show(shortestPaths); } } } - private boolean hasNoSkeletons(final AnalyzeSkeleton_ analyzeSkeleton_) { final Graph[] graphs = analyzeSkeleton_.getGraphs(); return graphs == null || graphs.length == 0;