synapse_net.tools.base_widget
1import os 2import sys 3from pathlib import Path 4 5import napari 6import numpy as np 7import qtpy.QtWidgets as QtWidgets 8 9from napari.utils.notifications import show_info 10from qtpy.QtWidgets import ( 11 QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSpinBox, QComboBox, QCheckBox 12) 13from superqt import QCollapsible 14 15try: 16 from napari_skimage_regionprops import add_table, get_table 17except ImportError: 18 add_table, get_table = None, None 19 20 21class _SilencePrint: 22 def __enter__(self): 23 self._original_stdout = sys.stdout 24 sys.stdout = open(os.devnull, "w") 25 26 def __exit__(self, exc_type, exc_val, exc_tb): 27 sys.stdout.close() 28 sys.stdout = self._original_stdout 29 30 31class BaseWidget(QWidget): 32 def __init__(self): 33 super().__init__() 34 self.viewer = napari.current_viewer() 35 self.attribute_dict = {} 36 37 def _create_layer_selector(self, selector_name, layer_type="Image", prefer_substring=None): 38 """Create a layer selector for an image or labels and store it in a dictionary. 39 40 Args: 41 selector_name (str): The name of the selector, used as a key in the dictionary. 42 layer_type (str): The type of layer to filter for ("Image" or "Labels"). 43 prefer_substring (str, optional): If given, the selector auto-defaults to the first 44 layer whose name contains this substring (case-insensitive); falls back to the first 45 layer otherwise. Re-applied whenever layers are added/removed. 46 """ 47 if not hasattr(self, "layer_selectors"): 48 self.layer_selectors = {} 49 50 # Determine the annotation type for the widget 51 if layer_type == "Image": 52 layer_filter = napari.layers.Image 53 elif layer_type == "Labels": 54 layer_filter = napari.layers.Labels 55 elif layer_type == "Shapes": 56 layer_filter = napari.layers.Shapes 57 else: 58 raise ValueError("layer_type must be either 'Image' or 'Labels'.") 59 60 selector_widget = QtWidgets.QWidget() 61 image_selector = QtWidgets.QComboBox() 62 layer_label = QtWidgets.QLabel(f"{selector_name}:") 63 64 # Populate initial options 65 self._update_selector(selector=image_selector, layer_filter=layer_filter, prefer_substring=prefer_substring) 66 67 # Update selector on layer events 68 self.viewer.layers.events.inserted.connect( 69 lambda event: self._update_selector(image_selector, layer_filter, prefer_substring) 70 ) 71 self.viewer.layers.events.removed.connect( 72 lambda event: self._update_selector(image_selector, layer_filter, prefer_substring) 73 ) 74 75 # Store the selector in the dictionary 76 self.layer_selectors[selector_name] = selector_widget 77 78 # Set up layout 79 layout = QVBoxLayout() 80 layout.addWidget(layer_label) 81 layout.addWidget(image_selector) 82 selector_widget.setLayout(layout) 83 return selector_widget 84 85 def _update_selector(self, selector, layer_filter, prefer_substring=None): 86 """Update a single selector with the current image layers in the viewer. 87 88 If ``prefer_substring`` is given, auto-select the first layer whose name contains it 89 (case-insensitive); otherwise the first layer stays selected (QComboBox default). 90 """ 91 selector.clear() 92 image_layers = [layer.name for layer in self.viewer.layers if isinstance(layer, layer_filter)] 93 selector.addItems(image_layers) 94 if prefer_substring: 95 needle = prefer_substring.lower() 96 match = next((name for name in image_layers if needle in name.lower()), None) 97 if match is not None: 98 selector.setCurrentText(match) 99 100 def _get_layer_selector_layer(self, selector_name): 101 """Return the layer currently selected in a given selector.""" 102 if selector_name in self.layer_selectors: 103 selector_widget = self.layer_selectors[selector_name] 104 105 # Retrieve the QComboBox from the QWidget's layout 106 image_selector = selector_widget.layout().itemAt(1).widget() 107 108 if isinstance(image_selector, QComboBox): 109 selected_layer_name = image_selector.currentText() 110 if selected_layer_name in self.viewer.layers: 111 return self.viewer.layers[selected_layer_name] 112 return None # Return None if layer not found 113 114 def _get_layer_selector_data(self, selector_name, return_metadata=False): 115 """Return the data for the layer currently selected in a given selector.""" 116 if selector_name in self.layer_selectors: 117 selector_widget = self.layer_selectors[selector_name] 118 119 # Retrieve the QComboBox from the QWidget's layout 120 image_selector = selector_widget.layout().itemAt(1).widget() 121 122 if isinstance(image_selector, QComboBox): 123 selected_layer_name = image_selector.currentText() 124 if selected_layer_name in self.viewer.layers: 125 if return_metadata: 126 return self.viewer.layers[selected_layer_name].metadata 127 else: 128 return self.viewer.layers[selected_layer_name].data 129 return None # Return None if layer not found 130 131 def _add_string_param(self, name, value, title=None, placeholder=None, layout=None, tooltip=None): 132 if layout is None: 133 layout = QtWidgets.QHBoxLayout() 134 label = QtWidgets.QLabel(title or name) 135 if tooltip: 136 label.setToolTip(tooltip) 137 layout.addWidget(label) 138 param = QtWidgets.QLineEdit() 139 param.setText(value) 140 if placeholder is not None: 141 param.setPlaceholderText(placeholder) 142 param.textChanged.connect(lambda val: setattr(self, name, val)) 143 if tooltip: 144 param.setToolTip(tooltip) 145 layout.addWidget(param) 146 return param, layout 147 148 def _add_float_param(self, name, value, title=None, min_val=0.0, max_val=1.0, decimals=2, 149 step=0.01, layout=None, tooltip=None): 150 if layout is None: 151 layout = QtWidgets.QHBoxLayout() 152 label = QtWidgets.QLabel(title or name) 153 if tooltip: 154 label.setToolTip(tooltip) 155 layout.addWidget(label) 156 param = QtWidgets.QDoubleSpinBox() 157 param.setRange(min_val, max_val) 158 param.setDecimals(decimals) 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_int_param(self, name, value, min_val, max_val, title=None, step=1, layout=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 param = QSpinBox() 175 param.setRange(min_val, max_val) 176 param.setValue(value) 177 param.setSingleStep(step) 178 param.valueChanged.connect(lambda val: setattr(self, name, val)) 179 if tooltip: 180 param.setToolTip(tooltip) 181 layout.addWidget(param) 182 return param, layout 183 184 def _add_choice_param(self, name, value, options, title=None, layout=None, update=None, tooltip=None): 185 if layout is None: 186 layout = QHBoxLayout() 187 label = QLabel(title or name) 188 if tooltip: 189 label.setToolTip(tooltip) 190 layout.addWidget(label) 191 192 # Create the dropdown menu via QComboBox, set the available values. 193 dropdown = QComboBox() 194 dropdown.addItems(options) 195 if update is None: 196 dropdown.currentIndexChanged.connect(lambda index: setattr(self, name, options[index])) 197 else: 198 dropdown.currentIndexChanged.connect(update) 199 200 # Set the correct value for the value. 201 dropdown.setCurrentIndex(dropdown.findText(value)) 202 203 if tooltip: 204 dropdown.setToolTip(tooltip) 205 206 layout.addWidget(dropdown) 207 return dropdown, layout 208 209 def _add_shape_param(self, names, values, min_val, max_val, step=1, title=None, tooltip=None): 210 layout = QHBoxLayout() 211 212 x_layout = QVBoxLayout() 213 x_param, _ = self._add_int_param( 214 names[0], values[0], min_val=min_val, max_val=max_val, layout=x_layout, step=step, 215 title=title[0] if title is not None else title, tooltip=tooltip 216 ) 217 layout.addLayout(x_layout) 218 219 y_layout = QVBoxLayout() 220 y_param, _ = self._add_int_param( 221 names[1], values[1], min_val=min_val, max_val=max_val, layout=y_layout, step=step, 222 title=title[1] if title is not None else title, tooltip=tooltip 223 ) 224 layout.addLayout(y_layout) 225 226 if len(names) == 3: 227 z_layout = QVBoxLayout() 228 z_param, _ = self._add_int_param( 229 names[2], values[2], min_val=min_val, max_val=max_val, layout=z_layout, step=step, 230 title=title[2] if title is not None else title, tooltip=tooltip 231 ) 232 layout.addLayout(z_layout) 233 return x_param, y_param, z_param, layout 234 235 return x_param, y_param, layout 236 237 def _make_collapsible(self, widget, title): 238 parent_widget = QWidget() 239 parent_widget.setLayout(QVBoxLayout()) 240 collapsible = QCollapsible(title, parent_widget) 241 collapsible.addWidget(widget) 242 parent_widget.layout().addWidget(collapsible) 243 return parent_widget 244 245 def _add_boolean_param(self, name, value, title=None, tooltip=None): 246 checkbox = QCheckBox(name if title is None else title) 247 checkbox.setChecked(value) 248 checkbox.stateChanged.connect(lambda val: setattr(self, name, val)) 249 if tooltip: 250 checkbox.setToolTip(tooltip) 251 return checkbox 252 253 def _add_path_param(self, name, value, select_type, title=None, placeholder=None, tooltip=None): 254 assert select_type in ("directory", "file", "both") 255 256 layout = QtWidgets.QHBoxLayout() 257 label = QtWidgets.QLabel(title or name) 258 if tooltip: 259 label.setToolTip(tooltip) 260 layout.addWidget(label) 261 262 path_textbox = QtWidgets.QLineEdit() 263 path_textbox.setText(str(value)) 264 if placeholder is not None: 265 path_textbox.setPlaceholderText(placeholder) 266 path_textbox.textChanged.connect(lambda val: setattr(self, name, val)) 267 if tooltip: 268 path_textbox.setToolTip(tooltip) 269 270 layout.addWidget(path_textbox) 271 272 def add_path_button(select_type, tooltip=None): 273 # Adjust button text. 274 button_text = f"Select {select_type.capitalize()}" 275 path_button = QtWidgets.QPushButton(button_text) 276 277 # Call appropriate function based on select_type. 278 path_button.clicked.connect(lambda: getattr(self, f"_get_{select_type}_path")(name, path_textbox)) 279 if tooltip: 280 path_button.setToolTip(tooltip) 281 layout.addWidget(path_button) 282 283 if select_type == "both": 284 add_path_button("file") 285 add_path_button("directory") 286 287 else: 288 add_path_button(select_type) 289 290 return path_textbox, layout 291 292 def _get_directory_path(self, name, textbox, tooltip=None): 293 directory = QtWidgets.QFileDialog.getExistingDirectory( 294 self, "Select Directory", "", QtWidgets.QFileDialog.ShowDirsOnly 295 ) 296 if tooltip: 297 directory.setToolTip(tooltip) 298 if directory and Path(directory).is_dir(): 299 textbox.setText(str(directory)) 300 else: 301 # Handle the case where the selected path is not a directory 302 print("Invalid directory selected. Please try again.") 303 304 def _get_file_path(self, name, textbox, tooltip=None): 305 file_path, _ = QtWidgets.QFileDialog.getOpenFileName( 306 self, "Select File", "", "All Files (*)" 307 ) 308 if tooltip: 309 file_path.setToolTip(tooltip) 310 if file_path and Path(file_path).is_file(): 311 textbox.setText(str(file_path)) 312 else: 313 # Handle the case where the selected path is not a file 314 print("Invalid file selected. Please try again.") 315 316 def _handle_resolution(self, metadata, voxel_size_param, ndim, return_as_list=True): 317 # Get the resolution / voxel size from the layer metadata if available. 318 resolution = metadata.get("voxel_size", None) 319 320 # If user input was given then override resolution from metadata. 321 axes = "zyx" if ndim == 3 else "yx" 322 if voxel_size_param.value() != 0.0: # Changed from default. 323 resolution = {ax: voxel_size_param.value() for ax in axes} 324 325 if resolution is not None and return_as_list: 326 resolution = [resolution[ax] for ax in axes] 327 assert len(resolution) == ndim 328 329 return resolution 330 331 def _save_table(self, save_path, data): 332 ext = os.path.splitext(save_path)[1] 333 if ext == "": # No file extension given, By default we save to CSV. 334 file_path = f"{save_path}.csv" 335 data.to_csv(file_path, index=False) 336 elif ext == ".csv": # Extension was specified as csv 337 file_path = save_path 338 data.to_csv(file_path, index=False) 339 elif ext == ".xlsx": # We also support excel. 340 file_path = save_path 341 data.to_excel(file_path, index=False) 342 else: 343 raise ValueError("Invalid extension for table: {ext}. We support .csv or .xlsx.") 344 return file_path 345 346 def _add_properties_and_table(self, layer, table_data, save_path=""): 347 layer.properties = table_data 348 349 if add_table is not None: 350 with _SilencePrint(): 351 add_table(layer, self.viewer) 352 353 # Save table to file if save path is provided. 354 if save_path != "": 355 file_path = self._save_table(self.save_path.text(), table_data) 356 show_info(f"INFO: Added table and saved file to {file_path}.") 357 358 def _add_or_update_layer(self, add_fn, name, data, scale, translate, layer_kwargs): 359 """Add a layer via ``add_fn`` (e.g. ``self.viewer.add_labels``), or refresh it in place if a 360 layer with this name already exists. 361 362 On refresh the ``scale``/``translate`` are reapplied (when provided) because a persisted layer 363 keeps its original transform, which may be stale if the source layer / voxel size changed 364 between runs; any ``layer_kwargs`` (e.g. ``opacity``, ``blending``, ``colormap``) are reapplied 365 too. On first add these go through the layer constructor. Returns the (new or existing) layer. 366 """ 367 if name in self.viewer.layers: 368 layer = self.viewer.layers[name] 369 layer.data = data 370 if scale is not None: 371 layer.scale = scale 372 if translate is not None: 373 layer.translate = translate 374 for key, value in layer_kwargs.items(): 375 setattr(layer, key, value) 376 else: 377 ctor_kwargs = dict(layer_kwargs) 378 if scale is not None: 379 ctor_kwargs["scale"] = scale 380 if translate is not None: 381 ctor_kwargs["translate"] = translate 382 layer = add_fn(data, name=name, **ctor_kwargs) 383 return layer 384 385 def add_or_update_labels(self, name, data, *, scale=None, translate=None, **layer_kwargs): 386 """Add a Labels layer, or refresh it in place if one with this name already exists. 387 388 See :meth:`_add_or_update_layer` for the refresh/transform semantics. Extra keyword arguments 389 (``opacity``, ``blending``, ``colormap``, …) are forwarded to the layer. Returns the layer. 390 """ 391 return self._add_or_update_layer( 392 self.viewer.add_labels, name, data, scale, translate, layer_kwargs 393 ) 394 395 def add_or_update_surface(self, name, vertices, faces, *, scale=None, translate=None, 396 values=None, **layer_kwargs): 397 """Add a Surface layer, or refresh it in place if one with this name already exists. 398 399 Surface layers colour by per-vertex ``values``; a constant array (the default) gives a 400 flat-coloured surface. See :meth:`_add_or_update_layer` for the refresh/transform semantics. 401 Returns the layer. 402 """ 403 if values is None: 404 values = np.ones(len(vertices), dtype="float32") 405 return self._add_or_update_layer( 406 self.viewer.add_surface, name, (vertices, faces, values), scale, translate, layer_kwargs 407 )
class
BaseWidget(PyQt5.QtWidgets.QWidget):
32class BaseWidget(QWidget): 33 def __init__(self): 34 super().__init__() 35 self.viewer = napari.current_viewer() 36 self.attribute_dict = {} 37 38 def _create_layer_selector(self, selector_name, layer_type="Image", prefer_substring=None): 39 """Create a layer selector for an image or labels and store it in a dictionary. 40 41 Args: 42 selector_name (str): The name of the selector, used as a key in the dictionary. 43 layer_type (str): The type of layer to filter for ("Image" or "Labels"). 44 prefer_substring (str, optional): If given, the selector auto-defaults to the first 45 layer whose name contains this substring (case-insensitive); falls back to the first 46 layer otherwise. Re-applied whenever layers are added/removed. 47 """ 48 if not hasattr(self, "layer_selectors"): 49 self.layer_selectors = {} 50 51 # Determine the annotation type for the widget 52 if layer_type == "Image": 53 layer_filter = napari.layers.Image 54 elif layer_type == "Labels": 55 layer_filter = napari.layers.Labels 56 elif layer_type == "Shapes": 57 layer_filter = napari.layers.Shapes 58 else: 59 raise ValueError("layer_type must be either 'Image' or 'Labels'.") 60 61 selector_widget = QtWidgets.QWidget() 62 image_selector = QtWidgets.QComboBox() 63 layer_label = QtWidgets.QLabel(f"{selector_name}:") 64 65 # Populate initial options 66 self._update_selector(selector=image_selector, layer_filter=layer_filter, prefer_substring=prefer_substring) 67 68 # Update selector on layer events 69 self.viewer.layers.events.inserted.connect( 70 lambda event: self._update_selector(image_selector, layer_filter, prefer_substring) 71 ) 72 self.viewer.layers.events.removed.connect( 73 lambda event: self._update_selector(image_selector, layer_filter, prefer_substring) 74 ) 75 76 # Store the selector in the dictionary 77 self.layer_selectors[selector_name] = selector_widget 78 79 # Set up layout 80 layout = QVBoxLayout() 81 layout.addWidget(layer_label) 82 layout.addWidget(image_selector) 83 selector_widget.setLayout(layout) 84 return selector_widget 85 86 def _update_selector(self, selector, layer_filter, prefer_substring=None): 87 """Update a single selector with the current image layers in the viewer. 88 89 If ``prefer_substring`` is given, auto-select the first layer whose name contains it 90 (case-insensitive); otherwise the first layer stays selected (QComboBox default). 91 """ 92 selector.clear() 93 image_layers = [layer.name for layer in self.viewer.layers if isinstance(layer, layer_filter)] 94 selector.addItems(image_layers) 95 if prefer_substring: 96 needle = prefer_substring.lower() 97 match = next((name for name in image_layers if needle in name.lower()), None) 98 if match is not None: 99 selector.setCurrentText(match) 100 101 def _get_layer_selector_layer(self, selector_name): 102 """Return the layer currently selected in a given selector.""" 103 if selector_name in self.layer_selectors: 104 selector_widget = self.layer_selectors[selector_name] 105 106 # Retrieve the QComboBox from the QWidget's layout 107 image_selector = selector_widget.layout().itemAt(1).widget() 108 109 if isinstance(image_selector, QComboBox): 110 selected_layer_name = image_selector.currentText() 111 if selected_layer_name in self.viewer.layers: 112 return self.viewer.layers[selected_layer_name] 113 return None # Return None if layer not found 114 115 def _get_layer_selector_data(self, selector_name, return_metadata=False): 116 """Return the data for the layer currently selected in a given selector.""" 117 if selector_name in self.layer_selectors: 118 selector_widget = self.layer_selectors[selector_name] 119 120 # Retrieve the QComboBox from the QWidget's layout 121 image_selector = selector_widget.layout().itemAt(1).widget() 122 123 if isinstance(image_selector, QComboBox): 124 selected_layer_name = image_selector.currentText() 125 if selected_layer_name in self.viewer.layers: 126 if return_metadata: 127 return self.viewer.layers[selected_layer_name].metadata 128 else: 129 return self.viewer.layers[selected_layer_name].data 130 return None # Return None if layer not found 131 132 def _add_string_param(self, name, value, title=None, placeholder=None, 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.QLineEdit() 140 param.setText(value) 141 if placeholder is not None: 142 param.setPlaceholderText(placeholder) 143 param.textChanged.connect(lambda val: setattr(self, name, val)) 144 if tooltip: 145 param.setToolTip(tooltip) 146 layout.addWidget(param) 147 return param, layout 148 149 def _add_float_param(self, name, value, title=None, min_val=0.0, max_val=1.0, decimals=2, 150 step=0.01, layout=None, tooltip=None): 151 if layout is None: 152 layout = QtWidgets.QHBoxLayout() 153 label = QtWidgets.QLabel(title or name) 154 if tooltip: 155 label.setToolTip(tooltip) 156 layout.addWidget(label) 157 param = QtWidgets.QDoubleSpinBox() 158 param.setRange(min_val, max_val) 159 param.setDecimals(decimals) 160 param.setValue(value) 161 param.setSingleStep(step) 162 param.valueChanged.connect(lambda val: setattr(self, name, val)) 163 if tooltip: 164 param.setToolTip(tooltip) 165 layout.addWidget(param) 166 return param, layout 167 168 def _add_int_param(self, name, value, min_val, max_val, title=None, step=1, layout=None, tooltip=None): 169 if layout is None: 170 layout = QHBoxLayout() 171 label = QLabel(title or name) 172 if tooltip: 173 label.setToolTip(tooltip) 174 layout.addWidget(label) 175 param = QSpinBox() 176 param.setRange(min_val, max_val) 177 param.setValue(value) 178 param.setSingleStep(step) 179 param.valueChanged.connect(lambda val: setattr(self, name, val)) 180 if tooltip: 181 param.setToolTip(tooltip) 182 layout.addWidget(param) 183 return param, layout 184 185 def _add_choice_param(self, name, value, options, title=None, layout=None, update=None, tooltip=None): 186 if layout is None: 187 layout = QHBoxLayout() 188 label = QLabel(title or name) 189 if tooltip: 190 label.setToolTip(tooltip) 191 layout.addWidget(label) 192 193 # Create the dropdown menu via QComboBox, set the available values. 194 dropdown = QComboBox() 195 dropdown.addItems(options) 196 if update is None: 197 dropdown.currentIndexChanged.connect(lambda index: setattr(self, name, options[index])) 198 else: 199 dropdown.currentIndexChanged.connect(update) 200 201 # Set the correct value for the value. 202 dropdown.setCurrentIndex(dropdown.findText(value)) 203 204 if tooltip: 205 dropdown.setToolTip(tooltip) 206 207 layout.addWidget(dropdown) 208 return dropdown, layout 209 210 def _add_shape_param(self, names, values, min_val, max_val, step=1, title=None, tooltip=None): 211 layout = QHBoxLayout() 212 213 x_layout = QVBoxLayout() 214 x_param, _ = self._add_int_param( 215 names[0], values[0], min_val=min_val, max_val=max_val, layout=x_layout, step=step, 216 title=title[0] if title is not None else title, tooltip=tooltip 217 ) 218 layout.addLayout(x_layout) 219 220 y_layout = QVBoxLayout() 221 y_param, _ = self._add_int_param( 222 names[1], values[1], min_val=min_val, max_val=max_val, layout=y_layout, step=step, 223 title=title[1] if title is not None else title, tooltip=tooltip 224 ) 225 layout.addLayout(y_layout) 226 227 if len(names) == 3: 228 z_layout = QVBoxLayout() 229 z_param, _ = self._add_int_param( 230 names[2], values[2], min_val=min_val, max_val=max_val, layout=z_layout, step=step, 231 title=title[2] if title is not None else title, tooltip=tooltip 232 ) 233 layout.addLayout(z_layout) 234 return x_param, y_param, z_param, layout 235 236 return x_param, y_param, layout 237 238 def _make_collapsible(self, widget, title): 239 parent_widget = QWidget() 240 parent_widget.setLayout(QVBoxLayout()) 241 collapsible = QCollapsible(title, parent_widget) 242 collapsible.addWidget(widget) 243 parent_widget.layout().addWidget(collapsible) 244 return parent_widget 245 246 def _add_boolean_param(self, name, value, title=None, tooltip=None): 247 checkbox = QCheckBox(name if title is None else title) 248 checkbox.setChecked(value) 249 checkbox.stateChanged.connect(lambda val: setattr(self, name, val)) 250 if tooltip: 251 checkbox.setToolTip(tooltip) 252 return checkbox 253 254 def _add_path_param(self, name, value, select_type, title=None, placeholder=None, tooltip=None): 255 assert select_type in ("directory", "file", "both") 256 257 layout = QtWidgets.QHBoxLayout() 258 label = QtWidgets.QLabel(title or name) 259 if tooltip: 260 label.setToolTip(tooltip) 261 layout.addWidget(label) 262 263 path_textbox = QtWidgets.QLineEdit() 264 path_textbox.setText(str(value)) 265 if placeholder is not None: 266 path_textbox.setPlaceholderText(placeholder) 267 path_textbox.textChanged.connect(lambda val: setattr(self, name, val)) 268 if tooltip: 269 path_textbox.setToolTip(tooltip) 270 271 layout.addWidget(path_textbox) 272 273 def add_path_button(select_type, tooltip=None): 274 # Adjust button text. 275 button_text = f"Select {select_type.capitalize()}" 276 path_button = QtWidgets.QPushButton(button_text) 277 278 # Call appropriate function based on select_type. 279 path_button.clicked.connect(lambda: getattr(self, f"_get_{select_type}_path")(name, path_textbox)) 280 if tooltip: 281 path_button.setToolTip(tooltip) 282 layout.addWidget(path_button) 283 284 if select_type == "both": 285 add_path_button("file") 286 add_path_button("directory") 287 288 else: 289 add_path_button(select_type) 290 291 return path_textbox, layout 292 293 def _get_directory_path(self, name, textbox, tooltip=None): 294 directory = QtWidgets.QFileDialog.getExistingDirectory( 295 self, "Select Directory", "", QtWidgets.QFileDialog.ShowDirsOnly 296 ) 297 if tooltip: 298 directory.setToolTip(tooltip) 299 if directory and Path(directory).is_dir(): 300 textbox.setText(str(directory)) 301 else: 302 # Handle the case where the selected path is not a directory 303 print("Invalid directory selected. Please try again.") 304 305 def _get_file_path(self, name, textbox, tooltip=None): 306 file_path, _ = QtWidgets.QFileDialog.getOpenFileName( 307 self, "Select File", "", "All Files (*)" 308 ) 309 if tooltip: 310 file_path.setToolTip(tooltip) 311 if file_path and Path(file_path).is_file(): 312 textbox.setText(str(file_path)) 313 else: 314 # Handle the case where the selected path is not a file 315 print("Invalid file selected. Please try again.") 316 317 def _handle_resolution(self, metadata, voxel_size_param, ndim, return_as_list=True): 318 # Get the resolution / voxel size from the layer metadata if available. 319 resolution = metadata.get("voxel_size", None) 320 321 # If user input was given then override resolution from metadata. 322 axes = "zyx" if ndim == 3 else "yx" 323 if voxel_size_param.value() != 0.0: # Changed from default. 324 resolution = {ax: voxel_size_param.value() for ax in axes} 325 326 if resolution is not None and return_as_list: 327 resolution = [resolution[ax] for ax in axes] 328 assert len(resolution) == ndim 329 330 return resolution 331 332 def _save_table(self, save_path, data): 333 ext = os.path.splitext(save_path)[1] 334 if ext == "": # No file extension given, By default we save to CSV. 335 file_path = f"{save_path}.csv" 336 data.to_csv(file_path, index=False) 337 elif ext == ".csv": # Extension was specified as csv 338 file_path = save_path 339 data.to_csv(file_path, index=False) 340 elif ext == ".xlsx": # We also support excel. 341 file_path = save_path 342 data.to_excel(file_path, index=False) 343 else: 344 raise ValueError("Invalid extension for table: {ext}. We support .csv or .xlsx.") 345 return file_path 346 347 def _add_properties_and_table(self, layer, table_data, save_path=""): 348 layer.properties = table_data 349 350 if add_table is not None: 351 with _SilencePrint(): 352 add_table(layer, self.viewer) 353 354 # Save table to file if save path is provided. 355 if save_path != "": 356 file_path = self._save_table(self.save_path.text(), table_data) 357 show_info(f"INFO: Added table and saved file to {file_path}.") 358 359 def _add_or_update_layer(self, add_fn, name, data, scale, translate, layer_kwargs): 360 """Add a layer via ``add_fn`` (e.g. ``self.viewer.add_labels``), or refresh it in place if a 361 layer with this name already exists. 362 363 On refresh the ``scale``/``translate`` are reapplied (when provided) because a persisted layer 364 keeps its original transform, which may be stale if the source layer / voxel size changed 365 between runs; any ``layer_kwargs`` (e.g. ``opacity``, ``blending``, ``colormap``) are reapplied 366 too. On first add these go through the layer constructor. Returns the (new or existing) layer. 367 """ 368 if name in self.viewer.layers: 369 layer = self.viewer.layers[name] 370 layer.data = data 371 if scale is not None: 372 layer.scale = scale 373 if translate is not None: 374 layer.translate = translate 375 for key, value in layer_kwargs.items(): 376 setattr(layer, key, value) 377 else: 378 ctor_kwargs = dict(layer_kwargs) 379 if scale is not None: 380 ctor_kwargs["scale"] = scale 381 if translate is not None: 382 ctor_kwargs["translate"] = translate 383 layer = add_fn(data, name=name, **ctor_kwargs) 384 return layer 385 386 def add_or_update_labels(self, name, data, *, scale=None, translate=None, **layer_kwargs): 387 """Add a Labels layer, or refresh it in place if one with this name already exists. 388 389 See :meth:`_add_or_update_layer` for the refresh/transform semantics. Extra keyword arguments 390 (``opacity``, ``blending``, ``colormap``, …) are forwarded to the layer. Returns the layer. 391 """ 392 return self._add_or_update_layer( 393 self.viewer.add_labels, name, data, scale, translate, layer_kwargs 394 ) 395 396 def add_or_update_surface(self, name, vertices, faces, *, scale=None, translate=None, 397 values=None, **layer_kwargs): 398 """Add a Surface layer, or refresh it in place if one with this name already exists. 399 400 Surface layers colour by per-vertex ``values``; a constant array (the default) gives a 401 flat-coloured surface. See :meth:`_add_or_update_layer` for the refresh/transform semantics. 402 Returns the layer. 403 """ 404 if values is None: 405 values = np.ones(len(vertices), dtype="float32") 406 return self._add_or_update_layer( 407 self.viewer.add_surface, name, (vertices, faces, values), scale, translate, layer_kwargs 408 )
QWidget(parent: Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())
def
add_or_update_labels(self, name, data, *, scale=None, translate=None, **layer_kwargs):
386 def add_or_update_labels(self, name, data, *, scale=None, translate=None, **layer_kwargs): 387 """Add a Labels layer, or refresh it in place if one with this name already exists. 388 389 See :meth:`_add_or_update_layer` for the refresh/transform semantics. Extra keyword arguments 390 (``opacity``, ``blending``, ``colormap``, …) are forwarded to the layer. Returns the layer. 391 """ 392 return self._add_or_update_layer( 393 self.viewer.add_labels, name, data, scale, translate, layer_kwargs 394 )
Add a Labels layer, or refresh it in place if one with this name already exists.
See _add_or_update_layer() for the refresh/transform semantics. Extra keyword arguments
(opacity, blending, colormap, …) are forwarded to the layer. Returns the layer.
def
add_or_update_surface( self, name, vertices, faces, *, scale=None, translate=None, values=None, **layer_kwargs):
396 def add_or_update_surface(self, name, vertices, faces, *, scale=None, translate=None, 397 values=None, **layer_kwargs): 398 """Add a Surface layer, or refresh it in place if one with this name already exists. 399 400 Surface layers colour by per-vertex ``values``; a constant array (the default) gives a 401 flat-coloured surface. See :meth:`_add_or_update_layer` for the refresh/transform semantics. 402 Returns the layer. 403 """ 404 if values is None: 405 values = np.ones(len(vertices), dtype="float32") 406 return self._add_or_update_layer( 407 self.viewer.add_surface, name, (vertices, faces, values), scale, translate, layer_kwargs 408 )
Add a Surface layer, or refresh it in place if one with this name already exists.
Surface layers colour by per-vertex values; a constant array (the default) gives a
flat-coloured surface. See _add_or_update_layer() for the refresh/transform semantics.
Returns the layer.