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 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: dict, default_tiling: dict, model_type: str):
104    # get tiling values from qt objects
105    for k, v in tiling.items():
106        for k2, v2 in v.items():
107            if isinstance(v2, int):
108                continue
109            elif hasattr(v2, "value"):  # If it's a QSpinBox, extract the value
110                tiling[k][k2] = v2.value()
111            else:
112                raise TypeError(f"Unexpected type for tiling value: {type(v2)} at {k}/{k2}")
113    # check if user inputs tiling/halo or not
114    if default_tiling == tiling:
115        if "2d" in model_type:
116            # if its a 2d model expand x,y and set z to 1
117            tiling = {
118                "tile": {"x": 512, "y": 512, "z": 1},
119                "halo": {"x": 64, "y": 64, "z": 1},
120            }
121    else:
122        show_info(f"Using custom tiling: {tiling}")
123    if "2d" in model_type:
124        # if its a 2d model set z to 1
125        tiling["tile"]["z"] = 1
126        tiling["halo"]["z"] = 0
127        show_info(f"Using tiling: {tiling}")
128    return tiling
129
130
131def _clean_filepath(filepath):
132    """Cleans a given filepath by:
133    - Removing newline characters (\n)
134    - Removing escape sequences
135    - Stripping the 'file://' prefix if present
136
137    Args:
138        filepath (str): The original filepath
139
140    Returns:
141        str: The cleaned filepath
142    """
143    # Remove 'file://' prefix if present
144    if filepath.startswith("file://"):
145        filepath = filepath[7:]
146
147    # Remove escape sequences and newlines
148    filepath = re.sub(r'\\.', '', filepath)
149    filepath = filepath.replace('\n', '').replace('\r', '')
150
151    return filepath
152
153
154class SegmentationWidget(BaseWidget):
155    def __init__(self):
156        super().__init__()
157
158        self.viewer = napari.current_viewer()
159        layout = QVBoxLayout()
160        self.tiling = {}
161
162        # Create the image selection dropdown.
163        self.image_selector_name = "Image data"
164        self.image_selector_widget = self._create_layer_selector(self.image_selector_name, layer_type="Image")
165
166        # Create buttons and widgets.
167        self.predict_button = QPushButton("Run Segmentation")
168        self.predict_button.clicked.connect(self.on_predict)
169        self.model_selector_widget = self.load_model_widget()
170        self.settings = self._create_settings_widget()
171
172        # Add the widgets to the layout.
173        layout.addWidget(self.image_selector_widget)
174        layout.addWidget(self.model_selector_widget)
175        layout.addWidget(self.settings)
176        layout.addWidget(self.predict_button)
177
178        self.setLayout(layout)
179
180    @staticmethod
181    def _clear_layout(layout):
182        while layout.count():
183            item = layout.takeAt(0)
184            widget = item.widget()
185            child_layout = item.layout()
186            if widget is not None:
187                widget.deleteLater()
188            elif child_layout is not None:
189                SegmentationWidget._clear_layout(child_layout)
190                child_layout.deleteLater()
191
192    def _update_postprocessing_settings(self, model_type):
193        self._clear_layout(self.postprocessing_settings_layout)
194        self.postprocessing_parameter_widgets = {}
195        if model_type == "- choose -":
196            return
197
198        segmentation_function = get_segmentation_function(model_type)
199        parameter_specs = _POSTPROCESSING_PARAMETER_SPECS.get(segmentation_function, {})
200        function_parameters = inspect.signature(segmentation_function).parameters
201
202        for name, spec in parameter_specs.items():
203            if name not in function_parameters or function_parameters[name].default is inspect.Parameter.empty:
204                raise ValueError(
205                    f"Configured post-processing parameter '{name}' is not an optional parameter "
206                    f"of {segmentation_function.__name__}."
207                )
208            default = spec.get("default", function_parameters[name].default)
209            tooltip = spec.get("tooltip")
210            if spec["type"] == "int":
211                parameter_widget, parameter_layout = self._add_int_param(
212                    name, default, min_val=spec["min"], max_val=spec["max"], step=spec["step"], tooltip=tooltip
213                )
214                self.postprocessing_settings_layout.addLayout(parameter_layout)
215            elif spec["type"] == "float":
216                parameter_widget, parameter_layout = self._add_float_param(
217                    name,
218                    default,
219                    min_val=spec["min"],
220                    max_val=spec["max"],
221                    step=spec["step"],
222                    decimals=spec["decimals"],
223                    tooltip=tooltip,
224                )
225                self.postprocessing_settings_layout.addLayout(parameter_layout)
226            elif spec["type"] == "bool":
227                parameter_widget = self._add_boolean_param(name, default, tooltip=tooltip)
228                self.postprocessing_settings_layout.addWidget(parameter_widget)
229            elif spec["type"] == "choice":
230                parameter_widget, parameter_layout = self._add_choice_param(
231                    name, default, spec["options"], tooltip=tooltip
232                )
233                self.postprocessing_settings_layout.addLayout(parameter_layout)
234            else:
235                raise ValueError(f"Unsupported post-processing parameter type: {spec['type']}")
236            self.postprocessing_parameter_widgets[name] = parameter_widget
237
238    def _get_postprocessing_kwargs(self):
239        kwargs = {}
240        for name, widget in self.postprocessing_parameter_widgets.items():
241            if isinstance(widget, QCheckBox):
242                value = widget.isChecked()
243            elif isinstance(widget, QComboBox):
244                value = widget.currentText()
245            else:
246                value = widget.value()
247            kwargs[name] = value
248        return kwargs
249
250    def load_model_widget(self):
251        model_widget = QWidget()
252        title_label = QLabel("Select Model:")
253
254        # Exclude the models that are only offered through the CLI and not in the plugin.
255        model_list = set(_get_model_registry().urls.keys())
256        # These are the models exlcuded due to their specificity and to keep the menu simple.
257        # TODO: we should at some point update the logic here, to make it easier to support further models
258        # without cluttering the UI.
259        excluded_models = ["vesicles_2d_maus"]
260        model_list = [name for name in model_list if name not in excluded_models]
261
262        models = ["- choose -"] + model_list
263        self.model_selector = QComboBox()
264        self.model_selector.addItems(models)
265        # Create a layout and add the title label and combo box
266        layout = QVBoxLayout()
267        layout.addWidget(title_label)
268        layout.addWidget(self.model_selector)
269
270        # Set layout on the model widget
271        model_widget.setLayout(layout)
272        return model_widget
273
274    def on_predict(self):
275        # Get the model and postprocessing settings.
276        model_type = self.model_selector.currentText()
277        custom_model_path = self.checkpoint_param.text()
278        if model_type == "- choose -":
279            show_info("INFO: Please choose a model.")
280            return
281
282        device = get_device(self.device_dropdown.currentText())
283
284        # Get the image data.
285        image = self._get_layer_selector_data(self.image_selector_name)
286        if image is None:
287            show_info("INFO: Please choose an image.")
288            return
289
290        with self._computing(
291            self.predict_button, "Computing…", "Run Segmentation", "INFO: Running segmentation…"
292        ):
293            # Load the model. Override if user chose custom model.
294            rescale_input = True
295            if custom_model_path:
296                model = _load_custom_model(custom_model_path, device)
297                rescale_input = False
298                if model:
299                    show_info(f"INFO: Using custom model from path: {custom_model_path}")
300                else:
301                    show_info(f"ERROR: Failed to load custom model from path: {custom_model_path}")
302                    return
303            else:
304                model = get_model(model_type, device)
305
306            # Get the current tiling.
307            self.tiling = _get_current_tiling(self.tiling, self.default_tiling, model_type)
308
309            # Get the voxel size.
310            metadata = self._get_layer_selector_data(self.image_selector_name, return_metadata=True)
311            voxel_size = self._handle_resolution(metadata, self.voxel_size_param, image.ndim, return_as_list=False)
312
313            # Determine the scaling based on the voxel size.
314            scale = None
315            if voxel_size and rescale_input:
316                # Calculate scale so voxel_size is the same as in training.
317                scale = compute_scale_from_voxel_size(voxel_size, model_type)
318                scale_info = list(map(lambda x: np.round(x, 2), scale))
319                show_info(f"INFO: Rescaled the image by {scale_info} to optimize for the selected model.")
320
321            # Some models require an additional segmentation for inference or postprocessing.
322            # For these models we read out the 'Extra Segmentation' widget.
323            if model_type == "ribbon":  # Currently only the ribbon model needs the extra seg.
324                extra_seg = self._get_layer_selector_data(self.extra_seg_selector_name)
325                resolution = tuple(voxel_size[ax] for ax in "zyx")
326                kwargs = {"extra_segmentation": extra_seg, "resolution": resolution}
327            elif "cristae" in model_type:  # Cristae model expects 2 3D volumes
328                kwargs = {
329                    "extra_segmentation": self._get_layer_selector_data(self.extra_seg_selector_name),
330                    "with_channels": True,
331                    "channels_to_standardize": [0]
332                }
333            else:
334                kwargs = {}
335            kwargs.update(self._get_postprocessing_kwargs())
336            segmentation = run_segmentation(
337                image, model=model, model_type=model_type, tiling=self.tiling, scale=scale, **kwargs
338            )
339
340            # Add the segmentation layer(s).
341            if isinstance(segmentation, dict):
342                for name, seg in segmentation.items():
343                    self.viewer.add_labels(seg, name=name, metadata=metadata)
344            else:
345                self.viewer.add_labels(segmentation, name=f"{model_type}", metadata=metadata)
346
347        show_info(f"INFO: Segmentation of {model_type} added to layers.")
348
349    def _create_settings_widget(self):
350        setting_values = QWidget()
351        # setting_values.setToolTip(get_tooltip("embedding", "settings"))
352        setting_values.setLayout(QVBoxLayout())
353
354        # Create UI for the device.
355        device = "auto"
356        device_options = ["auto"] + _available_devices()
357
358        self.device_dropdown, layout = self._add_choice_param("device", device, device_options)
359        setting_values.layout().addLayout(layout)
360
361        # Create UI for the tile shape.
362        self.default_tiling = get_default_tiling()
363        self.tiling = copy.deepcopy(self.default_tiling)
364        self.tiling["tile"]["x"], self.tiling["tile"]["y"], self.tiling["tile"]["z"], layout = self._add_shape_param(
365            ("tile_x", "tile_y", "tile_z"),
366            (self.default_tiling["tile"]["x"], self.default_tiling["tile"]["y"], self.default_tiling["tile"]["z"]),
367            min_val=0, max_val=2048, step=16,
368            # tooltip=get_tooltip("embedding", "tiling")
369        )
370        setting_values.layout().addLayout(layout)
371
372        # Create UI for the halo.
373        self.tiling["halo"]["x"], self.tiling["halo"]["y"], self.tiling["halo"]["z"], layout = self._add_shape_param(
374            ("halo_x", "halo_y", "halo_z"),
375            (self.default_tiling["halo"]["x"], self.default_tiling["halo"]["y"], self.default_tiling["halo"]["z"]),
376            min_val=0, max_val=512,
377            # tooltip=get_tooltip("embedding", "halo")
378        )
379        setting_values.layout().addLayout(layout)
380
381        # Read voxel size from layer metadata.
382        self.voxel_size_param, layout = self._add_float_param(
383            "voxel_size", 0.0, min_val=0.0, max_val=100.0,
384        )
385        setting_values.layout().addLayout(layout)
386
387        self.checkpoint_param, layout = self._add_string_param(
388            name="checkpoint", value="", title="Load Custom Model",
389            placeholder="path/to/checkpoint.pt",
390        )
391        setting_values.layout().addLayout(layout)
392
393        # Add selection UI for additional segmentation, which some models require for inference or postproc.
394        self.extra_seg_selector_name = "Extra Segmentation"
395        self.extra_selector_widget = self._create_layer_selector(self.extra_seg_selector_name, layer_type="Labels")
396        setting_values.layout().addWidget(self.extra_selector_widget)
397
398        # Add model-specific post-processing settings that are updated when the selected model changes.
399        setting_values.layout().addWidget(QLabel("Post-processing:"))
400        self.postprocessing_settings_widget = QWidget()
401        self.postprocessing_settings_layout = QVBoxLayout()
402        self.postprocessing_settings_layout.setContentsMargins(0, 0, 0, 0)
403        self.postprocessing_settings_widget.setLayout(self.postprocessing_settings_layout)
404        setting_values.layout().addWidget(self.postprocessing_settings_widget)
405        self.postprocessing_parameter_widgets = {}
406        self.model_selector.currentTextChanged.connect(self._update_postprocessing_settings)
407        self._update_postprocessing_settings(self.model_selector.currentText())
408
409        settings = self._make_collapsible(widget=setting_values, title="Advanced Settings")
410        return settings
class SegmentationWidget(synapse_net.tools.base_widget.BaseWidget):
155class SegmentationWidget(BaseWidget):
156    def __init__(self):
157        super().__init__()
158
159        self.viewer = napari.current_viewer()
160        layout = QVBoxLayout()
161        self.tiling = {}
162
163        # Create the image selection dropdown.
164        self.image_selector_name = "Image data"
165        self.image_selector_widget = self._create_layer_selector(self.image_selector_name, layer_type="Image")
166
167        # Create buttons and widgets.
168        self.predict_button = QPushButton("Run Segmentation")
169        self.predict_button.clicked.connect(self.on_predict)
170        self.model_selector_widget = self.load_model_widget()
171        self.settings = self._create_settings_widget()
172
173        # Add the widgets to the layout.
174        layout.addWidget(self.image_selector_widget)
175        layout.addWidget(self.model_selector_widget)
176        layout.addWidget(self.settings)
177        layout.addWidget(self.predict_button)
178
179        self.setLayout(layout)
180
181    @staticmethod
182    def _clear_layout(layout):
183        while layout.count():
184            item = layout.takeAt(0)
185            widget = item.widget()
186            child_layout = item.layout()
187            if widget is not None:
188                widget.deleteLater()
189            elif child_layout is not None:
190                SegmentationWidget._clear_layout(child_layout)
191                child_layout.deleteLater()
192
193    def _update_postprocessing_settings(self, model_type):
194        self._clear_layout(self.postprocessing_settings_layout)
195        self.postprocessing_parameter_widgets = {}
196        if model_type == "- choose -":
197            return
198
199        segmentation_function = get_segmentation_function(model_type)
200        parameter_specs = _POSTPROCESSING_PARAMETER_SPECS.get(segmentation_function, {})
201        function_parameters = inspect.signature(segmentation_function).parameters
202
203        for name, spec in parameter_specs.items():
204            if name not in function_parameters or function_parameters[name].default is inspect.Parameter.empty:
205                raise ValueError(
206                    f"Configured post-processing parameter '{name}' is not an optional parameter "
207                    f"of {segmentation_function.__name__}."
208                )
209            default = spec.get("default", function_parameters[name].default)
210            tooltip = spec.get("tooltip")
211            if spec["type"] == "int":
212                parameter_widget, parameter_layout = self._add_int_param(
213                    name, default, min_val=spec["min"], max_val=spec["max"], step=spec["step"], tooltip=tooltip
214                )
215                self.postprocessing_settings_layout.addLayout(parameter_layout)
216            elif spec["type"] == "float":
217                parameter_widget, parameter_layout = self._add_float_param(
218                    name,
219                    default,
220                    min_val=spec["min"],
221                    max_val=spec["max"],
222                    step=spec["step"],
223                    decimals=spec["decimals"],
224                    tooltip=tooltip,
225                )
226                self.postprocessing_settings_layout.addLayout(parameter_layout)
227            elif spec["type"] == "bool":
228                parameter_widget = self._add_boolean_param(name, default, tooltip=tooltip)
229                self.postprocessing_settings_layout.addWidget(parameter_widget)
230            elif spec["type"] == "choice":
231                parameter_widget, parameter_layout = self._add_choice_param(
232                    name, default, spec["options"], tooltip=tooltip
233                )
234                self.postprocessing_settings_layout.addLayout(parameter_layout)
235            else:
236                raise ValueError(f"Unsupported post-processing parameter type: {spec['type']}")
237            self.postprocessing_parameter_widgets[name] = parameter_widget
238
239    def _get_postprocessing_kwargs(self):
240        kwargs = {}
241        for name, widget in self.postprocessing_parameter_widgets.items():
242            if isinstance(widget, QCheckBox):
243                value = widget.isChecked()
244            elif isinstance(widget, QComboBox):
245                value = widget.currentText()
246            else:
247                value = widget.value()
248            kwargs[name] = value
249        return kwargs
250
251    def load_model_widget(self):
252        model_widget = QWidget()
253        title_label = QLabel("Select Model:")
254
255        # Exclude the models that are only offered through the CLI and not in the plugin.
256        model_list = set(_get_model_registry().urls.keys())
257        # These are the models exlcuded due to their specificity and to keep the menu simple.
258        # TODO: we should at some point update the logic here, to make it easier to support further models
259        # without cluttering the UI.
260        excluded_models = ["vesicles_2d_maus"]
261        model_list = [name for name in model_list if name not in excluded_models]
262
263        models = ["- choose -"] + model_list
264        self.model_selector = QComboBox()
265        self.model_selector.addItems(models)
266        # Create a layout and add the title label and combo box
267        layout = QVBoxLayout()
268        layout.addWidget(title_label)
269        layout.addWidget(self.model_selector)
270
271        # Set layout on the model widget
272        model_widget.setLayout(layout)
273        return model_widget
274
275    def on_predict(self):
276        # Get the model and postprocessing settings.
277        model_type = self.model_selector.currentText()
278        custom_model_path = self.checkpoint_param.text()
279        if model_type == "- choose -":
280            show_info("INFO: Please choose a model.")
281            return
282
283        device = get_device(self.device_dropdown.currentText())
284
285        # Get the image data.
286        image = self._get_layer_selector_data(self.image_selector_name)
287        if image is None:
288            show_info("INFO: Please choose an image.")
289            return
290
291        with self._computing(
292            self.predict_button, "Computing…", "Run Segmentation", "INFO: Running segmentation…"
293        ):
294            # Load the model. Override if user chose custom model.
295            rescale_input = True
296            if custom_model_path:
297                model = _load_custom_model(custom_model_path, device)
298                rescale_input = False
299                if model:
300                    show_info(f"INFO: Using custom model from path: {custom_model_path}")
301                else:
302                    show_info(f"ERROR: Failed to load custom model from path: {custom_model_path}")
303                    return
304            else:
305                model = get_model(model_type, device)
306
307            # Get the current tiling.
308            self.tiling = _get_current_tiling(self.tiling, self.default_tiling, model_type)
309
310            # Get the voxel size.
311            metadata = self._get_layer_selector_data(self.image_selector_name, return_metadata=True)
312            voxel_size = self._handle_resolution(metadata, self.voxel_size_param, image.ndim, return_as_list=False)
313
314            # Determine the scaling based on the voxel size.
315            scale = None
316            if voxel_size and rescale_input:
317                # Calculate scale so voxel_size is the same as in training.
318                scale = compute_scale_from_voxel_size(voxel_size, model_type)
319                scale_info = list(map(lambda x: np.round(x, 2), scale))
320                show_info(f"INFO: Rescaled the image by {scale_info} to optimize for the selected model.")
321
322            # Some models require an additional segmentation for inference or postprocessing.
323            # For these models we read out the 'Extra Segmentation' widget.
324            if model_type == "ribbon":  # Currently only the ribbon model needs the extra seg.
325                extra_seg = self._get_layer_selector_data(self.extra_seg_selector_name)
326                resolution = tuple(voxel_size[ax] for ax in "zyx")
327                kwargs = {"extra_segmentation": extra_seg, "resolution": resolution}
328            elif "cristae" in model_type:  # Cristae model expects 2 3D volumes
329                kwargs = {
330                    "extra_segmentation": self._get_layer_selector_data(self.extra_seg_selector_name),
331                    "with_channels": True,
332                    "channels_to_standardize": [0]
333                }
334            else:
335                kwargs = {}
336            kwargs.update(self._get_postprocessing_kwargs())
337            segmentation = run_segmentation(
338                image, model=model, model_type=model_type, tiling=self.tiling, scale=scale, **kwargs
339            )
340
341            # Add the segmentation layer(s).
342            if isinstance(segmentation, dict):
343                for name, seg in segmentation.items():
344                    self.viewer.add_labels(seg, name=name, metadata=metadata)
345            else:
346                self.viewer.add_labels(segmentation, name=f"{model_type}", metadata=metadata)
347
348        show_info(f"INFO: Segmentation of {model_type} added to layers.")
349
350    def _create_settings_widget(self):
351        setting_values = QWidget()
352        # setting_values.setToolTip(get_tooltip("embedding", "settings"))
353        setting_values.setLayout(QVBoxLayout())
354
355        # Create UI for the device.
356        device = "auto"
357        device_options = ["auto"] + _available_devices()
358
359        self.device_dropdown, layout = self._add_choice_param("device", device, device_options)
360        setting_values.layout().addLayout(layout)
361
362        # Create UI for the tile shape.
363        self.default_tiling = get_default_tiling()
364        self.tiling = copy.deepcopy(self.default_tiling)
365        self.tiling["tile"]["x"], self.tiling["tile"]["y"], self.tiling["tile"]["z"], layout = self._add_shape_param(
366            ("tile_x", "tile_y", "tile_z"),
367            (self.default_tiling["tile"]["x"], self.default_tiling["tile"]["y"], self.default_tiling["tile"]["z"]),
368            min_val=0, max_val=2048, step=16,
369            # tooltip=get_tooltip("embedding", "tiling")
370        )
371        setting_values.layout().addLayout(layout)
372
373        # Create UI for the halo.
374        self.tiling["halo"]["x"], self.tiling["halo"]["y"], self.tiling["halo"]["z"], layout = self._add_shape_param(
375            ("halo_x", "halo_y", "halo_z"),
376            (self.default_tiling["halo"]["x"], self.default_tiling["halo"]["y"], self.default_tiling["halo"]["z"]),
377            min_val=0, max_val=512,
378            # tooltip=get_tooltip("embedding", "halo")
379        )
380        setting_values.layout().addLayout(layout)
381
382        # Read voxel size from layer metadata.
383        self.voxel_size_param, layout = self._add_float_param(
384            "voxel_size", 0.0, min_val=0.0, max_val=100.0,
385        )
386        setting_values.layout().addLayout(layout)
387
388        self.checkpoint_param, layout = self._add_string_param(
389            name="checkpoint", value="", title="Load Custom Model",
390            placeholder="path/to/checkpoint.pt",
391        )
392        setting_values.layout().addLayout(layout)
393
394        # Add selection UI for additional segmentation, which some models require for inference or postproc.
395        self.extra_seg_selector_name = "Extra Segmentation"
396        self.extra_selector_widget = self._create_layer_selector(self.extra_seg_selector_name, layer_type="Labels")
397        setting_values.layout().addWidget(self.extra_selector_widget)
398
399        # Add model-specific post-processing settings that are updated when the selected model changes.
400        setting_values.layout().addWidget(QLabel("Post-processing:"))
401        self.postprocessing_settings_widget = QWidget()
402        self.postprocessing_settings_layout = QVBoxLayout()
403        self.postprocessing_settings_layout.setContentsMargins(0, 0, 0, 0)
404        self.postprocessing_settings_widget.setLayout(self.postprocessing_settings_layout)
405        setting_values.layout().addWidget(self.postprocessing_settings_widget)
406        self.postprocessing_parameter_widgets = {}
407        self.model_selector.currentTextChanged.connect(self._update_postprocessing_settings)
408        self._update_postprocessing_settings(self.model_selector.currentText())
409
410        settings = self._make_collapsible(widget=setting_values, title="Advanced Settings")
411        return settings

