Source code for acia.segm.processor.cellpose_sam

"""Segmenter using CellposeSAM: https://doi.org/10.1101/2025.04.28.651001"""

import os

from cellpose import core, models
from tqdm.auto import tqdm

from acia.attribute import attribute_segmentation
from acia.base import ImageSequenceSource, Overlay
from acia.segm.formats import overlay_from_masks

from . import SegmentationProcessor


[docs] class CellposeSAMSegmenter(SegmentationProcessor): """CellposeSAMSegmenter using Cellpose SAM: https://doi.org/10.1101/2025.04.28.651001"""
[docs] def __init__(self, use_GPU=None, pretrained_model=None, autorelease: bool = True): """Initialize the Cellpose-SAM segmenter. Args: use_GPU: Force GPU on/off. ``None`` (default) auto-detects via :func:`cellpose.core.use_gpu`. pretrained_model: Which Cellpose model to load. ``None`` (default) loads Cellpose's built-in default model. Pass a model name or a path to weights to override it (forwarded to :class:`cellpose.models.CellposeModel`). The actually loaded model is printed on first use. autorelease: Release the model after each call to free GPU memory. """ super().__init__(autorelease=autorelease) if use_GPU is None: use_GPU = core.use_gpu() self.use_GPU = use_GPU self.pretrained_model = pretrained_model print(f"Use GPU? {self.use_GPU}")
def _load_model(self): # create CellPose model kwargs = {"gpu": self.use_GPU} if self.pretrained_model is not None: kwargs["pretrained_model"] = self.pretrained_model model = models.CellposeModel(**kwargs) self._describe_model(model) return model @staticmethod def _describe_model(model) -> None: """Print which underlying Cellpose model / device is in use. Cellpose ships several different pretrained models; this makes it explicit which one was loaded (rather than silently using a default). """ pretrained = getattr(model, "pretrained_model", None) if isinstance(pretrained, (list, tuple)): pretrained = pretrained[0] if pretrained else None parts = [] if pretrained: parts.append(f"weights={os.path.basename(str(pretrained))}") device = getattr(model, "device", None) if device is not None: parts.append(f"device={device}") diam_mean = getattr(model, "diam_mean", None) if diam_mean is not None: parts.append(f"diam_mean={diam_mean}") if parts: print("Cellpose model loaded: " + ", ".join(str(p) for p in parts)) else: print("Cellpose model loaded") @staticmethod def __predict(images, model, cellpose_params=None): if cellpose_params is None: cellpose_params = {} # Segment image-by-image so we can render a real progress bar. # Cellpose-SAM's eval() already loops per image internally when handed a # list (models.py: `for i in trange(nimg)`), so this does NOT cost any # cross-image GPU batching -- it is the same work, just with visible # progress. GPU batching happens at the tile level via the `batch_size` # cellpose param (number of 256x256 patches run at once), which still # applies within each per-image call. Cellpose's own progress goes to a # logger at 30s intervals, which is why it looked frozen in notebooks. # (channels= is deprecated/ignored in Cellpose v4, so it is not passed.) masks = [] for image in tqdm(images, desc="CellposeSAM segmenting"): mask, _, _ = model.eval(image, **cellpose_params) masks.append(mask) return masks def _segment(self, images: ImageSequenceSource, cellpose_params=None) -> Overlay: # list of images imgs = [im.raw for im in images] # perform the prediction masks = self.__predict(imgs, self.model, cellpose_params=cellpose_params) # parse the overlay ov = overlay_from_masks(masks) attribute_segmentation(ov, self) return ov