micro_sam.bioimageio.model_export

  1import os
  2import tempfile
  3from pathlib import Path
  4from typing import Optional, Union
  5
  6import xarray
  7import numpy as np
  8import matplotlib.pyplot as plt
  9
 10import torch
 11
 12import bioimageio.core
 13import bioimageio.spec.model.v0_5 as spec
 14from bioimageio.spec import save_bioimageio_package
 15from bioimageio.core.digest_spec import create_sample_for_model
 16
 17from .. import util
 18from ..prompt_generators import PointAndBoxPromptGenerator
 19from ..evaluation.model_comparison import _enhance_image, _overlay_outline, _overlay_box
 20from ..prompt_based_segmentation import _compute_logits_from_mask
 21from .predictor_adaptor import PredictorAdaptor
 22
 23
 24DEFAULTS = {
 25    "authors": [
 26        spec.Author(name="Anwai Archit", affiliation="University Goettingen", github_user="anwai98"),
 27        spec.Author(name="Constantin Pape", affiliation="University Goettingen", github_user="constantinpape"),
 28    ],
 29    "description": "Finetuned Segment Anything Model for Microscopy",
 30    "cite": [
 31        spec.CiteEntry(
 32            text="Archit et al. Segment Anything for Microscopy",
 33            doi=spec.Doi("10.1038/s41592-024-02580-4")
 34        ),
 35    ],
 36    "tags": ["segment-anything", "instance-segmentation"],
 37}
 38
 39# Reference: https://github.com/bioimage-io/spec-bioimage-io/commit/39d343681d427ec93cf69eef7597d9eb9678deb1#diff-0bbdaa8196fa31f945afabcf04a4295ff098f1f24400ef9e59b0f684d411905eL269  # noqa
 40# We had this parameter in bioimageio.spec. This has been removed. We just make a copy of the same parameter.
 41ARBITRARY_SIZE = spec.ParameterizedSize(min=1, step=1)
 42
 43
 44def _create_test_inputs_and_outputs(image, labels, model_type, checkpoint_path, tmp_dir):
 45
 46    # For now we just generate a single box prompt here, but we could also generate more input prompts.
 47    generator = PointAndBoxPromptGenerator(
 48        n_positive_points=1,
 49        n_negative_points=2,
 50        dilation_strength=2,
 51        get_point_prompts=True,
 52        get_box_prompts=True,
 53    )
 54    centers, bounding_boxes = util.get_centers_and_bounding_boxes(labels)
 55    masks = util.segmentation_to_one_hot(labels.astype("int64"), segmentation_ids=[1, 2])  # type: ignore
 56    point_prompts, point_labels, box_prompts, _ = generator(masks, [bounding_boxes[1], bounding_boxes[2]])
 57
 58    box_prompts = box_prompts.numpy()[None]
 59    point_prompts = point_prompts.numpy()[None]
 60    point_labels = point_labels.numpy()[None]
 61
 62    # Generate logits from the two
 63    mask_prompts = np.stack(
 64        [_compute_logits_from_mask(labels == 1), _compute_logits_from_mask(labels == 2)]
 65    )[None]
 66
 67    predictor = PredictorAdaptor(model_type=model_type)
 68    predictor.load_state_dict(torch.load(checkpoint_path))
 69
 70    input_ = util._to_image(image).transpose(2, 0, 1)[None]
 71    image_path = os.path.join(tmp_dir, "input.npy")
 72    np.save(image_path, input_)
 73
 74    masks, scores, embeddings = predictor(
 75        image=torch.from_numpy(input_),
 76        embeddings=None,
 77        box_prompts=torch.from_numpy(box_prompts),
 78        point_prompts=torch.from_numpy(point_prompts),
 79        point_labels=torch.from_numpy(point_labels),
 80        mask_prompts=torch.from_numpy(mask_prompts),
 81    )
 82
 83    box_prompt_path = os.path.join(tmp_dir, "box_prompts.npy")
 84    point_prompt_path = os.path.join(tmp_dir, "point_prompts.npy")
 85    point_label_path = os.path.join(tmp_dir, "point_labels.npy")
 86    mask_prompt_path = os.path.join(tmp_dir, "mask_prompts.npy")
 87    np.save(box_prompt_path, box_prompts.astype("int64"))
 88    np.save(point_prompt_path, point_prompts)
 89    np.save(point_label_path, point_labels)
 90    np.save(mask_prompt_path, mask_prompts)
 91
 92    mask_path = os.path.join(tmp_dir, "mask.npy")
 93    score_path = os.path.join(tmp_dir, "scores.npy")
 94    embed_path = os.path.join(tmp_dir, "embeddings.npy")
 95    np.save(mask_path, masks.numpy())
 96    np.save(score_path, scores.numpy())
 97    np.save(embed_path, embeddings.numpy())
 98
 99    inputs = {
100        "image": image_path,
101        "box_prompts": box_prompt_path,
102        "point_prompts": point_prompt_path,
103        "point_labels": point_label_path,
104        "mask_prompts": mask_prompt_path,
105    }
106    outputs = {"mask": mask_path, "score": score_path, "embeddings": embed_path}
107    return inputs, outputs
108
109
110def _write_documentation(doc, model_type, tmp_dir):
111    tmp_doc_path = os.path.join(tmp_dir, "documentation.md")
112
113    if doc is None:
114        with open(tmp_doc_path, "w") as f:
115            f.write("# Segment Anything for Microscopy\n")
116            f.write("We extend Segment Anything, a vision foundation model for image segmentation ")
117            f.write("by training specialized models for microscopy data.\n")
118        return tmp_doc_path
119
120    elif os.path.exists(doc):
121        return doc
122
123    else:
124        with open(tmp_doc_path, "w") as f:
125            f.write(doc)
126        return tmp_doc_path
127
128
129def _get_checkpoint(model_type, checkpoint_path, tmp_dir):
130    # If we don't have a checkpoint we get the corresponding model from the registry.
131    if checkpoint_path is None:
132        model_registry = util.models()
133        checkpoint_path = model_registry.fetch(model_type)
134        return checkpoint_path, None
135
136    # Otherwise we have to load the checkpoint to see if it is the state dict of an encoder,
137    # or the checkpoint for a custom SAM model.
138    state, model_state = util._load_checkpoint(checkpoint_path)
139
140    if "model_state" in state:  # This is a finetuning checkpoint -> we have to resave the state.
141        new_checkpoint_path = os.path.join(tmp_dir, f"{model_type}.pt")
142        torch.save(model_state, new_checkpoint_path)
143
144        # We may also have an instance segmentation decoder in that case.
145        # If we have it we also resave this one and return it.
146        if "decoder_state" in state:
147            decoder_path = os.path.join(tmp_dir, f"{model_type}_decoder.pt")
148            decoder_state = state["decoder_state"]
149            torch.save(decoder_state, decoder_path)
150        else:
151            decoder_path = None
152
153        return new_checkpoint_path, decoder_path
154
155    else:  # This is a SAM encoder state -> we don't have to resave.
156        return checkpoint_path, None
157
158
159# TODO: Update this with our latest yaml file updates.
160def _write_dependencies(dependency_file, require_mobile_sam):
161    content = """name: sam
162channels:
163    - pytorch
164    - conda-forge
165dependencies:
166    - segment-anything"""
167    if require_mobile_sam:
168        content += """
169    - timm
170    - pip:
171        - git+https://github.com/ChaoningZhang/MobileSAM.git"""
172    with open(dependency_file, "w") as f:
173        f.write(content)
174
175
176def _generate_covers(input_paths, result_paths, tmp_dir):
177    image = np.load(input_paths["image"]).squeeze()
178    prompts = np.load(input_paths["box_prompts"])
179    mask = np.load(result_paths["mask"])
180
181    # create the image overlay
182    if image.ndim == 2:
183        overlay = np.stack([image, image, image]).transpose((1, 2, 0))
184    elif image.shape[0] == 3:
185        overlay = image.transpose((1, 2, 0))
186    else:
187        overlay = image
188    overlay = _enhance_image(overlay.astype("float32"))
189
190    # overlay the mask as outline
191    overlay = _overlay_outline(overlay, mask[0, 0, 0], outline_dilation=2)
192
193    # overlay the bounding box prompt
194    prompt = prompts[0, 0][[1, 0, 3, 2]]
195    prompt = np.array([prompt[:2], prompt[2:]])
196    overlay = _overlay_box(overlay, prompt, outline_dilation=4)
197
198    # write  the cover image
199    fig, ax = plt.subplots(1)
200    ax.axis("off")
201    ax.imshow(overlay.astype("uint8"))
202    cover_path = os.path.join(tmp_dir, "cover.jpeg")
203    plt.savefig(cover_path, bbox_inches="tight")
204    plt.close()
205
206    covers = [cover_path]
207    return covers
208
209
210def _check_model(model_description, input_paths, result_paths):
211    # Load inputs.
212    image = xarray.DataArray(np.load(input_paths["image"]), dims=("batch", "channel", "y", "x"))
213    embeddings = xarray.DataArray(np.load(result_paths["embeddings"]), dims=("batch", "channel", "y", "x"))
214    box_prompts = xarray.DataArray(np.load(input_paths["box_prompts"]), dims=("batch", "object", "channel"))
215    point_prompts = xarray.DataArray(
216        np.load(input_paths["point_prompts"]), dims=("batch", "object", "point", "channel")
217    )
218    point_labels = xarray.DataArray(np.load(input_paths["point_labels"]), dims=("batch", "object", "point"))
219    mask_prompts = xarray.DataArray(np.load(input_paths["mask_prompts"]), dims=("batch", "object", "channel", "y", "x"))
220
221    # Load outputs.
222    mask = np.load(result_paths["mask"])
223
224    # Match the device used to generate the reference outputs.
225    with bioimageio.core.create_prediction_pipeline(model_description, devices=["cpu"]) as pp:
226
227        # Check with all prompts. We only check the result for this setting,
228        # because this was used to generate the test data.
229        sample = create_sample_for_model(
230            model=model_description,
231            inputs={
232                "image": image,
233                "box_prompts": box_prompts,
234                "point_prompts": point_prompts,
235                "point_labels": point_labels,
236                "mask_prompts": mask_prompts,
237                "embeddings": embeddings,
238            },
239        )
240        prediction = pp.predict_sample_without_blocking(sample)
241
242        predicted_mask = prediction.members["masks"].data
243        assert predicted_mask.shape == mask.shape
244        assert np.allclose(mask, predicted_mask)
245
246        # Run the checks with partial prompts.
247        prompt_kwargs = [
248            # With boxes.
249            {"box_prompts": box_prompts},
250            # With point prompts.
251            {"point_prompts": point_prompts, "point_labels": point_labels},
252            # With masks.
253            {"mask_prompts": mask_prompts},
254            # With boxes and points.
255            {"box_prompts": box_prompts, "point_prompts": point_prompts, "point_labels": point_labels},
256            # With boxes and masks.
257            {"box_prompts": box_prompts, "mask_prompts": mask_prompts},
258            # With points and masks.
259            {"mask_prompts": mask_prompts, "point_prompts": point_prompts, "point_labels": point_labels},
260        ]
261
262        for kwargs in prompt_kwargs:
263            sample = create_sample_for_model(
264                model=model_description, inputs={"image": image, "embeddings": embeddings, **kwargs},
265            )
266            prediction = pp.predict_sample_without_blocking(sample)
267            predicted_mask = prediction.members["masks"].data
268            assert predicted_mask.shape == mask.shape
269
270
271def export_sam_model(
272    image: np.ndarray,
273    label_image: np.ndarray,
274    model_type: str,
275    name: str,
276    output_path: Union[str, os.PathLike],
277    checkpoint_path: Optional[Union[str, os.PathLike]] = None,
278    **kwargs
279) -> None:
280    """Export SAM model to BioImage.IO model format.
281
282    The exported model can be uploaded to [bioimage.io](https://bioimage.io/#/) and
283    be used in tools that support the BioImage.IO model format.
284
285    Args:
286        image: The image for generating test data.
287        label_image: The segmentation corresponding to `image`.
288            It is used to derive prompt inputs for the model.
289        model_type: The type of the SAM model.
290        name: The name of the exported model.
291        output_path: Where the exported model is saved.
292        checkpoint_path: Optional checkpoint for loading the SAM model.
293    """
294    with tempfile.TemporaryDirectory() as tmp_dir:
295        checkpoint_path, decoder_path = _get_checkpoint(model_type, checkpoint_path, tmp_dir)
296        input_paths, result_paths = _create_test_inputs_and_outputs(
297            image, label_image, model_type, checkpoint_path, tmp_dir,
298        )
299        input_descriptions = [
300            # First input: the image data.
301            spec.InputTensorDescr(
302                id=spec.TensorId("image"),
303                axes=[
304                    spec.BatchAxis(size=1),
305                    # NOTE: to support 1 and 3 channels we can add another preprocessing.
306                    # Best solution: Have a pre-processing for this! (1C -> RGB)
307                    spec.ChannelAxis(channel_names=[spec.Identifier(cname) for cname in "RGB"]),
308                    spec.SpaceInputAxis(id=spec.AxisId("y"), size=ARBITRARY_SIZE),
309                    spec.SpaceInputAxis(id=spec.AxisId("x"), size=ARBITRARY_SIZE),
310                ],
311                test_tensor=spec.FileDescr(source=input_paths["image"]),
312                data=spec.IntervalOrRatioDataDescr(type="uint8")
313            ),
314
315            # Second input: the box prompts (optional)
316            spec.InputTensorDescr(
317                id=spec.TensorId("box_prompts"),
318                optional=True,
319                axes=[
320                    spec.BatchAxis(size=1),
321                    spec.IndexInputAxis(
322                        id=spec.AxisId("object"),
323                        size=ARBITRARY_SIZE
324                    ),
325                    spec.ChannelAxis(channel_names=[spec.Identifier(bname) for bname in "hwxy"]),
326                ],
327                test_tensor=spec.FileDescr(source=input_paths["box_prompts"]),
328                data=spec.IntervalOrRatioDataDescr(type="int64")
329            ),
330
331            # Third input: the point prompt coordinates (optional)
332            spec.InputTensorDescr(
333                id=spec.TensorId("point_prompts"),
334                optional=True,
335                axes=[
336                    spec.BatchAxis(size=1),
337                    spec.IndexInputAxis(
338                        id=spec.AxisId("object"),
339                        size=ARBITRARY_SIZE
340                    ),
341                    spec.IndexInputAxis(
342                        id=spec.AxisId("point"),
343                        size=ARBITRARY_SIZE
344                    ),
345                    spec.ChannelAxis(channel_names=[spec.Identifier(bname) for bname in "xy"]),
346                ],
347                test_tensor=spec.FileDescr(source=input_paths["point_prompts"]),
348                data=spec.IntervalOrRatioDataDescr(type="int64")
349            ),
350
351            # Fourth input: the point prompt labels (optional)
352            spec.InputTensorDescr(
353                id=spec.TensorId("point_labels"),
354                optional=True,
355                axes=[
356                    spec.BatchAxis(size=1),
357                    spec.IndexInputAxis(
358                        id=spec.AxisId("object"),
359                        size=ARBITRARY_SIZE
360                    ),
361                    spec.IndexInputAxis(
362                        id=spec.AxisId("point"),
363                        size=ARBITRARY_SIZE
364                    ),
365                ],
366                test_tensor=spec.FileDescr(source=input_paths["point_labels"]),
367                data=spec.IntervalOrRatioDataDescr(type="int64")
368            ),
369
370            # Fifth input: the mask prompts (optional)
371            spec.InputTensorDescr(
372                id=spec.TensorId("mask_prompts"),
373                optional=True,
374                axes=[
375                    spec.BatchAxis(size=1),
376                    spec.IndexInputAxis(
377                        id=spec.AxisId("object"),
378                        size=ARBITRARY_SIZE
379                    ),
380                    spec.ChannelAxis(channel_names=["channel"]),
381                    spec.SpaceInputAxis(id=spec.AxisId("y"), size=256),
382                    spec.SpaceInputAxis(id=spec.AxisId("x"), size=256),
383                ],
384                test_tensor=spec.FileDescr(source=input_paths["mask_prompts"]),
385                data=spec.IntervalOrRatioDataDescr(type="float32")
386            ),
387
388            # Sixth input: the image embeddings (optional)
389            spec.InputTensorDescr(
390                id=spec.TensorId("embeddings"),
391                optional=True,
392                axes=[
393                    spec.BatchAxis(size=1),
394                    # NOTE: we currently have to specify all the channel names
395                    # (It would be nice to also support size)
396                    spec.ChannelAxis(channel_names=[spec.Identifier(f"c{i}") for i in range(256)]),
397                    spec.SpaceInputAxis(id=spec.AxisId("y"), size=64),
398                    spec.SpaceInputAxis(id=spec.AxisId("x"), size=64),
399                ],
400                test_tensor=spec.FileDescr(source=result_paths["embeddings"]),
401                data=spec.IntervalOrRatioDataDescr(type="float32")
402            ),
403
404        ]
405
406        output_descriptions = [
407            # First output: The mask predictions.
408            spec.OutputTensorDescr(
409                id=spec.TensorId("masks"),
410                axes=[
411                    spec.BatchAxis(size=1),
412                    # NOTE: we use the data dependent size here to avoid dependency on optional inputs
413                    spec.IndexOutputAxis(
414                        id=spec.AxisId("object"), size=spec.DataDependentSize(),
415                    ),
416                    # NOTE: this could be a 3 once we use multi-masking
417                    spec.ChannelAxis(channel_names=[spec.Identifier("mask")]),
418                    spec.SpaceOutputAxis(
419                        id=spec.AxisId("y"),
420                        size=spec.SizeReference(
421                            tensor_id=spec.TensorId("image"), axis_id=spec.AxisId("y"),
422                        )
423                    ),
424                    spec.SpaceOutputAxis(
425                        id=spec.AxisId("x"),
426                        size=spec.SizeReference(
427                            tensor_id=spec.TensorId("image"), axis_id=spec.AxisId("x"),
428                        )
429                    )
430                ],
431                data=spec.IntervalOrRatioDataDescr(type="uint8"),
432                test_tensor=spec.FileDescr(source=result_paths["mask"])
433            ),
434
435            # The score predictions
436            spec.OutputTensorDescr(
437                id=spec.TensorId("scores"),
438                axes=[
439                    spec.BatchAxis(size=1),
440                    # NOTE: we use the data dependent size here to avoid dependency on optional inputs
441                    spec.IndexOutputAxis(
442                        id=spec.AxisId("object"), size=spec.DataDependentSize(),
443                    ),
444                    # NOTE: this could be a 3 once we use multi-masking
445                    spec.ChannelAxis(channel_names=[spec.Identifier("mask")]),
446                ],
447                data=spec.IntervalOrRatioDataDescr(type="float32"),
448                test_tensor=spec.FileDescr(source=result_paths["score"])
449            ),
450
451            # The image embeddings
452            spec.OutputTensorDescr(
453                id=spec.TensorId("embeddings"),
454                axes=[
455                    spec.BatchAxis(size=1),
456                    spec.ChannelAxis(channel_names=[spec.Identifier(f"c{i}") for i in range(256)]),
457                    spec.SpaceOutputAxis(id=spec.AxisId("y"), size=64),
458                    spec.SpaceOutputAxis(id=spec.AxisId("x"), size=64),
459                ],
460                data=spec.IntervalOrRatioDataDescr(type="float32"),
461                test_tensor=spec.FileDescr(source=result_paths["embeddings"])
462            )
463        ]
464
465        architecture_path = os.path.join(os.path.split(__file__)[0], "predictor_adaptor.py")
466        architecture = spec.ArchitectureFromFileDescr(
467            source=Path(architecture_path),
468            callable="PredictorAdaptor",
469            kwargs={"model_type": model_type}
470        )
471
472        dependency_file = os.path.join(tmp_dir, "environment.yaml")
473        _write_dependencies(dependency_file, require_mobile_sam=model_type.startswith("vit_t"))
474
475        weight_descriptions = spec.WeightsDescr(
476            pytorch_state_dict=spec.PytorchStateDictWeightsDescr(
477                source=Path(checkpoint_path),
478                architecture=architecture,
479                pytorch_version=spec.Version(torch.__version__),
480                dependencies=spec.FileDescr(source=dependency_file),
481            )
482        )
483
484        doc_path = _write_documentation(kwargs.get("documentation", None), model_type, tmp_dir)
485
486        covers = kwargs.get("covers", None)
487        if covers is None:
488            covers = _generate_covers(input_paths, result_paths, tmp_dir)
489        else:
490            assert all(os.path.exists(cov) for cov in covers)
491
492        # the uploader information is only added if explicitly passed
493        extra_kwargs = {}
494        if "id" in kwargs:
495            extra_kwargs["id"] = kwargs["id"]
496        if "id_emoji" in kwargs:
497            extra_kwargs["id_emoji"] = kwargs["id_emoji"]
498        if "uploader" in kwargs:
499            extra_kwargs["uploader"] = kwargs["uploader"]
500        if "version" in kwargs:
501            extra_kwargs["version"] = kwargs["version"]
502
503        if decoder_path is not None:
504            extra_kwargs["attachments"] = [spec.FileDescr(source=decoder_path)]
505
506        model_description = spec.ModelDescr(
507            name=name,
508            inputs=input_descriptions,
509            outputs=output_descriptions,
510            weights=weight_descriptions,
511            description=kwargs.get("description", DEFAULTS["description"]),
512            authors=kwargs.get("authors", DEFAULTS["authors"]),
513            cite=kwargs.get("cite", DEFAULTS["cite"]),
514            license=spec.LicenseId("CC-BY-4.0"),
515            documentation=Path(doc_path),
516            git_repo=spec.HttpUrl("https://github.com/computational-cell-analytics/micro-sam"),
517            tags=kwargs.get("tags", DEFAULTS["tags"]),
518            covers=covers,
519            **extra_kwargs,
520            # TODO write specific settings in the config
521            # dict with yaml values, key must be a str
522            # micro_sam: ...
523            # config=
524        )
525
526        _check_model(model_description, input_paths, result_paths)
527
528        save_bioimageio_package(model_description, output_path=output_path)
DEFAULTS = {'authors': [Author(affiliation='University Goettingen', email=None, orcid=None, name='Anwai Archit', github_user='anwai98'), Author(affiliation='University Goettingen', email=None, orcid=None, name='Constantin Pape', github_user='constantinpape')], 'description': 'Finetuned Segment Anything Model for Microscopy', 'cite': [CiteEntry(text='Archit et al. Segment Anything for Microscopy', doi='10.1038/s41592-024-02580-4', url=None)], 'tags': ['segment-anything', 'instance-segmentation']}
ARBITRARY_SIZE = ParameterizedSize(min=1, step=1)
def export_sam_model( image: numpy.ndarray, label_image: numpy.ndarray, model_type: str, name: str, output_path: Union[str, os.PathLike], checkpoint_path: Union[str, os.PathLike, NoneType] = None, **kwargs) -> None:
272def export_sam_model(
273    image: np.ndarray,
274    label_image: np.ndarray,
275    model_type: str,
276    name: str,
277    output_path: Union[str, os.PathLike],
278    checkpoint_path: Optional[Union[str, os.PathLike]] = None,
279    **kwargs
280) -> None:
281    """Export SAM model to BioImage.IO model format.
282
283    The exported model can be uploaded to [bioimage.io](https://bioimage.io/#/) and
284    be used in tools that support the BioImage.IO model format.
285
286    Args:
287        image: The image for generating test data.
288        label_image: The segmentation corresponding to `image`.
289            It is used to derive prompt inputs for the model.
290        model_type: The type of the SAM model.
291        name: The name of the exported model.
292        output_path: Where the exported model is saved.
293        checkpoint_path: Optional checkpoint for loading the SAM model.
294    """
295    with tempfile.TemporaryDirectory() as tmp_dir:
296        checkpoint_path, decoder_path = _get_checkpoint(model_type, checkpoint_path, tmp_dir)
297        input_paths, result_paths = _create_test_inputs_and_outputs(
298            image, label_image, model_type, checkpoint_path, tmp_dir,
299        )
300        input_descriptions = [
301            # First input: the image data.
302            spec.InputTensorDescr(
303                id=spec.TensorId("image"),
304                axes=[
305                    spec.BatchAxis(size=1),
306                    # NOTE: to support 1 and 3 channels we can add another preprocessing.
307                    # Best solution: Have a pre-processing for this! (1C -> RGB)
308                    spec.ChannelAxis(channel_names=[spec.Identifier(cname) for cname in "RGB"]),
309                    spec.SpaceInputAxis(id=spec.AxisId("y"), size=ARBITRARY_SIZE),
310                    spec.SpaceInputAxis(id=spec.AxisId("x"), size=ARBITRARY_SIZE),
311                ],
312                test_tensor=spec.FileDescr(source=input_paths["image"]),
313                data=spec.IntervalOrRatioDataDescr(type="uint8")
314            ),
315
316            # Second input: the box prompts (optional)
317            spec.InputTensorDescr(
318                id=spec.TensorId("box_prompts"),
319                optional=True,
320                axes=[
321                    spec.BatchAxis(size=1),
322                    spec.IndexInputAxis(
323                        id=spec.AxisId("object"),
324                        size=ARBITRARY_SIZE
325                    ),
326                    spec.ChannelAxis(channel_names=[spec.Identifier(bname) for bname in "hwxy"]),
327                ],
328                test_tensor=spec.FileDescr(source=input_paths["box_prompts"]),
329                data=spec.IntervalOrRatioDataDescr(type="int64")
330            ),
331
332            # Third input: the point prompt coordinates (optional)
333            spec.InputTensorDescr(
334                id=spec.TensorId("point_prompts"),
335                optional=True,
336                axes=[
337                    spec.BatchAxis(size=1),
338                    spec.IndexInputAxis(
339                        id=spec.AxisId("object"),
340                        size=ARBITRARY_SIZE
341                    ),
342                    spec.IndexInputAxis(
343                        id=spec.AxisId("point"),
344                        size=ARBITRARY_SIZE
345                    ),
346                    spec.ChannelAxis(channel_names=[spec.Identifier(bname) for bname in "xy"]),
347                ],
348                test_tensor=spec.FileDescr(source=input_paths["point_prompts"]),
349                data=spec.IntervalOrRatioDataDescr(type="int64")
350            ),
351
352            # Fourth input: the point prompt labels (optional)
353            spec.InputTensorDescr(
354                id=spec.TensorId("point_labels"),
355                optional=True,
356                axes=[
357                    spec.BatchAxis(size=1),
358                    spec.IndexInputAxis(
359                        id=spec.AxisId("object"),
360                        size=ARBITRARY_SIZE
361                    ),
362                    spec.IndexInputAxis(
363                        id=spec.AxisId("point"),
364                        size=ARBITRARY_SIZE
365                    ),
366                ],
367                test_tensor=spec.FileDescr(source=input_paths["point_labels"]),
368                data=spec.IntervalOrRatioDataDescr(type="int64")
369            ),
370
371            # Fifth input: the mask prompts (optional)
372            spec.InputTensorDescr(
373                id=spec.TensorId("mask_prompts"),
374                optional=True,
375                axes=[
376                    spec.BatchAxis(size=1),
377                    spec.IndexInputAxis(
378                        id=spec.AxisId("object"),
379                        size=ARBITRARY_SIZE
380                    ),
381                    spec.ChannelAxis(channel_names=["channel"]),
382                    spec.SpaceInputAxis(id=spec.AxisId("y"), size=256),
383                    spec.SpaceInputAxis(id=spec.AxisId("x"), size=256),
384                ],
385                test_tensor=spec.FileDescr(source=input_paths["mask_prompts"]),
386                data=spec.IntervalOrRatioDataDescr(type="float32")
387            ),
388
389            # Sixth input: the image embeddings (optional)
390            spec.InputTensorDescr(
391                id=spec.TensorId("embeddings"),
392                optional=True,
393                axes=[
394                    spec.BatchAxis(size=1),
395                    # NOTE: we currently have to specify all the channel names
396                    # (It would be nice to also support size)
397                    spec.ChannelAxis(channel_names=[spec.Identifier(f"c{i}") for i in range(256)]),
398                    spec.SpaceInputAxis(id=spec.AxisId("y"), size=64),
399                    spec.SpaceInputAxis(id=spec.AxisId("x"), size=64),
400                ],
401                test_tensor=spec.FileDescr(source=result_paths["embeddings"]),
402                data=spec.IntervalOrRatioDataDescr(type="float32")
403            ),
404
405        ]
406
407        output_descriptions = [
408            # First output: The mask predictions.
409            spec.OutputTensorDescr(
410                id=spec.TensorId("masks"),
411                axes=[
412                    spec.BatchAxis(size=1),
413                    # NOTE: we use the data dependent size here to avoid dependency on optional inputs
414                    spec.IndexOutputAxis(
415                        id=spec.AxisId("object"), size=spec.DataDependentSize(),
416                    ),
417                    # NOTE: this could be a 3 once we use multi-masking
418                    spec.ChannelAxis(channel_names=[spec.Identifier("mask")]),
419                    spec.SpaceOutputAxis(
420                        id=spec.AxisId("y"),
421                        size=spec.SizeReference(
422                            tensor_id=spec.TensorId("image"), axis_id=spec.AxisId("y"),
423                        )
424                    ),
425                    spec.SpaceOutputAxis(
426                        id=spec.AxisId("x"),
427                        size=spec.SizeReference(
428                            tensor_id=spec.TensorId("image"), axis_id=spec.AxisId("x"),
429                        )
430                    )
431                ],
432                data=spec.IntervalOrRatioDataDescr(type="uint8"),
433                test_tensor=spec.FileDescr(source=result_paths["mask"])
434            ),
435
436            # The score predictions
437            spec.OutputTensorDescr(
438                id=spec.TensorId("scores"),
439                axes=[
440                    spec.BatchAxis(size=1),
441                    # NOTE: we use the data dependent size here to avoid dependency on optional inputs
442                    spec.IndexOutputAxis(
443                        id=spec.AxisId("object"), size=spec.DataDependentSize(),
444                    ),
445                    # NOTE: this could be a 3 once we use multi-masking
446                    spec.ChannelAxis(channel_names=[spec.Identifier("mask")]),
447                ],
448                data=spec.IntervalOrRatioDataDescr(type="float32"),
449                test_tensor=spec.FileDescr(source=result_paths["score"])
450            ),
451
452            # The image embeddings
453            spec.OutputTensorDescr(
454                id=spec.TensorId("embeddings"),
455                axes=[
456                    spec.BatchAxis(size=1),
457                    spec.ChannelAxis(channel_names=[spec.Identifier(f"c{i}") for i in range(256)]),
458                    spec.SpaceOutputAxis(id=spec.AxisId("y"), size=64),
459                    spec.SpaceOutputAxis(id=spec.AxisId("x"), size=64),
460                ],
461                data=spec.IntervalOrRatioDataDescr(type="float32"),
462                test_tensor=spec.FileDescr(source=result_paths["embeddings"])
463            )
464        ]
465
466        architecture_path = os.path.join(os.path.split(__file__)[0], "predictor_adaptor.py")
467        architecture = spec.ArchitectureFromFileDescr(
468            source=Path(architecture_path),
469            callable="PredictorAdaptor",
470            kwargs={"model_type": model_type}
471        )
472
473        dependency_file = os.path.join(tmp_dir, "environment.yaml")
474        _write_dependencies(dependency_file, require_mobile_sam=model_type.startswith("vit_t"))
475
476        weight_descriptions = spec.WeightsDescr(
477            pytorch_state_dict=spec.PytorchStateDictWeightsDescr(
478                source=Path(checkpoint_path),
479                architecture=architecture,
480                pytorch_version=spec.Version(torch.__version__),
481                dependencies=spec.FileDescr(source=dependency_file),
482            )
483        )
484
485        doc_path = _write_documentation(kwargs.get("documentation", None), model_type, tmp_dir)
486
487        covers = kwargs.get("covers", None)
488        if covers is None:
489            covers = _generate_covers(input_paths, result_paths, tmp_dir)
490        else:
491            assert all(os.path.exists(cov) for cov in covers)
492
493        # the uploader information is only added if explicitly passed
494        extra_kwargs = {}
495        if "id" in kwargs:
496            extra_kwargs["id"] = kwargs["id"]
497        if "id_emoji" in kwargs:
498            extra_kwargs["id_emoji"] = kwargs["id_emoji"]
499        if "uploader" in kwargs:
500            extra_kwargs["uploader"] = kwargs["uploader"]
501        if "version" in kwargs:
502            extra_kwargs["version"] = kwargs["version"]
503
504        if decoder_path is not None:
505            extra_kwargs["attachments"] = [spec.FileDescr(source=decoder_path)]
506
507        model_description = spec.ModelDescr(
508            name=name,
509            inputs=input_descriptions,
510            outputs=output_descriptions,
511            weights=weight_descriptions,
512            description=kwargs.get("description", DEFAULTS["description"]),
513            authors=kwargs.get("authors", DEFAULTS["authors"]),
514            cite=kwargs.get("cite", DEFAULTS["cite"]),
515            license=spec.LicenseId("CC-BY-4.0"),
516            documentation=Path(doc_path),
517            git_repo=spec.HttpUrl("https://github.com/computational-cell-analytics/micro-sam"),
518            tags=kwargs.get("tags", DEFAULTS["tags"]),
519            covers=covers,
520            **extra_kwargs,
521            # TODO write specific settings in the config
522            # dict with yaml values, key must be a str
523            # micro_sam: ...
524            # config=
525        )
526
527        _check_model(model_description, input_paths, result_paths)
528
529        save_bioimageio_package(model_description, output_path=output_path)

Export SAM model to BioImage.IO model format.

The exported model can be uploaded to bioimage.io and be used in tools that support the BioImage.IO model format.

Arguments:
  • image: The image for generating test data.
  • label_image: The segmentation corresponding to image. It is used to derive prompt inputs for the model.
  • model_type: The type of the SAM model.
  • name: The name of the exported model.
  • output_path: Where the exported model is saved.
  • checkpoint_path: Optional checkpoint for loading the SAM model.