From 951b50b21d00e85898b06d5d8453a807764713ac Mon Sep 17 00:00:00 2001 From: mdoube Date: Wed, 24 Jun 2026 09:32:46 +0200 Subject: [PATCH] Expose the topology-tagged image and the tree labelled image Disambiguate the output image naming. Will break scripts that rely on prior naming. --- .../AnalyseSkeletonWrapper.java | 188 ++++++++++++++---- 1 file changed, 154 insertions(+), 34 deletions(-) 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. + * + * The latter two require an additional grayscale intensity image. + */ @Parameter(label = "Cycle pruning method", description = "Which method is used to prune cycles in the skeleton graph", required = false, style = ChoiceWidget.LIST_BOX_STYLE, choices = { "None", "Shortest branch", "Lowest intensity voxel", "Lowest intensity branch" }) private String pruneCycleMethod = "None"; + /** If true, prune terminal branches (very short edges with no slab voxels). */ @Parameter(label = "Prune ends", description = "Prune very short edges with no slabs", required = false) private boolean pruneEnds; + /** + * If true, the current ROI is excluded from end-branch pruning. + * Voxels inside the ROI are spared even if they are end-points. + * Ignored in headless/batch mode. + */ @Parameter(label = "Exclude ROI from pruning", description = "Exclude the current selection from pruning", required = false, visibility = ItemVisibility.INVISIBLE, persist = false) private boolean excludeRoi; + /** + * If true, compute the longest shortest path (graph diameter) for each + * skeleton tree using the Floyd–Warshall algorithm. + * Populates the {@link #shortestPaths} output image and the + * "Longest Shortest Path" / "spx" / "spy" / "spz" columns in the results table. + */ @Parameter(label = "Calculate largest shortest paths", description = "Calculate and display the largest shortest skeleton paths", required = false) private boolean calculateShortestPaths; + /** + * If true, populate the {@link #verboseTable} with per-branch details + * (lengths, vertex coordinates, intensities, Euclidean distances). + */ @Parameter(label = "Show detailed info", description = "Show detailed branch info in an additional table", required = false) private boolean verbose; - @Parameter(label = "Display labelled skeletons", - description = "Show skeleton images labelled with their IDs", + /** + * If true, populate the {@link #taggedImage} and {@link #treeLabeledImage} + * output images. If false, both will be null (saving memory for batch jobs + * that only need numeric results). + */ + @Parameter(label = "Display skeleton images", + description = "Output topology-tagged and tree-labeled skeleton images", required = false) private boolean displaySkeletons; /** - * Additional analysis details in a {@link DefaultGenericTable}, null if - * {@link #verbose} is false, or there are no results. + * Topology-tagged skeleton image (8-bit). + *

+ * 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;