Skip to content

Commit 0555bbc

Browse files
authored
Refactor automatic segmentation v2 API design (#1301)
* Refactor automatic segmentation v2 API design * Improve code design by removing redundant code * Clarfiy AMG helpers and drop checkpoint module patches * Consolidate AIS stub encoders into one class * Update training CLI entry point for joint training
1 parent 8592e16 commit 0555bbc

13 files changed

Lines changed: 1069 additions & 1035 deletions

finetuning/v2/evaluation/common.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -467,13 +467,13 @@ def load_unisam2_model(checkpoint_path, device):
467467

468468

469469
def predict_unisam2(model, raw, ndim, device):
470-
from micro_sam.v2.automatic_segmentation import run_unisam2_inference
470+
from micro_sam.v2.instance_segmentation import get_unisam2_segmentation_generator
471471
is_3d = (ndim == 3)
472472
tile_shape = (4, 384, 384) if is_3d else (384, 384)
473473
halo = (2, 64, 64) if is_3d else (64, 64)
474-
return run_unisam2_inference(
475-
model=model, raw=raw, ndim=ndim, device=device, tile_shape=tile_shape, halo=halo,
476-
)
474+
segmenter = get_unisam2_segmentation_generator(model, is_tiled=True, device=device)
475+
segmenter.initialize(raw, ndim=ndim, tile_shape=tile_shape, halo=halo)
476+
return segmenter.get_state()["prediction"]
477477

478478

479479
def postprocess_unisam2(out, dataset_name, backend="python"):

finetuning/v2/evaluation/grid_search_automatic_cells.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def load_model(device, checkpoint_path=None, model_name=MODEL_NAME):
122122
Returns:
123123
The UniSAM2 model in eval mode.
124124
"""
125-
from micro_sam.v2.automatic_segmentation import get_unisam2_model
125+
from micro_sam.v2.instance_segmentation import get_unisam2_model
126126

127127
if checkpoint_path is not None:
128128
print(f"Loading UniSAM2 model from custom checkpoint '{checkpoint_path}'.")

micro_sam/_cli.py

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -492,15 +492,16 @@ def inference_segmentation(
492492

493493
from .v2.util import DEFAULT_MODEL
494494
from .v1.automatic_segmentation import _get_inputs_from_paths
495-
from .v2.automatic_segmentation import get_segmenter, automatic_instance_segmentation
495+
from .v2.automatic_segmentation import get_predictor_and_segmenter, automatic_instance_segmentation
496496

497497
model_type = model_type or DEFAULT_MODEL
498498
tile_shape = _parse_shape(tile_shape)
499499
halo = _parse_shape(halo)
500500
generate_kwargs = _parse_extra(ctx.args)
501501

502-
segmenter = get_segmenter(
503-
model_type=model_type, checkpoint=checkpoint_path, device=device, is_tiled=tile_shape is not None,
502+
predictor, segmenter = get_predictor_and_segmenter(
503+
model_type=model_type, checkpoint=checkpoint_path, device=device,
504+
segmentation_mode="ais", is_tiled=tile_shape is not None,
504505
)
505506

506507
input_paths = _get_inputs_from_paths(list(input_path), pattern)
@@ -521,6 +522,7 @@ def inference_segmentation(
521522
embedding_fpath = os.path.join(embedding_folder, f"{os.path.splitext(os.path.basename(path))[0]}.zarr")
522523

523524
segmentation = automatic_instance_segmentation(
525+
predictor=predictor,
524526
segmenter=segmenter,
525527
input_path=path,
526528
output_path=output_fpath,
@@ -580,17 +582,19 @@ def inference_tracking(
580582
for sparse or '--beta' for dense) can be passed through and are forwarded.
581583
"""
582584
from .v2.util import DEFAULT_MODEL
583-
from .v2.automatic_segmentation import get_segmenter, automatic_tracking
585+
from .v2.automatic_segmentation import get_predictor_and_segmenter, automatic_tracking
584586

585587
model_type = model_type or DEFAULT_MODEL
586588
tile_shape = _parse_shape(tile_shape)
587589
halo = _parse_shape(halo)
588590
generate_kwargs = _parse_extra(ctx.args)
589591

590-
segmenter = get_segmenter(
591-
model_type=model_type, checkpoint=checkpoint_path, device=device, is_tiled=tile_shape is not None,
592+
predictor, segmenter = get_predictor_and_segmenter(
593+
model_type=model_type, checkpoint=checkpoint_path, device=device,
594+
segmentation_mode="ais", is_tiled=tile_shape is not None,
592595
)
593596
automatic_tracking(
597+
predictor=predictor,
594598
segmenter=segmenter,
595599
input_path=input_path,
596600
output_path=output_path,
@@ -791,28 +795,48 @@ def precompute_embeddings(
791795

792796

793797
@cli.command("train")
794-
@click.option("-c", "--config", required=True, help="The filepath to the SAM2 training config file.")
795-
@click.option("--use_cluster", type=int, default=None, help="Whether to launch on a cluster: 0 local, 1 cluster.")
796-
@click.option("--partition", default=None, help="SLURM partition.")
797-
@click.option("--account", default=None, help="SLURM account.")
798-
@click.option("--qos", default=None, help="SLURM qos.")
799-
@click.option("--num_gpus", type=int, default=None, help="Number of GPUs per node.")
800-
@click.option("--num_nodes", type=int, default=None, help="Number of nodes.")
801-
def train(config, use_cluster, partition, account, qos, num_gpus, num_nodes):
802-
"""Training a custom `micro-sam2` model."""
803-
from .v2.train import train_sam2, register_omegaconf_resolvers
804-
805-
register_omegaconf_resolvers()
806-
train_sam2(
807-
config=config,
808-
use_cluster=bool(use_cluster) if use_cluster is not None else None,
809-
partition=partition,
810-
account=account,
811-
qos=qos,
812-
num_gpus=num_gpus,
813-
num_nodes=num_nodes,
798+
@click.option("-i", "--input_path", required=True, help="Root of the generalist training data (dataset subfolders).")
799+
@click.option("-m", "--model_type", default="hvit_t", help="SAM2 variant: hvit_t, hvit_s, hvit_b, hvit_l.")
800+
@click.option("-s", "--save_root", default=None, help="Directory for checkpoints and logs. Default: current dir.")
801+
@click.option("--name", default=None, help="Checkpoint/log folder name. Auto-generated if unset.")
802+
@click.option("--dataset_choice", default="both", help="Data modality to train on: 'lm', 'em' or 'both'.")
803+
@click.option("--n_epochs", type=int, default=100, help="Number of training epochs.")
804+
@click.option("--n_iterations", type=int, default=None, help="Fixed iteration budget (overrides --n_epochs).")
805+
@click.option("--batch_size", type=int, default=1, help="Batch size per GPU for 3d groups.")
806+
@click.option("--batch_size_2d", type=int, default=8, help="Batch size per GPU for 2d groups.")
807+
@click.option("--z_slices", type=int, multiple=True, default=(8,), help="Z-slice counts for 3d groups.")
808+
@click.option("-c", "--checkpoint_path", default=None, help="SAM2 checkpoint to start from; default downloaded.")
809+
def train(
810+
input_path, model_type, save_root, name, dataset_choice, n_epochs, n_iterations,
811+
batch_size, batch_size_2d, z_slices, checkpoint_path,
812+
):
813+
"""Train a joint SAM2 + UniSAM2 `micro-sam2` model on the generalist datasets."""
814+
import torch
815+
816+
n_gpus = torch.cuda.device_count()
817+
if name is None:
818+
name = f"joint_sam2_{model_type}_{'multi' if n_gpus > 1 else 'single'}_gpu"
819+
820+
# Joint interactive + automatic recipe from finetuning/v2/generalist/train_joint.py.
821+
kwargs = dict(
822+
name=name, model_type=model_type, input_path=input_path, save_root=save_root,
823+
dataset_choice=dataset_choice, n_epochs=n_epochs, n_iterations=n_iterations,
824+
batch_size=batch_size, batch_size_2d=batch_size_2d, z_slices=list(z_slices),
825+
n_workers=8, checkpoint_path=checkpoint_path, lr=1e-5, max_num_objects=5,
826+
prob_to_use_pt_input=1.0, prob_to_use_box_input=0.5, num_frames_to_correct=2,
827+
rand_frames_to_correct=True, prob_to_sample_from_gt=0.1, add_all_frames_to_correct_as_cond=True,
828+
num_correction_pt_per_frame=7, num_init_cond_frames=2, clip_grad_norm=None, largest_first=True,
829+
bidirectional=True, use_focal_loss=True, focal_weight=1.0, use_object_score_loss=True,
830+
average_over_frames=False,
814831
)
815832

833+
if n_gpus > 1:
834+
from .v2.training import train_joint_sam2_multi_gpu
835+
train_joint_sam2_multi_gpu(**kwargs)
836+
else:
837+
from .v2.training import train_joint_sam2
838+
train_joint_sam2(**kwargs)
839+
816840

817841
@cli.command("info")
818842
@click.option(

micro_sam/precompute_state.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@ def _cache_amg_state_v2(
449449
def _cache_amg_slice(segmenter, save_path, i, init_fn, embedding_signature=None):
450450
"""Load slice `i`'s AMG state from `save_path` if present and matching, else init and save.
451451
452-
Used by `micro_sam.v2.instance_segmentation.automatic_3d_segmentation` to cache the per-slice
452+
Used by `micro_sam.v2.instance_segmentation.amg_3d_segmentation` to cache the per-slice
453453
grid-prediction state of a volume. `init_fn(i)` runs the (expensive) `initialize` for the slice.
454454
"""
455455
key = _autoseg_state_key(i)
@@ -553,7 +553,7 @@ def _cache_ais_state_v2(
553553
'save_path=None' to skip caching.
554554
555555
Args:
556-
decoder: The UniSAM2 model, loaded via `micro_sam.v2.automatic_segmentation.get_unisam2_model`.
556+
decoder: The UniSAM2 model, loaded via `micro_sam.v2.instance_segmentation.get_unisam2_model`.
557557
raw: The image data.
558558
image_embeddings: The (optionally precomputed) image embeddings. When given only the decoder
559559
is run on them (no encoder pass).
@@ -576,7 +576,7 @@ def _cache_ais_state_v2(
576576
Returns:
577577
The AIS segmenter with the (cached or freshly computed) state set.
578578
"""
579-
from .v2.automatic_segmentation import get_unisam2_segmentation_generator
579+
from .v2.instance_segmentation import get_unisam2_segmentation_generator
580580

581581
if is_tiled is None:
582582
is_tiled = image_embeddings is not None and image_embeddings.get("input_size") is None
@@ -612,12 +612,12 @@ def _cache_ais_state_v2(
612612
def _resolve_unisam2_decoder(model_type, checkpoint_path, device):
613613
"""Return a UniSAM2 decoder for the SAM2 model if one is available, else None (fall back to AMG).
614614
615-
Mirrors `micro_sam.v2.automatic_segmentation.get_segmenter`: a decoder from a custom
615+
Mirrors `micro_sam.v2.instance_segmentation.get_decoder`: a decoder from a custom
616616
`checkpoint_path`, or the registered decoder of a finetuned model (e.g. 'hvit_t_cells'). Any
617617
failure (e.g. an interactive-only checkpoint without a decoder) returns None.
618618
"""
619619
from .v2.util import FINETUNED_MODELS, has_registered_decoder, _download_finetuned_sam2_model
620-
from .v2.automatic_segmentation import get_unisam2_model
620+
from .v2.instance_segmentation import get_unisam2_model
621621

622622
encoder = model_type[:6]
623623
if checkpoint_path is not None:

micro_sam/sam_annotator/_state.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ def initialize_predictor(
201201
# finetuned checkpoint passed via 'checkpoint_path', or a finetuned model from the download
202202
# console (e.g. 'hvit_t_cells') whose decoder is downloaded from the SAM2 model registry.
203203
if self.is_sam2 and prefer_decoder and self.decoder is None:
204-
from micro_sam.v2.automatic_segmentation import get_unisam2_model
204+
from micro_sam.v2.instance_segmentation import get_unisam2_model
205205
from micro_sam.v2.util import FINETUNED_MODELS, _download_finetuned_sam2_model
206206

207207
# The decoder is built on the base SAM2 backbone, i.e. the first 6 characters of the

micro_sam/sam_annotator/_widgets.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4322,7 +4322,7 @@ def _run_unisam2(self, state, run_raw, ndim, z, pbar_init=None, pbar_update=None
43224322
return self._segmenter.generate(mode=self.mode, **self._postproc_kwargs())
43234323

43244324
def _run_amg(self, state, run_raw, ndim, z, pbar_init=None, pbar_update=None):
4325-
from micro_sam.v2.instance_segmentation import get_amg_segmenter, automatic_3d_segmentation
4325+
from micro_sam.v2.instance_segmentation import get_amg_segmenter, amg_3d_segmentation
43264326
from micro_sam.precompute_state import cache_autoseg_state
43274327

43284328
# The SAM2 model: 'state.predictor' is the image predictor (2d) wrapping the model, or the
@@ -4342,7 +4342,7 @@ def _run_amg(self, state, run_raw, ndim, z, pbar_init=None, pbar_update=None):
43424342
segmenter = get_amg_segmenter(model, is_tiled=tile_shape is not None, model_type=model_type, **amg_params)
43434343
# Reuse the precomputed 3d embeddings per slice (tiled or not) so AMG does not re-encode,
43444344
# and cache each slice's grid-prediction state in the embedding Zarr.
4345-
return automatic_3d_segmentation(
4345+
return amg_3d_segmentation(
43464346
run_raw, segmenter, tile_shape=tile_shape, halo=halo,
43474347
image_embeddings=state.image_embeddings, state_save_path=save_path,
43484348
pbar_init=pbar_init, pbar_update=pbar_update, **generate_kwargs,

0 commit comments

Comments
 (0)