1+ from typing import Dict
2+
13import napari
24import napari .layers
35import napari .viewer
1315COLORMAP = ["red" , "blue" , "yellow" , "cyan" , "purple" , "magenta" , "orange" , "green" ]
1416
1517
18+ # TODO Make selection of the distance layers optional and add a second distance layer.
1619class VesiclePoolWidget (BaseWidget ):
1720 def __init__ (self ):
1821 super ().__init__ ()
1922
2023 self .viewer = napari .current_viewer ()
2124 layout = QVBoxLayout ()
2225
23- self .image_selector_name = "Distances to Structure"
24- self .image_selector_name1 = "Vesicles Segmentation"
25- # Create the image selection dropdown.
26- self .image_selector_widget = self ._create_layer_selector (self .image_selector_name , layer_type = "Shapes" )
27- self .segmentation1_selector_widget = self ._create_layer_selector (self .image_selector_name1 , layer_type = "Labels" )
28-
29- # Create new layer name.
30- self .pool_layer_name_param , pool_layer_name_layout = self ._add_string_param (
31- name = "Output Layer Name" , value = "" ,
32- )
33-
34- # Create pool name.
35- self .pool_name_param , pool_name_layout = self ._add_string_param (
36- name = "Vesicle Pool" , value = "" ,
37- )
38-
39- # Create query string
26+ # Create the selectors for the layers:
27+ # 1. Selector for the labels layer with vesicles.
28+ self .vesicle_selector_name = "Vesicle Segmentation"
29+ self .vesicle_selector_widget = self ._create_layer_selector (self .vesicle_selector_name , layer_type = "Labels" )
30+ # 2. Selector for a distance layer.
31+ self .dist_selector_name1 = "Distances to Structure"
32+ self .dist_selector_widget1 = self ._create_layer_selector (self .dist_selector_name1 , layer_type = "Shapes" )
33+
34+ # Add the selector widgets to the layout.
35+ layout .addWidget (self .vesicle_selector_widget )
36+ layout .addWidget (self .dist_selector_widget1 )
37+
38+ # Create the UI elements for defining the vesicle pools:
39+ # The name of the output name, the name of the vesicle pool, and the criterion for the pool.
40+ self .pool_layer_name_param , pool_layer_name_layout = self ._add_string_param (name = "Layer Name" , value = "" )
41+ self .pool_name_param , pool_name_layout = self ._add_string_param (name = "Vesicle Pool" , value = "" )
4042 self .query_param , query_layout = self ._add_string_param (
4143 name = "Criterion" , value = "" ,
42- tooltip = "Enter a comma separated query string (e.g., 'radius > 15, distance > 250') "
44+ tooltip = "Enter a comma separated criterion (e.g., 'radius > 15, distance > 250') "
4345 "Possible filters: radius, distance, area, intensity_max, intensity_mean, intensity_min, intensity_std"
4446 )
45-
46- # Create advanced settings.
47- self .settings = self ._create_settings_widget ()
48-
49- # Create and connect buttons.
50- self .measure_button1 = QPushButton ("Create Vesicle Pool" )
51- self .measure_button1 .clicked .connect (self .on_pool_vesicles )
52-
53- # Add the widgets to the layout.
54- layout .addWidget (self .image_selector_widget )
55- layout .addWidget (self .segmentation1_selector_widget )
56- layout .addLayout (query_layout )
5747 layout .addLayout (pool_layer_name_layout )
5848 layout .addLayout (pool_name_layout )
59- layout .addWidget (self .measure_button1 )
49+ layout .addLayout (query_layout )
50+
51+ # Create the UI elements for advanced settings and the run button.
52+ self .settings = self ._create_settings_widget ()
53+ self .measure_button = QPushButton ("Create Vesicle Pool" )
54+ self .measure_button .clicked .connect (self .on_pool_vesicles )
55+ layout .addWidget (self .settings )
56+ layout .addWidget (self .measure_button )
6057
6158 self .setLayout (layout )
6259
60+ # The colormap for displaying the vesicle pools.
61+ self .pool_colors = {}
62+
6363 def on_pool_vesicles (self ):
64- distances_layer = self ._get_layer_selector_layer (self .image_selector_name )
65- distances = distances_layer .properties
66- segmentation = self ._get_layer_selector_data (self .image_selector_name1 )
67- morphology_layer = self ._get_layer_selector_layer (self .image_selector_name1 )
68- morphology = morphology_layer .properties
64+ segmentation = self ._get_layer_selector_data (self .vesicle_selector_name )
65+ morphology = self ._get_layer_selector_layer (self .vesicle_selector_name ).properties
66+ if not morphology :
67+ morphology = None
68+
69+ distance_layer = self ._get_layer_selector_layer (self .dist_selector_name1 )
70+ distances = None if distance_layer is None else distance_layer .properties
6971
7072 if segmentation is None :
7173 show_info ("INFO: Please choose a segmentation." )
@@ -76,61 +78,91 @@ def on_pool_vesicles(self):
7678 query = self .query_param .text ()
7779
7880 if self .pool_layer_name_param .text () == "" :
79- show_info ("INFO: Please enter a new layer name ." )
81+ show_info ("INFO: Please enter a name for the pool layer ." )
8082 return
8183 pool_layer_name = self .pool_layer_name_param .text ()
8284 if self .pool_name_param .text () == "" :
83- show_info ("INFO: Please enter a pooled group name ." )
85+ show_info ("INFO: Please enter a name for the vesicle pool ." )
8486 return
8587 pool_name = self .pool_name_param .text ()
8688
87- if distances is None :
88- show_info ("INFO: Distances layer could not be found or has no values." )
89- return
89+ pool_color = self .pool_color_param .text ()
90+ self ._compute_vesicle_pool (segmentation , distances , morphology , pool_layer_name , pool_name , query , pool_color )
9091
91- self ._compute_vesicle_pool (segmentation , distances , morphology , pool_layer_name , pool_name , query )
92-
93- def _compute_vesicle_pool (self , segmentation , distances , morphology , pool_layer_name , pool_name , query ):
94- """
95- Compute a vesicle pool based on the provided query parameters.
92+ def _update_pool_colors (self , pool_name , pool_color ):
93+ if pool_color == "" :
94+ next_color_id = len (self .pool_colors )
95+ next_color = COLORMAP [next_color_id ]
96+ else :
97+ # We could check here that this is a valid color.
98+ next_color = pool_color
99+ self .pool_colors [pool_name ] = next_color
100+
101+ def _compute_vesicle_pool (
102+ self ,
103+ segmentation : np .ndarray ,
104+ distances : Dict ,
105+ morphology : Dict ,
106+ pool_layer_name : str ,
107+ pool_name : str ,
108+ query : str ,
109+ pool_color : str ,
110+ ):
111+ """Compute a vesicle pool based on the provided query parameters.
96112
97113 Args:
98- segmentation (array): Segmentation data (e.g., labeled regions).
99- distances (dict): Properties from the distances layer.
100- morphology (dict): Properties from the morphology layer.
101- pool_layer_name (str): Name for the new layer to be created.
102- pool_name (str): Name for the pooled group to be assigned.
103- query (dict): Query parameters.
114+ segmentation: Segmentation data (e.g., labeled regions).
115+ distances: Properties from the distances layer.
116+ morphology: Properties from the morphology layer.
117+ pool_layer_name: Name for the new layer to be created.
118+ pool_name: Name for the pooled group to be assigned.
119+ query: Query parameters.
120+ pool_color: Optional color for the vesicle pool.
104121 """
105- distance_ids = distances .get ("label" , [])
106- morphology_ids = morphology .get ("label" , [])
107-
108- # Ensure that IDs are identical.
109- if set (distance_ids ) != set (morphology_ids ):
110- show_info ("ERROR: The IDs in distances and morphology are not identical." )
122+ # Check which of the properties are present and construct the combined properties based on this.
123+ if distances is None and morphology is None : # No properties were given -> we can't do anything.
124+ show_info ("ERROR: Neither distances nor vesicle morphology were found." )
111125 return
112-
113- # Create a merged dataframe from the dataframes which are relevant for the criterion.
114- # TODO: select the dataframes more dynamically depending on the criterion defined by the user.
115- distances = pd .DataFrame (distances )
116- morphology = pd .DataFrame (morphology )
117- merged_df = morphology .merge (distances , left_on = "label" , right_on = "label" , suffixes = ("_morph" , "_dist" ))
126+ elif distances is None and morphology is not None : # Only morphology props were found.
127+ merged_df = pd .DataFrame (morphology ).drop (columns = ["index" ])
128+ elif distances is not None and morphology is None : # Only distances were found.
129+ merged_df = pd .DataFrame (distances ).drop (columns = ["index" ])
130+ else : # Both were found.
131+ distance_ids = distances .get ("label" , [])
132+ morphology_ids = morphology .get ("label" , [])
133+
134+ # Ensure that IDs are identical.
135+ if set (distance_ids ) != set (morphology_ids ):
136+ show_info ("ERROR: The IDs in distances and morphology are not identical." )
137+ return
138+
139+ # Create a merged dataframe from the dataframes which are relevant for the criterion.
140+ distances = pd .DataFrame (distances ).drop (columns = ["index" ])
141+ morphology = pd .DataFrame (morphology ).drop (columns = ["index" ])
142+ merged_df = morphology .merge (distances , left_on = "label" , right_on = "label" , suffixes = ("_morph" , "_dist" ))
118143
119144 # Assign the vesicles to the current pool by filtering the mergeddataframe based on the query.
120145 filtered_df = self ._parse_query (query , merged_df )
146+ if len (filtered_df ) == 0 :
147+ show_info ("No vesicles were found matching the condition." )
148+ return
121149 pool_vesicle_ids = filtered_df .label .values .tolist ()
150+ vesicles_in_pool = len (pool_vesicle_ids )
122151
123152 # Check if this layer was already created in a previous pool assignment.
124153 if pool_layer_name in self .viewer .layers :
125154 # If yes then load the previous pool assignments and merge them with the new pool assignments
126155 pool_layer = self .viewer .layers [pool_layer_name ]
127156 pool_properties = pd .DataFrame .from_dict (pool_layer .properties )
128157
129- pool_names = pd .unique (pool_properties .pool ). tolist ()
158+ pool_names = pd .unique (pool_properties .pool )
130159 if pool_name in pool_names :
160+ show_info (f"Updating pool '{ pool_name } ' with { vesicles_in_pool } vesicles." )
131161 # This pool has already been assigned and we changed the criterion.
132162 # Its old assignment has to be over-written, remove the rows for this pool.
133163 pool_properties = pool_properties [pool_properties .pool != pool_name ]
164+ else :
165+ show_info (f"Creating pool '{ pool_name } ' with { vesicles_in_pool } vesicles." )
134166
135167 # Combine the vesicle ids corresponding to the previous assignment with the
136168 # assignment for the new / current pool.
@@ -146,6 +178,7 @@ def _compute_vesicle_pool(self, segmentation, distances, morphology, pool_layer_
146178 pool_values = [id_to_pool_name [ves_id ] for ves_id in pool_assignments ]
147179
148180 else :
181+ show_info (f"Creating pool '{ pool_name } ' with { vesicles_in_pool } vesicles." )
149182 # Otherwise, this is the first pool assignment.
150183 pool_assignments = pool_vesicle_ids
151184 pool_values = [pool_name ] * len (pool_assignments )
@@ -161,32 +194,27 @@ def _compute_vesicle_pool(self, segmentation, distances, morphology, pool_layer_
161194 col for col in pool_properties .columns
162195 if col not in ("x" , "y" , "z" , "begin-x" , "begin-y" , "begin-z" , "end-x" , "end-y" , "end-z" )
163196 ]
164- pool_properties = pool_properties [keep_columns ]. reset_index ()
197+ pool_properties = pool_properties [keep_columns ]
165198 # Add a colun for the pool.
166199 pool_properties .insert (1 , "pool" , pool_values )
167200
168- # Create the colormap to group the pools in the layer rendering.
169- # This can lead to color switches: if a new pool gets added which starts with
170- # a letter that's earlier in the alphabet the color will switch.
171- # To avoid this the user has to specify the pool color (not yet implemented, see next todo).
172- pool_names = np .unique (pool_values ).tolist ()
173- # TODO: add setting so that users can over-ride the color for a pool.
174- pool_colors = {pname : COLORMAP [pool_names .index (pname )] for pname in pool_names }
201+ # Update the colormap to display the pools.
202+ self ._update_pool_colors (pool_name , pool_color )
203+
204+ # Assign the vesicle ids to their pool color.
175205 vesicle_colors = {
176- label_id : pool_colors [pname ] for label_id , pname
177- in zip (pool_properties .label .values , pool_properties .pool .values )
206+ label_id : self .pool_colors [pname ] for label_id , pname in zip (
207+ pool_properties .label .values , pool_properties .pool .values
208+ )
178209 }
179210 vesicle_colors [None ] = "gray"
180211
181- # TODO print some messages
182212 # Add or replace the pool layer and properties.
183213 if pool_layer_name in self .viewer .layers :
184- # message about added or over-ridden pool, including number of vesicles in pool
185214 pool_layer = self .viewer .layers [pool_layer_name ]
186215 pool_layer .data = vesicle_pools
187216 pool_layer .colormap = vesicle_colors
188217 else :
189- # message about new pool, including number of vesicles in pool
190218 pool_layer = self .viewer .add_labels (vesicle_pools , name = pool_layer_name , colormap = vesicle_colors )
191219
192220 self ._add_properties_and_table (pool_layer , pool_properties , save_path = self .save_path .text ())
@@ -220,5 +248,8 @@ def _create_settings_widget(self):
220248 self .save_path , layout = self ._add_path_param (name = "Save Table" , select_type = "file" , value = "" )
221249 setting_values .layout ().addLayout (layout )
222250
251+ self .pool_color_param , layout = self ._add_string_param (name = "Pool Color" , value = "" )
252+ setting_values .layout ().addLayout (layout )
253+
223254 settings = self ._make_collapsible (widget = setting_values , title = "Advanced Settings" )
224255 return settings
0 commit comments