Skip to content

Commit e6deb38

Browse files
Implement structure for napari plugin
1 parent 478bc0f commit e6deb38

File tree

11 files changed

+614
-3
lines changed

11 files changed

+614
-3
lines changed

doc/documentation.md

Whitespace-only changes.

flamingo_tools/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
1+
"""
2+
.. include:: ../doc/documentation.md
3+
"""
4+
15
from .data_conversion import convert_lightsheet_to_bdv, convert_lightsheet_to_bdv_cli
26
from .test_data import create_test_data

flamingo_tools/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
3+
def get_model():
4+
pass
5+
6+
7+
def get_model_registry():
8+
pass

flamingo_tools/napari.yaml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: cochlea_net
2+
display_name: CochleaNet
3+
4+
# See https://napari.org/stable/plugins/manifest.html for valid categories.
5+
categories: ["Image Processing", "Annotation"]
6+
7+
contributions:
8+
commands:
9+
# Commands for widgets.
10+
- id: cochlea_net.segment
11+
python_name: flamingo_tools.plugin.segmentation_widget:SegmentationWidget
12+
title: Segmentation
13+
- id: cochlea_net.detect
14+
python_name: flamingo_tools.plugin.detection_widget:DetectionWidget
15+
title: Detection
16+
# Add a measurement widget?
17+
# Add a labeling / classification widget?
18+
19+
# Commands for sample data.
20+
- id: cochlea_net.sample_data_pv
21+
python_name: flamingo_tools.test_data:sample_data_pv
22+
title: Example parvalbumin (PV) data
23+
- id: cochlea_net.sample_data_vglut3
24+
python_name: flamingo_tools.test_data:sample_data_vglut3
25+
title: Example VGlut3 data
26+
- id: cochlea_net.sample_data_ctbp2
27+
python_name: flamingo_tools.test_data:sample_data_ctbp2
28+
title: Example CtBP2 data
29+
30+
widgets:
31+
- command: cochlea_net.segment
32+
display_name: Segmentation
33+
- command: cochlea_net.detect
34+
display_name: Detection
35+
36+
sample_data:
37+
- command: cochlea_net.sample_data_pv
38+
display_name: Sample Data PV
39+
key: cochlea-net-pv
40+
- command: cochlea_net.sample_data_vglut3
41+
display_name: Sample Data VGlut3
42+
key: cochlea-net-vglut3
43+
- command: cochlea_net.sample_data_ctbp2
44+
display_name: Sample Data CtBP2
45+
key: cochlea-net-ctbp2

flamingo_tools/plugin/__init__.py

