Skip to content

Commit efdee13

Browse files
Merge pull request #73 from computational-cell-analytics/68-create-a-widget-for-vesicle-pool-assignments
68 create a widget for vesicle pool assignments
2 parents afeb8e7 + 223baee commit efdee13

File tree

7 files changed

+343
-157
lines changed

7 files changed

+343
-157
lines changed

synaptic_reconstruction/napari.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: synaptic_reconstruction
2-
display_name: Synaptic Reconstruction
2+
display_name: SynapseNet
33
# see https://napari.org/stable/plugins/manifest.html for valid categories
44
categories: ["Image Processing", "Annotation"]
55
contributions:
@@ -16,6 +16,9 @@ contributions:
1616
- id: synaptic_reconstruction.morphology
1717
python_name: synaptic_reconstruction.tools.morphology_widget:MorphologyWidget
1818
title: Morphology Analysis
19+
- id: synaptic_reconstruction.vesicle_pooling
20+
python_name: synaptic_reconstruction.tools.vesicle_pool_widget:VesiclePoolWidget
21+
title: Vesicle Pooling
1922

2023
readers:
2124
- command: synaptic_reconstruction.file_reader
@@ -32,3 +35,5 @@ contributions:
3235
display_name: Distance Measurement
3336
- command: synaptic_reconstruction.morphology
3437
display_name: Morphology Analysis
38+
- command: synaptic_reconstruction.vesicle_pooling
39+
display_name: Vesicle Pooling

synaptic_reconstruction/tools/base_widget.py

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1+
import os
12
from pathlib import Path
23

34
import napari
45
import qtpy.QtWidgets as QtWidgets
56

7+
from napari.utils.notifications import show_info
68
from qtpy.QtWidgets import (
79
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSpinBox, QComboBox, QCheckBox
810
)
911
from superqt import QCollapsible
1012

13+
try:
14+
from napari_skimage_regionprops import add_table
15+
except ImportError:
16+
add_table = None
17+
1118

1219
class BaseWidget(QWidget):
1320
def __init__(self):
@@ -31,12 +38,14 @@ def _create_layer_selector(self, selector_name, layer_type="Image"):
3138
layer_filter = napari.layers.Image
3239
elif layer_type == "Labels":
3340
layer_filter = napari.layers.Labels
41+
elif layer_type == "Shapes":
42+
layer_filter = napari.layers.Shapes
3443
else:
3544
raise ValueError("layer_type must be either 'Image' or 'Labels'.")
3645

3746
selector_widget = QtWidgets.QWidget()
3847
image_selector = QtWidgets.QComboBox()
39-
layer_label = QtWidgets.QLabel(f"{selector_name} Layer:")
48+
layer_label = QtWidgets.QLabel(f"{selector_name}:")
4049

4150
# Populate initial options
4251
self._update_selector(selector=image_selector, layer_filter=layer_filter)
@@ -58,9 +67,23 @@ def _create_layer_selector(self, selector_name, layer_type="Image"):
5867
def _update_selector(self, selector, layer_filter):
5968
"""Update a single selector with the current image layers in the viewer."""
6069
selector.clear()
61-
image_layers = [layer.name for layer in self.viewer.layers if isinstance(layer, layer_filter)] # if isinstance(layer, napari.layers.Image)
70+
image_layers = [layer.name for layer in self.viewer.layers if isinstance(layer, layer_filter)]
6271
selector.addItems(image_layers)
6372

