micro_sam.sam_annotator.util

  1import os
  2import pickle
  3import warnings
  4import argparse
  5from glob import glob
  6from pathlib import Path
  7from typing import List, Optional, Tuple
  8
  9import h5py
 10import napari
 11import numpy as np
 12from skimage import draw
 13from scipy.ndimage import shift
 14
 15from .. import prompt_based_segmentation, util
 16from .. import _model_settings as model_settings
 17from ..multi_dimensional_segmentation import _validate_projection
 18
 19# Green and Red
 20LABEL_COLOR_CYCLE = ["#00FF00", "#FF0000"]
 21"""@private"""
 22
 23
 24#
 25# Misc helper functions
 26#
 27
 28
 29def toggle_label(prompts):
 30    """@private"""
 31    # get the currently selected label
 32    current_properties = prompts.current_properties
 33    current_label = current_properties["label"][0]
 34    new_label = "negative" if current_label == "positive" else "positive"
 35    current_properties["label"] = np.array([new_label])
 36    prompts.current_properties = current_properties
 37    prompts.refresh()
 38    prompts.refresh_colors()
 39
 40
 41def _initialize_parser(description, with_segmentation_result=True, with_instance_segmentation=True):
 42
 43    available_models = list(util.get_model_names())
 44    available_models = ", ".join(available_models)
 45
 46    parser = argparse.ArgumentParser(description=description)
 47
 48    parser.add_argument(
 49        "-i", "--input", required=True,
 50        help="The filepath to the image data. Supports all data types that can be read by imageio (e.g. tif, png, ...) "
 51        "or elf.io.open_file (e.g. hdf5, zarr, mrc). For the latter you also need to pass the 'key' parameter."
 52    )
 53    parser.add_argument(
 54        "-k", "--key",
 55        help="The key for opening data with elf.io.open_file. This is the internal path for a hdf5 or zarr container, "
 56        "for a image series it is a wild-card, e.g. '*.png' and for mrc it is 'data'."
 57    )
 58    parser.add_argument(
 59        "-e", "--embedding_path",
 60        help="The filepath for saving/loading the pre-computed image embeddings. "
 61        "It is recommended to pass this argument and store the embeddings if you want to open the annotator "
 62        "multiple times for this image. Otherwise the embeddings will be recomputed every time."
 63    )
 64
 65    if with_segmentation_result:
 66        parser.add_argument(
 67            "-s", "--segmentation_result",
 68            help="Optional filepath to a precomputed segmentation. If passed this will be used to initialize the "
 69            "'committed_objects' layer. This can be useful if you want to correct an existing segmentation or if you "
 70            "have saved intermediate results from the annotator and want to continue with your annotations. "
 71            "Supports the same file formats as 'input'."
 72        )
 73        parser.add_argument(
 74            "-sk", "--segmentation_key",
 75            help="The key for opening the segmentation data. Same rules as for 'key' apply."
 76        )
 77
 78    parser.add_argument(
 79        "-m", "--model_type", default=util._DEFAULT_MODEL,
 80        help=f"The segment anything model that will be used, one of {available_models}."
 81    )
 82    parser.add_argument(
 83        "-c", "--checkpoint", default=None,
 84        help="Checkpoint from which the SAM model will be loaded."
 85    )
 86    parser.add_argument(
 87        "--decoder_path", default=None,
 88        help="Optional checkpoint path to decoder-only weights to enable decoder-based instance segmentation."
 89    )
 90    parser.add_argument(
 91        "-d", "--device", default=None,
 92        help="The device to use for the predictor. Can be one of 'cuda', 'cpu' or 'mps' (only MAC)."
 93        "By default the most performant available device will be selected."
 94    )
 95    parser.add_argument(
 96        "--tile_shape", nargs="+", type=int, help="The tile shape for using tiled prediction", default=None
 97    )
 98    parser.add_argument(
 99        "--halo", nargs="+", type=int, help="The halo for using tiled prediction", default=None
100    )
101
102    if with_instance_segmentation:
103        parser.add_argument(
104            "--precompute_amg_state", action="store_true",
105            help="Whether to precompute the state for automatic instance segmentation. "
106            "This will lead to a longer start-up time, but the automatic instance segmentation can "
107            "be run directly once the tool has started."
108        )
109        parser.add_argument(
110            "--prefer_decoder", action="store_false",
111            help="Whether to use decoder based instance segmentation if the model "
112            "being used has an additional decoder for that purpose."
113        )
114
115    return parser
116
117
118def clear_annotations(viewer: napari.Viewer, clear_segmentations=True) -> None:
119    """@private"""
120    point_layer = viewer.layers["point_prompts"]
121    point_layer.selected_data = set(range(len(point_layer.data)))
122    point_layer.remove_selected()
123    point_layer.refresh()
124    if "prompts" in viewer.layers:
125        # Select all prompts and then remove them.
126        # This is how it worked before napari 0.5.
127        # viewer.layers["prompts"].data = []
128        viewer.layers["prompts"].selected_data = set(range(len(viewer.layers["prompts"].data)))
129        viewer.layers["prompts"].remove_selected()
130        viewer.layers["prompts"].refresh()
131    if not clear_segmentations:
132        return
133    viewer.layers["current_object"].data = np.zeros(viewer.layers["current_object"].data.shape, dtype="uint32")
134    viewer.layers["current_object"].refresh()
135
136
137def clear_annotations_slice(viewer: napari.Viewer, i: int, clear_segmentations=True) -> None:
138    """@private"""
139    point_layer = viewer.layers["point_prompts"]
140    point_layer.selected_data = set(np.flatnonzero(point_layer.data[:, 0] == i))
141    point_layer.remove_selected()
142    point_layer.refresh()
143    if "prompts" in viewer.layers:
144        prompts = viewer.layers["prompts"].data
145        prompts = [prompt for prompt in prompts if not (prompt[:, 0] == i).all()]
146        viewer.layers["prompts"].data = prompts
147        viewer.layers["prompts"].refresh()
148    if not clear_segmentations:
149        return
150    viewer.layers["current_object"].data[i] = 0
151    viewer.layers["current_object"].refresh()
152
153
154#
155# Helper functions to extract prompts from napari layers.
156#
157
158
159def point_layer_to_prompts(
160    layer: napari.layers.Points, i=None, track_id=None, with_stop_annotation=True,
161) -> Optional[Tuple[np.ndarray, np.ndarray]]:
162    """Extract point prompts for SAM from a napari point layer.
163
164    Args:
165        layer: The point layer from which to extract the prompts.
166        i: Index for the data (required for 3d or timeseries data).
167        track_id: Id of the current track (required for tracking data).
168        with_stop_annotation: Whether a single negative point will be interpreted
169            as stop annotation or just returned as normal prompt.
170
171    Returns:
172        The point coordinates for the prompts.
173        The labels (positive or negative / 1 or 0) for the prompts.
174    """
175
176    points = layer.data
177    labels = layer.properties["label"]
178    assert len(points) == len(labels)
179
180    if i is None:
181        assert points.shape[1] == 2, f"{points.shape}"
182        this_points, this_labels = points, labels
183    else:
184        assert points.shape[1] == 3, f"{points.shape}"
185        mask = np.round(points[:, 0]) == i
186        this_points = points[mask][:, 1:]
187        this_labels = labels[mask]
188    assert len(this_points) == len(this_labels)
189
190    if track_id is not None:
191        assert i is not None
192        track_ids = np.array(list(map(int, layer.properties["track_id"])))[mask]
193        track_id_mask = track_ids == track_id
194        this_labels, this_points = this_labels[track_id_mask], this_points[track_id_mask]
195    assert len(this_points) == len(this_labels)
196
197    this_labels = np.array([1 if label == "positive" else 0 for label in this_labels])
198    # a single point with a negative label is interpreted as 'stop' signal
199    # in this case we return None
200    if with_stop_annotation and (len(this_points) == 1 and this_labels[0] == 0):
201        return None
202
203    return this_points, this_labels
204
205
206def shape_layer_to_prompts(
207    layer: napari.layers.Shapes, shape: Tuple[int, int], i=None, track_id=None
208) -> Tuple[List[np.ndarray], List[Optional[np.ndarray]]]:
209    """Extract prompts for SAM from a napari shape layer.
210
211    Extracts the bounding box for 'rectangle' shapes and the bounding box and corresponding mask
212    for 'ellipse' and 'polygon' shapes.
213
214    Args:
215        prompt_layer: The napari shape layer.
216        shape: The image shape.
217        i: Index for the data (required for 3d or timeseries data).
218        track_id: Id of the current track (required for tracking data).
219
220    Returns:
221        The box prompts.
222        The mask prompts.
223    """
224
225    def _to_prompts(shape_data, shape_types):
226        boxes, masks = [], []
227
228        for data, type_ in zip(shape_data, shape_types):
229
230            if type_ == "rectangle":
231                boxes.append(data)
232                masks.append(None)
233
234            elif type_ == "ellipse":
235                boxes.append(data)
236                center = np.mean(data, axis=0)
237                radius_r = ((data[2] - data[1]) / 2)[0]
238                radius_c = ((data[1] - data[0]) / 2)[1]
239                rr, cc = draw.ellipse(center[0], center[1], radius_r, radius_c, shape=shape)
240                mask = np.zeros(shape, dtype=bool)
241                mask[rr, cc] = 1
242                masks.append(mask)
243
244            elif type_ == "polygon":
245                boxes.append(data)
246                rr, cc = draw.polygon(data[:, 0], data[:, 1], shape=shape)
247                mask = np.zeros(shape, dtype=bool)
248                mask[rr, cc] = 1
249                masks.append(mask)
250
251            else:
252                warnings.warn(f"Shape type {type_} is not supported and will be ignored.")
253
254        # map to correct box format
255        boxes = [
256            np.array([box[:, 0].min(), box[:, 1].min(), box[:, 0].max(), box[:, 1].max()]) for box in boxes
257        ]
258        return boxes, masks
259
260    shape_data, shape_types = layer.data, layer.shape_type
261    assert len(shape_data) == len(shape_types)
262    if len(shape_data) == 0:
263        return [], []
264
265    if i is not None:
266        if track_id is None:
267            prompt_selection = [j for j, data in enumerate(shape_data) if (data[:, 0] == i).all()]
268        else:
269            track_ids = np.array(list(map(int, layer.properties["track_id"])))
270            prompt_selection = [
271                j for j, (data, this_track_id) in enumerate(zip(shape_data, track_ids))
272                if ((data[:, 0] == i).all() and this_track_id == track_id)
273            ]
274
275        shape_data = [shape_data[j][:, 1:] for j in prompt_selection]
276        shape_types = [shape_types[j] for j in prompt_selection]
277
278    boxes, masks = _to_prompts(shape_data, shape_types)
279    return boxes, masks
280
281
282def prompt_layer_to_state(prompt_layer: napari.layers.Points, i: int) -> str:
283    """Get the state of the track from a point layer for a given timeframe.
284
285    Only relevant for annotator_tracking.
286
287    Args:
288        prompt_layer: The napari layer.
289        i: Timeframe of the data.
290
291    Returns:
292        The state of this frame (either "division" or "track").
293    """
294    state = prompt_layer.properties["state"]
295
296    points = prompt_layer.data
297    assert points.shape[1] == 3, f"{points.shape}"
298    mask = points[:, 0] == i
299    this_points = points[mask][:, 1:]
300    this_state = state[mask]
301    assert len(this_points) == len(this_state)
302
303    # we set the state to 'division' if at least one point in this frame has a division label
304    if any(st == "division" for st in this_state):
305        return "division"
306    else:
307        return "track"
308
309
310def prompt_layers_to_state(point_layer: napari.layers.Points, box_layer: napari.layers.Shapes, i: int) -> str:
311    """Get the state of the track from a point layer and shape layer for a given timeframe.
312
313    Only relevant for annotator_tracking.
314
315    Args:
316        point_layer: The napari point layer.
317        box_layer: The napari box layer.
318        i: Timeframe of the data.
319
320    Returns:
321        The state of this frame (either "division" or "track").
322    """
323    state = point_layer.properties["state"]
324
325    points = point_layer.data
326    assert points.shape[1] == 3, f"{points.shape}"
327    mask = points[:, 0] == i
328    if mask.sum() > 0:
329        this_state = state[mask].tolist()
330    else:
331        this_state = []
332
333    box_states = box_layer.properties["state"]
334    this_box_states = [
335        state for box, state in zip(box_layer.data, box_states)
336        if (box[:, 0] == i).all()
337    ]
338    this_state.extend(this_box_states)
339
340    # we set the state to 'division' if at least one point in this frame has a division label
341    if any(st == "division" for st in this_state):
342        return "division"
343    else:
344        return "track"
345
346
347#
348# Helper functions to run (multi-dimensional) segmentation on napari layers.
349#
350
351
352def segment_slices_with_prompts(
353    predictor, point_prompts, box_prompts, image_embeddings, shape, track_id=None, update_progress=None,
354):
355    """@private"""
356    assert len(shape) == 3
357    image_shape = shape[1:]
358    seg = np.zeros(shape, dtype="uint32")
359
360    z_values = np.round(point_prompts.data[:, 0])
361    z_values_boxes = np.concatenate([box[:1, 0] for box in box_prompts.data]) if box_prompts.data else\
362        np.zeros(0, dtype="int")
363
364    if track_id is not None:
365        track_ids_points = np.array(list(map(int, point_prompts.properties["track_id"])))
366        assert len(track_ids_points) == len(z_values)
367        z_values = z_values[track_ids_points == track_id]
368
369        if len(z_values_boxes) > 0:
370            track_ids_boxes = np.array(list(map(int, box_prompts.properties["track_id"])))
371            assert len(track_ids_boxes) == len(z_values_boxes), f"{len(track_ids_boxes)}, {len(z_values_boxes)}"
372            z_values_boxes = z_values_boxes[track_ids_boxes == track_id]
373
374    slices = np.unique(np.concatenate([z_values, z_values_boxes])).astype("int")
375    stop_lower, stop_upper = False, False
376
377    if update_progress is None:
378        def update_progress(*args):
379            pass
380
381    for i in slices:
382        points_i = point_layer_to_prompts(point_prompts, i, track_id)
383
384        # do we end the segmentation at the outer slices?
385        if points_i is None:
386
387            if i == slices[0]:  # The bottom slice is a stop slice.
388                stop_lower = True
389                seg[i] = 0
390            elif i == slices[-1]:  # The top sloce is a stop slice.
391                stop_upper = True
392                seg[i] = 0
393            else:  # We have a stop annotation somewhere in the middle. Ignore this.
394                # Remove this slice from the annotated slices, so that it is segmented via
395                # projection in the next step.
396                slices = np.setdiff1d(slices, i)
397                print(f"You have provided a stop annotation (single red point) in slice {i},")
398                print("but you have annotated slices above or below it. This stop annotation will")
399                print(f"be ignored and the slice {i} will be segmented normally.")
400
401            update_progress(1)
402            continue
403
404        boxes, masks = shape_layer_to_prompts(box_prompts, image_shape, i=i, track_id=track_id)
405        points, labels = points_i
406
407        seg_i = prompt_segmentation(
408            predictor, points, labels, boxes, masks, image_shape, multiple_box_prompts=False,
409            image_embeddings=image_embeddings, i=i
410        )
411        if seg_i is None:
412            print(f"The prompts at slice or frame {i} are invalid and the segmentation was skipped.")
413            print("This will lead to a wrong segmentation across slices or frames.")
414            print(f"Please correct the prompts in {i} and rerun the segmentation.")
415            continue
416
417        seg[i] = seg_i
418        update_progress(1)
419
420    return seg, slices, stop_lower, stop_upper
421
422
423# For advanced batching: match prompts to already segmented objects and continue segmentation.
424def _match_prompts(previous_segmentation, points, boxes, seg_ids):
425    # Create a mapping between ids and prompts.
426    batched_prompts = {}
427    # seg_boundaries = find_boundaries(previous_segmentation, mode="inner")
428    # indices = distance_transform_edt(seg_boundaries, return_distance=False, return_index=True)
429    return batched_prompts
430
431
432def _batched_interactive_segmentation(predictor, points, labels, boxes, image_embeddings, i, previous_segmentation):
433    prev_seg = previous_segmentation if i is None else previous_segmentation[i]
434    seg = np.zeros(prev_seg.shape, dtype="uint32")
435
436    # seg_ids = np.unique(previous_segmentation)
437    # assert seg_ids[0] == 0
438
439    batched_points, batched_labels = [], []
440    negative_points, negative_labels = [], []
441    for j in range(len(points)):
442        if labels[j] == 1:  # positive point
443            batched_points.append(points[j:j+1])
444            batched_labels.append(labels[j:j+1])
445        else:  # negative points
446            negative_points.append(points[j:j+1])
447            negative_labels.append(labels[j:j+1])
448
449    batched_prompts = [(None, point, label) for point, label in zip(batched_points, batched_labels)]
450    batched_prompts.extend([(box, None, None) for box in boxes])
451    batched_prompts = {i: prompt for i, prompt in enumerate(batched_prompts, 1)}
452
453    # For advanced batching: match prompts to already segmented objects and continue segmentation.
454    # (This is left here as a reference for how this can be implemented.
455    #  I have not decided yet if this is actually a good idea or not.)
456    # # If we have no objects: this is the first call for a batched segmentation.
457    # # We treat each positive point or box as a separate object.
458    # if len(seg_ids) == 1:
459    #     # Create a list of all prompts.
460    #     batched_prompts = [(None, point, label) for point, label in zip(batched_points, batched_labels)]
461    #     batched_prompts.extend([(box, None, None) for box in boxes])
462    #     batched_prompts = {i: prompt for i, prompt in enumerate(batched_prompts, 1)}
463
464    # # Otherwise we match the prompts to existing objects.
465    # else:
466    #     batched_prompts = _match_prompts(prev_seg, batched_points, boxes, seg_ids)
467
468    for seg_id, prompt in batched_prompts.items():
469        box, point, label = prompt
470        if len(negative_points) > 0:
471            if point is None:
472                point, label = negative_points, negative_labels
473            else:
474                point = np.concatenate([point] + negative_points)
475                label = np.concatenate([label] + negative_labels)
476
477        if (box is not None) and (point is not None):
478            prediction = prompt_based_segmentation.segment_from_box_and_points(
479                predictor, box, point, label, image_embeddings=image_embeddings, i=i
480            ).squeeze()
481        elif (box is not None) and (point is None):
482            prediction = prompt_based_segmentation.segment_from_box(
483                predictor, box, image_embeddings=image_embeddings, i=i
484            ).squeeze()
485        else:
486            prediction = prompt_based_segmentation.segment_from_points(
487                predictor, point, label, image_embeddings=image_embeddings, i=i
488            ).squeeze()
489
490        seg[prediction] = seg_id
491
492    return seg
493
494
495def prompt_segmentation(
496    predictor, points, labels, boxes, masks, shape, multiple_box_prompts,
497    image_embeddings=None, i=None, box_extension=0, batched=None, previous_segmentation=None,
498):
499    """@private"""
500    assert len(points) == len(labels)
501    have_points = len(points) > 0
502    have_boxes = len(boxes) > 0
503
504    # No prompts were given, return None.
505    if not have_points and not have_boxes:
506        return
507
508    # Batched interactive segmentation.
509    elif batched:
510        assert previous_segmentation is not None
511        seg = _batched_interactive_segmentation(
512            predictor, points, labels, boxes, image_embeddings, i, previous_segmentation
513        )
514
515    # Box and point prompts were given.
516    elif have_points and have_boxes:
517        if len(boxes) > 1:
518            print("You have provided point prompts and more than one box prompt.")
519            print("This setting is currently not supported.")
520            print("When providing both points and prompts you can only segment one object at a time.")
521            return
522        mask = masks[0]
523        if mask is None:
524            seg = prompt_based_segmentation.segment_from_box_and_points(
525                predictor, boxes[0], points, labels, image_embeddings=image_embeddings, i=i
526            ).squeeze()
527        else:
528            seg = prompt_based_segmentation.segment_from_mask(
529                predictor, mask, box=boxes[0], points=points, labels=labels, image_embeddings=image_embeddings, i=i
530            ).squeeze()
531
532    # Only point prompts were given.
533    elif have_points and not have_boxes:
534        seg = prompt_based_segmentation.segment_from_points(
535            predictor, points, labels, image_embeddings=image_embeddings, i=i
536        ).squeeze()
537
538    # Only box prompts were given.
539    elif not have_points and have_boxes:
540        seg = np.zeros(shape, dtype="uint32")
541
542        if len(boxes) > 1 and not multiple_box_prompts:
543            print("You have provided more than one box annotation. This is not yet supported in the 3d annotator.")
544            print("You can only segment one object at a time in 3d.")
545            return
546
547        # Batch this?
548        for seg_id, (box, mask) in enumerate(zip(boxes, masks), 1):
549            if mask is None:
550                prediction = prompt_based_segmentation.segment_from_box(
551                    predictor, box, image_embeddings=image_embeddings, i=i
552                ).squeeze()
553            else:
554                prediction = prompt_based_segmentation.segment_from_mask(
555                    predictor, mask, box=box, image_embeddings=image_embeddings, i=i,
556                    box_extension=box_extension,
557                ).squeeze()
558            seg[prediction] = seg_id
559
560    return seg
561
562
563def _compute_movement(seg, t0, t1):
564
565    def compute_center(t):
566        # computation with center of mass
567        center = np.where(seg[t] == 1)
568        center = np.array([np.mean(center[0]), np.mean(center[1])])
569        return center
570
571    center0 = compute_center(t0)
572    center1 = compute_center(t1)
573
574    move = center0 - center1
575    return move.astype("float64")
576
577
578def _shift_object(mask, motion_model):
579    mask_shifted = np.zeros_like(mask)
580    shift(mask, motion_model, output=mask_shifted, order=0, prefilter=False)
581    return mask_shifted
582
583
584def track_from_prompts(
585    point_prompts, box_prompts, seg, predictor, slices, image_embeddings,
586    stop_upper, threshold, projection, motion_smoothing=0.5, box_extension=0, update_progress=None,
587):
588    """@private
589    """
590    use_box, use_mask, use_points, use_single_point = _validate_projection(projection)
591
592    if update_progress is None:
593        def update_progress(*args):
594            pass
595
596    # shift the segmentation based on the motion model and update the motion model
597    def _update_motion_model(seg, t, t0, motion_model):
598        if t in (t0, t0 + 1):  # this is the first or second frame, we don't have a motion yet
599            pass
600        elif t == t0 + 2:  # this the third frame, we initialize the motion model
601            current_move = _compute_movement(seg, t - 1, t - 2)
602            motion_model = current_move
603        else:  # we already have a motion model and update it
604            current_move = _compute_movement(seg, t - 1, t - 2)
605            alpha = motion_smoothing
606            motion_model = alpha * motion_model + (1 - alpha) * current_move
607
608        return motion_model
609
610    has_division = False
611    motion_model = None
612    verbose = False
613
614    t0 = int(slices.min())
615    t = t0 + 1
616    while True:
617
618        # update the motion model
619        motion_model = _update_motion_model(seg, t, t0, motion_model)
620
621        # use the segmentation from prompts if we are in a slice with prompts
622        if t in slices:
623            seg_prev = None
624            seg_t = seg[t]
625            # currently using the box layer doesn't work for keeping track of the track state
626            # track_state = prompt_layers_to_state(point_prompts, box_prompts, t)
627            track_state = prompt_layer_to_state(point_prompts, t)
628
629        # otherwise project the mask (under the motion model) and segment the next slice from the mask
630        else:
631            if verbose:
632                print(f"Tracking object in frame {t} with movement {motion_model}")
633
634            seg_prev = seg[t - 1]
635            # shift the segmentation according to the motion model
636            if motion_model is not None:
637                seg_prev = _shift_object(seg_prev, motion_model)
638
639            seg_t = prompt_based_segmentation.segment_from_mask(
640                predictor, seg_prev, image_embeddings=image_embeddings, i=t,
641                use_mask=use_mask, use_box=use_box, use_points=use_points,
642                box_extension=box_extension, use_single_point=use_single_point,
643            )
644            track_state = "track"
645
646            # are we beyond the last slice with prompt?
647            # if no: we continue tracking because we know we need to connect to a future frame
648            # if yes: we only continue tracking if overlaps are above the threshold
649            if t < slices[-1]:
650                seg_prev = None
651
652            update_progress(1)
653
654        if (threshold is not None) and (seg_prev is not None):
655            iou = util.compute_iou(seg_prev, seg_t)
656            if iou < threshold:
657                msg = f"Segmentation stopped at frame {t} due to IOU {iou} < {threshold}."
658                print(msg)
659                break
660
661        # stop if we have a division
662        if track_state == "division":
663            has_division = True
664            break
665
666        seg[t] = seg_t
667        t += 1
668
669        # stop tracking if we have stop upper set (i.e. single negative point was set to indicate stop track)
670        if t == slices[-1] and stop_upper:
671            break
672
673        # stop if we are at the last slce
674        if t == seg.shape[0]:
675            break
676
677    return seg, has_division
678
679
680def _sync_embedding_widget(widget, model_type, save_path, checkpoint_path, device, tile_shape, halo):
681
682    # Update the index for model family, eg. 'Natural Images (SAM)', 'Light Microscopy', etc.
683    supported_dropdown_maps = {
684        "lm": "Light Microscopy",
685        "em_organelles": "Electron Microscopy",
686        "medical_imaging": "Medical Imaging",
687        "histopathology": "Histopathology",
688    }
689
690    model_family = "Natural Images (SAM)"  # If no suffix patterns match, stick to 'Natural Images (SAM)' family.
691    for k, v in supported_dropdown_maps.items():
692        if model_type.endswith(k):
693            model_family = v
694            break
695
696    index = widget.model_family_dropdown.findText(model_family)
697    if index > 0:
698        widget.model_family_dropdown.setCurrentIndex(index)
699
700    # Update the index for model size, eg. 'base', 'tiny', etc.
701    size_map = {"t": "tiny", "b": "base", "l": "large", "h": "huge"}
702    model_size = size_map[model_type[4]]
703
704    index = widget.model_size_dropdown.findText(model_size)
705    if index > 0:
706        widget.model_size_dropdown.setCurrentIndex(index)
707
708    if save_path is not None and isinstance(save_path, str):
709        widget.embeddings_save_path_param.setText(str(save_path))
710
711    if checkpoint_path is not None:
712        widget.custom_weights_param.setText(str(checkpoint_path))
713
714    if device is not None:
715        widget.device = device
716        index = widget.device_dropdown.findText(device)
717        widget.device_dropdown.setCurrentIndex(index)
718
719    if tile_shape is not None:
720        widget.tile_x_param.setValue(tile_shape[0])
721        widget.tile_y_param.setValue(tile_shape[1])
722
723    if halo is not None:
724        widget.halo_x_param.setValue(halo[0])
725        widget.halo_y_param.setValue(halo[1])
726
727
728# Read parameters from checkpoint path if it is given instead.
729def _sync_autosegment_widget(widget, model_type, checkpoint_path, update_decoder=None):
730    if update_decoder is not None:
731        widget._reset_segmentation_mode(update_decoder)
732
733    if widget.with_decoder:
734        settings = model_settings.AIS_SETTINGS.get(model_type, {})
735        params = ("center_distance_thresh", "boundary_distance_thresh")
736        for param in params:
737            if param in settings:
738                getattr(widget, f"{param}_param").setValue(settings[param])
739    else:
740        settings = model_settings.AMG_SETTINGS.get(model_type, {})
741        params = ("pred_iou_thresh", "stability_score_thresh", "min_object_size")
742        for param in params:
743            if param in settings:
744                getattr(widget, f"{param}_param").setValue(settings[param])
745
746
747# Read parameters from checkpoint path if it is given instead.
748def _sync_ndsegment_widget(widget, model_type, checkpoint_path):
749    settings = model_settings.ND_SEGMENT_SETTINGS.get(model_type, {})
750
751    if "projection_mode" in settings:
752        projection_mode = settings["projection_mode"]
753        widget.projection = projection_mode
754        index = widget.projection_dropdown.findText(projection_mode)
755        if index > 0:
756            widget.projection_dropdown.setCurrentIndex(index)
757
758    params = ("iou_threshold", "box_extension")
759    for param in params:
760        if param in settings:
761            getattr(widget, f"{param}_param").setValue(settings[param])
762
763
764def _load_amg_state(embedding_path):
765    if embedding_path is None or not os.path.exists(embedding_path):
766        return {"cache_folder": None}
767
768    cache_folder = os.path.join(embedding_path, "amg_state")
769    os.makedirs(cache_folder, exist_ok=True)
770    amg_state = {"cache_folder": cache_folder}
771
772    state_paths = glob(os.path.join(cache_folder, "*.pkl"))
773    for path in state_paths:
774        with open(path, "rb") as f:
775            state = pickle.load(f)
776        i = int(Path(path).stem.split("-")[-1])
777        amg_state[i] = state
778    return amg_state
779
780
781def _load_is_state(embedding_path):
782    if embedding_path is None or not os.path.exists(embedding_path):
783        return {"cache_path": None}
784
785    cache_path = os.path.join(embedding_path, "is_state.h5")
786    is_state = {"cache_path": cache_path}
787
788    with h5py.File(cache_path, "a") as f:
789        for name, g in f.items():
790            i = int(name.split("-")[-1])
791            state = {
792                "foreground": g["foreground"][:],
793                "boundary_distances": g["boundary_distances"][:],
794                "center_distances": g["center_distances"][:],
795            }
796            is_state[i] = state
797
798    return is_state
def point_layer_to_prompts( layer: napari.layers.points.points.Points, i=None, track_id=None, with_stop_annotation=True) -> Optional[Tuple[numpy.ndarray, numpy.ndarray]]:
160def point_layer_to_prompts(
161    layer: napari.layers.Points, i=None, track_id=None, with_stop_annotation=True,
162) -> Optional[Tuple[np.ndarray, np.ndarray]]:
163    """Extract point prompts for SAM from a napari point layer.
164
165    Args:
166        layer: The point layer from which to extract the prompts.
167        i: Index for the data (required for 3d or timeseries data).
168        track_id: Id of the current track (required for tracking data).
169        with_stop_annotation: Whether a single negative point will be interpreted
170            as stop annotation or just returned as normal prompt.
171
172    Returns:
173        The point coordinates for the prompts.
174        The labels (positive or negative / 1 or 0) for the prompts.
175    """
176
177    points = layer.data
178    labels = layer.properties["label"]
179    assert len(points) == len(labels)
180
181    if i is None:
182        assert points.shape[1] == 2, f"{points.shape}"
183        this_points, this_labels = points, labels
184    else:
185        assert points.shape[1] == 3, f"{points.shape}"
186        mask = np.round(points[:, 0]) == i
187        this_points = points[mask][:, 1:]
188        this_labels = labels[mask]
189    assert len(this_points) == len(this_labels)
190
191    if track_id is not None:
192        assert i is not None
193        track_ids = np.array(list(map(int, layer.properties["track_id"])))[mask]
194        track_id_mask = track_ids == track_id
195        this_labels, this_points = this_labels[track_id_mask], this_points[track_id_mask]
196    assert len(this_points) == len(this_labels)
197
198    this_labels = np.array([1 if label == "positive" else 0 for label in this_labels])
199    # a single point with a negative label is interpreted as 'stop' signal
200    # in this case we return None
201    if with_stop_annotation and (len(this_points) == 1 and this_labels[0] == 0):
202        return None
203
204    return this_points, this_labels

