2323package sc .fiji .snt .plugin ;
2424
2525import java .awt .image .IndexColorModel ;
26+ import java .util .Collection ;
2627import java .util .HashMap ;
28+ import java .util .List ;
2729import java .util .Map ;
30+ import java .util .stream .Collectors ;
2831
2932import net .imagej .ImageJ ;
3033
3134import org .scijava .command .Command ;
35+ import org .scijava .module .MutableModuleItem ;
3236import org .scijava .plugin .Parameter ;
3337import 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
3939import ij .ImagePlus ;
4040import ij .ImageStack ;
4141import ij .gui .Roi ;
4242import ij .plugin .LutLoader ;
4343import ij .process .ImageProcessor ;
44+ import org .scijava .widget .ChoiceWidget ;
4445import sc .fiji .analyzeSkeleton .AnalyzeSkeleton_ ;
4546import sc .fiji .skeletonize3D .Skeletonize3D_ ;
46- import sc .fiji .snt .SNT ;
47- import sc .fiji .snt .SNTService ;
47+ import sc .fiji .snt .Path ;
4848import sc .fiji .snt .Tree ;
49+ import sc .fiji .snt .gui .cmds .CommonDynamicCmd ;
50+ import sc .fiji .snt .util .BoundingBox ;
4951import 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