77from typing import TYPE_CHECKING
88
99from magicgui import magicgui
10- from magicgui .widgets import Container , PushButton
10+ from magicgui .widgets import Container , Label , PushButton
1111from napari .layers import Layer
1212from napari .utils .notifications import show_error , show_info
1313from qtpy .QtWidgets import QFileDialog
1414
15+ from vesskel ._batch import _save_skeleton , _save_radius , _write_csv
16+ from vesskel .pipeline import analyze_binary_image
17+
1518if TYPE_CHECKING :
1619 # These imports are only used for annotations and are therefore
1720 # guarded by TYPE_CHECKING to avoid runtime import-time coupling.
1821 from napari .layers import Image # noqa: F401
1922
2023from vesskel .config import (
24+ COLORABLE_BRANCH_PROPERTIES ,
2125 ExtractionConfig ,
2226 OutputConfig ,
2327 PipelineConfig ,
2428 load_pipeline_config ,
2529 save_pipeline_config ,
2630)
27- from vesskel ._batch import _save_skeleton , _save_radius , _write_csv
28- from vesskel .pipeline import analyze_binary_image
31+
32+ _RADIUS_REQUIRED_PROPS = {
33+ "mean_radius" ,
34+ "std_radius" ,
35+ "min_radius" ,
36+ "max_radius" ,
37+ "mean_diameter" ,
38+ "std_diameter" ,
39+ "min_diameter" ,
40+ "max_diameter" ,
41+ "volume" ,
42+ "surface_area" ,
43+ }
2944
3045
3146class VesselAnalysisWidget (Container ):
@@ -44,6 +59,7 @@ def _setup_ui(self):
4459 def _extraction_params (
4560 image : "napari.layers.Image" , # noqa: F821
4661 extract_branches : bool = False ,
62+ branch_color_property : str = "tortuosity" ,
4763 extract_branch_text : bool = False ,
4864 extract_nodes : bool = False ,
4965 extract_summary : bool = False ,
@@ -62,6 +78,12 @@ def _extraction_params(
6278 _extraction_params ,
6379 image = {"label" : "Input image" },
6480 extract_branches = {"annotation" : bool , "value" : False },
81+ branch_color_property = {
82+ "annotation" : str ,
83+ "value" : "tortuosity" ,
84+ "choices" : COLORABLE_BRANCH_PROPERTIES ,
85+ "widget_type" : "ComboBox" ,
86+ },
6587 extract_branch_text = {"annotation" : bool , "value" : False },
6688 extract_summary = {"annotation" : bool , "value" : False },
6789 include_fractal = {"annotation" : bool , "value" : False },
@@ -120,6 +142,28 @@ def _output_params(
120142 self .extract_branches_widget = extraction_gui .extract_branches
121143 self .extract_branches_widget .label = "Extract branches"
122144
145+ self .branch_color_widget = extraction_gui .branch_color_property
146+ self .branch_color_widget .label = "Branch color by"
147+ self .branch_color_widget .enabled = False
148+
149+ self .branch_color_warning = Label (value = "⚠️ Requires Vessel Radius" )
150+ self .branch_color_warning .visible = False
151+
152+ def _on_branches_toggle (enabled : bool | None = None ) -> None :
153+ self .branch_color_widget .enabled = self .extract_branches_widget .value
154+
155+ self .extract_branches_widget .changed .connect (_on_branches_toggle )
156+
157+ def _update_branch_color_warning (* args ) -> None :
158+ needs_radius = self .branch_color_widget .value in _RADIUS_REQUIRED_PROPS
159+ radius_off = not self .include_vessel_radius_widget .value
160+ self .branch_color_warning .visible = needs_radius and radius_off
161+
162+ self .branch_color_widget .changed .connect (_update_branch_color_warning )
163+
164+ # connection to include_vessel_radius_widget happens below
165+ # after that widget is created
166+
123167 self .extract_branch_text_widget = extraction_gui .extract_branch_text
124168 self .extract_branch_text_widget .label = "Add branch labels"
125169
@@ -130,51 +174,81 @@ def _output_params(
130174 self .extract_nodes_widget .label = "Extract node features"
131175
132176 extraction_group .append (self .extract_branches_widget )
177+ extraction_group .append (self .branch_color_widget )
178+ extraction_group .append (self .branch_color_warning )
133179 extraction_group .append (self .extract_branch_text_widget )
134180 extraction_group .append (self .extract_summary_widget )
135181 extraction_group .append (self .extract_nodes_widget )
136182
137183 # ============================================================
138- # Advanced Features
184+ # Cleanup
139185 # ============================================================
140- advanced_group = Container ()
141- advanced_group .label = "Advanced Features "
186+ cleanup_group = Container ()
187+ cleanup_group .label = "Cleanup "
142188
143- self .include_fractal_widget = extraction_gui .include_fractal
144- self .include_fractal_widget .label = "Include fractal dimension (slow) "
189+ self .fill_holes_widget = extraction_gui .fill_holes
190+ self .fill_holes_widget .label = "Fill holes in segmentation "
145191
146- advanced_group .append (self .include_fractal_widget )
192+ self .max_hole_size_widget = extraction_gui .max_hole_size
193+ self .max_hole_size_widget .label = "Max hole size (pixels)"
194+ self .max_hole_size_widget .enabled = False
147195
148- self . include_vessel_radius_widget = extraction_gui . include_vessel_radius
149- self .include_vessel_radius_widget . label = "Compute vessel radius and diameter"
196+ def _on_fill_holes_toggle ( enabled : bool | None = None ) -> None :
197+ self .max_hole_size_widget . enabled = self . fill_holes_widget . value
150198
151- advanced_group .append (self .include_vessel_radius_widget )
199+ self .fill_holes_widget .changed .connect (_on_fill_holes_toggle )
200+
201+ self .closing_iterations_widget = extraction_gui .closing_iterations
202+ self .closing_iterations_widget .label = "Closing iterations"
152203
153204 self .junction_cleanup_widget = extraction_gui .junction_cleanup
154205 self .junction_cleanup_widget .label = "Collapse triangle junction artifacts"
155206
156207 self .cleanup_threshold_widget = extraction_gui .cleanup_threshold_factor
157208 self .cleanup_threshold_widget .label = "Cleanup threshold factor"
209+ self .cleanup_threshold_widget .enabled = False
158210
159- advanced_group .append (self .junction_cleanup_widget )
160- advanced_group .append (self .cleanup_threshold_widget )
161-
162- self .fill_holes_widget = extraction_gui .fill_holes
163- self .fill_holes_widget .label = "Fill holes in segmentation"
164-
165- self .closing_iterations_widget = extraction_gui .closing_iterations
166- self .closing_iterations_widget .label = "Closing iterations"
211+ def _on_junction_cleanup_toggle (enabled : bool | None = None ) -> None :
212+ self .cleanup_threshold_widget .enabled = self .junction_cleanup_widget .value
167213
168- self .max_hole_size_widget = extraction_gui .max_hole_size
169- self .max_hole_size_widget .label = "Max hole size (pixels)"
214+ self .junction_cleanup_widget .changed .connect (_on_junction_cleanup_toggle )
170215
171216 self .show_preprocessed_widget = extraction_gui .show_preprocessed
172217 self .show_preprocessed_widget .label = "Show preprocessed binary layer"
218+ self .show_preprocessed_widget .enabled = False
219+
220+ def _update_preprocessed_enabled (* args ) -> None :
221+ self .show_preprocessed_widget .enabled = (
222+ self .fill_holes_widget .value or self .closing_iterations_widget .value > 0
223+ )
173224
174- advanced_group .append (self .fill_holes_widget )
175- advanced_group .append (self .closing_iterations_widget )
176- advanced_group .append (self .max_hole_size_widget )
177- advanced_group .append (self .show_preprocessed_widget )
225+ self .fill_holes_widget .changed .connect (_update_preprocessed_enabled )
226+ self .closing_iterations_widget .changed .connect (_update_preprocessed_enabled )
227+
228+ cleanup_group .append (self .fill_holes_widget )
229+ cleanup_group .append (self .max_hole_size_widget )
230+ cleanup_group .append (self .closing_iterations_widget )
231+ cleanup_group .append (self .junction_cleanup_widget )
232+ cleanup_group .append (self .cleanup_threshold_widget )
233+ cleanup_group .append (self .show_preprocessed_widget )
234+
235+ # ============================================================
236+ # Advanced Features
237+ # ============================================================
238+ advanced_group = Container ()
239+ advanced_group .label = "Advanced Features"
240+
241+ self .include_fractal_widget = extraction_gui .include_fractal
242+ self .include_fractal_widget .label = "Fractal dimension"
243+
244+ advanced_group .append (self .include_fractal_widget )
245+
246+ self .include_vessel_radius_widget = extraction_gui .include_vessel_radius
247+ self .include_vessel_radius_widget .label = "Radius features"
248+
249+ self .include_vessel_radius_widget .changed .connect (_update_branch_color_warning )
250+
251+ advanced_group .append (self .include_vessel_radius_widget )
178252
179253 # ============================================================
180254 # Output Settings (CLI file export options)
@@ -244,6 +318,7 @@ def _output_params(
244318 # ============================================================
245319 self .append (self .image_widget )
246320 self .append (extraction_group )
321+ self .append (cleanup_group )
247322 self .append (advanced_group )
248323 self .append (output_group )
249324 self .append (outdir_group )
@@ -259,6 +334,7 @@ def _get_current_pipeline_config(self) -> PipelineConfig:
259334 return PipelineConfig (
260335 extraction = ExtractionConfig (
261336 branches = self .extract_branches_widget .value ,
337+ branch_color_property = self .branch_color_widget .value ,
262338 branch_text = self .extract_branch_text_widget .value ,
263339 nodes = self .extract_nodes_widget .value ,
264340 summary = self .extract_summary_widget .value ,
@@ -284,6 +360,7 @@ def _get_current_pipeline_config(self) -> PipelineConfig:
284360 def _set_pipeline_config (self , config : PipelineConfig ) -> None :
285361 e = config .extraction
286362 self .extract_branches_widget .value = e .branches
363+ self .branch_color_widget .value = e .branch_color_property
287364 self .extract_branch_text_widget .value = e .branch_text
288365 self .extract_nodes_widget .value = e .nodes
289366 self .extract_summary_widget .value = e .summary
0 commit comments