Extract point prompts for SAM from a napari point layer.

Arguments:
  • layer: The point layer from which to extract the prompts.
  • i: Index for the data (required for 3d or timeseries data).
  • track_id: Id of the current track (required for tracking data).
  • with_stop_annotation: Whether a single negative point will be interpreted as stop annotation or just returned as normal prompt.
Returns:

The point coordinates for the prompts. The labels (positive or negative / 1 or 0) for the prompts.

def shape_layer_to_prompts( layer: napari.layers.shapes.shapes.Shapes, shape: Tuple[int, int], i=None, track_id=None) -> Tuple[List[numpy.ndarray], List[Optional[numpy.ndarray]]]:
207def shape_layer_to_prompts(
208    layer: napari.layers.Shapes, shape: Tuple[int, int], i=None, track_id=None
209) -> Tuple[List[np.ndarray], List[Optional[np.ndarray]]]:
210    """Extract prompts for SAM from a napari shape layer.
211
212    Extracts the bounding box for 'rectangle' shapes and the bounding box and corresponding mask
213    for 'ellipse' and 'polygon' shapes.
214
215    Args:
216        prompt_layer: The napari shape layer.
217        shape: The image shape.
218        i: Index for the data (required for 3d or timeseries data).
219        track_id: Id of the current track (required for tracking data).
220
221    Returns:
222        The box prompts.
223        The mask prompts.
224    """
225
226    def _to_prompts(shape_data, shape_types):
227        boxes, masks = [], []
228
229        for data, type_ in zip(shape_data, shape_types):
230
231            if type_ == "rectangle":
232                boxes.append(data)
233                masks.append(None)
234
235            elif type_ == "ellipse":
236                boxes.append(data)
237                center = np.mean(data, axis=0)
238                radius_r = ((data[2] - data[1]) / 2)[0]
239                radius_c = ((data[1] - data[0]) / 2)[1]
240                rr, cc = draw.ellipse(center[0], center[1], radius_r, radius_c, shape=shape)
241                mask = np.zeros(shape, dtype=bool)
242                mask[rr, cc] = 1
243                masks.append(mask)
244
245            elif type_ == "polygon":
246                boxes.append(data)
247                rr, cc = draw.polygon(data[:, 0], data[:, 1], shape=shape)
248                mask = np.zeros(shape, dtype=bool)
249                mask[rr, cc] = 1
250                masks.append(mask)
251
252            else:
253                warnings.warn(f"Shape type {type_} is not supported and will be ignored.")
254
255        # map to correct box format
256        boxes = [
257            np.array([box[:, 0].min(), box[:, 1].min(), box[:, 0].max(), box[:, 1].max()]) for box in boxes
258        ]
259        return boxes, masks
260
261    shape_data, shape_types = layer.data, layer.shape_type
262    assert len(shape_data) == len(shape_types)
263    if len(shape_data) == 0:
264        return [], []
265
266    if i is not None:
267        if track_id is None:
268            prompt_selection = [j for j, data in enumerate(shape_data) if (data[:, 0] == i).all()]
269        else:
270            track_ids = np.array(list(map(int, layer.properties["track_id"])))
271            prompt_selection = [
272                j for j, (data, this_track_id) in enumerate(zip(shape_data, track_ids))
273                if ((data[:, 0] == i).all() and this_track_id == track_id)
274            ]
275
276        shape_data = [shape_data[j][:, 1:] for j in prompt_selection]
277        shape_types = [shape_types[j] for j in prompt_selection]
278
279    boxes, masks = _to_prompts(shape_data, shape_types)
280    return boxes, masks

