CoolFace
Apppublic

VisionLanguageGroup/MicroscopyMatching

sourceHugging Faceupdated 2mo agoView on Hugging Face
0likes
models.py435 linesDownload Raw Back to seg_post_model
1"""2Copyright © 2025 Howard Hughes Medical Institute, Authored by Carsen Stringer, Michael Rariden and Marius Pachitariu.3"""4 5import os, time6# from pathlib import Path7import numpy as np8from tqdm import trange9import torch10from scipy.ndimage import gaussian_filter11# import gc12import cv213 14import logging15 16models_logger = logging.getLogger(__name__)17 18from . import transforms, dynamics, utils19from .vit import Transformer20from .core import assign_device, run_net21 22# _MODEL_DIR_ENV = os.environ.get("CELLPOSE_LOCAL_MODELS_PATH")23# _MODEL_DIR_DEFAULT = Path("/media/data1/huix/seg/cellpose_models")24# MODEL_DIR = Path(_MODEL_DIR_ENV) if _MODEL_DIR_ENV else _MODEL_DIR_DEFAULT25 26# MODEL_NAMES = ["cpsam"]27 28# MODEL_LIST_PATH = os.fspath(MODEL_DIR.joinpath("gui_models.txt"))29 30normalize_default = {31    "lowhigh": None,32    "percentile": None,33    "normalize": True,34    "norm3D": True,35    "sharpen_radius": 0,36    "smooth_radius": 0,37    "tile_norm_blocksize": 0,38    "tile_norm_smooth3D": 1,39    "invert": False40}41 42 43# def get_user_models():44#     model_strings = []45#     if os.path.exists(MODEL_LIST_PATH):46#         with open(MODEL_LIST_PATH, "r") as textfile:47#             lines = [line.rstrip() for line in textfile]48#             if len(lines) > 0:49#                 model_strings.extend(lines)50#     return model_strings51 52 53class SegModel():54    """55    Class representing a Cellpose model.56 57    Attributes:58        diam_mean (float): Mean "diameter" value for the model.59        builtin (bool): Whether the model is a built-in model or not.60        device (torch device): Device used for model running / training.61        nclasses (int): Number of classes in the model.62        nbase (list): List of base values for the model.63        net (CPnet): Cellpose network.64        pretrained_model (str): Path to pretrained cellpose model.65        pretrained_model_ortho (str): Path or model_name for pretrained cellpose model for ortho views in 3D.66        backbone (str): Type of network ("default" is the standard res-unet, "transformer" for the segformer).67 68    Methods:69        __init__(self, gpu=False, pretrained_model=False, model_type=None, diam_mean=30., device=None):70            Initialize the CellposeModel.71        72        eval(self, x, batch_size=8, resample=True, channels=None, channel_axis=None, z_axis=None, normalize=True, invert=False, rescale=None, diameter=None, flow_threshold=0.4, cellprob_threshold=0.0, do_3D=False, anisotropy=None, stitch_threshold=0.0, min_size=15, niter=None, augment=False, tile_overlap=0.1, bsize=224, interp=True, compute_masks=True, progress=None):73            Segment list of images x, or 4D array - Z x C x Y x X.74 75    """76 77    def __init__(self, gpu=False, pretrained_model="", model_type=None,78                 diam_mean=None, device=None, nchan=None, use_bfloat16=True, vit_checkpoint=None):79        """80        Initialize the CellposeModel.81 82        Parameters:83            gpu (bool, optional): Whether or not to save model to GPU, will check if GPU available.84            pretrained_model (str or list of strings, optional): Full path to pretrained cellpose model(s), if None or False, no model loaded.85            model_type (str, optional): Any model that is available in the GUI, use name in GUI e.g. "livecell" (can be user-trained or model zoo).86            diam_mean (float, optional): Mean "diameter", 30. is built-in value for "cyto" model; 17. is built-in value for "nuclei" model; if saved in custom model file (cellpose>=2.0) then it will be loaded automatically and overwrite this value.87            device (torch device, optional): Device used for model running / training (torch.device("cuda") or torch.device("cpu")), overrides gpu input, recommended if you want to use a specific GPU (e.g. torch.device("cuda:1")).88            use_bfloat16 (bool, optional): Use 16bit float precision instead of 32bit for model weights. Default to 16bit (True).89        """90        ### assign model device91        self.device = assign_device(gpu=gpu)[0] if device is None else device92        if torch.cuda.is_available():93            device_gpu = self.device.type == "cuda"94        elif torch.backends.mps.is_available():95            device_gpu = self.device.type == "mps"96        else:97            device_gpu = False98        self.gpu = device_gpu99 100        if pretrained_model is None:101            # raise ValueError("Must specify a pretrained model, training from scratch is not implemented")102            pretrained_model = ""103 104        self.pretrained_model = pretrained_model105        dtype = torch.bfloat16 if use_bfloat16 else torch.float32106        self.net = Transformer(dtype=dtype, checkpoint=vit_checkpoint).to(self.device)107 108        109    def eval(self, x, feat=None, batch_size=8, resample=True, channels=None, channel_axis=None,110             z_axis=None, normalize=True, invert=False, rescale=None, diameter=None,111             flow_threshold=0.4, cellprob_threshold=0.0, do_3D=False, anisotropy=None,112             flow3D_smooth=0, stitch_threshold=0.0, 113             min_size=15, max_size_fraction=0.4, niter=None, 114             augment=False, tile_overlap=0.1, bsize=256, 115             compute_masks=True, progress=None):116        117        if isinstance(x, list) or x.squeeze().ndim == 5:118            self.timing = []119            masks, styles, flows = [], [], []120            tqdm_out = utils.TqdmToLogger(models_logger, level=logging.INFO)121            nimg = len(x)122            iterator = trange(nimg, file=tqdm_out,123                              mininterval=30) if nimg > 1 else range(nimg)124            for i in iterator:125                tic = time.time()126                maski, flowi, stylei = self.eval(127                    x[i], 128                    feat=None if feat is None else feat[i],129                    batch_size=batch_size,130                    channel_axis=channel_axis, 131                    z_axis=z_axis,132                    normalize=normalize, 133                    invert=invert,134                    diameter=diameter[i] if isinstance(diameter, list) or135                        isinstance(diameter, np.ndarray) else diameter, 136                    do_3D=do_3D,137                    anisotropy=anisotropy, 138                    augment=augment, 139                    tile_overlap=tile_overlap, 140                    bsize=bsize, 141                    resample=resample,142                    flow_threshold=flow_threshold,143                    cellprob_threshold=cellprob_threshold, 144                    compute_masks=compute_masks,145                    min_size=min_size, 146                    max_size_fraction=max_size_fraction, 147                    stitch_threshold=stitch_threshold, 148                    flow3D_smooth=flow3D_smooth,149                    progress=progress, 150                    niter=niter)151                masks.append(maski)152                flows.append(flowi)153                styles.append(stylei)154                self.timing.append(time.time() - tic)155            return masks, flows, styles156 157        ############# actual eval code ############158        # reshape image159        x = transforms.convert_image(x, channel_axis=channel_axis,160                                        z_axis=z_axis, 161                                        do_3D=(do_3D or stitch_threshold > 0))162        163        # Add batch dimension if not present164        if x.ndim < 4:165            x = x[np.newaxis, ...]166        if feat is not None:167            if feat.ndim < 4:168                feat = feat[np.newaxis, ...]169        nimg = x.shape[0]170        171        image_scaling = None172        Ly_0 = x.shape[1]173        Lx_0 = x.shape[2]174        Lz_0 = None175        if stitch_threshold > 0:176            Lz_0 = x.shape[0]177        if diameter is not None:178            image_scaling = 30. / diameter179            x = transforms.resize_image(x,180                                        Ly=int(x.shape[1] * image_scaling),181                                        Lx=int(x.shape[2] * image_scaling))182            if feat is not None:183                feat = transforms.resize_image(feat,184                                        Ly=int(feat.shape[1] * image_scaling),185                                        Lx=int(feat.shape[2] * image_scaling))186 187 188        # normalize image189        normalize_params = normalize_default190        if isinstance(normalize, dict):191            normalize_params = {**normalize_params, **normalize}192        elif not isinstance(normalize, bool):193            raise ValueError("normalize parameter must be a bool or a dict")194        else:195            normalize_params["normalize"] = normalize196            normalize_params["invert"] = invert197 198        # pre-normalize if 3D stack for stitching or do_3D199        do_normalization = True if normalize_params["normalize"] else False200        if nimg > 1 and do_normalization and (stitch_threshold or do_3D):201            normalize_params["norm3D"] = True if do_3D else normalize_params["norm3D"]202            x = transforms.normalize_img(x, **normalize_params)203            do_normalization = False # do not normalize again204        else:205            if normalize_params["norm3D"] and nimg > 1 and do_normalization:206                models_logger.warning(207                    "normalize_params['norm3D'] is True but do_3D is False and stitch_threshold=0, so setting to False"208                )209                normalize_params["norm3D"] = False210        if do_normalization:211            x = transforms.normalize_img(x, **normalize_params)212 213        if feat is not None:214            if feat.shape[-1] > feat.shape[1]:215                # transpose feat to have channels last216                feat = np.moveaxis(feat, 1, -1)217 218        # adjust the anisotropy when diameter is specified and images are resized:219        if isinstance(anisotropy, (float, int)) and image_scaling:220            anisotropy = image_scaling * anisotropy221 222        dP, cellprob, styles = self._run_net(223            x, 224            feat=feat,225            augment=augment, 226            batch_size=batch_size, 227            tile_overlap=tile_overlap, 228            bsize=bsize,229            do_3D=do_3D, 230            anisotropy=anisotropy)231 232 233        if resample:234            # upsample flows before computing them: 235            dP = self._resize_gradients(dP, to_y_size=Ly_0, to_x_size=Lx_0, to_z_size=Lz_0)236            cellprob = self._resize_cellprob(cellprob, to_x_size=Lx_0, to_y_size=Ly_0, to_z_size=Lz_0)237 238 239        if compute_masks:240            niter0 = 200241            niter = niter0 if niter is None or niter == 0 else niter242            masks = self._compute_masks(x.shape, dP, cellprob, flow_threshold=flow_threshold,243                            cellprob_threshold=cellprob_threshold, min_size=min_size,244                        max_size_fraction=max_size_fraction, niter=niter,245                        stitch_threshold=stitch_threshold, do_3D=do_3D)246        else:247            masks = np.zeros(0) #pass back zeros if not compute_masks248        249        masks = masks.squeeze()250 251        # undo resizing:252        if image_scaling is not None or anisotropy is not None:253 254            if compute_masks:255                masks = transforms.resize_image(masks, Ly=Ly_0, Lx=Lx_0, no_channels=True, interpolation=cv2.INTER_NEAREST)256 257        return masks258 259    260 261    def _resize_cellprob(self, prob: np.ndarray, to_y_size: int, to_x_size: int, to_z_size: int = None) -> np.ndarray:262        """263        Resize cellprob array to specified dimensions for either 2D or 3D.264 265        Parameters:266            prob (numpy.ndarray): The cellprobs to resize, either in 2D or 3D. Returns the same ndim as provided.267            to_y_size (int): The target size along the Y-axis.268            to_x_size (int): The target size along the X-axis.269            to_z_size (int, optional): The target size along the Z-axis. Required270                for 3D cellprobs.271 272        Returns:273            numpy.ndarray: The resized cellprobs array with the same number of dimensions274            as the input.275 276        Raises:277            ValueError: If the input cellprobs array does not have 3 or 4 dimensions.278        """279        prob_shape = prob.shape280        prob = prob.squeeze()281        squeeze_happened = prob.shape != prob_shape282        prob_shape = np.array(prob_shape)283 284        if prob.ndim == 2:285            # 2D case:286            prob = transforms.resize_image(prob, Ly=to_y_size, Lx=to_x_size, no_channels=True)287            if squeeze_happened:288                prob = np.expand_dims(prob, int(np.argwhere(prob_shape == 1))) # add back empty axis for compatibility289        elif prob.ndim == 3:290            # 3D case: 291            prob = transforms.resize_image(prob, Ly=to_y_size, Lx=to_x_size, no_channels=True)292            prob = prob.transpose(1, 0, 2)293            prob = transforms.resize_image(prob, Ly=to_z_size, Lx=to_x_size, no_channels=True)294            prob = prob.transpose(1, 0, 2)295        else:296            raise ValueError(f'gradients have incorrect dimension after squeezing. Should be 2 or 3, prob shape: {prob.shape}')297        298        return prob299 300 301    def _resize_gradients(self, grads: np.ndarray, to_y_size: int, to_x_size: int, to_z_size: int = None) -> np.ndarray:302        """303        Resize gradient arrays to specified dimensions for either 2D or 3D gradients.304 305        Parameters:306            grads (np.ndarray): The gradients to resize, either in 2D or 3D. Returns the same ndim as provided.307            to_y_size (int): The target size along the Y-axis.308            to_x_size (int): The target size along the X-axis.309            to_z_size (int, optional): The target size along the Z-axis. Required310                for 3D gradients.311 312        Returns:313            numpy.ndarray: The resized gradient array with the same number of dimensions314            as the input.315 316        Raises:317            ValueError: If the input gradient array does not have 3 or 4 dimensions.318        """319        grads_shape = grads.shape320        grads = grads.squeeze()321        squeeze_happened = grads.shape != grads_shape322        grads_shape = np.array(grads_shape)323 324        if grads.ndim == 3:325            # 2D case, with XY flows in 2 channels:326            grads = np.moveaxis(grads, 0, -1) # Put gradients last327            grads = transforms.resize_image(grads, Ly=to_y_size, Lx=to_x_size, no_channels=False)328            grads = np.moveaxis(grads, -1, 0) # Put gradients first329 330            if squeeze_happened:331                grads = np.expand_dims(grads, int(np.argwhere(grads_shape == 1))) # add back empty axis for compatibility332        elif grads.ndim == 4:333            # dP has gradients that can be treated as channels:334            grads = grads.transpose(1, 2, 3, 0) # move gradients last:335            grads = transforms.resize_image(grads, Ly=to_y_size, Lx=to_x_size, no_channels=False)336            grads = grads.transpose(1, 0, 2, 3) # switch axes to resize again337            grads = transforms.resize_image(grads, Ly=to_z_size, Lx=to_x_size, no_channels=False)338            grads = grads.transpose(3, 1, 0, 2) # undo transposition339        else:340            raise ValueError(f'gradients have incorrect dimension after squeezing. Should be 3 or 4, grads shape: {grads.shape}')341        342        return grads343 344 345    def _run_net(self, x, feat=None,346                augment=False, 347                batch_size=8, tile_overlap=0.1,348                bsize=224, anisotropy=1.0, do_3D=False):349        """ run network on image x """350        tic = time.time()351        shape = x.shape352        nimg = shape[0]353 354 355        356        yf, styles = run_net(self.net, x, feat=feat, bsize=bsize, augment=augment,357                            batch_size=batch_size,  358                            tile_overlap=tile_overlap, 359                            )360        cellprob = yf[..., -1]361        dP = yf[..., -3:-1].transpose((3, 0, 1, 2))362        if yf.shape[-1] > 3:363            styles = yf[..., :-3]364        365        styles = styles.squeeze()366 367        net_time = time.time() - tic368        if nimg > 1:369            models_logger.info("network run in %2.2fs" % (net_time))370 371        return dP, cellprob, styles372    373    def _compute_masks(self, shape, dP, cellprob, flow_threshold=0.4, cellprob_threshold=0.0,374                       min_size=15, max_size_fraction=0.4, niter=None,375                       do_3D=False, stitch_threshold=0.0):376        """ compute masks from flows and cell probability """377        changed_device_from = None378        if self.device.type == "mps" and do_3D:379            models_logger.warning("MPS does not support 3D post-processing, switching to CPU")380            self.device = torch.device("cpu")381            changed_device_from = "mps"382        Lz, Ly, Lx = shape[:3]383        tic = time.time()384        # if do_3D:385        #     masks = dynamics.resize_and_compute_masks(386        #         dP, cellprob, niter=niter, cellprob_threshold=cellprob_threshold,387        #         flow_threshold=flow_threshold, do_3D=do_3D,388        #         min_size=min_size, max_size_fraction=max_size_fraction, 389        #         resize=shape[:3] if (np.array(dP.shape[-3:])!=np.array(shape[:3])).sum() 390        #                 else None,391        #         device=self.device)392        # else:393        nimg = shape[0]394        Ly0, Lx0 = cellprob[0].shape 395        resize = None if Ly0==Ly and Lx0==Lx else [Ly, Lx]396        tqdm_out = utils.TqdmToLogger(models_logger, level=logging.INFO)397        iterator = trange(nimg, file=tqdm_out,398                        mininterval=30) if nimg > 1 else range(nimg)399        for i in iterator:400            # turn off min_size for 3D stitching401            min_size0 = min_size if stitch_threshold == 0 or nimg == 1 else -1402            outputs = dynamics.resize_and_compute_masks(403                dP[:, i], cellprob[i],404                niter=niter, cellprob_threshold=cellprob_threshold,405                flow_threshold=flow_threshold, resize=resize,406                min_size=min_size0, max_size_fraction=max_size_fraction,407                device=self.device)408            if i==0 and nimg > 1:409                masks = np.zeros((nimg, shape[1], shape[2]), outputs.dtype)410            if nimg > 1:411                masks[i] = outputs412            else:413                masks = outputs414 415        if stitch_threshold > 0 and nimg > 1:416            models_logger.info(417                f"stitching {nimg} planes using stitch_threshold={stitch_threshold:0.3f} to make 3D masks"418            )419            masks = utils.stitch3D(masks, stitch_threshold=stitch_threshold)420            masks = utils.fill_holes_and_remove_small_masks(421                masks, min_size=min_size)422        elif nimg > 1:423            models_logger.warning(424                "3D stack used, but stitch_threshold=0 and do_3D=False, so masks are made per plane only"425            )426 427        flow_time = time.time() - tic428        if shape[0] > 1:429            models_logger.info("masks created in %2.2fs" % (flow_time))430        431        if changed_device_from is not None:432            models_logger.info("switching back to device %s" % self.device)433            self.device = torch.device(changed_device_from)434        return masks435