Whitespace-only changes.
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
import os
2+
import sys
3+
from pathlib import Path
4+
5+
import napari
6+
import qtpy.QtWidgets as QtWidgets
7+
8+
from napari.utils.notifications import show_info
9+
from qtpy.QtWidgets import (
10+
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSpinBox, QComboBox, QCheckBox
11+
)
12+
from superqt import QCollapsible
13+
14+
try:
15+
from napari_skimage_regionprops import add_table, get_table
16+
except ImportError:
17+
add_table, get_table = None, None
18+
19+
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+
30+
class BaseWidget(QWidget):
31+
def __init__(self):
32+
super().__init__()
33+
self.viewer = napari.current_viewer()
34+
self.attribute_dict = {}
35+
36+
def _create_layer_selector(self, selector_name, layer_type="Image"):
37+
"""Create a layer selector for an image or labels and store it in a dictionary.
38+
39+
Args:
40+
selector_name (str): The name of the selector, used as a key in the dictionary.
41+
layer_type (str): The type of layer to filter for ("Image" or "Labels").
42+
"""
43+
if not hasattr(self, "layer_selectors"):
44+
self.layer_selectors = {}
45+
46+
# Determine the annotation type for the widget
47+
if layer_type == "Image":
48+
layer_filter = napari.layers.Image
49+
elif layer_type == "Labels":
50+
layer_filter = napari.layers.Labels
51+
elif layer_type == "Shapes":
52+
layer_filter = napari.layers.Shapes
53+
else:
54+
raise ValueError("layer_type must be either 'Image' or 'Labels'.")
55+
56+
selector_widget = QtWidgets.QWidget()
57+
image_selector = QtWidgets.QComboBox()
58+
layer_label = QtWidgets.QLabel(f"{selector_name}:")
59+
60+
# Populate initial options
61+
self._update_selector(selector=image_selector, layer_filter=layer_filter)
62+
63+
# Update selector on layer events
64+
self.viewer.layers.events.inserted.connect(lambda event: self._update_selector(image_selector, layer_filter))
65+
self.viewer.layers.events.removed.connect(lambda event: self._update_selector(image_selector, layer_filter))
66+
67+
# Store the selector in the dictionary
68+
self.layer_selectors[selector_name] = selector_widget
69+
70+
# Set up layout
71+
layout = QVBoxLayout()
72+
layout.addWidget(layer_label)
73+
layout.addWidget(image_selector)
74+
selector_widget.setLayout(layout)
75+
return selector_widget
76+
77+
def _update_selector(self, selector, layer_filter):
78+
"""Update a single selector with the current image layers in the viewer."""
79+
selector.clear()
80+
image_layers = [layer.name for layer in self.viewer.layers if isinstance(layer, layer_filter)]
81+
selector.addItems(image_layers)
82+
83+
def _get_layer_selector_layer(self, selector_name):
84+
"""Return the layer currently selected in a given selector."""
85+
if selector_name in self.layer_selectors:
86+
selector_widget = self.layer_selectors[selector_name]
87+
88+
# Retrieve the QComboBox from the QWidget's layout
89+
image_selector = selector_widget.layout().itemAt(1).widget()
90+
91+
if isinstance(image_selector, QComboBox):
92+
selected_layer_name = image_selector.currentText()
93+
if selected_layer_name in self.viewer.layers:
94+
return self.viewer.layers[selected_layer_name]
95+
return None # Return None if layer not found
96+
97+
def _get_layer_selector_data(self, selector_name, return_metadata=False):
98+
"""Return the data for the layer currently selected in a given selector."""
99+
if selector_name in self.layer_selectors:
100+
selector_widget = self.layer_selectors[selector_name]
101+
102+
# Retrieve the QComboBox from the QWidget's layout
103+
image_selector = selector_widget.layout().itemAt(1).widget()
104+
105+
if isinstance(image_selector, QComboBox):
106+
selected_layer_name = image_selector.currentText()
107+
if selected_layer_name in self.viewer.layers:
108+
if return_metadata:
109+
return self.viewer.layers[selected_layer_name].metadata
110+
else:
111+
return self.viewer.layers[selected_layer_name].data
112+
return None # Return None if layer not found
113+
114+
def _add_string_param(self, name, value, title=None, placeholder=None, layout=None, tooltip=None):
115+
if layout is None:
116+
layout = QtWidgets.QHBoxLayout()
117+
label = QtWidgets.QLabel(title or name)
118+
if tooltip:
119+
label.setToolTip(tooltip)
120+
layout.addWidget(label)
121+
param = QtWidgets.QLineEdit()
122+
param.setText(value)
123+
if placeholder is not None:
124+
param.setPlaceholderText(placeholder)
125+
param.textChanged.connect(lambda val: setattr(self, name, val))
126+
if tooltip:
127+
param.setToolTip(tooltip)
128+
layout.addWidget(param)
129+
return param, layout
130+
131+
def _add_float_param(self, name, value, title=None, min_val=0.0, max_val=1.0, decimals=2,
132+
step=0.01, layout=None, tooltip=None):
133+
if layout is None:
134+
layout = QtWidgets.QHBoxLayout()
135+
label = QtWidgets.QLabel(title or name)
136+
if tooltip:
137+
label.setToolTip(tooltip)
138+
layout.addWidget(label)
139+
param = QtWidgets.QDoubleSpinBox()
140+
param.setRange(min_val, max_val)
141+
param.setDecimals(decimals)
142+
param.setValue(value)
143+
param.setSingleStep(step)
144+
param.valueChanged.connect(lambda val: setattr(self, name, val))
145+
if tooltip:
146+
param.setToolTip(tooltip)
147+
layout.addWidget(param)
148+
return param, layout
149+
150+
def _add_int_param(self, name, value, min_val, max_val, title=None, step=1, layout=None, tooltip=None):
151+
if layout is None:
152+
layout = QHBoxLayout()
153+
label = QLabel(title or name)
154+
if tooltip:
155+
label.setToolTip(tooltip)
156+
layout.addWidget(label)
157+
param = QSpinBox()
158+
param.setRange(min_val, max_val)
159+
param.setValue(value)
160+
param.setSingleStep(step)
161+
param.valueChanged.connect(lambda val: setattr(self, name, val))
162+
if tooltip:
163+
param.setToolTip(tooltip)
164+
layout.addWidget(param)
165+
return param, layout
166+
167+
def _add_choice_param(self, name, value, options, title=None, layout=None, update=None, tooltip=None):
168+
if layout is None:
169+
layout = QHBoxLayout()
170+
label = QLabel(title or name)
171+
if tooltip:
172+
label.setToolTip(tooltip)
173+
layout.addWidget(label)
174+
175+
# Create the dropdown menu via QComboBox, set the available values.
176+
dropdown = QComboBox()
177+
dropdown.addItems(options)
178+
if update is None:
179+
dropdown.currentIndexChanged.connect(lambda index: setattr(self, name, options[index]))
180+
else:
181+
dropdown.currentIndexChanged.connect(update)
182+
183+
# Set the correct value for the value.
184+
dropdown.setCurrentIndex(dropdown.findText(value))
185+
186+
if tooltip:
187+
dropdown.setToolTip(tooltip)
188+
189+
layout.addWidget(dropdown)
190+
return dropdown, layout
191+
192+
def _add_shape_param(self, names, values, min_val, max_val, step=1, title=None, tooltip=None):
193+
layout = QHBoxLayout()
194+
195+
x_layout = QVBoxLayout()
196+
x_param, _ = self._add_int_param(
197+
names[0], values[0], min_val=min_val, max_val=max_val, layout=x_layout, step=step,
198+
title=title[0] if title is not None else title, tooltip=tooltip
199+
)
200+
layout.addLayout(x_layout)
201+
202+
y_layout = QVBoxLayout()
203+
y_param, _ = self._add_int_param(
204+
names[1], values[1], min_val=min_val, max_val=max_val, layout=y_layout, step=step,
205+
title=title[1] if title is not None else title, tooltip=tooltip
206+
)
207+
layout.addLayout(y_layout)
208+
209+
if len(names) == 3:
210+
z_layout = QVBoxLayout()
211+
z_param, _ = self._add_int_param(
212+
names[2], values[2], min_val=min_val, max_val=max_val, layout=z_layout, step=step,
213+
title=title[2] if title is not None else title, tooltip=tooltip
214+
)
215+
layout.addLayout(z_layout)
216+
return x_param, y_param, z_param, layout
217+
218+
return x_param, y_param, layout
219+
220+
def _make_collapsible(self, widget, title):
221+
parent_widget = QWidget()
222+
parent_widget.setLayout(QVBoxLayout())
223+
collapsible = QCollapsible(title, parent_widget)
224+
collapsible.addWidget(widget)
225+
parent_widget.layout().addWidget(collapsible)
226+
return parent_widget
227+
228+
def _add_boolean_param(self, name, value, title=None, tooltip=None):
229+
checkbox = QCheckBox(name if title is None else title)
230+
checkbox.setChecked(value)
231+
checkbox.stateChanged.connect(lambda val: setattr(self, name, val))
232+
if tooltip:
233+
checkbox.setToolTip(tooltip)
234+
return checkbox
235+
236+
def _add_path_param(self, name, value, select_type, title=None, placeholder=None, tooltip=None):
237+
assert select_type in ("directory", "file", "both")
238+
239+
layout = QtWidgets.QHBoxLayout()
240+
label = QtWidgets.QLabel(title or name)
241+
if tooltip:
242+
label.setToolTip(tooltip)
243+
layout.addWidget(label)
244+
245+
path_textbox = QtWidgets.QLineEdit()
246+
path_textbox.setText(str(value))
247+
if placeholder is not None:
248+
path_textbox.setPlaceholderText(placeholder)
249+
path_textbox.textChanged.connect(lambda val: setattr(self, name, val))
250+
if tooltip:
251+
path_textbox.setToolTip(tooltip)
252+
253+
layout.addWidget(path_textbox)
254+
255+
def add_path_button(select_type, tooltip=None):
256+
# Adjust button text.
257+
button_text = f"Select {select_type.capitalize()}"
258+
path_button = QtWidgets.QPushButton(button_text)
259+
260+
# Call appropriate function based on select_type.
261+
path_button.clicked.connect(lambda: getattr(self, f"_get_{select_type}_path")(name, path_textbox))
262+
if tooltip:
263+
path_button.setToolTip(tooltip)
264+
layout.addWidget(path_button)
265+
266+
if select_type == "both":
267+
add_path_button("file")
268+
add_path_button("directory")
269+
270+
else:
271+
add_path_button(select_type)
272+
273+
return path_textbox, layout
274+
275+
def _get_directory_path(self, name, textbox, tooltip=None):
276+
directory = QtWidgets.QFileDialog.getExistingDirectory(
277+
self, "Select Directory", "", QtWidgets.QFileDialog.ShowDirsOnly
278+
)
279+
if tooltip:
280+
directory.setToolTip(tooltip)
281+
if directory and Path(directory).is_dir():
282+
textbox.setText(str(directory))
283+
else:
284+
# Handle the case where the selected path is not a directory
285+
print("Invalid directory selected. Please try again.")
286+
287+
def _get_file_path(self, name, textbox, tooltip=None):
288+
file_path, _ = QtWidgets.QFileDialog.getOpenFileName(
289+
self, "Select File", "", "All Files (*)"
290+
)
291+
if tooltip:
292+
file_path.setToolTip(tooltip)
293+
if file_path and Path(file_path).is_file():
294+
textbox.setText(str(file_path))
295+
else:
296+
# Handle the case where the selected path is not a file
297+
print("Invalid file selected. Please try again.")
298+
299+
def _save_table(self, save_path, data):
300+
ext = os.path.splitext(save_path)[1]
301+
if ext == "": # No file extension given, By default we save to CSV.
302+
file_path = f"{save_path}.csv"
303+
data.to_csv(file_path, index=False)
304+
elif ext == ".csv": # Extension was specified as csv
305+
file_path = save_path
306+
data.to_csv(file_path, index=False)
307+
elif ext == ".xlsx": # We also support excel.
308+
file_path = save_path
309+
data.to_excel(file_path, index=False)
310+
else:
311+
raise ValueError("Invalid extension for table: {ext}. We support .csv or .xlsx.")
312+
return file_path
313+
314+
def _add_properties_and_table(self, layer, table_data, save_path=""):
315+
layer.properties = table_data
316+
317+
if add_table is not None:
318+
with _SilencePrint():
319+
add_table(layer, self.viewer)
320+
321+
# Save table to file if save path is provided.
322+
if save_path != "":
323+
file_path = self._save_table(self.save_path.text(), table_data)
324+
show_info(f"INFO: Added table and saved file to {file_path}.")
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .base_widget import BaseWidget
2+
3+
4+
class SegmentationWidget(BaseWidget):
5+
pass

0 commit comments

Comments
 (0)