Extract prompts for SAM from a napari shape layer.

Extracts the bounding box for 'rectangle' shapes and the bounding box and corresponding mask for 'ellipse' and 'polygon' shapes.

Arguments:
  • prompt_layer: The napari shape layer.
  • shape: The image shape.
  • i: Index for the data (required for 3d or timeseries data).
  • track_id: Id of the current track (required for tracking data).
Returns:

The box prompts. The mask prompts.

def prompt_layer_to_state(prompt_layer: napari.layers.points.points.Points, i: int) -> str:
283def prompt_layer_to_state(prompt_layer: napari.layers.Points, i: int) -> str:
284    """Get the state of the track from a point layer for a given timeframe.
285
286    Only relevant for annotator_tracking.
287
288    Args:
289        prompt_layer: The napari layer.
290        i: Timeframe of the data.
291
292    Returns:
293        The state of this frame (either "division" or "track").
294    """
295    state = prompt_layer.properties["state"]
296
297    points = prompt_layer.data
298    assert points.shape[1] == 3, f"{points.shape}"
299    mask = points[:, 0] == i
300    this_points = points[mask][:, 1:]
301    this_state = state[mask]
302    assert len(this_points) == len(this_state)
303
304    # we set the state to 'division' if at least one point in this frame has a division label
305    if any(st == "division" for st in this_state):
306        return "division"
307    else:
308        return "track"

