synapse_net.tools.segmentation_widget
1import inspect 2import re 3from typing import Optional, Union 4 5import napari 6import numpy as np 7import torch 8 9from napari.utils.notifications import show_info 10from qtpy.QtWidgets import QCheckBox, QComboBox, QLabel, QPushButton, QVBoxLayout, QWidget 11 12from .base_widget import BaseWidget 13from ..inference.active_zone import segment_active_zone 14from ..inference.compartments import segment_compartments 15from ..inference.cristae import segment_cristae 16from ..inference.inference import ( 17 _get_model_registry, 18 _segment_ribbon_AZ, 19 compute_scale_from_voxel_size, 20 get_model, 21 get_model_training_patch_shape, 22 get_segmentation_function, 23 run_segmentation, 24) 25from ..inference.mitochondria import segment_mitochondria 26from ..inference.util import get_default_tiling, get_device 27from ..inference.vesicles import VESICLE_SEGMENTATION_MODES, segment_vesicles 28 29 30_MAX_MIN_SIZE = 100_000_000 31_POSTPROCESSING_PARAMETER_SPECS = { 32 segment_vesicles: { 33 "min_size": {"type": "int", "min": 0, "max": _MAX_MIN_SIZE, "step": 1}, 34 "mode": {"type": "choice", "options": list(VESICLE_SEGMENTATION_MODES)}, 35 "threshold": {"type": "float", "min": 0.0, "max": 1.0, "step": 0.01, "decimals": 2}, 36 }, 37 segment_mitochondria: { 38 "min_size": {"type": "int", "min": 0, "max": _MAX_MIN_SIZE, "step": 1}, 39 "seed_distance": {"type": "int", "min": 0, "max": 10_000, "step": 1}, 40 }, 41 segment_active_zone: { 42 "min_size": {"type": "int", "min": 0, "max": _MAX_MIN_SIZE, "step": 1}, 43 "foreground_threshold": {"type": "float", "min": 0.0, "max": 1.0, "step": 0.01, "decimals": 2}, 44 }, 45 segment_compartments: { 46 "boundary_threshold": {"type": "float", "min": 0.0, "max": 1.0, "step": 0.01, "decimals": 2}, 47 "n_slices_exclude": {"type": "int", "min": 0, "max": 10_000, "step": 1}, 48 "min_z_extent": {"type": "int", "min": 0, "max": 10_000, "step": 1}, 49 }, 50 _segment_ribbon_AZ: { 51 "threshold": {"type": "float", "min": 0.0, "max": 1.0, "step": 0.01, "decimals": 2}, 52 "n_slices_exclude": {"type": "int", "min": 0, "max": 10_000, "step": 1}, 53 "min_membrane_size": { 54 "type": "int", "min": 0, "max": _MAX_MIN_SIZE, "step": 1, "default": 50_000, 55 }, 56 "n_ribbons": {"type": "int", "min": 1, "max": 1_000, "step": 1}, 57 }, 58 segment_cristae: { 59 "min_size": { 60 "type": "int", "min": 0, "max": _MAX_MIN_SIZE, "step": 1, 61 "tooltip": "Minimum cristae size in voxels. Smaller connected components are removed — " 62 "raise this to filter out small, detached cristae on the mitochondria membranes.", 63 }, 64 "foreground_threshold": { 65 "type": "float", "min": 0.0, "max": 1.0, "step": 0.01, "decimals": 2, 66 "tooltip": "Probability cutoff for the cristae foreground. Higher values keep only more " 67 "confident predictions (less foreground).", 68 }, 69 "erosion_distance_nm": { 70 "type": "float", "min": 0.0, "max": 1_000.0, "step": 0.1, "decimals": 1, 71 "tooltip": "Distance in nm to shrink the mitochondria mask inward before restricting " 72 "cristae to it. 0 disables erosion (rely on Minimum Size instead).", 73 }, 74 }, 75} 76 77 78def _load_custom_model(model_path: str, device: Optional[Union[str, torch.device]] = None) -> torch.nn.Module: 79 model_path = _clean_filepath(model_path) 80 if device is None: 81 device = get_device(device) 82 try: 83 model = torch.load(model_path, map_location=torch.device(device), weights_only=False) 84 except Exception as e: 85 print(e) 86 print("model path", model_path) 87 return None 88 return model 89 90 91def _available_devices(): 92 available_devices = [] 93 for i in ["cuda", "mps", "cpu"]: 94 try: 95 device = get_device(i) 96 except RuntimeError: 97 pass 98 else: 99 available_devices.append(device) 100 return available_devices 101 102 103def _get_current_tiling(tiling_params: dict, model_type: str): 104 # Read the tiling values from the spin boxes. 105 tiling = {name: {axis: param.value() for axis, param in params.items()} for name, params in tiling_params.items()} 106 if "2d" in model_type: # A 2d model predicts single slices. 107 tiling["tile"]["z"] = 1 108 tiling["halo"]["z"] = 0 109 show_info(f"Using tiling: {tiling}") 110 return tiling 111 112 113def _clean_filepath(filepath): 114 """Cleans a given filepath by: 115 - Removing newline characters (\n) 116 - Removing escape sequences 117 - Stripping the 'file://' prefix if present 118 119 Args: 120 filepath (str): The original filepath 121 122 Returns: 123 str: The cleaned filepath 124 """ 125 # Remove 'file://' prefix if present 126 if filepath.startswith("file://"): 127 filepath = filepath[7:] 128 129 # Remove escape sequences and newlines 130 filepath = re.sub(r'\\.', '', filepath) 131 filepath = filepath.replace('\n', '').replace('\r', '') 132 133 return filepath 134 135 136class SegmentationWidget(BaseWidget): 137 def __init__(self): 138 super().__init__() 139 140 self.viewer = napari.current_viewer() 141 layout = QVBoxLayout() 142 143 # Create the image selection dropdown. 144 self.image_selector_name = "Image data" 145 self.image_selector_widget = self._create_layer_selector(self.image_selector_name, layer_type="Image") 146 147 # Create buttons and widgets. 148 self.predict_button = QPushButton("Run Segmentation") 149 self.predict_button.clicked.connect(self.on_predict) 150 self.model_selector_widget = self.load_model_widget() 151 self.settings = self._create_settings_widget() 152 153 # Add the widgets to the layout. 154 layout.addWidget(self.image_selector_widget) 155 layout.addWidget(self.model_selector_widget) 156 layout.addWidget(self.settings) 157 layout.addWidget(self.predict_button) 158 159 self.setLayout(layout) 160 161 @staticmethod 162 def _clear_layout(layout): 163 while layout.count(): 164 item = layout.takeAt(0) 165 widget = item.widget() 166 child_layout = item.layout() 167 if widget is not None: 168 widget.deleteLater() 169 elif child_layout is not None: 170 SegmentationWidget._clear_layout(child_layout) 171 child_layout.deleteLater() 172 173 def _update_tiling(self, model_type): 174 patch_shape = None if model_type == "- choose -" else get_model_training_patch_shape(model_type) 175 tiling = get_default_tiling(patch_shape=patch_shape) 176 for name, values in tiling.items(): 177 for axis, value in values.items(): 178 self.tiling[name][axis].setValue(value) 179 180 def _update_postprocessing_settings(self, model_type): 181 self._clear_layout(self.postprocessing_settings_layout) 182 self.postprocessing_parameter_widgets = {} 183 if model_type == "- choose -": 184 return 185 186 segmentation_function = get_segmentation_function(model_type) 187 parameter_specs = _POSTPROCESSING_PARAMETER_SPECS.get(segmentation_function, {}) 188 function_parameters = inspect.signature(segmentation_function).parameters 189 190 for name, spec in parameter_specs.items(): 191 if name not in function_parameters or function_parameters[name].default is inspect.Parameter.empty: 192 raise ValueError( 193 f"Configured post-processing parameter '{name}' is not an optional parameter " 194 f"of {segmentation_function.__name__}." 195 ) 196 default = spec.get("default", function_parameters[name].default) 197 tooltip = spec.get("tooltip") 198 if spec["type"] == "int": 199 parameter_widget, parameter_layout = self._add_int_param( 200 name, default, min_val=spec["min"], max_val=spec["max"], step=spec["step"], tooltip=tooltip 201 ) 202 self.postprocessing_settings_layout.addLayout(parameter_layout) 203 elif spec["type"] == "float": 204 parameter_widget, parameter_layout = self._add_float_param( 205 name, 206 default, 207 min_val=spec["min"], 208 max_val=spec["max"], 209 step=spec["step"], 210 decimals=spec["decimals"], 211 tooltip=tooltip, 212 ) 213 self.postprocessing_settings_layout.addLayout(parameter_layout) 214 elif spec["type"] == "bool": 215 parameter_widget = self._add_boolean_param(name, default, tooltip=tooltip) 216 self.postprocessing_settings_layout.addWidget(parameter_widget) 217 elif spec["type"] == "choice": 218 parameter_widget, parameter_layout = self._add_choice_param( 219 name, default, spec["options"], tooltip=tooltip 220 ) 221 self.postprocessing_settings_layout.addLayout(parameter_layout) 222 else: 223 raise ValueError(f"Unsupported post-processing parameter type: {spec['type']}") 224 self.postprocessing_parameter_widgets[name] = parameter_widget 225 226 def _get_postprocessing_kwargs(self): 227 kwargs = {} 228 for name, widget in self.postprocessing_parameter_widgets.items(): 229 if isinstance(widget, QCheckBox): 230 value = widget.isChecked() 231 elif isinstance(widget, QComboBox): 232 value = widget.currentText() 233 else: 234 value = widget.value() 235 kwargs[name] = value 236 return kwargs 237 238 def load_model_widget(self): 239 model_widget = QWidget() 240 title_label = QLabel("Select Model:") 241 242 # Exclude the models that are only offered through the CLI and not in the plugin. 243 model_list = set(_get_model_registry().urls.keys()) 244 # These are the models exlcuded due to their specificity and to keep the menu simple. 245 # TODO: we should at some point update the logic here, to make it easier to support further models 246 # without cluttering the UI. 247 excluded_models = ["vesicles_2d_maus"] 248 model_list = [name for name in model_list if name not in excluded_models] 249 250 models = ["- choose -"] + model_list 251 self.model_selector = QComboBox() 252 self.model_selector.addItems(models) 253 # Create a layout and add the title label and combo box 254 layout = QVBoxLayout() 255 layout.addWidget(title_label) 256 layout.addWidget(self.model_selector) 257 258 # Set layout on the model widget 259 model_widget.setLayout(layout) 260 return model_widget 261 262 def on_predict(self): 263 # Get the model and postprocessing settings. 264 model_type = self.model_selector.currentText() 265 custom_model_path = self.checkpoint_param.text() 266 if model_type == "- choose -": 267 show_info("INFO: Please choose a model.") 268 return 269 270 device = get_device(self.device_dropdown.currentText()) 271 272 # Get the image data. 273 image = self._get_layer_selector_data(self.image_selector_name) 274 if image is None: 275 show_info("INFO: Please choose an image.") 276 return 277 278 with self._computing( 279 self.predict_button, "Computing…", "Run Segmentation", "INFO: Running segmentation…" 280 ): 281 # Load the model. Override if user chose custom model. 282 rescale_input = True 283 if custom_model_path: 284 model = _load_custom_model(custom_model_path, device) 285 rescale_input = False 286 if model: 287 show_info(f"INFO: Using custom model from path: {custom_model_path}") 288 else: 289 show_info(f"ERROR: Failed to load custom model from path: {custom_model_path}") 290 return 291 else: 292 model = get_model(model_type, device) 293 294 # Get the current tiling. 295 tiling = _get_current_tiling(self.tiling, model_type) 296 297 # Get the voxel size. 298 metadata = self._get_layer_selector_data(self.image_selector_name, return_metadata=True) 299 voxel_size = self._handle_resolution(metadata, self.voxel_size_param, image.ndim, return_as_list=False) 300 301 # Determine the scaling based on the voxel size. 302 scale = None 303 if voxel_size and rescale_input: 304 # Calculate scale so voxel_size is the same as in training. 305 scale = compute_scale_from_voxel_size(voxel_size, model_type) 306 scale_info = list(map(lambda x: np.round(x, 2), scale)) 307 show_info(f"INFO: Rescaled the image by {scale_info} to optimize for the selected model.") 308 309 # Some models require an additional segmentation for inference or postprocessing. 310 # For these models we read out the 'Extra Segmentation' widget. 311 if model_type == "ribbon": # Currently only the ribbon model needs the extra seg. 312 extra_seg = self._get_layer_selector_data(self.extra_seg_selector_name) 313 resolution = tuple(voxel_size[ax] for ax in "zyx") 314 kwargs = {"extra_segmentation": extra_seg, "resolution": resolution} 315 elif "cristae" in model_type: # Cristae model expects 2 3D volumes 316 kwargs = { 317 "extra_segmentation": self._get_layer_selector_data(self.extra_seg_selector_name), 318 "with_channels": True, 319 "channels_to_standardize": [0] 320 } 321 else: 322 kwargs = {} 323 kwargs.update(self._get_postprocessing_kwargs()) 324 segmentation = run_segmentation( 325 image, model=model, model_type=model_type, tiling=tiling, scale=scale, **kwargs 326 ) 327 328 # Add the segmentation layer(s). 329 if isinstance(segmentation, dict): 330 for name, seg in segmentation.items(): 331 self.viewer.add_labels(seg, name=name, metadata=metadata) 332 else: 333 self.viewer.add_labels(segmentation, name=f"{model_type}", metadata=metadata) 334 335 show_info(f"INFO: Segmentation of {model_type} added to layers.") 336 337 def _create_settings_widget(self): 338 setting_values = QWidget() 339 # setting_values.setToolTip(get_tooltip("embedding", "settings")) 340 setting_values.setLayout(QVBoxLayout()) 341 342 # Create UI for the device. 343 device = "auto" 344 device_options = ["auto"] + _available_devices() 345 346 self.device_dropdown, layout = self._add_choice_param("device", device, device_options) 347 setting_values.layout().addLayout(layout) 348 349 # Create UI for the tile shape. The values are set to the default tiling of the selected model. 350 self.tiling = {"tile": {}, "halo": {}} 351 self.tiling["tile"]["x"], self.tiling["tile"]["y"], self.tiling["tile"]["z"], layout = self._add_shape_param( 352 ("tile_x", "tile_y", "tile_z"), (0, 0, 0), min_val=0, max_val=2048, step=16, 353 # tooltip=get_tooltip("embedding", "tiling") 354 ) 355 setting_values.layout().addLayout(layout) 356 357 # Create UI for the halo. 358 self.tiling["halo"]["x"], self.tiling["halo"]["y"], self.tiling["halo"]["z"], layout = self._add_shape_param( 359 ("halo_x", "halo_y", "halo_z"), (0, 0, 0), min_val=0, max_val=512, 360 # tooltip=get_tooltip("embedding", "halo") 361 ) 362 setting_values.layout().addLayout(layout) 363 self.model_selector.currentTextChanged.connect(self._update_tiling) 364 self._update_tiling(self.model_selector.currentText()) 365 366 # Read voxel size from layer metadata. 367 self.voxel_size_param, layout = self._add_float_param( 368 "voxel_size", 0.0, min_val=0.0, max_val=100.0, 369 ) 370 setting_values.layout().addLayout(layout) 371 372 self.checkpoint_param, layout = self._add_string_param( 373 name="checkpoint", value="", title="Load Custom Model", 374 placeholder="path/to/checkpoint.pt", 375 ) 376 setting_values.layout().addLayout(layout) 377 378 # Add selection UI for additional segmentation, which some models require for inference or postproc. 379 self.extra_seg_selector_name = "Extra Segmentation" 380 self.extra_selector_widget = self._create_layer_selector(self.extra_seg_selector_name, layer_type="Labels") 381 setting_values.layout().addWidget(self.extra_selector_widget) 382 383 # Add model-specific post-processing settings that are updated when the selected model changes. 384 setting_values.layout().addWidget(QLabel("Post-processing:")) 385 self.postprocessing_settings_widget = QWidget() 386 self.postprocessing_settings_layout = QVBoxLayout() 387 self.postprocessing_settings_layout.setContentsMargins(0, 0, 0, 0) 388 self.postprocessing_settings_widget.setLayout(self.postprocessing_settings_layout) 389 setting_values.layout().addWidget(self.postprocessing_settings_widget) 390 self.postprocessing_parameter_widgets = {} 391 self.model_selector.currentTextChanged.connect(self._update_postprocessing_settings) 392 self._update_postprocessing_settings(self.model_selector.currentText()) 393 394 settings = self._make_collapsible(widget=setting_values, title="Advanced Settings") 395 return settings
137class SegmentationWidget(BaseWidget): 138 def __init__(self): 139 super().__init__() 140 141 self.viewer = napari.current_viewer() 142 layout = QVBoxLayout() 143 144 # Create the image selection dropdown. 145 self.image_selector_name = "Image data" 146 self.image_selector_widget = self._create_layer_selector(self.image_selector_name, layer_type="Image") 147 148 # Create buttons and widgets. 149 self.predict_button = QPushButton("Run Segmentation") 150 self.predict_button.clicked.connect(self.on_predict) 151 self.model_selector_widget = self.load_model_widget() 152 self.settings = self._create_settings_widget() 153 154 # Add the widgets to the layout. 155 layout.addWidget(self.image_selector_widget) 156 layout.addWidget(self.model_selector_widget) 157 layout.addWidget(self.settings) 158 layout.addWidget(self.predict_button) 159 160 self.setLayout(layout) 161 162 @staticmethod 163 def _clear_layout(layout): 164 while layout.count(): 165 item = layout.takeAt(0) 166 widget = item.widget() 167 child_layout = item.layout() 168 if widget is not None: 169 widget.deleteLater() 170 elif child_layout is not None: 171 SegmentationWidget._clear_layout(child_layout) 172 child_layout.deleteLater() 173 174 def _update_tiling(self, model_type): 175 patch_shape = None if model_type == "- choose -" else get_model_training_patch_shape(model_type) 176 tiling = get_default_tiling(patch_shape=patch_shape) 177 for name, values in tiling.items(): 178 for axis, value in values.items(): 179 self.tiling[name][axis].setValue(value) 180 181 def _update_postprocessing_settings(self, model_type): 182 self._clear_layout(self.postprocessing_settings_layout) 183 self.postprocessing_parameter_widgets = {} 184 if model_type == "- choose -": 185 return 186 187 segmentation_function = get_segmentation_function(model_type) 188 parameter_specs = _POSTPROCESSING_PARAMETER_SPECS.get(segmentation_function, {}) 189 function_parameters = inspect.signature(segmentation_function).parameters 190 191 for name, spec in parameter_specs.items(): 192 if name not in function_parameters or function_parameters[name].default is inspect.Parameter.empty: 193 raise ValueError( 194 f"Configured post-processing parameter '{name}' is not an optional parameter " 195 f"of {segmentation_function.__name__}." 196 ) 197 default = spec.get("default", function_parameters[name].default) 198 tooltip = spec.get("tooltip") 199 if spec["type"] == "int": 200 parameter_widget, parameter_layout = self._add_int_param( 201 name, default, min_val=spec["min"], max_val=spec["max"], step=spec["step"], tooltip=tooltip 202 ) 203 self.postprocessing_settings_layout.addLayout(parameter_layout) 204 elif spec["type"] == "float": 205 parameter_widget, parameter_layout = self._add_float_param( 206 name, 207 default, 208 min_val=spec["min"], 209 max_val=spec["max"], 210 step=spec["step"], 211 decimals=spec["decimals"], 212 tooltip=tooltip, 213 ) 214 self.postprocessing_settings_layout.addLayout(parameter_layout) 215 elif spec["type"] == "bool": 216 parameter_widget = self._add_boolean_param(name, default, tooltip=tooltip) 217 self.postprocessing_settings_layout.addWidget(parameter_widget) 218 elif spec["type"] == "choice": 219 parameter_widget, parameter_layout = self._add_choice_param( 220 name, default, spec["options"], tooltip=tooltip 221 ) 222 self.postprocessing_settings_layout.addLayout(parameter_layout) 223 else: 224 raise ValueError(f"Unsupported post-processing parameter type: {spec['type']}") 225 self.postprocessing_parameter_widgets[name] = parameter_widget 226 227 def _get_postprocessing_kwargs(self): 228 kwargs = {} 229 for name, widget in self.postprocessing_parameter_widgets.items(): 230 if isinstance(widget, QCheckBox): 231 value = widget.isChecked() 232 elif isinstance(widget, QComboBox): 233 value = widget.currentText() 234 else: 235 value = widget.value() 236 kwargs[name] = value 237 return kwargs 238 239 def load_model_widget(self): 240 model_widget = QWidget() 241 title_label = QLabel("Select Model:") 242 243 # Exclude the models that are only offered through the CLI and not in the plugin. 244 model_list = set(_get_model_registry().urls.keys()) 245 # These are the models exlcuded due to their specificity and to keep the menu simple. 246 # TODO: we should at some point update the logic here, to make it easier to support further models 247 # without cluttering the UI. 248 excluded_models = ["vesicles_2d_maus"] 249 model_list = [name for name in model_list if name not in excluded_models] 250 251 models = ["- choose -"] + model_list 252 self.model_selector = QComboBox() 253 self.model_selector.addItems(models) 254 # Create a layout and add the title label and combo box 255 layout = QVBoxLayout() 256 layout.addWidget(title_label) 257 layout.addWidget(self.model_selector) 258 259 # Set layout on the model widget 260 model_widget.setLayout(layout) 261 return model_widget 262 263 def on_predict(self): 264 # Get the model and postprocessing settings. 265 model_type = self.model_selector.currentText() 266 custom_model_path = self.checkpoint_param.text() 267 if model_type == "- choose -": 268 show_info("INFO: Please choose a model.") 269 return 270 271 device = get_device(self.device_dropdown.currentText()) 272 273 # Get the image data. 274 image = self._get_layer_selector_data(self.image_selector_name) 275 if image is None: 276 show_info("INFO: Please choose an image.") 277 return 278 279 with self._computing( 280 self.predict_button, "Computing…", "Run Segmentation", "INFO: Running segmentation…" 281 ): 282 # Load the model. Override if user chose custom model. 283 rescale_input = True 284 if custom_model_path: 285 model = _load_custom_model(custom_model_path, device) 286 rescale_input = False 287 if model: 288 show_info(f"INFO: Using custom model from path: {custom_model_path}") 289 else: 290 show_info(f"ERROR: Failed to load custom model from path: {custom_model_path}") 291 return 292 else: 293 model = get_model(model_type, device) 294 295 # Get the current tiling. 296 tiling = _get_current_tiling(self.tiling, model_type) 297 298 # Get the voxel size. 299 metadata = self._get_layer_selector_data(self.image_selector_name, return_metadata=True) 300 voxel_size = self._handle_resolution(metadata, self.voxel_size_param, image.ndim, return_as_list=False) 301 302 # Determine the scaling based on the voxel size. 303 scale = None 304 if voxel_size and rescale_input: 305 # Calculate scale so voxel_size is the same as in training. 306 scale = compute_scale_from_voxel_size(voxel_size, model_type) 307 scale_info = list(map(lambda x: np.round(x, 2), scale)) 308 show_info(f"INFO: Rescaled the image by {scale_info} to optimize for the selected model.") 309 310 # Some models require an additional segmentation for inference or postprocessing. 311 # For these models we read out the 'Extra Segmentation' widget. 312 if model_type == "ribbon": # Currently only the ribbon model needs the extra seg. 313 extra_seg = self._get_layer_selector_data(self.extra_seg_selector_name) 314 resolution = tuple(voxel_size[ax] for ax in "zyx") 315 kwargs = {"extra_segmentation": extra_seg, "resolution": resolution} 316 elif "cristae" in model_type: # Cristae model expects 2 3D volumes 317 kwargs = { 318 "extra_segmentation": self._get_layer_selector_data(self.extra_seg_selector_name), 319 "with_channels": True, 320 "channels_to_standardize": [0] 321 } 322 else: 323 kwargs = {} 324 kwargs.update(self._get_postprocessing_kwargs()) 325 segmentation = run_segmentation( 326 image, model=model, model_type=model_type, tiling=tiling, scale=scale, **kwargs 327 ) 328 329 # Add the segmentation layer(s). 330 if isinstance(segmentation, dict): 331 for name, seg in segmentation.items(): 332 self.viewer.add_labels(seg, name=name, metadata=metadata) 333 else: 334 self.viewer.add_labels(segmentation, name=f"{model_type}", metadata=metadata) 335 336 show_info(f"INFO: Segmentation of {model_type} added to layers.") 337 338 def _create_settings_widget(self): 339 setting_values = QWidget() 340 # setting_values.setToolTip(get_tooltip("embedding", "settings")) 341 setting_values.setLayout(QVBoxLayout()) 342 343 # Create UI for the device. 344 device = "auto" 345 device_options = ["auto"] + _available_devices() 346 347 self.device_dropdown, layout = self._add_choice_param("device", device, device_options) 348 setting_values.layout().addLayout(layout) 349 350 # Create UI for the tile shape. The values are set to the default tiling of the selected model. 351 self.tiling = {"tile": {}, "halo": {}} 352 self.tiling["tile"]["x"], self.tiling["tile"]["y"], self.tiling["tile"]["z"], layout = self._add_shape_param( 353 ("tile_x", "tile_y", "tile_z"), (0, 0, 0), min_val=0, max_val=2048, step=16, 354 # tooltip=get_tooltip("embedding", "tiling") 355 ) 356 setting_values.layout().addLayout(layout) 357 358 # Create UI for the halo. 359 self.tiling["halo"]["x"], self.tiling["halo"]["y"], self.tiling["halo"]["z"], layout = self._add_shape_param( 360 ("halo_x", "halo_y", "halo_z"), (0, 0, 0), min_val=0, max_val=512, 361 # tooltip=get_tooltip("embedding", "halo") 362 ) 363 setting_values.layout().addLayout(layout) 364 self.model_selector.currentTextChanged.connect(self._update_tiling) 365 self._update_tiling(self.model_selector.currentText()) 366 367 # Read voxel size from layer metadata. 368 self.voxel_size_param, layout = self._add_float_param( 369 "voxel_size", 0.0, min_val=0.0, max_val=100.0, 370 ) 371 setting_values.layout().addLayout(layout) 372 373 self.checkpoint_param, layout = self._add_string_param( 374 name="checkpoint", value="", title="Load Custom Model", 375 placeholder="path/to/checkpoint.pt", 376 ) 377 setting_values.layout().addLayout(layout) 378 379 # Add selection UI for additional segmentation, which some models require for inference or postproc. 380 self.extra_seg_selector_name = "Extra Segmentation" 381 self.extra_selector_widget = self._create_layer_selector(self.extra_seg_selector_name, layer_type="Labels") 382 setting_values.layout().addWidget(self.extra_selector_widget) 383 384 # Add model-specific post-processing settings that are updated when the selected model changes. 385 setting_values.layout().addWidget(QLabel("Post-processing:")) 386 self.postprocessing_settings_widget = QWidget() 387 self.postprocessing_settings_layout = QVBoxLayout() 388 self.postprocessing_settings_layout.setContentsMargins(0, 0, 0, 0) 389 self.postprocessing_settings_widget.setLayout(self.postprocessing_settings_layout) 390 setting_values.layout().addWidget(self.postprocessing_settings_widget) 391 self.postprocessing_parameter_widgets = {} 392 self.model_selector.currentTextChanged.connect(self._update_postprocessing_settings) 393 self._update_postprocessing_settings(self.model_selector.currentText()) 394 395 settings = self._make_collapsible(widget=setting_values, title="Advanced Settings") 396 return settings
QWidget(parent: Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())
def
load_model_widget(self):
239 def load_model_widget(self): 240 model_widget = QWidget() 241 title_label = QLabel("Select Model:") 242 243 # Exclude the models that are only offered through the CLI and not in the plugin. 244 model_list = set(_get_model_registry().urls.keys()) 245 # These are the models exlcuded due to their specificity and to keep the menu simple. 246 # TODO: we should at some point update the logic here, to make it easier to support further models 247 # without cluttering the UI. 248 excluded_models = ["vesicles_2d_maus"] 249 model_list = [name for name in model_list if name not in excluded_models] 250 251 models = ["- choose -"] + model_list 252 self.model_selector = QComboBox() 253 self.model_selector.addItems(models) 254 # Create a layout and add the title label and combo box 255 layout = QVBoxLayout() 256 layout.addWidget(title_label) 257 layout.addWidget(self.model_selector) 258 259 # Set layout on the model widget 260 model_widget.setLayout(layout) 261 return model_widget
def
on_predict(self):
263 def on_predict(self): 264 # Get the model and postprocessing settings. 265 model_type = self.model_selector.currentText() 266 custom_model_path = self.checkpoint_param.text() 267 if model_type == "- choose -": 268 show_info("INFO: Please choose a model.") 269 return 270 271 device = get_device(self.device_dropdown.currentText()) 272 273 # Get the image data. 274 image = self._get_layer_selector_data(self.image_selector_name) 275 if image is None: 276 show_info("INFO: Please choose an image.") 277 return 278 279 with self._computing( 280 self.predict_button, "Computing…", "Run Segmentation", "INFO: Running segmentation…" 281 ): 282 # Load the model. Override if user chose custom model. 283 rescale_input = True 284 if custom_model_path: 285 model = _load_custom_model(custom_model_path, device) 286 rescale_input = False 287 if model: 288 show_info(f"INFO: Using custom model from path: {custom_model_path}") 289 else: 290 show_info(f"ERROR: Failed to load custom model from path: {custom_model_path}") 291 return 292 else: 293 model = get_model(model_type, device) 294 295 # Get the current tiling. 296 tiling = _get_current_tiling(self.tiling, model_type) 297 298 # Get the voxel size. 299 metadata = self._get_layer_selector_data(self.image_selector_name, return_metadata=True) 300 voxel_size = self._handle_resolution(metadata, self.voxel_size_param, image.ndim, return_as_list=False) 301 302 # Determine the scaling based on the voxel size. 303 scale = None 304 if voxel_size and rescale_input: 305 # Calculate scale so voxel_size is the same as in training. 306 scale = compute_scale_from_voxel_size(voxel_size, model_type) 307 scale_info = list(map(lambda x: np.round(x, 2), scale)) 308 show_info(f"INFO: Rescaled the image by {scale_info} to optimize for the selected model.") 309 310 # Some models require an additional segmentation for inference or postprocessing. 311 # For these models we read out the 'Extra Segmentation' widget. 312 if model_type == "ribbon": # Currently only the ribbon model needs the extra seg. 313 extra_seg = self._get_layer_selector_data(self.extra_seg_selector_name) 314 resolution = tuple(voxel_size[ax] for ax in "zyx") 315 kwargs = {"extra_segmentation": extra_seg, "resolution": resolution} 316 elif "cristae" in model_type: # Cristae model expects 2 3D volumes 317 kwargs = { 318 "extra_segmentation": self._get_layer_selector_data(self.extra_seg_selector_name), 319 "with_channels": True, 320 "channels_to_standardize": [0] 321 } 322 else: 323 kwargs = {} 324 kwargs.update(self._get_postprocessing_kwargs()) 325 segmentation = run_segmentation( 326 image, model=model, model_type=model_type, tiling=tiling, scale=scale, **kwargs 327 ) 328 329 # Add the segmentation layer(s). 330 if isinstance(segmentation, dict): 331 for name, seg in segmentation.items(): 332 self.viewer.add_labels(seg, name=name, metadata=metadata) 333 else: 334 self.viewer.add_labels(segmentation, name=f"{model_type}", metadata=metadata) 335 336 show_info(f"INFO: Segmentation of {model_type} added to layers.")