micro_sam.sam_annotator.object_classifier
1import os 2from joblib import dump 3from multiprocessing import cpu_count 4from pathlib import Path 5from typing import List, Optional, Tuple, Union 6 7import imageio.v3 as imageio 8import napari 9import numpy as np 10import torch 11 12from magicgui import magic_factory, magicgui 13from magicgui.widgets import Widget, Container, FunctionGui, create_widget 14from qtpy import QtWidgets 15 16from skimage.measure import regionprops_table 17from sklearn.ensemble import RandomForestClassifier 18 19from .. import util 20from ..object_classification import compute_object_features, project_prediction_to_segmentation 21from ._state import AnnotatorState 22from . import _widgets as widgets 23from .util import _sync_embedding_widget 24 25# 26# Utility functionality. 27# Some of this could be refactored to general purpose functionality that can also 28# be used for inference with the trained classifier. 29# 30 31 32def _accumulate_labels(segmentation, annotations): 33 34 def majority_label(mask, annotation): 35 ids, counts = np.unique(annotation[mask], return_counts=True) 36 if len(ids) == 1 and ids[0] == 0: 37 return 0 38 if ids[0] == 0: 39 ids, counts = ids[1:], counts[1:] 40 return ids[np.argmax(counts)] 41 42 all_features = regionprops_table( 43 segmentation, intensity_image=annotations, properties=("label",), 44 extra_properties=[majority_label], 45 ) 46 return all_features["majority_label"].astype("int") 47 48 49def _train_rf(features, labels, previous_features=None, previous_labels=None, **rf_kwargs): 50 assert len(features) == len(labels) 51 valid = labels != 0 52 X, y = features[valid], labels[valid] 53 54 if previous_features is not None: 55 assert previous_labels is not None and len(previous_features) == len(previous_labels) 56 X = np.concatenate([previous_features, X], axis=0) 57 y = np.concatenate([previous_labels, y], axis=0) 58 59 rf = RandomForestClassifier(**rf_kwargs) 60 rf.fit(X, y) 61 return rf 62 63 64# TODO do we add a shortcut? 65@magic_factory(call_button="Train and predict") 66def _train_and_predict_rf_widget(viewer: "napari.viewer.Viewer") -> None: 67 # Get the object features and the annotations. 68 state = AnnotatorState() 69 state.annotator._require_layers() 70 annotations = viewer.layers["annotations"].data 71 segmentation = state.segmentation_selection.get_value().data 72 73 if state.object_features is None: 74 if widgets._validate_embeddings(viewer): 75 return None 76 image_embeddings = state.image_embeddings 77 seg_ids, features = compute_object_features(image_embeddings, segmentation) 78 state.seg_ids = seg_ids 79 state.object_features = features 80 else: 81 features, seg_ids = state.object_features, state.seg_ids 82 83 previous_features, previous_labels = state.previous_features, state.previous_labels 84 labels = _accumulate_labels(segmentation, annotations) 85 if (labels == 0).all() and (previous_labels is None): 86 return widgets._generate_message("error", "You have not provided any annotations.") 87 88 # Run RF training and store it in the state. 89 rf = _train_rf( 90 features, labels, previous_features=previous_features, previous_labels=previous_labels, 91 n_estimators=200, max_depth=10, n_jobs=cpu_count(), 92 ) 93 state.object_rf = rf 94 95 # Run and set the prediction. 96 pred = rf.predict(features) 97 prediction_data = project_prediction_to_segmentation(segmentation, pred, seg_ids) 98 viewer.layers["prediction"].data = prediction_data 99 100 state.annotator._refresh_label_widget() 101 102 103@magic_factory(call_button="Export Classifier") 104def _create_export_rf_widget(export_path: Optional[Path] = None) -> None: 105 state = AnnotatorState() 106 rf = state.object_rf 107 if rf is None: 108 return widgets._generate_message("error", "You have not run training yet.") 109 if export_path is None or export_path == "": 110 return widgets._generate_message("error", "You have to provide an export path.") 111 # Do we add an extension? .joblib? 112 dump(rf, export_path) 113 # TODO show an info method about the export 114 115# 116# Object classifier implementation. 117# 118 119 120# TODO add a gui element that shows the current label ids, how many objects are labeled, and that 121# enables naming them so that the user can keep track of what has been labeled 122class ObjectClassifier(QtWidgets.QScrollArea): 123 124 def _require_layers(self, layer_choices: Optional[List[str]] = None): 125 # Check whether the image is initialized already. And use the image shape and scale for the layers. 126 state = AnnotatorState() 127 shape = self._shape if state.image_shape is None else state.image_shape 128 129 # Add the label layers for the current object, the automatic segmentation and the committed segmentation. 130 dummy_data = np.zeros(shape, dtype="uint32") 131 image_scale = state.image_scale 132 133 # Before adding new layers, we always check whether a layer with this name already exists or not. 134 if "annotations" not in self._viewer.layers: 135 if layer_choices and "annotations" in layer_choices: 136 widgets._validation_window_for_missing_layer("annotations") 137 annotation_layer = self._viewer.add_labels(data=dummy_data, name="annotations") 138 if image_scale is not None: 139 self._viewer.layers["annotations"].scale = image_scale 140 # Reduce the brush size and set the default mode to "paint" brush mode. 141 annotation_layer.brush_size = 3 142 annotation_layer.mode = "paint" 143 144 if "prediction" not in self._viewer.layers: 145 if layer_choices and "prediction" in layer_choices: 146 widgets._validation_window_for_missing_layer("prediction") 147 self._viewer.add_labels(data=dummy_data, name="prediction") 148 if image_scale is not None: 149 self._viewer.layers["prediction"].scale = image_scale 150 151 def _create_segmentation_layer_section(self): 152 segmentation_selection = QtWidgets.QVBoxLayout() 153 segmentation_layer_widget = QtWidgets.QLabel("Segmentation:") 154 segmentation_selection.addWidget(segmentation_layer_widget) 155 self.segmentation_selection = create_widget(annotation=napari.layers.Labels) 156 state = AnnotatorState() 157 state.segmentation_selection = self.segmentation_selection 158 segmentation_selection.addWidget(self.segmentation_selection.native) 159 return segmentation_selection 160 161 def _create_label_widget(self): 162 self._label_form = QtWidgets.QFormLayout() 163 scroll_area = QtWidgets.QScrollArea() 164 inner = QtWidgets.QWidget() 165 inner.setLayout(self._label_form) 166 scroll_area.setWidget(inner) 167 scroll_area.setWidgetResizable(True) 168 169 layout = QtWidgets.QVBoxLayout() 170 layout.addWidget(QtWidgets.QLabel("Object label names:")) 171 layout.addWidget(scroll_area) 172 173 return layout 174 175 def _refresh_label_widget(self): 176 state = AnnotatorState() 177 178 # Get the current label ids. 179 ids = np.unique(self._viewer.layers["annotations"].data)[1:] 180 if state.previous_labels is not None: 181 ids = np.union1d(ids, np.unique(state.previous_labels)) 182 183 # Add new rows. 184 for lbl in ids: 185 if lbl in self._label_names: 186 continue 187 line = QtWidgets.QLineEdit(self._label_names.get(lbl, "")) 188 self._label_names[lbl] = "" 189 self._label_form.addRow(f"ID {lbl}", line) 190 line.textChanged.connect(lambda txt, lbl=lbl: self._label_names.__setitem__(lbl, txt)) 191 192 # Remove rows whose label vanished. 193 for row in reversed(range(self._label_form.rowCount())): 194 lbl_text = self._label_form.itemAt(row, QtWidgets.QFormLayout.LabelRole).widget().text() 195 lbl_id = int(lbl_text.split()[1]) 196 if lbl_id not in ids: 197 # Remove label+field widgets completely. 198 w_label = self._label_form.itemAt(row, QtWidgets.QFormLayout.LabelRole).widget() 199 w_edit = self._label_form.itemAt(row, QtWidgets.QFormLayout.FieldRole).widget() 200 self._label_form.removeRow(row) 201 w_label.deleteLater() 202 w_edit.deleteLater() 203 self.names.pop(lbl_id, None) 204 205 def _create_widgets(self): 206 # Create the embedding widget and connect all events related to it. 207 self._embedding_widget = widgets.EmbeddingWidget() 208 # Connect events for the image selection box. 209 self._viewer.layers.events.inserted.connect(self._embedding_widget.image_selection.reset_choices) 210 self._viewer.layers.events.removed.connect(self._embedding_widget.image_selection.reset_choices) 211 # Connect the run button with the function to update the image. 212 self._embedding_widget.run_button.clicked.connect(self._update_image) 213 214 # Create the widget for training and prediction of the classifier. 215 self._train_and_predict_widget = _train_and_predict_rf_widget() 216 217 # Create the widget for segmentation selection. 218 self._seg_selection_widget = self._create_segmentation_layer_section() 219 220 # Create the widget for displaying the current label state. 221 self._label_widget = self._create_label_widget() 222 223 # Cretate the widget for exporting the RF. 224 self._export_rf_widget = _create_export_rf_widget() 225 226 self._widgets = { 227 "embeddings": self._embedding_widget, 228 "segmentation_selection": self._seg_selection_widget, 229 "train_and_predict": self._train_and_predict_widget, 230 "label_widget": self._label_widget, 231 "export_rf": self._export_rf_widget, 232 } 233 234 def __init__(self, viewer: "napari.viewer.Viewer") -> None: 235 """Create the GUI for the object classifier. 236 237 Args: 238 viewer: The napari viewer. 239 """ 240 super().__init__() 241 self._viewer = viewer 242 self._annotator_widget = QtWidgets.QWidget() 243 self._annotator_widget.setLayout(QtWidgets.QVBoxLayout()) 244 245 # Add the layers for prompts and segmented obejcts. 246 # Initialize with a dummy shape, which is reset to the correct shape once an image is set. 247 self._shape = (256, 256) 248 self._require_layers() 249 self._ndim = len(self._shape) 250 251 # Create all the widgets and add them to the layout. 252 self._label_names = {} # The names for the object labels. 253 self._create_widgets() 254 255 # We could refactor this. 256 for widget_name, widget in self._widgets.items(): 257 widget_frame = QtWidgets.QGroupBox() 258 widget_layout = QtWidgets.QVBoxLayout() 259 if isinstance(widget, (Container, FunctionGui, Widget)): 260 # This is a magicgui type and we need to get the native qt widget. 261 widget_layout.addWidget(widget.native) 262 elif isinstance(widget, QtWidgets.QLayout): 263 widget_layout.addLayout(widget) 264 else: 265 # This is a qt type and we add the widget directly. 266 widget_layout.addWidget(widget) 267 widget_frame.setLayout(widget_layout) 268 self._annotator_widget.layout().addWidget(widget_frame) 269 270 # Connect the label layer and the refresh function. 271 self._refresh_label_widget() 272 273 # Set the expected annotator class to the state. 274 state = AnnotatorState() 275 state.annotator = self 276 277 # Add the widgets to the state. 278 state.widgets = self._widgets 279 280 # Add the widget to the scroll area. 281 self.setWidgetResizable(True) # Allow widget to resize within scroll area. 282 self.setWidget(self._annotator_widget) 283 284 # napari 0.9 caps the height of dock widget content at its size hint (napari/napari#9393). 285 def showEvent(self, event): 286 size_policy = self.sizePolicy() 287 size_policy.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) 288 self.setSizePolicy(size_policy) 289 super().showEvent(event) 290 291 def _update_image(self, segmentation_result=None): 292 state = AnnotatorState() 293 294 # Whether embeddings already exist and avoid clearing objects in layers. 295 if state.skip_recomputing_embeddings: 296 return 297 298 if state.image_shape is None: 299 return 300 301 # Update the dimension and image shape if it has changed. 302 if state.image_shape != self._shape: 303 self._ndim = len(state.image_shape) 304 self._shape = state.image_shape 305 306 # Before we reset the layers, we ensure all expected layers exist. 307 self._require_layers() 308 309 # Update the image scale. 310 scale = state.image_scale 311 312 # Reset all layers. 313 self._viewer.layers["annotations"].data = np.zeros(self._shape, dtype="uint32") 314 self._viewer.layers["annotations"].scale = scale 315 self._viewer.layers["prediction"].data = np.zeros(self._shape, dtype="uint32") 316 self._viewer.layers["prediction"].scale = scale 317 318 319def object_classifier( 320 image: np.ndarray, 321 segmentation: np.ndarray, 322 embedding_path: Optional[Union[str, util.ImageEmbeddings]] = None, 323 model_type: str = util._DEFAULT_MODEL, 324 tile_shape: Optional[Tuple[int, int]] = None, 325 halo: Optional[Tuple[int, int]] = None, 326 return_viewer: bool = False, 327 viewer: Optional["napari.viewer.Viewer"] = None, 328 checkpoint_path: Optional[str] = None, 329 device: Optional[Union[str, torch.device]] = None, 330 ndim: Optional[int] = None, 331) -> Optional["napari.viewer.Viewer"]: 332 """Start the object classifier for a given image and segmentation. 333 334 Args: 335 image: The image data. 336 segmentation: The segmentation data. 337 embedding_path: Filepath where to save the embeddings 338 or the precompted image embeddings computed by `precompute_image_embeddings`. 339 model_type: The Segment Anything model to use. For details on the available models check out 340 https://computational-cell-analytics.github.io/micro-sam/micro_sam.html#finetuned-models. 341 tile_shape: Shape of tiles for tiled embedding prediction. 342 If `None` then the whole image is passed to Segment Anything. 343 halo: Shape of the overlap between tiles, which is needed to segment objects on tile borders. 344 return_viewer: Whether to return the napari viewer to further modify it before starting the tool. 345 By default, does not return the napari viewer. 346 viewer: The viewer to which the Segment Anything functionality should be added. 347 This enables using a pre-initialized viewer. 348 checkpoint_path: Path to a custom checkpoint from which to load the SAM model. 349 device: The computational device to use for the SAM model. 350 By default, automatically chooses the best available device. 351 ndim: The dimensionality of the data. If not given will be derived from the data. 352 353 Returns: 354 The napari viewer, only returned if `return_viewer=True`. 355 """ 356 if ndim is None: 357 ndim = image.ndim - 1 if image.shape[-1] == 3 and image.ndim in (3, 4) else image.ndim 358 359 state = AnnotatorState() 360 state.image_shape = image.shape[:ndim] 361 362 state.initialize_predictor( 363 image, model_type=model_type, save_path=embedding_path, 364 halo=halo, tile_shape=tile_shape, precompute_amg_state=False, 365 ndim=ndim, checkpoint_path=checkpoint_path, device=device, 366 skip_load=False, use_cli=True, 367 ) 368 369 if viewer is None: 370 viewer = napari.Viewer() 371 372 viewer.add_image(image, name="image") 373 viewer.add_labels(segmentation, name="segmentation") 374 375 annotator = ObjectClassifier(viewer) 376 377 # Trigger layer update of the annotator so that layers have the correct shape. 378 # And initialize the 'committed_objects' with the segmentation result if it was given. 379 annotator._update_image() 380 381 # Add the annotator widget to the viewer and sync widgets. 382 viewer.window.add_dock_widget(annotator) 383 _sync_embedding_widget( 384 widget=state.widgets["embeddings"], 385 model_type=model_type if checkpoint_path is None else state.predictor.model_type, 386 save_path=embedding_path, 387 checkpoint_path=checkpoint_path, 388 device=device, 389 tile_shape=tile_shape, 390 halo=halo, 391 ) 392 393 if return_viewer: 394 return viewer 395 396 napari.run() 397 398 399def image_series_object_classifier( 400 images: List[np.ndarray], 401 segmentations: List[np.ndarray], 402 output_folder: str, 403 embedding_paths: Optional[List[Union[str, util.ImageEmbeddings]]] = None, 404 model_type: str = util._DEFAULT_MODEL, 405 tile_shape: Optional[Tuple[int, int]] = None, 406 halo: Optional[Tuple[int, int]] = None, 407 checkpoint_path: Optional[str] = None, 408 device: Optional[Union[str, torch.device]] = None, 409 ndim: Optional[int] = None, 410) -> None: 411 """Start the object classifier for a list of images and segmentations. 412 413 This function will save the all features and labels for annotated objects, 414 to enable training a random forest on multiple images. 415 416 Args: 417 images: The input images. 418 segmentations: The input segmentations. 419 output_folder: The folder where segmentation results, trained random forest 420 and the features, labels aggregated during training will be saved. 421 embedding_paths: Filepaths where to save the embeddings 422 or the precompted image embeddings computed by `precompute_image_embeddings`. 423 model_type: The Segment Anything model to use. For details on the available models check out 424 https://computational-cell-analytics.github.io/micro-sam/micro_sam.html#finetuned-models. 425 tile_shape: Shape of tiles for tiled embedding prediction. 426 If `None` then the whole image is passed to Segment Anything. 427 halo: Shape of the overlap between tiles, which is needed to segment objects on tile borders. 428 checkpoint_path: Path to a custom checkpoint from which to load the SAM model. 429 device: The computational device to use for the SAM model. 430 By default, automatically chooses the best available device. 431 ndim: The dimensionality of the data. If not given will be derived from the data. 432 """ 433 # TODO precompute the embeddings if not computed, can re-use 'precompute' from image series annotator. 434 # TODO support file paths as inputs 435 # TODO option to skip segmented 436 if len(images) != len(segmentations): 437 raise ValueError( 438 f"Expect the same number of images and segmentations, got {len(images)}, {len(segmentations)}." 439 ) 440 441 end_msg = "You have annotated the last image. Do you wish to close napari?" 442 443 # Initialize the object classifier on the fist image / segmentation. 444 viewer = object_classifier( 445 image=images[0], segmentation=segmentations[0], 446 embedding_path=None if embedding_paths is None else embedding_paths[0], 447 model_type=model_type, tile_shape=tile_shape, halo=halo, 448 return_viewer=True, checkpoint_path=checkpoint_path, 449 device=device, ndim=ndim, 450 ) 451 452 os.makedirs(output_folder, exist_ok=True) 453 next_image_id = 0 454 455 def _save_prediction(image, pred, image_id): 456 fname = f"{Path(image).stem}_prediction.tif" if isinstance(image, str) else f"prediction_{image_id}.tif" 457 save_path = os.path.join(output_folder, fname) 458 imageio.imwrite(save_path, pred, compression="zlib") 459 460 # TODO handle cases where rf for the image was not trained, raise a message, enable contnuing 461 # Add functionality for going to the next image. 462 @magicgui(call_button="Next Image [N]") 463 def next_image(*args): 464 nonlocal next_image_id 465 466 # Get the state and the current segmentation (note that next image id has not yet been increased) 467 state = AnnotatorState() 468 segmentation = segmentations[next_image_id] 469 470 # Keep track of the previous features and labels. 471 labels = _accumulate_labels(segmentation, viewer.layers["annotations"].data) 472 valid = labels != 0 473 if valid.sum() > 0: 474 features, labels = state.object_features[valid], labels[valid] 475 if state.previous_features is None: 476 state.previous_features, state.previous_labels = features, labels 477 else: 478 state.previous_features = np.concatenate([state.previous_features, features], axis=0) 479 state.previous_labels = np.concatenate([state.previous_labels, labels], axis=0) 480 # Save the accumulated features and labels. 481 np.save(os.path.join(output_folder, "features.npy"), state.previous_features) 482 np.save(os.path.join(output_folder, "labels.npy"), state.previous_labels) 483 484 # Save the current prediction and RF. 485 _save_prediction(images[next_image_id], viewer.layers["prediction"].data, next_image_id) 486 dump(state.object_rf, os.path.join(output_folder, "rf.joblib")) 487 488 # Go to the next image. 489 next_image_id += 1 490 491 # Check if we are done. 492 if next_image_id == len(images): 493 # Inform the user via dialog. 494 abort = widgets._generate_message("info", end_msg) 495 if not abort: 496 viewer.close() 497 return 498 499 # Get the next image, segmentation and embedding_path. 500 image = images[next_image_id] 501 segmentation = segmentations[next_image_id] 502 embedding_path = None if embedding_paths is None else embedding_paths[next_image_id] 503 504 # Set the new image in the viewer, state and annotator. 505 viewer.layers["image"].data = image 506 viewer.layers["segmentation"].data = segmentation 507 508 state.initialize_predictor( 509 image, model_type=model_type, ndim=ndim, 510 save_path=embedding_path, 511 tile_shape=tile_shape, halo=halo, 512 predictor=state.predictor, device=device, 513 ) 514 state.image_shape = image.shape if image.ndim == ndim else image.shape[:-1] 515 state.annotator._update_image() 516 517 # Clear the object features and seg-ids from the state. 518 state.object_features = None 519 state.seg_ids = None 520 521 viewer.window.add_dock_widget(next_image) 522 523 @viewer.bind_key("n", overwrite=True) 524 def _next_image(viewer): 525 next_image(viewer) 526 527 napari.run() 528 529 530# TODO: folder annotator 531# TODO: main function
class
ObjectClassifier(PyQt6.QtWidgets.QScrollArea):
123class ObjectClassifier(QtWidgets.QScrollArea): 124 125 def _require_layers(self, layer_choices: Optional[List[str]] = None): 126 # Check whether the image is initialized already. And use the image shape and scale for the layers. 127 state = AnnotatorState() 128 shape = self._shape if state.image_shape is None else state.image_shape 129 130 # Add the label layers for the current object, the automatic segmentation and the committed segmentation. 131 dummy_data = np.zeros(shape, dtype="uint32") 132 image_scale = state.image_scale 133 134 # Before adding new layers, we always check whether a layer with this name already exists or not. 135 if "annotations" not in self._viewer.layers: 136 if layer_choices and "annotations" in layer_choices: 137 widgets._validation_window_for_missing_layer("annotations") 138 annotation_layer = self._viewer.add_labels(data=dummy_data, name="annotations") 139 if image_scale is not None: 140 self._viewer.layers["annotations"].scale = image_scale 141 # Reduce the brush size and set the default mode to "paint" brush mode. 142 annotation_layer.brush_size = 3 143 annotation_layer.mode = "paint" 144 145 if "prediction" not in self._viewer.layers: 146 if layer_choices and "prediction" in layer_choices: 147 widgets._validation_window_for_missing_layer("prediction") 148 self._viewer.add_labels(data=dummy_data, name="prediction") 149 if image_scale is not None: 150 self._viewer.layers["prediction"].scale = image_scale 151 152 def _create_segmentation_layer_section(self): 153 segmentation_selection = QtWidgets.QVBoxLayout() 154 segmentation_layer_widget = QtWidgets.QLabel("Segmentation:") 155 segmentation_selection.addWidget(segmentation_layer_widget) 156 self.segmentation_selection = create_widget(annotation=napari.layers.Labels) 157 state = AnnotatorState() 158 state.segmentation_selection = self.segmentation_selection 159 segmentation_selection.addWidget(self.segmentation_selection.native) 160 return segmentation_selection 161 162 def _create_label_widget(self): 163 self._label_form = QtWidgets.QFormLayout() 164 scroll_area = QtWidgets.QScrollArea() 165 inner = QtWidgets.QWidget() 166 inner.setLayout(self._label_form) 167 scroll_area.setWidget(inner) 168 scroll_area.setWidgetResizable(True) 169 170 layout = QtWidgets.QVBoxLayout() 171 layout.addWidget(QtWidgets.QLabel("Object label names:")) 172 layout.addWidget(scroll_area) 173 174 return layout 175 176 def _refresh_label_widget(self): 177 state = AnnotatorState() 178 179 # Get the current label ids. 180 ids = np.unique(self._viewer.layers["annotations"].data)[1:] 181 if state.previous_labels is not None: 182 ids = np.union1d(ids, np.unique(state.previous_labels)) 183 184 # Add new rows. 185 for lbl in ids: 186 if lbl in self._label_names: 187 continue 188 line = QtWidgets.QLineEdit(self._label_names.get(lbl, "")) 189 self._label_names[lbl] = "" 190 self._label_form.addRow(f"ID {lbl}", line) 191 line.textChanged.connect(lambda txt, lbl=lbl: self._label_names.__setitem__(lbl, txt)) 192 193 # Remove rows whose label vanished. 194 for row in reversed(range(self._label_form.rowCount())): 195 lbl_text = self._label_form.itemAt(row, QtWidgets.QFormLayout.LabelRole).widget().text() 196 lbl_id = int(lbl_text.split()[1]) 197 if lbl_id not in ids: 198 # Remove label+field widgets completely. 199 w_label = self._label_form.itemAt(row, QtWidgets.QFormLayout.LabelRole).widget() 200 w_edit = self._label_form.itemAt(row, QtWidgets.QFormLayout.FieldRole).widget() 201 self._label_form.removeRow(row) 202 w_label.deleteLater() 203 w_edit.deleteLater() 204 self.names.pop(lbl_id, None) 205 206 def _create_widgets(self): 207 # Create the embedding widget and connect all events related to it. 208 self._embedding_widget = widgets.EmbeddingWidget() 209 # Connect events for the image selection box. 210 self._viewer.layers.events.inserted.connect(self._embedding_widget.image_selection.reset_choices) 211 self._viewer.layers.events.removed.connect(self._embedding_widget.image_selection.reset_choices) 212 # Connect the run button with the function to update the image. 213 self._embedding_widget.run_button.clicked.connect(self._update_image) 214 215 # Create the widget for training and prediction of the classifier. 216 self._train_and_predict_widget = _train_and_predict_rf_widget() 217 218 # Create the widget for segmentation selection. 219 self._seg_selection_widget = self._create_segmentation_layer_section() 220 221 # Create the widget for displaying the current label state. 222 self._label_widget = self._create_label_widget() 223 224 # Cretate the widget for exporting the RF. 225 self._export_rf_widget = _create_export_rf_widget() 226 227 self._widgets = { 228 "embeddings": self._embedding_widget, 229 "segmentation_selection": self._seg_selection_widget, 230 "train_and_predict": self._train_and_predict_widget, 231 "label_widget": self._label_widget, 232 "export_rf": self._export_rf_widget, 233 } 234 235 def __init__(self, viewer: "napari.viewer.Viewer") -> None: 236 """Create the GUI for the object classifier. 237 238 Args: 239 viewer: The napari viewer. 240 """ 241 super().__init__() 242 self._viewer = viewer 243 self._annotator_widget = QtWidgets.QWidget() 244 self._annotator_widget.setLayout(QtWidgets.QVBoxLayout()) 245 246 # Add the layers for prompts and segmented obejcts. 247 # Initialize with a dummy shape, which is reset to the correct shape once an image is set. 248 self._shape = (256, 256) 249 self._require_layers() 250 self._ndim = len(self._shape) 251 252 # Create all the widgets and add them to the layout. 253 self._label_names = {} # The names for the object labels. 254 self._create_widgets() 255 256 # We could refactor this. 257 for widget_name, widget in self._widgets.items(): 258 widget_frame = QtWidgets.QGroupBox() 259 widget_layout = QtWidgets.QVBoxLayout() 260 if isinstance(widget, (Container, FunctionGui, Widget)): 261 # This is a magicgui type and we need to get the native qt widget. 262 widget_layout.addWidget(widget.native) 263 elif isinstance(widget, QtWidgets.QLayout): 264 widget_layout.addLayout(widget) 265 else: 266 # This is a qt type and we add the widget directly. 267 widget_layout.addWidget(widget) 268 widget_frame.setLayout(widget_layout) 269 self._annotator_widget.layout().addWidget(widget_frame) 270 271 # Connect the label layer and the refresh function. 272 self._refresh_label_widget() 273 274 # Set the expected annotator class to the state. 275 state = AnnotatorState() 276 state.annotator = self 277 278 # Add the widgets to the state. 279 state.widgets = self._widgets 280 281 # Add the widget to the scroll area. 282 self.setWidgetResizable(True) # Allow widget to resize within scroll area. 283 self.setWidget(self._annotator_widget) 284 285 # napari 0.9 caps the height of dock widget content at its size hint (napari/napari#9393). 286 def showEvent(self, event): 287 size_policy = self.sizePolicy() 288 size_policy.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) 289 self.setSizePolicy(size_policy) 290 super().showEvent(event) 291 292 def _update_image(self, segmentation_result=None): 293 state = AnnotatorState() 294 295 # Whether embeddings already exist and avoid clearing objects in layers. 296 if state.skip_recomputing_embeddings: 297 return 298 299 if state.image_shape is None: 300 return 301 302 # Update the dimension and image shape if it has changed. 303 if state.image_shape != self._shape: 304 self._ndim = len(state.image_shape) 305 self._shape = state.image_shape 306 307 # Before we reset the layers, we ensure all expected layers exist. 308 self._require_layers() 309 310 # Update the image scale. 311 scale = state.image_scale 312 313 # Reset all layers. 314 self._viewer.layers["annotations"].data = np.zeros(self._shape, dtype="uint32") 315 self._viewer.layers["annotations"].scale = scale 316 self._viewer.layers["prediction"].data = np.zeros(self._shape, dtype="uint32") 317 self._viewer.layers["prediction"].scale = scale
QScrollArea(parent: Optional[QWidget] = None)
ObjectClassifier(viewer: napari.viewer.Viewer)
235 def __init__(self, viewer: "napari.viewer.Viewer") -> None: 236 """Create the GUI for the object classifier. 237 238 Args: 239 viewer: The napari viewer. 240 """ 241 super().__init__() 242 self._viewer = viewer 243 self._annotator_widget = QtWidgets.QWidget() 244 self._annotator_widget.setLayout(QtWidgets.QVBoxLayout()) 245 246 # Add the layers for prompts and segmented obejcts. 247 # Initialize with a dummy shape, which is reset to the correct shape once an image is set. 248 self._shape = (256, 256) 249 self._require_layers() 250 self._ndim = len(self._shape) 251 252 # Create all the widgets and add them to the layout. 253 self._label_names = {} # The names for the object labels. 254 self._create_widgets() 255 256 # We could refactor this. 257 for widget_name, widget in self._widgets.items(): 258 widget_frame = QtWidgets.QGroupBox() 259 widget_layout = QtWidgets.QVBoxLayout() 260 if isinstance(widget, (Container, FunctionGui, Widget)): 261 # This is a magicgui type and we need to get the native qt widget. 262 widget_layout.addWidget(widget.native) 263 elif isinstance(widget, QtWidgets.QLayout): 264 widget_layout.addLayout(widget) 265 else: 266 # This is a qt type and we add the widget directly. 267 widget_layout.addWidget(widget) 268 widget_frame.setLayout(widget_layout) 269 self._annotator_widget.layout().addWidget(widget_frame) 270 271 # Connect the label layer and the refresh function. 272 self._refresh_label_widget() 273 274 # Set the expected annotator class to the state. 275 state = AnnotatorState() 276 state.annotator = self 277 278 # Add the widgets to the state. 279 state.widgets = self._widgets 280 281 # Add the widget to the scroll area. 282 self.setWidgetResizable(True) # Allow widget to resize within scroll area. 283 self.setWidget(self._annotator_widget)
Create the GUI for the object classifier.
Arguments:
- viewer: The napari viewer.
def
object_classifier( image: numpy.ndarray, segmentation: numpy.ndarray, embedding_path: Union[str, Dict[str, Any], NoneType] = None, model_type: str = 'vit_b_lm', tile_shape: Optional[Tuple[int, int]] = None, halo: Optional[Tuple[int, int]] = None, return_viewer: bool = False, viewer: Optional[napari.viewer.Viewer] = None, checkpoint_path: Optional[str] = None, device: Union[str, torch.device, NoneType] = None, ndim: Optional[int] = None) -> Optional[napari.viewer.Viewer]:
320def object_classifier( 321 image: np.ndarray, 322 segmentation: np.ndarray, 323 embedding_path: Optional[Union[str, util.ImageEmbeddings]] = None, 324 model_type: str = util._DEFAULT_MODEL, 325 tile_shape: Optional[Tuple[int, int]] = None, 326 halo: Optional[Tuple[int, int]] = None, 327 return_viewer: bool = False, 328 viewer: Optional["napari.viewer.Viewer"] = None, 329 checkpoint_path: Optional[str] = None, 330 device: Optional[Union[str, torch.device]] = None, 331 ndim: Optional[int] = None, 332) -> Optional["napari.viewer.Viewer"]: 333 """Start the object classifier for a given image and segmentation. 334 335 Args: 336 image: The image data. 337 segmentation: The segmentation data. 338 embedding_path: Filepath where to save the embeddings 339 or the precompted image embeddings computed by `precompute_image_embeddings`. 340 model_type: The Segment Anything model to use. For details on the available models check out 341 https://computational-cell-analytics.github.io/micro-sam/micro_sam.html#finetuned-models. 342 tile_shape: Shape of tiles for tiled embedding prediction. 343 If `None` then the whole image is passed to Segment Anything. 344 halo: Shape of the overlap between tiles, which is needed to segment objects on tile borders. 345 return_viewer: Whether to return the napari viewer to further modify it before starting the tool. 346 By default, does not return the napari viewer. 347 viewer: The viewer to which the Segment Anything functionality should be added. 348 This enables using a pre-initialized viewer. 349 checkpoint_path: Path to a custom checkpoint from which to load the SAM model. 350 device: The computational device to use for the SAM model. 351 By default, automatically chooses the best available device. 352 ndim: The dimensionality of the data. If not given will be derived from the data. 353 354 Returns: 355 The napari viewer, only returned if `return_viewer=True`. 356 """ 357 if ndim is None: 358 ndim = image.ndim - 1 if image.shape[-1] == 3 and image.ndim in (3, 4) else image.ndim 359 360 state = AnnotatorState() 361 state.image_shape = image.shape[:ndim] 362 363 state.initialize_predictor( 364 image, model_type=model_type, save_path=embedding_path, 365 halo=halo, tile_shape=tile_shape, precompute_amg_state=False, 366 ndim=ndim, checkpoint_path=checkpoint_path, device=device, 367 skip_load=False, use_cli=True, 368 ) 369 370 if viewer is None: 371 viewer = napari.Viewer() 372 373 viewer.add_image(image, name="image") 374 viewer.add_labels(segmentation, name="segmentation") 375 376 annotator = ObjectClassifier(viewer) 377 378 # Trigger layer update of the annotator so that layers have the correct shape. 379 # And initialize the 'committed_objects' with the segmentation result if it was given. 380 annotator._update_image() 381 382 # Add the annotator widget to the viewer and sync widgets. 383 viewer.window.add_dock_widget(annotator) 384 _sync_embedding_widget( 385 widget=state.widgets["embeddings"], 386 model_type=model_type if checkpoint_path is None else state.predictor.model_type, 387 save_path=embedding_path, 388 checkpoint_path=checkpoint_path, 389 device=device, 390 tile_shape=tile_shape, 391 halo=halo, 392 ) 393 394 if return_viewer: 395 return viewer 396 397 napari.run()
Start the object classifier for a given image and segmentation.
Arguments:
- image: The image data.
- segmentation: The segmentation data.
- embedding_path: Filepath where to save the embeddings
or the precompted image embeddings computed by
precompute_image_embeddings. - model_type: The Segment Anything model to use. For details on the available models check out https://computational-cell-analytics.github.io/micro-sam/micro_sam.html#finetuned-models.
- tile_shape: Shape of tiles for tiled embedding prediction.
If
Nonethen the whole image is passed to Segment Anything. - halo: Shape of the overlap between tiles, which is needed to segment objects on tile borders.
- return_viewer: Whether to return the napari viewer to further modify it before starting the tool. By default, does not return the napari viewer.
- viewer: The viewer to which the Segment Anything functionality should be added. This enables using a pre-initialized viewer.
- checkpoint_path: Path to a custom checkpoint from which to load the SAM model.
- device: The computational device to use for the SAM model. By default, automatically chooses the best available device.
- ndim: The dimensionality of the data. If not given will be derived from the data.
Returns:
The napari viewer, only returned if
return_viewer=True.
def
image_series_object_classifier( images: List[numpy.ndarray], segmentations: List[numpy.ndarray], output_folder: str, embedding_paths: Optional[List[Union[str, Dict[str, Any]]]] = None, model_type: str = 'vit_b_lm', tile_shape: Optional[Tuple[int, int]] = None, halo: Optional[Tuple[int, int]] = None, checkpoint_path: Optional[str] = None, device: Union[str, torch.device, NoneType] = None, ndim: Optional[int] = None) -> None:
400def image_series_object_classifier( 401 images: List[np.ndarray], 402 segmentations: List[np.ndarray], 403 output_folder: str, 404 embedding_paths: Optional[List[Union[str, util.ImageEmbeddings]]] = None, 405 model_type: str = util._DEFAULT_MODEL, 406 tile_shape: Optional[Tuple[int, int]] = None, 407 halo: Optional[Tuple[int, int]] = None, 408 checkpoint_path: Optional[str] = None, 409 device: Optional[Union[str, torch.device]] = None, 410 ndim: Optional[int] = None, 411) -> None: 412 """Start the object classifier for a list of images and segmentations. 413 414 This function will save the all features and labels for annotated objects, 415 to enable training a random forest on multiple images. 416 417 Args: 418 images: The input images. 419 segmentations: The input segmentations. 420 output_folder: The folder where segmentation results, trained random forest 421 and the features, labels aggregated during training will be saved. 422 embedding_paths: Filepaths where to save the embeddings 423 or the precompted image embeddings computed by `precompute_image_embeddings`. 424 model_type: The Segment Anything model to use. For details on the available models check out 425 https://computational-cell-analytics.github.io/micro-sam/micro_sam.html#finetuned-models. 426 tile_shape: Shape of tiles for tiled embedding prediction. 427 If `None` then the whole image is passed to Segment Anything. 428 halo: Shape of the overlap between tiles, which is needed to segment objects on tile borders. 429 checkpoint_path: Path to a custom checkpoint from which to load the SAM model. 430 device: The computational device to use for the SAM model. 431 By default, automatically chooses the best available device. 432 ndim: The dimensionality of the data. If not given will be derived from the data. 433 """ 434 # TODO precompute the embeddings if not computed, can re-use 'precompute' from image series annotator. 435 # TODO support file paths as inputs 436 # TODO option to skip segmented 437 if len(images) != len(segmentations): 438 raise ValueError( 439 f"Expect the same number of images and segmentations, got {len(images)}, {len(segmentations)}." 440 ) 441 442 end_msg = "You have annotated the last image. Do you wish to close napari?" 443 444 # Initialize the object classifier on the fist image / segmentation. 445 viewer = object_classifier( 446 image=images[0], segmentation=segmentations[0], 447 embedding_path=None if embedding_paths is None else embedding_paths[0], 448 model_type=model_type, tile_shape=tile_shape, halo=halo, 449 return_viewer=True, checkpoint_path=checkpoint_path, 450 device=device, ndim=ndim, 451 ) 452 453 os.makedirs(output_folder, exist_ok=True) 454 next_image_id = 0 455 456 def _save_prediction(image, pred, image_id): 457 fname = f"{Path(image).stem}_prediction.tif" if isinstance(image, str) else f"prediction_{image_id}.tif" 458 save_path = os.path.join(output_folder, fname) 459 imageio.imwrite(save_path, pred, compression="zlib") 460 461 # TODO handle cases where rf for the image was not trained, raise a message, enable contnuing 462 # Add functionality for going to the next image. 463 @magicgui(call_button="Next Image [N]") 464 def next_image(*args): 465 nonlocal next_image_id 466 467 # Get the state and the current segmentation (note that next image id has not yet been increased) 468 state = AnnotatorState() 469 segmentation = segmentations[next_image_id] 470 471 # Keep track of the previous features and labels. 472 labels = _accumulate_labels(segmentation, viewer.layers["annotations"].data) 473 valid = labels != 0 474 if valid.sum() > 0: 475 features, labels = state.object_features[valid], labels[valid] 476 if state.previous_features is None: 477 state.previous_features, state.previous_labels = features, labels 478 else: 479 state.previous_features = np.concatenate([state.previous_features, features], axis=0) 480 state.previous_labels = np.concatenate([state.previous_labels, labels], axis=0) 481 # Save the accumulated features and labels. 482 np.save(os.path.join(output_folder, "features.npy"), state.previous_features) 483 np.save(os.path.join(output_folder, "labels.npy"), state.previous_labels) 484 485 # Save the current prediction and RF. 486 _save_prediction(images[next_image_id], viewer.layers["prediction"].data, next_image_id) 487 dump(state.object_rf, os.path.join(output_folder, "rf.joblib")) 488 489 # Go to the next image. 490 next_image_id += 1 491 492 # Check if we are done. 493 if next_image_id == len(images): 494 # Inform the user via dialog. 495 abort = widgets._generate_message("info", end_msg) 496 if not abort: 497 viewer.close() 498 return 499 500 # Get the next image, segmentation and embedding_path. 501 image = images[next_image_id] 502 segmentation = segmentations[next_image_id] 503 embedding_path = None if embedding_paths is None else embedding_paths[next_image_id] 504 505 # Set the new image in the viewer, state and annotator. 506 viewer.layers["image"].data = image 507 viewer.layers["segmentation"].data = segmentation 508 509 state.initialize_predictor( 510 image, model_type=model_type, ndim=ndim, 511 save_path=embedding_path, 512 tile_shape=tile_shape, halo=halo, 513 predictor=state.predictor, device=device, 514 ) 515 state.image_shape = image.shape if image.ndim == ndim else image.shape[:-1] 516 state.annotator._update_image() 517 518 # Clear the object features and seg-ids from the state. 519 state.object_features = None 520 state.seg_ids = None 521 522 viewer.window.add_dock_widget(next_image) 523 524 @viewer.bind_key("n", overwrite=True) 525 def _next_image(viewer): 526 next_image(viewer) 527 528 napari.run()
Start the object classifier for a list of images and segmentations.
This function will save the all features and labels for annotated objects, to enable training a random forest on multiple images.
Arguments:
- images: The input images.
- segmentations: The input segmentations.
- output_folder: The folder where segmentation results, trained random forest and the features, labels aggregated during training will be saved.
- embedding_paths: Filepaths where to save the embeddings
or the precompted image embeddings computed by
precompute_image_embeddings. - model_type: The Segment Anything model to use. For details on the available models check out https://computational-cell-analytics.github.io/micro-sam/micro_sam.html#finetuned-models.
- tile_shape: Shape of tiles for tiled embedding prediction.
If
Nonethen the whole image is passed to Segment Anything. - halo: Shape of the overlap between tiles, which is needed to segment objects on tile borders.
- checkpoint_path: Path to a custom checkpoint from which to load the SAM model.
- device: The computational device to use for the SAM model. By default, automatically chooses the best available device.
- ndim: The dimensionality of the data. If not given will be derived from the data.