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