Skip to content

Commit 223baee

Browse files
Refactor plugin functionality, update pool widget
1 parent ec1ff3e commit 223baee

File tree

7 files changed

+187
-248
lines changed

7 files changed

+187
-248
lines changed

synaptic_reconstruction/napari.yaml

Lines changed: 1 addition & 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:

synaptic_reconstruction/tools/base_widget.py

Lines changed: 49 additions & 2 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):
@@ -60,7 +67,7 @@ def _create_layer_selector(self, selector_name, layer_type="Image"):
6067
def _update_selector(self, selector, layer_filter):
6168
"""Update a single selector with the current image layers in the viewer."""
6269
selector.clear()
63-
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)]
6471
selector.addItems(image_layers)
6572

6673
def _get_layer_selector_layer(self, selector_name):
@@ -188,7 +195,7 @@ def _add_shape_param(self, names, values, min_val, max_val, step=1, title=None,
188195
title=title[1] if title is not None else title, tooltip=tooltip
189196
)
190197
layout.addLayout(y_layout)
191-
198+
192199
if len(names) == 3:
193200
z_layout = QVBoxLayout()
194201
z_param, _ = self._add_int_param(
@@ -278,3 +285,43 @@ def _get_file_path(self, name, textbox, tooltip=None):
278285
else:
279286
# Handle the case where the selected path is not a file
280287
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 & 57 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,50 +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-
91-
line_layer.properties["distances"] = table_data
92-
93-
if add_table is not None:
94-
add_table(line_layer, self.viewer)
95-
96-
if self.save_path.text() != "":
97-
file_path = _save_distance_table(self.save_path.text(), table_data)
98-
99-
if self.save_path.text() != "":
100-
show_info(f"Added distance lines and saved file to {file_path}.")
101-
else:
102-
show_info("Added distance lines.")
66+
self._add_properties_and_table(line_layer, table_data, self.save_path.text())
10367

10468
def on_measure_seg_to_object(self):
10569
segmentation = self._get_layer_selector_data(self.image_selector_name1)
10670
object_data = self._get_layer_selector_data(self.image_selector_name2)
107-
# get metadata from layer if available
71+
72+
# Get the resolution / voxel size.
10873
metadata = self._get_layer_selector_data(self.image_selector_name1, return_metadata=True)
109-
resolution = metadata.get("voxel_size", None)
110-
if resolution is not None:
111-
resolution = [v for v in resolution.values()]
112-
# if user input is present override metadata
113-
if self.voxel_size_param.value() != 0.0: # changed from default
114-
resolution = segmentation.ndim * [self.voxel_size_param.value()]
74+
resolution = self._handle_resolution(metadata, self.voxel_size_param, segmentation.ndim)
11575

11676
(distances,
11777
endpoints1,
@@ -120,28 +80,23 @@ def on_measure_seg_to_object(self):
12080
segmentation=segmentation, segmented_object=object_data, distance_type="boundary",
12181
resolution=resolution
12282
)
123-
lines, properties = distance_measurements.create_object_distance_lines(
83+
lines, _ = distance_measurements.create_object_distance_lines(
12484
distances=distances,
12585
endpoints1=endpoints1,
12686
endpoints2=endpoints2,
12787
seg_ids=seg_ids,
12888
)
12989
table_data = self._to_table_data(distances, seg_ids, endpoints1, endpoints2)
130-
self._add_lines_and_table(lines, properties, table_data, name="distances")
90+
self._add_lines_and_table(lines, table_data, name="distances")
13191

13292
def on_measure_pairwise(self):
13393
segmentation = self._get_layer_selector_data(self.image_selector_name1)
13494
if segmentation is None:
13595
show_info("Please choose a segmentation.")
13696
return
137-
# get metadata from layer if available
97+
13898
metadata = self._get_layer_selector_data(self.image_selector_name1, return_metadata=True)
139-
resolution = metadata.get("voxel_size", None)
140-
if resolution is not None:
141-
resolution = [v for v in resolution.values()]
142-
# if user input is present, override metadata
143-
if self.voxel_size_param.value() != 0.0: # changed from default
144-
resolution = segmentation.ndim * [self.voxel_size_param.value()]
99+
resolution = self._handle_resolution(metadata, self.voxel_size_param, segmentation.ndim)
145100

146101
(distances,
147102
endpoints1,
@@ -156,7 +111,7 @@ def on_measure_pairwise(self):
156111
distances=properties["distance"],
157112
seg_ids=np.concatenate([properties["id_a"][:, None], properties["id_b"][:, None]], axis=1)
158113
)
159-
self._add_lines_and_table(lines, properties, table_data, name="pairwise-distances")
114+
self._add_lines_and_table(lines, table_data, name="pairwise-distances")
160115

161116
def _create_settings_widget(self):
162117
setting_values = QWidget()

synaptic_reconstruction/tools/morphology_widget.py

Lines changed: 19 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,6 @@
1111
from .base_widget import BaseWidget
1212
from synaptic_reconstruction.imod.to_imod import convert_segmentation_to_spheres
1313
from synaptic_reconstruction.morphology import compute_object_morphology
14-
from synaptic_reconstruction.tools.util import _save_table
15-
16-
try:
17-
from napari_skimage_regionprops import add_table
18-
except ImportError:
19-
add_table = None
2014

2115

2216
class MorphologyWidget(BaseWidget):
@@ -125,51 +119,19 @@ def _to_table_data(self, coords, radii, props):
125119
assert len(coords) == len(radii), f"Shape mismatch: {coords.shape}, {radii.shape}"
126120

127121
# Define columns based on dimension (2D or 3D)
128-
col_names = ['x', 'y'] if coords.shape[1] == 2 else ['x', 'y', 'z']
122+
col_names = ["x", "y"] if coords.shape[1] == 2 else ["x", "y", "z"]
129123
table_data = {
130-
'label_id': [prop.label for prop in props],
124+
"label_id": [prop.label for prop in props],
131125
**{col: coords[:, i] for i, col in enumerate(col_names)},
132-
'radius': radii,
133-
'intensity_max': [prop.intensity_max for prop in props],
134-
'intensity_mean': [prop.intensity_mean for prop in props],
135-
'intensity_min': [prop.intensity_min for prop in props],
136-
'intensity_std': [prop.intensity_std for prop in props],
126+
"radius": radii,
127+
"intensity_max": [prop.intensity_max for prop in props],
128+
"intensity_mean": [prop.intensity_mean for prop in props],
129+
"intensity_min": [prop.intensity_min for prop in props],
130+
"intensity_std": [prop.intensity_std for prop in props],
137131
}
138132

139133
return pd.DataFrame(table_data)
140134

141-
def _add_table(self, coords, radii, props, name="Shapes Layer"):
142-
"""
143-
Add a Shapes layer and table data to the Napari viewer.
144-
145-
Args:
146-
viewer (napari.Viewer): The Napari viewer instance.
147-
coords (np.ndarray): Array of 2D or 3D coordinates.
148-
radii (np.ndarray): Array of radii corresponding to the coordinates.
149-
props (list): List of properties containing intensity statistics.
150-
name (str): Name of the Shapes layer.
151-
save_path (str): Path to save the table data, if provided.
152-
"""
153-
# Create table data
154-
table_data = self._to_table_data(coords, radii, props)
155-
156-
# Add the shapes layer
157-
layer = self._get_layer_selector_layer(self.image_selector_name1)
158-
if layer.properties:
159-
layer.properties = layer.properties.update(table_data)
160-
else:
161-
layer.properties = table_data
162-
163-
if add_table is not None:
164-
add_table(layer, self.viewer)
165-
166-
# Save the table to a file if a save path is provided
167-
if self.save_path.text():
168-
table_data.to_csv(self.save_path, index=False)
169-
print(f"INFO: Added table and saved file to {self.save_path}.")
170-
else:
171-
print("INFO: Table added to viewer.")
172-
173135
def on_measure_vesicle_morphology(self):
174136
segmentation = self._get_layer_selector_data(self.image_selector_name1)
175137
image = self._get_layer_selector_data(self.image_selector_name)
@@ -180,23 +142,22 @@ def on_measure_vesicle_morphology(self):
180142
show_info("INFO: Please choose an image.")
181143
return
182144

183-
# get metadata from layer if available
145+
# Get the resolution / voxel size.
184146
metadata = self._get_layer_selector_data(self.image_selector_name, return_metadata=True)
185-
resolution = metadata.get("voxel_size", None)
186-
if resolution is not None:
187-
resolution = [v for v in resolution.values()]
188-
# if user input is present override metadata
189-
if self.voxel_size_param.value() != 0.0: # changed from default
190-
resolution = segmentation.ndim * [self.voxel_size_param.value()]
147+
resolution = self._handle_resolution(metadata, self.voxel_size_param, segmentation.ndim)
191148

149+
# Compute the mophology parameter.
192150
props = regionprops(label_image=segmentation, intensity_image=image)
193-
194151
coords, radii = convert_segmentation_to_spheres(
195152
segmentation=segmentation,
196153
resolution=resolution,
197154
props=props,
198155
)
199-
self._add_table(coords, radii, props, name="Vesicle Morphology")
156+
157+
# Create table data and add the properties and table to the layer.
158+
table_data = self._to_table_data(coords, radii, props)
159+
layer = self._get_layer_selector_layer(self.image_selector_name1)
160+
self._add_properties_and_table(layer, table_data, self.save_path.text())
200161

201162
def on_measure_structure_morphology(self):
202163
"""
@@ -213,30 +174,12 @@ def on_measure_structure_morphology(self):
213174
if resolution is not None:
214175
resolution = [v for v in resolution.values()]
215176
morphology = compute_object_morphology(
216-
object_=segmentation, structure_name=self.image_selector_name1,
217-
resolution=resolution
218-
)
219-
220-
self._add_table_structure(morphology)
177+
object_=segmentation, structure_name=self.image_selector_name1, resolution=resolution
178+
)
221179

222-
def _add_table_structure(self, morphology):
180+
# Add the properties to the layer and add/save the table.
223181
layer = self._get_layer_selector_layer(self.image_selector_name1)
224-
# Add properties to layer for add_table function
225-
if layer.properties:
226-
layer.properties = layer.properties.update(morphology)
227-
else:
228-
layer.properties = morphology
229-
230-
# Add a table layer to the Napari viewer
231-
if add_table is not None:
232-
add_table(layer, self.viewer)
233-
234-
# Save table to file if save path is provided
235-
if self.save_path.text() != "":
236-
file_path = _save_table(self.save_path.text(), morphology)
237-
show_info(f"INFO: Added table and saved file to {file_path}.")
238-
else:
239-
print("INFO: Table added to viewer.")
182+
self._add_properties_and_table(layer, morphology, self.save_path.text())
240183

241184
def _create_settings_widget(self):
242185
setting_values = QWidget()

synaptic_reconstruction/tools/util.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,6 @@
1212
from ..inference.vesicles import segment_vesicles
1313

1414

15-
def _save_table(save_path, data):
16-
ext = os.path.splitext(save_path)[1]
17-
if ext == "": # No file extension given, By default we save to CSV.
18-
file_path = f"{save_path}.csv"
19-
data.to_csv(file_path, index=False)
20-
elif ext == ".csv": # Extension was specified as csv
21-
file_path = save_path
22-
data.to_csv(file_path, index=False)
23-
elif ext == ".xlsx": # We also support excel.
24-
file_path = save_path
25-
data.to_excel(file_path, index=False)
26-
else:
27-
raise ValueError("Invalid extension for table: {ext}. We support .csv or .xlsx.")
28-
return file_path
29-
30-
3115
def load_custom_model(model_path: str, device: Optional[Union[str, torch.device]] = None) -> torch.nn.Module:
3216
model_path = _clean_filepath(model_path)
3317
if device is None:

0 commit comments

Comments
 (0)