Get the state of the track from a point layer for a given timeframe.

Only relevant for annotator_tracking.

Arguments:
  • prompt_layer: The napari layer.
  • i: Timeframe of the data.
Returns:

The state of this frame (either "division" or "track").

def prompt_layers_to_state( point_layer: napari.layers.points.points.Points, box_layer: napari.layers.shapes.shapes.Shapes, i: int) -> str:
311def prompt_layers_to_state(point_layer: napari.layers.Points, box_layer: napari.layers.Shapes, i: int) -> str:
312    """Get the state of the track from a point layer and shape layer for a given timeframe.
313
314    Only relevant for annotator_tracking.
315
316    Args:
317        point_layer: The napari point layer.
318        box_layer: The napari box layer.
319        i: Timeframe of the data.
320
321    Returns:
322        The state of this frame (either "division" or "track").
323    """
324    state = point_layer.properties["state"]
325
326    points = point_layer.data
327    assert points.shape[1] == 3, f"{points.shape}"
328    mask = points[:, 0] == i
329    if mask.sum() > 0:
330        this_state = state[mask].tolist()
331    else:
332        this_state = []
333
334    box_states = box_layer.properties["state"]
335    this_box_states = [
336        state for box, state in zip(box_layer.data, box_states)
337        if (box[:, 0] == i).all()
338    ]
339    this_state.extend(this_box_states)
340
341    # we set the state to 'division' if at least one point in this frame has a division label
342    if any(st == "division" for st in this_state):
343        return "division"
344    else:
345        return "track"

Get the state of the track from a point layer and shape layer for a given timeframe.

Only relevant for annotator_tracking.

Arguments:
  • point_layer: The napari point layer.
  • box_layer: The napari box layer.
  • i: Timeframe of the data.
Returns:

The state of this frame (either "division" or "track").