Skip to content

Commit 9a69484

Browse files
Finish vesicle pool widget
1 parent 14d4fe9 commit 9a69484

File tree

3 files changed

+128
-87
lines changed

3 files changed

+128
-87
lines changed

synapse_net/napari.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ contributions:
99
# Commands for widgets.
1010
- id: synapse_net.segment
1111
python_name: synapse_net.tools.segmentation_widget:SegmentationWidget
12-
title: Segment
12+
title: Segmentation
1313
- id: synapse_net.distance_measure
1414
python_name: synapse_net.tools.distance_measure_widget:DistanceMeasureWidget
1515
title: Distance Measurement
@@ -21,7 +21,7 @@ contributions:
2121
title: Morphology Analysis
2222
- id: synapse_net.vesicle_pooling
2323
python_name: synapse_net.tools.vesicle_pool_widget:VesiclePoolWidget
24-
title: Vesicle Pooling
24+
title: Pool Assignment
2525

2626
# Commands for sample data.
2727
- id: synapse_net.sample_data_tem_2d
@@ -47,7 +47,7 @@ contributions:
4747
- command: synapse_net.morphology
4848
display_name: Morphology Analysis
4949
- command: synapse_net.vesicle_pooling
50-
display_name: Vesicle Pooling
50+
display_name: Pool Assignment
5151

5252
sample_data:
5353
- command: synapse_net.sample_data_tem_2d

synapse_net/tools/base_widget.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import sys
23
from pathlib import Path
34

45
import napari
@@ -16,6 +17,16 @@
1617
add_table, get_table = None, None
1718

1819

20+
class _SilencePrint:
21+
def __enter__(self):
22+
self._original_stdout = sys.stdout
23+
sys.stdout = open(os.devnull, "w")
24+
25+
def __exit__(self, exc_type, exc_val, exc_tb):
26+
sys.stdout.close()
27+
sys.stdout = self._original_stdout
28+
29+
1930
class BaseWidget(QWidget):
2031
def __init__(self):
2132
super().__init__()
@@ -316,18 +327,17 @@ def _save_table(self, save_path, data):
316327
return file_path
317328

318329
def _add_properties_and_table(self, layer, table_data, save_path=""):
319-
if layer.properties:
320-
layer.properties.update(table_data)
321-
else:
322-
layer.properties = table_data
330+
layer.properties = table_data
323331

324332
if add_table is not None:
325333
table = get_table(layer, self.viewer)
326334
if table is None:
327-
add_table(layer, self.viewer)
335+
with _SilencePrint():
336+
add_table(layer, self.viewer)
328337
else:
329338
# FIXME updating the table does not yet work
330-
table.update_content()
339+
with _SilencePrint():
340+
table.update_content()
331341
# table_dict = table_data.to_dict()
332342
# table_dict["index"] = table_dict["label"]
333343
# table.set_content(table_dict)
Lines changed: 109 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Dict
2+
13
import napari
24
import napari.layers
35
import napari.viewer
@@ -13,59 +15,59 @@
1315
COLORMAP = ["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.
1619
class 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

Comments
 (0)