QWidget(parent: Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

viewer
tiling
image_selector_name
image_selector_widget
predict_button
model_selector_widget
settings
def load_model_widget(self):
251    def load_model_widget(self):
252        model_widget = QWidget()
253        title_label = QLabel("Select Model:")
254
255        # Exclude the models that are only offered through the CLI and not in the plugin.
256        model_list = set(_get_model_registry().urls.keys())
257        # These are the models exlcuded due to their specificity and to keep the menu simple.
258        # TODO: we should at some point update the logic here, to make it easier to support further models
259        # without cluttering the UI.
260        excluded_models = ["vesicles_2d_maus"]
261        model_list = [name for name in model_list if name not in excluded_models]
262
263        models = ["- choose -"] + model_list
264        self.model_selector = QComboBox()
265        self.model_selector.addItems(models)
266        # Create a layout and add the title label and combo box
267        layout = QVBoxLayout()
268        layout.addWidget(title_label)
269        layout.addWidget(self.model_selector)
270
271        # Set layout on the model widget
272        model_widget.setLayout(layout)
273        return model_widget
def on_predict(self):
275    def on_predict(self):
276        # Get the model and postprocessing settings.
277        model_type = self.model_selector.currentText()
278        custom_model_path = self.checkpoint_param.text()
279        if model_type == "- choose -":
280            show_info("INFO: Please choose a model.")
281            return
282
283        device = get_device(self.device_dropdown.currentText())
284
285        # Get the image data.
286        image = self._get_layer_selector_data(self.image_selector_name)
287        if image is None:
288            show_info("INFO: Please choose an image.")
289            return
290
291        with self._computing(
292            self.predict_button, "Computing…", "Run Segmentation", "INFO: Running segmentation…"
293        ):
294            # Load the model. Override if user chose custom model.
295            rescale_input = True
296            if custom_model_path:
297                model = _load_custom_model(custom_model_path, device)
298                rescale_input = False
299                if model:
300                    show_info(f"INFO: Using custom model from path: {custom_model_path}")
301                else:
302                    show_info(f"ERROR: Failed to load custom model from path: {custom_model_path}")
303                    return
304            else:
305                model = get_model(model_type, device)
306
307            # Get the current tiling.
308            self.tiling = _get_current_tiling(self.tiling, self.default_tiling, model_type)
309
310            # Get the voxel size.
311            metadata = self._get_layer_selector_data(self.image_selector_name, return_metadata=True)
312            voxel_size = self._handle_resolution(metadata, self.voxel_size_param, image.ndim, return_as_list=False)
313
314            # Determine the scaling based on the voxel size.
315            scale = None
316            if voxel_size and rescale_input:
317                # Calculate scale so voxel_size is the same as in training.
318                scale = compute_scale_from_voxel_size(voxel_size, model_type)
319                scale_info = list(map(lambda x: np.round(x, 2), scale))
320                show_info(f"INFO: Rescaled the image by {scale_info} to optimize for the selected model.")
321
322            # Some models require an additional segmentation for inference or postprocessing.
323            # For these models we read out the 'Extra Segmentation' widget.
324            if model_type == "ribbon":  # Currently only the ribbon model needs the extra seg.
325                extra_seg = self._get_layer_selector_data(self.extra_seg_selector_name)
326                resolution = tuple(voxel_size[ax] for ax in "zyx")
327                kwargs = {"extra_segmentation": extra_seg, "resolution": resolution}
328            elif "cristae" in model_type:  # Cristae model expects 2 3D volumes
329                kwargs = {
330                    "extra_segmentation": self._get_layer_selector_data(self.extra_seg_selector_name),
331                    "with_channels": True,
332                    "channels_to_standardize": [0]
333                }
334            else:
335                kwargs = {}
336            kwargs.update(self._get_postprocessing_kwargs())
337            segmentation = run_segmentation(
338                image, model=model, model_type=model_type, tiling=self.tiling, scale=scale, **kwargs
339            )
340
341            # Add the segmentation layer(s).
342            if isinstance(segmentation, dict):
343                for name, seg in segmentation.items():
344                    self.viewer.add_labels(seg, name=name, metadata=metadata)
345            else:
346                self.viewer.add_labels(segmentation, name=f"{model_type}", metadata=metadata)
347
348        show_info(f"INFO: Segmentation of {model_type} added to layers.")