micro_sam.bioimageio.model_export
1import os 2import shutil 3import tempfile 4from pathlib import Path 5from typing import Optional, Union 6 7import xarray 8import numpy as np 9import matplotlib.pyplot as plt 10 11import torch 12 13import bioimageio.core 14import bioimageio.spec.model.v0_5 as spec 15from bioimageio.spec import save_bioimageio_package 16from bioimageio.core.digest_spec import create_sample_for_model 17 18from .. import util 19from ..prompt_generators import PointAndBoxPromptGenerator 20from ..evaluation.model_comparison import _enhance_image, _overlay_outline, _overlay_box 21from ..prompt_based_segmentation import _compute_logits_from_mask 22from .predictor_adaptor import PredictorAdaptor 23 24 25DEFAULTS = { 26 "authors": [ 27 spec.Author(name="Anwai Archit", affiliation="University Goettingen", github_user="anwai98"), 28 spec.Author(name="Constantin Pape", affiliation="University Goettingen", github_user="constantinpape"), 29 ], 30 "description": "Finetuned Segment Anything Model for Microscopy", 31 "cite": [ 32 spec.CiteEntry( 33 text="Archit et al. Segment Anything for Microscopy", 34 doi=spec.Doi("10.1038/s41592-024-02580-4") 35 ), 36 ], 37 "tags": ["segment-anything", "instance-segmentation"], 38} 39 40# Reference: https://github.com/bioimage-io/spec-bioimage-io/commit/39d343681d427ec93cf69eef7597d9eb9678deb1#diff-0bbdaa8196fa31f945afabcf04a4295ff098f1f24400ef9e59b0f684d411905eL269 # noqa 41# We had this parameter in bioimageio.spec. This has been removed. We just make a copy of the same parameter. 42ARBITRARY_SIZE = spec.ParameterizedSize(min=1, step=1) 43 44 45def _get_architecture_model_type(model_type): 46 # Derived models use their base SAM architecture. 47 return model_type[:5] 48 49 50def _create_test_inputs_and_outputs(image, labels, model_type, checkpoint_path, tmp_dir, with_decoder): 51 52 predictor = PredictorAdaptor(model_type=_get_architecture_model_type(model_type)) 53 predictor.load_state_dict(torch.load(checkpoint_path, map_location="cpu", weights_only=True)) 54 # Match the evaluation mode that BioImageIO uses. 55 predictor.eval() 56 57 input_ = util._to_image(image).transpose(2, 0, 1)[None] 58 image_path = os.path.join(tmp_dir, "input.npy") 59 np.save(image_path, input_) 60 61 if with_decoder: 62 # Test automatic instance segmentation through prompt-free inference. 63 with torch.no_grad(): 64 masks, scores, embeddings = predictor(image=torch.from_numpy(input_)) 65 if masks.shape[1] == 0: 66 raise RuntimeError( 67 "Automatic instance segmentation did not find any objects in the test image. " 68 "The exported model would fail its test procedure, which requires at least one object " 69 "in the mask output. Please use a test image in which the model finds instances." 70 ) 71 inputs = {"image": image_path} 72 else: 73 # For now we just generate a single box prompt here, but we could also generate more input prompts. 74 generator = PointAndBoxPromptGenerator( 75 n_positive_points=1, 76 n_negative_points=2, 77 dilation_strength=2, 78 get_point_prompts=True, 79 get_box_prompts=True, 80 ) 81 centers, bounding_boxes = util.get_centers_and_bounding_boxes(labels) 82 masks = util.segmentation_to_one_hot(labels.astype("int64"), segmentation_ids=[1, 2]) # type: ignore 83 point_prompts, point_labels, box_prompts, _ = generator(masks, [bounding_boxes[1], bounding_boxes[2]]) 84 85 box_prompts = box_prompts.numpy()[None] 86 point_prompts = point_prompts.numpy()[None] 87 point_labels = point_labels.numpy()[None] 88 89 # Generate logits from the two 90 mask_prompts = np.stack( 91 [_compute_logits_from_mask(labels == 1), _compute_logits_from_mask(labels == 2)] 92 )[None] 93 94 with torch.no_grad(): 95 masks, scores, embeddings = predictor( 96 image=torch.from_numpy(input_), 97 embeddings=None, 98 box_prompts=torch.from_numpy(box_prompts), 99 point_prompts=torch.from_numpy(point_prompts), 100 point_labels=torch.from_numpy(point_labels), 101 mask_prompts=torch.from_numpy(mask_prompts), 102 ) 103 104 box_prompt_path = os.path.join(tmp_dir, "box_prompts.npy") 105 point_prompt_path = os.path.join(tmp_dir, "point_prompts.npy") 106 point_label_path = os.path.join(tmp_dir, "point_labels.npy") 107 mask_prompt_path = os.path.join(tmp_dir, "mask_prompts.npy") 108 np.save(box_prompt_path, box_prompts.astype("int64")) 109 np.save(point_prompt_path, point_prompts) 110 np.save(point_label_path, point_labels) 111 np.save(mask_prompt_path, mask_prompts) 112 113 inputs = { 114 "image": image_path, 115 "box_prompts": box_prompt_path, 116 "point_prompts": point_prompt_path, 117 "point_labels": point_label_path, 118 "mask_prompts": mask_prompt_path, 119 } 120 121 mask_path = os.path.join(tmp_dir, "mask.npy") 122 score_path = os.path.join(tmp_dir, "scores.npy") 123 embed_path = os.path.join(tmp_dir, "embeddings.npy") 124 np.save(mask_path, masks.numpy()) 125 np.save(score_path, scores.numpy()) 126 np.save(embed_path, embeddings.numpy()) 127 128 outputs = {"mask": mask_path, "score": score_path, "embeddings": embed_path} 129 return inputs, outputs 130 131 132def _write_documentation(doc, model_type, tmp_dir): 133 tmp_doc_path = os.path.join(tmp_dir, "documentation.md") 134 135 if doc is None: 136 with open(tmp_doc_path, "w") as f: 137 f.write("# Segment Anything for Microscopy\n") 138 f.write("We extend Segment Anything, a vision foundation model for image segmentation ") 139 f.write("by training specialized models for microscopy data.\n") 140 return tmp_doc_path 141 142 elif os.path.exists(doc): 143 return doc 144 145 else: 146 with open(tmp_doc_path, "w") as f: 147 f.write(doc) 148 return tmp_doc_path 149 150 151def _get_checkpoint(model_type, checkpoint_path, tmp_dir): 152 # If we don't have a checkpoint we get the corresponding model from the registry. 153 if checkpoint_path is None: 154 model_registry = util.models() 155 checkpoint_path = model_registry.fetch(model_type) 156 decoder_name = f"{model_type}_decoder" 157 decoder_path = model_registry.fetch(decoder_name) if decoder_name in model_registry.registry else None 158 return checkpoint_path, decoder_path 159 160 # Otherwise we have to load the checkpoint to see if it is the state dict of an encoder, 161 # or the checkpoint for a custom SAM model. 162 state, model_state = util._load_checkpoint(checkpoint_path) 163 164 if "model_state" in state: # This is a finetuning checkpoint -> we have to resave the state. 165 new_checkpoint_path = os.path.join(tmp_dir, f"{model_type}.pt") 166 torch.save(model_state, new_checkpoint_path) 167 168 # We may also have an instance segmentation decoder in that case. 169 # If we have it we also resave this one and return it. 170 if "decoder_state" in state: 171 decoder_path = os.path.join(tmp_dir, f"{model_type}_decoder.pt") 172 decoder_state = state["decoder_state"] 173 torch.save(decoder_state, decoder_path) 174 else: 175 decoder_path = None 176 177 return new_checkpoint_path, decoder_path 178 179 else: # This is a SAM encoder state -> we don't have to resave. 180 return checkpoint_path, None 181 182 183def _get_weight_checkpoint(model_type, checkpoint_path, decoder_path, tmp_dir): 184 """Combine the SAM and decoder states for the exported architecture.""" 185 if decoder_path is None: 186 return checkpoint_path 187 188 weight_dir = os.path.join(tmp_dir, "combined_weights") 189 os.makedirs(weight_dir, exist_ok=True) 190 weight_path = os.path.join(weight_dir, f"{model_type}.pt") 191 192 model_state = torch.load(checkpoint_path, map_location="cpu", weights_only=True) 193 decoder_state = torch.load(decoder_path, map_location="cpu", weights_only=True) 194 torch.save({"model_state": model_state, "decoder_state": decoder_state}, weight_path) 195 return weight_path 196 197 198def _get_decoder_attachment(model_type, decoder_path, tmp_dir): 199 # Registry decoder files do not have an extension. 200 attachment_name = f"{_get_architecture_model_type(model_type)}_decoder.pt" 201 if os.path.basename(decoder_path) == attachment_name: 202 return decoder_path 203 attachment_path = os.path.join(tmp_dir, attachment_name) 204 shutil.copyfile(decoder_path, attachment_path) 205 return attachment_path 206 207 208# TODO: Update this with our latest yaml file updates. 209def _write_dependencies(dependency_file, require_mobile_sam, require_micro_sam): 210 if require_micro_sam: 211 content = """name: sam 212channels: 213 - conda-forge 214dependencies: 215 - micro_sam""" 216 else: 217 content = """name: sam 218channels: 219 - pytorch 220 - conda-forge 221dependencies: 222 - segment-anything""" 223 if require_mobile_sam: 224 content += """ 225 - timm 226 - pip: 227 - git+https://github.com/ChaoningZhang/MobileSAM.git""" 228 with open(dependency_file, "w") as f: 229 f.write(content) 230 231 232def _generate_covers(input_paths, result_paths, tmp_dir, with_decoder): 233 image = np.load(input_paths["image"]).squeeze() 234 mask = np.load(result_paths["mask"]) 235 236 # create the image overlay 237 if image.ndim == 2: 238 overlay = np.stack([image, image, image]).transpose((1, 2, 0)) 239 elif image.shape[0] == 3: 240 overlay = image.transpose((1, 2, 0)) 241 else: 242 overlay = image 243 overlay = _enhance_image(overlay.astype("float32")) 244 245 if with_decoder: 246 # Outline all automatic instances. 247 overlay = _overlay_outline(overlay, mask[0, :, 0].max(axis=0), outline_dilation=2) 248 else: 249 # overlay the mask as outline 250 overlay = _overlay_outline(overlay, mask[0, 0, 0], outline_dilation=2) 251 252 # overlay the bounding box prompt 253 prompts = np.load(input_paths["box_prompts"]) 254 prompt = prompts[0, 0][[1, 0, 3, 2]] 255 prompt = np.array([prompt[:2], prompt[2:]]) 256 overlay = _overlay_box(overlay, prompt, outline_dilation=4) 257 258 # write the cover image 259 fig, ax = plt.subplots(1) 260 ax.axis("off") 261 ax.imshow(overlay.astype("uint8")) 262 cover_path = os.path.join(tmp_dir, "cover.jpeg") 263 plt.savefig(cover_path, bbox_inches="tight") 264 plt.close() 265 266 covers = [cover_path] 267 return covers 268 269 270def _check_model(model_description, input_paths, result_paths, with_decoder): 271 image = xarray.DataArray(np.load(input_paths["image"]), dims=("batch", "channel", "y", "x")) 272 273 if with_decoder: 274 # Check automatic instance segmentation against its reference outputs. 275 mask = np.load(result_paths["mask"]) 276 scores = np.load(result_paths["score"]) 277 embeddings = np.load(result_paths["embeddings"]) 278 279 with bioimageio.core.create_prediction_pipeline(model_description, devices=["cpu"]) as pp: 280 sample = create_sample_for_model(model=model_description, inputs={"image": image}) 281 prediction = pp.predict_sample_without_blocking(sample) 282 assert np.allclose(mask, prediction.members["masks"].data) 283 assert np.allclose(scores, prediction.members["scores"].data) 284 assert np.allclose(embeddings, prediction.members["embeddings"].data) 285 return 286 287 # Load inputs. 288 embeddings = xarray.DataArray(np.load(result_paths["embeddings"]), dims=("batch", "channel", "y", "x")) 289 box_prompts = xarray.DataArray(np.load(input_paths["box_prompts"]), dims=("batch", "object", "channel")) 290 point_prompts = xarray.DataArray( 291 np.load(input_paths["point_prompts"]), dims=("batch", "object", "point", "channel") 292 ) 293 point_labels = xarray.DataArray(np.load(input_paths["point_labels"]), dims=("batch", "object", "point")) 294 mask_prompts = xarray.DataArray(np.load(input_paths["mask_prompts"]), dims=("batch", "object", "channel", "y", "x")) 295 296 # Load outputs. 297 mask = np.load(result_paths["mask"]) 298 299 # Match the device used to generate the reference outputs. 300 with bioimageio.core.create_prediction_pipeline(model_description, devices=["cpu"]) as pp: 301 302 # Check with all prompts. We only check the result for this setting, 303 # because this was used to generate the test data. 304 sample = create_sample_for_model( 305 model=model_description, 306 inputs={ 307 "image": image, 308 "box_prompts": box_prompts, 309 "point_prompts": point_prompts, 310 "point_labels": point_labels, 311 "mask_prompts": mask_prompts, 312 "embeddings": embeddings, 313 }, 314 ) 315 prediction = pp.predict_sample_without_blocking(sample) 316 317 predicted_mask = prediction.members["masks"].data 318 assert predicted_mask.shape == mask.shape 319 assert np.allclose(mask, predicted_mask) 320 321 # Run the checks with partial prompts. 322 prompt_kwargs = [ 323 # With boxes. 324 {"box_prompts": box_prompts}, 325 # With point prompts. 326 {"point_prompts": point_prompts, "point_labels": point_labels}, 327 # With masks. 328 {"mask_prompts": mask_prompts}, 329 # With boxes and points. 330 {"box_prompts": box_prompts, "point_prompts": point_prompts, "point_labels": point_labels}, 331 # With boxes and masks. 332 {"box_prompts": box_prompts, "mask_prompts": mask_prompts}, 333 # With points and masks. 334 {"mask_prompts": mask_prompts, "point_prompts": point_prompts, "point_labels": point_labels}, 335 ] 336 337 for kwargs in prompt_kwargs: 338 sample = create_sample_for_model( 339 model=model_description, inputs={"image": image, "embeddings": embeddings, **kwargs}, 340 ) 341 prediction = pp.predict_sample_without_blocking(sample) 342 predicted_mask = prediction.members["masks"].data 343 assert predicted_mask.shape == mask.shape 344 345 # Use a fresh pipeline to verify image-only inference. 346 with bioimageio.core.create_prediction_pipeline(model_description, devices=["cpu"]) as pp: 347 sample = create_sample_for_model(model=model_description, inputs={"image": image}) 348 prediction = pp.predict_sample_without_blocking(sample) 349 predicted_mask = prediction.members["masks"].data 350 # AIS may return no objects. 351 assert predicted_mask.ndim == 5 352 assert predicted_mask.shape[-2:] == mask.shape[-2:] 353 354 355def _regenerate_reference_outputs(model_description, input_paths, result_paths): 356 """Regenerate reference outputs through the packaged model. 357 358 This avoids numerical differences between direct calls and BioImageIO calls. 359 """ 360 from bioimageio.core.digest_spec import get_test_input_sample 361 362 sample = get_test_input_sample(model_description) 363 # Keep reference generation deterministic across CI runners. 364 with bioimageio.core.create_prediction_pipeline(model_description, devices=["cpu"]) as pp: 365 pp.apply_preprocessing(sample) 366 prediction = pp.predict_sample_without_blocking( 367 sample, 368 skip_preprocessing=True, 369 skip_postprocessing=True, 370 skip_input_padding=True, 371 skip_output_cropping=True, 372 ) 373 pp.apply_postprocessing(prediction) 374 np.save(result_paths["mask"], np.asarray(prediction.members["masks"].data).astype("uint8")) 375 np.save(result_paths["score"], np.asarray(prediction.members["scores"].data).astype("float32")) 376 np.save(result_paths["embeddings"], np.asarray(prediction.members["embeddings"].data).astype("float32")) 377 378 379def _build_model_description( 380 name, input_paths, result_paths, weight_descriptions, doc_path, covers, extra_kwargs, with_decoder, **kwargs 381): 382 # Rebuild after reference generation to update the test tensor hashes. 383 if with_decoder: 384 # Prevent BioImageIO size probes from cropping instances. 385 test_image_shape = np.load(input_paths["image"], mmap_mode="r").shape 386 image_y_size = spec.ParameterizedSize(min=test_image_shape[-2], step=1) 387 image_x_size = spec.ParameterizedSize(min=test_image_shape[-1], step=1) 388 else: 389 image_y_size = image_x_size = ARBITRARY_SIZE 390 391 input_descriptions = [ 392 # First input: the image data. 393 spec.InputTensorDescr( 394 id=spec.TensorId("image"), 395 axes=[ 396 spec.BatchAxis(size=1), 397 # NOTE: to support 1 and 3 channels we can add another preprocessing. 398 # Best solution: Have a pre-processing for this! (1C -> RGB) 399 spec.ChannelAxis(channel_names=[spec.Identifier(cname) for cname in "RGB"]), 400 spec.SpaceInputAxis(id=spec.AxisId("y"), size=image_y_size), 401 spec.SpaceInputAxis(id=spec.AxisId("x"), size=image_x_size), 402 ], 403 test_tensor=spec.FileDescr(source=input_paths["image"]), 404 data=spec.IntervalOrRatioDataDescr(type="uint8") 405 ), 406 ] 407 408 # Decoder exports keep only the image input to test automatic instance segmentation. 409 if not with_decoder: 410 input_descriptions += [ 411 # Second input: the box prompts (optional) 412 spec.InputTensorDescr( 413 id=spec.TensorId("box_prompts"), 414 optional=True, 415 axes=[ 416 spec.BatchAxis(size=1), 417 spec.IndexInputAxis( 418 id=spec.AxisId("object"), 419 size=ARBITRARY_SIZE 420 ), 421 spec.ChannelAxis(channel_names=[spec.Identifier(bname) for bname in "hwxy"]), 422 ], 423 test_tensor=spec.FileDescr(source=input_paths["box_prompts"]), 424 data=spec.IntervalOrRatioDataDescr(type="int64") 425 ), 426 427 # Third input: the point prompt coordinates (optional) 428 spec.InputTensorDescr( 429 id=spec.TensorId("point_prompts"), 430 optional=True, 431 axes=[ 432 spec.BatchAxis(size=1), 433 spec.IndexInputAxis( 434 id=spec.AxisId("object"), 435 size=ARBITRARY_SIZE 436 ), 437 spec.IndexInputAxis( 438 id=spec.AxisId("point"), 439 size=ARBITRARY_SIZE 440 ), 441 spec.ChannelAxis(channel_names=[spec.Identifier(bname) for bname in "xy"]), 442 ], 443 test_tensor=spec.FileDescr(source=input_paths["point_prompts"]), 444 data=spec.IntervalOrRatioDataDescr(type="int64") 445 ), 446 447 # Fourth input: the point prompt labels (optional) 448 spec.InputTensorDescr( 449 id=spec.TensorId("point_labels"), 450 optional=True, 451 axes=[ 452 spec.BatchAxis(size=1), 453 spec.IndexInputAxis( 454 id=spec.AxisId("object"), 455 size=ARBITRARY_SIZE 456 ), 457 spec.IndexInputAxis( 458 id=spec.AxisId("point"), 459 size=ARBITRARY_SIZE 460 ), 461 ], 462 test_tensor=spec.FileDescr(source=input_paths["point_labels"]), 463 data=spec.IntervalOrRatioDataDescr(type="int64") 464 ), 465 466 # Fifth input: the mask prompts (optional) 467 spec.InputTensorDescr( 468 id=spec.TensorId("mask_prompts"), 469 optional=True, 470 axes=[ 471 spec.BatchAxis(size=1), 472 spec.IndexInputAxis( 473 id=spec.AxisId("object"), 474 size=ARBITRARY_SIZE 475 ), 476 spec.ChannelAxis(channel_names=["channel"]), 477 spec.SpaceInputAxis(id=spec.AxisId("y"), size=256), 478 spec.SpaceInputAxis(id=spec.AxisId("x"), size=256), 479 ], 480 test_tensor=spec.FileDescr(source=input_paths["mask_prompts"]), 481 data=spec.IntervalOrRatioDataDescr(type="float32") 482 ), 483 484 # Sixth input: the image embeddings (optional) 485 spec.InputTensorDescr( 486 id=spec.TensorId("embeddings"), 487 optional=True, 488 axes=[ 489 spec.BatchAxis(size=1), 490 # NOTE: we currently have to specify all the channel names 491 # (It would be nice to also support size) 492 spec.ChannelAxis(channel_names=[spec.Identifier(f"c{i}") for i in range(256)]), 493 spec.SpaceInputAxis(id=spec.AxisId("y"), size=64), 494 spec.SpaceInputAxis(id=spec.AxisId("x"), size=64), 495 ], 496 test_tensor=spec.FileDescr(source=result_paths["embeddings"]), 497 data=spec.IntervalOrRatioDataDescr(type="float32") 498 ), 499 ] 500 501 output_descriptions = [ 502 # First output: The mask predictions. 503 spec.OutputTensorDescr( 504 id=spec.TensorId("masks"), 505 axes=[ 506 spec.BatchAxis(size=1), 507 # NOTE: we use the data dependent size here to avoid dependency on optional inputs 508 spec.IndexOutputAxis( 509 id=spec.AxisId("object"), size=spec.DataDependentSize(), 510 ), 511 # NOTE: this could be a 3 once we use multi-masking 512 spec.ChannelAxis(channel_names=[spec.Identifier("mask")]), 513 spec.SpaceOutputAxis( 514 id=spec.AxisId("y"), 515 size=spec.SizeReference( 516 tensor_id=spec.TensorId("image"), axis_id=spec.AxisId("y"), 517 ) 518 ), 519 spec.SpaceOutputAxis( 520 id=spec.AxisId("x"), 521 size=spec.SizeReference( 522 tensor_id=spec.TensorId("image"), axis_id=spec.AxisId("x"), 523 ) 524 ) 525 ], 526 data=spec.IntervalOrRatioDataDescr(type="uint8"), 527 test_tensor=spec.FileDescr(source=result_paths["mask"]) 528 ), 529 530 # The score predictions 531 spec.OutputTensorDescr( 532 id=spec.TensorId("scores"), 533 axes=[ 534 spec.BatchAxis(size=1), 535 # NOTE: we use the data dependent size here to avoid dependency on optional inputs 536 spec.IndexOutputAxis( 537 id=spec.AxisId("object"), size=spec.DataDependentSize(), 538 ), 539 # NOTE: this could be a 3 once we use multi-masking 540 spec.ChannelAxis(channel_names=[spec.Identifier("mask")]), 541 ], 542 data=spec.IntervalOrRatioDataDescr(type="float32"), 543 test_tensor=spec.FileDescr(source=result_paths["score"]) 544 ), 545 546 # The image embeddings 547 spec.OutputTensorDescr( 548 id=spec.TensorId("embeddings"), 549 axes=[ 550 spec.BatchAxis(size=1), 551 spec.ChannelAxis(channel_names=[spec.Identifier(f"c{i}") for i in range(256)]), 552 spec.SpaceOutputAxis(id=spec.AxisId("y"), size=64), 553 spec.SpaceOutputAxis(id=spec.AxisId("x"), size=64), 554 ], 555 data=spec.IntervalOrRatioDataDescr(type="float32"), 556 test_tensor=spec.FileDescr(source=result_paths["embeddings"]) 557 ) 558 ] 559 560 return spec.ModelDescr( 561 name=name, 562 inputs=input_descriptions, 563 outputs=output_descriptions, 564 weights=weight_descriptions, 565 description=kwargs.get("description", DEFAULTS["description"]), 566 authors=kwargs.get("authors", DEFAULTS["authors"]), 567 cite=kwargs.get("cite", DEFAULTS["cite"]), 568 license=spec.LicenseId("CC-BY-4.0"), 569 documentation=spec.FileDescr(source=Path(doc_path)), 570 git_repo=spec.HttpUrl("https://github.com/computational-cell-analytics/micro-sam"), 571 tags=kwargs.get("tags", DEFAULTS["tags"]), 572 covers=[spec.FileDescr(source=Path(cover)) for cover in covers], 573 **extra_kwargs, 574 config=spec.Config( 575 bioimageio=spec.BioimageioConfig( 576 # Allow minor numerical differences in raw embeddings across devices. 577 reproducibility_tolerance=[ 578 spec.ReproducibilityTolerance( 579 mismatched_elements_per_million=1000, 580 output_ids=[spec.TensorId("embeddings")], 581 ) 582 ], 583 ), 584 ), 585 ) 586 587 588def export_sam_model( 589 image: np.ndarray, 590 label_image: np.ndarray, 591 model_type: str, 592 name: str, 593 output_path: Union[str, os.PathLike], 594 checkpoint_path: Optional[Union[str, os.PathLike]] = None, 595 **kwargs 596) -> None: 597 """Export SAM model to BioImage.IO model format. 598 599 The exported model can be uploaded to [bioimage.io](https://bioimage.io/#/) and 600 be used in tools that support the BioImage.IO model format. 601 602 Models with a decoder expose only the image input. 603 Their test outputs use automatic instance segmentation. 604 Use micro_sam directly for prompted segmentation with these models. 605 Models without a decoder expose the interactive prompt inputs. 606 607 Args: 608 image: The image for generating test data. 609 label_image: The segmentation corresponding to `image`. 610 It is used to derive prompt inputs for the model. 611 model_type: The type of the SAM model. 612 name: The name of the exported model. 613 output_path: Where the exported model is saved. 614 checkpoint_path: Optional checkpoint for loading the SAM model. 615 """ 616 with tempfile.TemporaryDirectory() as tmp_dir: 617 checkpoint_path, decoder_path = _get_checkpoint(model_type, checkpoint_path, tmp_dir) 618 with_decoder = decoder_path is not None 619 weight_path = _get_weight_checkpoint(model_type, checkpoint_path, decoder_path, tmp_dir) 620 input_paths, result_paths = _create_test_inputs_and_outputs( 621 image, label_image, model_type, weight_path, tmp_dir, with_decoder=with_decoder, 622 ) 623 architecture_path = os.path.join(os.path.split(__file__)[0], "predictor_adaptor.py") 624 architecture = spec.ArchitectureFromFileDescr( 625 source=Path(architecture_path), 626 callable="PredictorAdaptor", 627 kwargs={"model_type": _get_architecture_model_type(model_type)} 628 ) 629 630 dependency_file = os.path.join(tmp_dir, "environment.yaml") 631 _write_dependencies( 632 dependency_file, require_mobile_sam=model_type.startswith("vit_t"), require_micro_sam=with_decoder, 633 ) 634 635 weight_descriptions = spec.WeightsDescr( 636 pytorch_state_dict=spec.PytorchStateDictWeightsDescr( 637 source=Path(weight_path), 638 architecture=architecture, 639 pytorch_version=spec.Version(torch.__version__), 640 dependencies=spec.FileDescr(source=dependency_file), 641 ) 642 ) 643 644 doc_path = _write_documentation(kwargs.get("documentation", None), model_type, tmp_dir) 645 646 covers = kwargs.pop("covers", None) 647 if covers is None: 648 covers = _generate_covers(input_paths, result_paths, tmp_dir, with_decoder=with_decoder) 649 else: 650 assert all(os.path.exists(cov) for cov in covers) 651 652 # the uploader information is only added if explicitly passed 653 extra_kwargs = {} 654 if "id" in kwargs: 655 extra_kwargs["id"] = kwargs["id"] 656 if "id_emoji" in kwargs: 657 extra_kwargs["id_emoji"] = kwargs["id_emoji"] 658 if "uploader" in kwargs: 659 extra_kwargs["uploader"] = kwargs["uploader"] 660 if "version" in kwargs: 661 extra_kwargs["version"] = kwargs["version"] 662 663 if with_decoder: 664 # Keep the decoder available as a standalone registry attachment. 665 attachment_path = _get_decoder_attachment(model_type, decoder_path, tmp_dir) 666 extra_kwargs["attachments"] = [spec.FileDescr(source=attachment_path)] 667 668 model_description = _build_model_description( 669 name, input_paths, result_paths, weight_descriptions, doc_path, covers, extra_kwargs, 670 with_decoder=with_decoder, **kwargs, 671 ) 672 673 if with_decoder: 674 # Rebuild the description after the reference output hashes change. 675 _regenerate_reference_outputs(model_description, input_paths, result_paths) 676 model_description = _build_model_description( 677 name, input_paths, result_paths, weight_descriptions, doc_path, covers, extra_kwargs, 678 with_decoder=with_decoder, **kwargs, 679 ) 680 681 _check_model(model_description, input_paths, result_paths, with_decoder=with_decoder) 682 683 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:
589def export_sam_model( 590 image: np.ndarray, 591 label_image: np.ndarray, 592 model_type: str, 593 name: str, 594 output_path: Union[str, os.PathLike], 595 checkpoint_path: Optional[Union[str, os.PathLike]] = None, 596 **kwargs 597) -> None: 598 """Export SAM model to BioImage.IO model format. 599 600 The exported model can be uploaded to [bioimage.io](https://bioimage.io/#/) and 601 be used in tools that support the BioImage.IO model format. 602 603 Models with a decoder expose only the image input. 604 Their test outputs use automatic instance segmentation. 605 Use micro_sam directly for prompted segmentation with these models. 606 Models without a decoder expose the interactive prompt inputs. 607 608 Args: 609 image: The image for generating test data. 610 label_image: The segmentation corresponding to `image`. 611 It is used to derive prompt inputs for the model. 612 model_type: The type of the SAM model. 613 name: The name of the exported model. 614 output_path: Where the exported model is saved. 615 checkpoint_path: Optional checkpoint for loading the SAM model. 616 """ 617 with tempfile.TemporaryDirectory() as tmp_dir: 618 checkpoint_path, decoder_path = _get_checkpoint(model_type, checkpoint_path, tmp_dir) 619 with_decoder = decoder_path is not None 620 weight_path = _get_weight_checkpoint(model_type, checkpoint_path, decoder_path, tmp_dir) 621 input_paths, result_paths = _create_test_inputs_and_outputs( 622 image, label_image, model_type, weight_path, tmp_dir, with_decoder=with_decoder, 623 ) 624 architecture_path = os.path.join(os.path.split(__file__)[0], "predictor_adaptor.py") 625 architecture = spec.ArchitectureFromFileDescr( 626 source=Path(architecture_path), 627 callable="PredictorAdaptor", 628 kwargs={"model_type": _get_architecture_model_type(model_type)} 629 ) 630 631 dependency_file = os.path.join(tmp_dir, "environment.yaml") 632 _write_dependencies( 633 dependency_file, require_mobile_sam=model_type.startswith("vit_t"), require_micro_sam=with_decoder, 634 ) 635 636 weight_descriptions = spec.WeightsDescr( 637 pytorch_state_dict=spec.PytorchStateDictWeightsDescr( 638 source=Path(weight_path), 639 architecture=architecture, 640 pytorch_version=spec.Version(torch.__version__), 641 dependencies=spec.FileDescr(source=dependency_file), 642 ) 643 ) 644 645 doc_path = _write_documentation(kwargs.get("documentation", None), model_type, tmp_dir) 646 647 covers = kwargs.pop("covers", None) 648 if covers is None: 649 covers = _generate_covers(input_paths, result_paths, tmp_dir, with_decoder=with_decoder) 650 else: 651 assert all(os.path.exists(cov) for cov in covers) 652 653 # the uploader information is only added if explicitly passed 654 extra_kwargs = {} 655 if "id" in kwargs: 656 extra_kwargs["id"] = kwargs["id"] 657 if "id_emoji" in kwargs: 658 extra_kwargs["id_emoji"] = kwargs["id_emoji"] 659 if "uploader" in kwargs: 660 extra_kwargs["uploader"] = kwargs["uploader"] 661 if "version" in kwargs: 662 extra_kwargs["version"] = kwargs["version"] 663 664 if with_decoder: 665 # Keep the decoder available as a standalone registry attachment. 666 attachment_path = _get_decoder_attachment(model_type, decoder_path, tmp_dir) 667 extra_kwargs["attachments"] = [spec.FileDescr(source=attachment_path)] 668 669 model_description = _build_model_description( 670 name, input_paths, result_paths, weight_descriptions, doc_path, covers, extra_kwargs, 671 with_decoder=with_decoder, **kwargs, 672 ) 673 674 if with_decoder: 675 # Rebuild the description after the reference output hashes change. 676 _regenerate_reference_outputs(model_description, input_paths, result_paths) 677 model_description = _build_model_description( 678 name, input_paths, result_paths, weight_descriptions, doc_path, covers, extra_kwargs, 679 with_decoder=with_decoder, **kwargs, 680 ) 681 682 _check_model(model_description, input_paths, result_paths, with_decoder=with_decoder) 683 684 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.
Models with a decoder expose only the image input. Their test outputs use automatic instance segmentation. Use micro_sam directly for prompted segmentation with these models. Models without a decoder expose the interactive prompt inputs.
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.