73+
def _get_layer_selector_layer(self, selector_name):
74+
"""Return the layer currently selected in a given selector."""
75+
if selector_name in self.layer_selectors:
76+
selector_widget = self.layer_selectors[selector_name]
77+
78+
# Retrieve the QComboBox from the QWidget's layout
79+
image_selector = selector_widget.layout().itemAt(1).widget()
80+
81+
if isinstance(image_selector, QComboBox):
82+
selected_layer_name = image_selector.currentText()
83+
if selected_layer_name in self.viewer.layers:
84+
return self.viewer.layers[selected_layer_name]
85+
return None # Return None if layer not found
86+
6487
def _get_layer_selector_data(self, selector_name, return_metadata=False):
6588
"""Return the data for the layer currently selected in a given selector."""
6689
if selector_name in self.layer_selectors:
@@ -172,7 +195,7 @@ def _add_shape_param(self, names, values, min_val, max_val, step=1, title=None,
172195
title=title[1] if title is not None else title, tooltip=tooltip
173196
)
174197
layout.addLayout(y_layout)
175-
198+
176199
if len(names) == 3:
177200
z_layout = QVBoxLayout()
178201
z_param, _ = self._add_int_param(
@@ -262,3 +285,43 @@ def _get_file_path(self, name, textbox, tooltip=None):
262285
else:
263286
# Handle the case where the selected path is not a file
264287
print("Invalid file selected. Please try again.")
288+
289+
def _handle_resolution(self, metadata, voxel_size_param, ndim):
290+
# Get the resolution / voxel size from the layer metadata if available.
291+
resolution = metadata.get("voxel_size", None)
292+
if resolution is not None:
293+
resolution = [resolution[ax] for ax in ("zyx" if ndim == 3 else "yx")]
294+
295+
# If user input was given then override resolution from metadata.
296+
if voxel_size_param.value() != 0.0: # Changed from default.
297+
resolution = ndim * [voxel_size_param.value()]
298+
299+
assert len(resolution) == ndim
300+
return resolution
301+
302+
def _save_table(self, save_path, data):
303+
ext = os.path.splitext(save_path)[1]
304+
if ext == "": # No file extension given, By default we save to CSV.
305+
file_path = f"{save_path}.csv"
306+
data.to_csv(file_path, index=False)
307+
elif ext == ".csv": # Extension was specified as csv
308+
file_path = save_path
309+
data.to_csv(file_path, index=False)
310+
elif ext == ".xlsx": # We also support excel.
311+
file_path = save_path
312+
data.to_excel(file_path, index=False)
313+
else:
314+
raise ValueError("Invalid extension for table: {ext}. We support .csv or .xlsx.")
315+
return file_path
316+
317+
def _add_properties_and_table(self, layer, table_data, save_path=""):
318+
if layer.properties:
319+
layer.properties = layer.properties.update(table_data)
320+
else:
321+
layer.properties = table_data
322+
if add_table is not None:
323+
add_table(layer, self.viewer)
324+
# Save table to file if save path is provided.
325+
if save_path != "":
326+
file_path = self._save_table(self.save_path.text(), table_data)
327+
show_info(f"INFO: Added table and saved file to {file_path}.")

synaptic_reconstruction/tools/distance_measure_widget.py

Lines changed: 12 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import os
2-
31
import napari
42
import napari.layers
53
import numpy as np
@@ -11,27 +9,6 @@
119
from .base_widget import BaseWidget
1210
from .. import distance_measurements
1311

14-
try:
15-
from napari_skimage_regionprops import add_table
16-
except ImportError:
17-
add_table = None
18-
19-
20-
def _save_distance_table(save_path, data):
21-
ext = os.path.splitext(save_path)[1]
22-
if ext == "": # No file extension given, By default we save to CSV.
23-
file_path = f"{save_path}.csv"
24-
data.to_csv(file_path, index=False)
25-
elif ext == ".csv": # Extension was specified as csv
26-
file_path = save_path
27-
data.to_csv(file_path, index=False)
28-
elif ext == ".xlsx": # We also support excel.
29-
file_path = save_path
30-
data.to_excel(file_path, index=False)
31-
else:
32-
raise ValueError("Invalid extension for table: {ext}. We support .csv or .xlsx.")
33-
return file_path
34-
3512

3613
class DistanceMeasureWidget(BaseWidget):
3714
def __init__(self):
@@ -68,47 +45,33 @@ def __init__(self):
6845
def _to_table_data(self, distances, seg_ids, endpoints1=None, endpoints2=None):
6946
assert len(distances) == len(seg_ids), f"{distances.shape}, {seg_ids.shape}"
7047
if seg_ids.ndim == 2:
71-
table_data = {"label1": seg_ids[:, 0], "label2": seg_ids[:, 1], "distance": distances}
48+
table_data = {"label_id1": seg_ids[:, 0], "label_id2": seg_ids[:, 1], "distance": distances}
7249
else:
73-
table_data = {"label": seg_ids, "distance": distances}
50+
table_data = {"label_id": seg_ids, "distance": distances}
7451
if endpoints1 is not None:
7552
axis_names = "zyx" if endpoints1.shape[1] == 3 else "yx"
7653
table_data.update({f"begin-{ax}": endpoints1[:, i] for i, ax in enumerate(axis_names)})
7754
table_data.update({f"end-{ax}": endpoints2[:, i] for i, ax in enumerate(axis_names)})
7855
return pd.DataFrame(table_data)
7956

80-
def _add_lines_and_table(self, lines, properties, table_data, name):
57+
def _add_lines_and_table(self, lines, table_data, name):
8158
line_layer = self.viewer.add_shapes(
8259
lines,
8360
name=name,
8461
shape_type="line",
8562
edge_width=2,
8663
edge_color="red",
8764
blending="additive",
88-
properties=properties,
8965
)
90-
if add_table is not None:
91-
add_table(line_layer, self.viewer)
92-
93-
if self.save_path.text() != "":
94-
file_path = _save_distance_table(self.save_path.text(), table_data)
95-
96-
if self.save_path.text() != "":
97-
show_info(f"Added distance lines and saved file to {file_path}.")
98-
else:
99-
show_info("Added distance lines.")
66+
self._add_properties_and_table(line_layer, table_data, self.save_path.text())
10067

10168
def on_measure_seg_to_object(self):
10269
segmentation = self._get_layer_selector_data(self.image_selector_name1)
10370
object_data = self._get_layer_selector_data(self.image_selector_name2)
104-
# get metadata from layer if available
71+
72+
# Get the resolution / voxel size.
10573
metadata = self._get_layer_selector_data(self.image_selector_name1, return_metadata=True)
106-
resolution = metadata.get("voxel_size", None)
107-
if resolution is not None:
108-
resolution = [v for v in resolution.values()]
109-
# if user input is present override metadata
110-
if self.voxel_size_param.value() != 0.0: # changed from default
111-
resolution = segmentation.ndim * [self.voxel_size_param.value()]
74+
resolution = self._handle_resolution(metadata, self.voxel_size_param, segmentation.ndim)
11275

11376
(distances,
11477
endpoints1,
@@ -117,28 +80,23 @@ def on_measure_seg_to_object(self):
11780
segmentation=segmentation, segmented_object=object_data, distance_type="boundary",
11881
resolution=resolution
11982
)
120-
lines, properties = distance_measurements.create_object_distance_lines(
83+
lines, _ = distance_measurements.create_object_distance_lines(
12184
distances=distances,
12285
endpoints1=endpoints1,
12386
endpoints2=endpoints2,
12487
seg_ids=seg_ids,
12588
)
12689
table_data = self._to_table_data(distances, seg_ids, endpoints1, endpoints2)
127-
self._add_lines_and_table(lines, properties, table_data, name="distances")
90+
self._add_lines_and_table(lines, table_data, name="distances")
12891

12992
def on_measure_pairwise(self):
13093
segmentation = self._get_layer_selector_data(self.image_selector_name1)
13194
if segmentation is None:
13295
show_info("Please choose a segmentation.")
13396
return
134-
# get metadata from layer if available
97+
13598
metadata = self._get_layer_selector_data(self.image_selector_name1, return_metadata=True)
136-
resolution = metadata.get("voxel_size", None)
137-
if resolution is not None:
138-
resolution = [v for v in resolution.values()]
139-
# if user input is present override metadata
140-
if self.voxel_size_param.value() != 0.0: # changed from default
141-
resolution = segmentation.ndim * [self.voxel_size_param.value()]
99+
resolution = self._handle_resolution(metadata, self.voxel_size_param, segmentation.ndim)
142100

143101
(distances,
144102
endpoints1,
@@ -153,7 +111,7 @@ def on_measure_pairwise(self):
153111
distances=properties["distance"],
154112
seg_ids=np.concatenate([properties["id_a"][:, None], properties["id_b"][:, None]], axis=1)
155113
)
156-
self._add_lines_and_table(lines, properties, table_data, name="pairwise-distances")
114+
self._add_lines_and_table(lines, table_data, name="pairwise-distances")
157115

158116
def _create_settings_widget(self):
159117
setting_values = QWidget()

0 commit comments

Comments
 (0)