CoolFace
Apppublic

VisionLanguageGroup/MicroscopyMatching

sourceHugging Faceupdated 2mo agoView on Hugging Face
0likes
dynamics.py547 linesDownload Raw Back to seg_post_model
1"""2Copyright © 2025 Howard Hughes Medical Institute, Authored by Carsen Stringer , Michael Rariden and Marius Pachitariu.3"""4import os5from scipy.ndimage import find_objects, center_of_mass, mean6import torch7import numpy as np8import tifffile9from tqdm import trange10import fastremap11 12import logging13 14dynamics_logger = logging.getLogger(__name__)15 16from . import utils17 18import torch19import torch.nn.functional as F20 21def _extend_centers_gpu(neighbors, meds, isneighbor, shape, n_iter=200, 22                        device=torch.device("cpu")):23    """Runs diffusion on GPU to generate flows for training images or quality control.24 25    Args:26        neighbors (torch.Tensor): 9 x pixels in masks.27        meds (torch.Tensor): Mask centers.28        isneighbor (torch.Tensor): Valid neighbor boolean 9 x pixels.29        shape (tuple): Shape of the tensor.30        n_iter (int, optional): Number of iterations. Defaults to 200.31        device (torch.device, optional): Device to run the computation on. Defaults to torch.device("cpu").32 33    Returns:34        torch.Tensor: Generated flows.35 36    """37    if torch.prod(torch.tensor(shape)) > 4e7 or device.type == "mps":38        T = torch.zeros(shape, dtype=torch.float, device=device)39    else:40        T = torch.zeros(shape, dtype=torch.double, device=device)41 42    for i in range(n_iter):43        T[tuple(meds.T)] += 144        Tneigh = T[tuple(neighbors)]45        Tneigh *= isneighbor46        T[tuple(neighbors[:, 0])] = Tneigh.mean(axis=0)47    del meds, isneighbor, Tneigh48 49    if T.ndim == 2:50        grads = T[neighbors[0, [2, 1, 4, 3]], neighbors[1, [2, 1, 4, 3]]]51        del neighbors52        dy = grads[0] - grads[1]53        dx = grads[2] - grads[3]54        del grads55        mu_torch = np.stack((dy.cpu().squeeze(0), dx.cpu().squeeze(0)), axis=-2)56    else:57        grads = T[tuple(neighbors[:, 1:])]58        del neighbors59        dz = grads[0] - grads[1]60        dy = grads[2] - grads[3]61        dx = grads[4] - grads[5]62        del grads63        mu_torch = np.stack(64            (dz.cpu().squeeze(0), dy.cpu().squeeze(0), dx.cpu().squeeze(0)), axis=-2)65    return mu_torch66 67def center_of_mass(mask):68    yi, xi = np.nonzero(mask)69    ymean = int(np.round(yi.sum() / len(yi)))70    xmean = int(np.round(xi.sum() / len(xi)))71    if not ((yi==ymean) * (xi==xmean)).sum():72        # center is closest point to (ymean, xmean) within mask73        imin = ((xi - xmean)**2 + (yi - ymean)**2).argmin()74        ymean = yi[imin]75        xmean = xi[imin]76    77    return ymean, xmean78 79def get_centers(masks, slices):80    centers = [center_of_mass(masks[slices[i]]==(i+1)) for i in range(len(slices))]81    centers = np.array([np.array([centers[i][0] + slices[i][0].start, centers[i][1] + slices[i][1].start]) 82                    for i in range(len(slices))])83    exts = np.array([(slc[0].stop - slc[0].start) + (slc[1].stop - slc[1].start) + 2 for slc in slices])84    return centers, exts85 86 87def masks_to_flows_gpu(masks, device=torch.device("cpu"), niter=None):88    """Convert masks to flows using diffusion from center pixel.89 90    Center of masks where diffusion starts is defined by pixel closest to median within the mask.91 92    Args:93        masks (int, 2D or 3D array): Labelled masks. 0=NO masks; 1,2,...=mask labels.94        device (torch.device, optional): The device to run the computation on. Defaults to torch.device("cpu").95        niter (int, optional): Number of iterations for the diffusion process. Defaults to None.96 97    Returns:98        np.ndarray: A 4D array representing the flows for each pixel in Z, X, and Y.99       100 101    Returns:102        A tuple containing (mu, meds_p). mu is float 3D or 4D array of flows in (Z)XY. 103        meds_p are cell centers.104    """105    if device is None:106        device = torch.device('cuda') if torch.cuda.is_available() else torch.device('mps') if torch.backends.mps.is_available() else None107 108    if masks.max() > 0:109        Ly0, Lx0 = masks.shape110        Ly, Lx = Ly0 + 2, Lx0 + 2111        112        masks_padded = torch.from_numpy(masks.astype("int64")).to(device)113        masks_padded = F.pad(masks_padded, (1, 1, 1, 1))114        shape = masks_padded.shape115        116        ### get mask pixel neighbors117        y, x = torch.nonzero(masks_padded, as_tuple=True)118        y = y.int()119        x = x.int()120        neighbors = torch.zeros((2, 9, y.shape[0]), dtype=torch.int, device=device)121        yxi = [[0, -1, 1, 0, 0, -1, -1, 1, 1], [0, 0, 0, -1, 1, -1, 1, -1, 1]]122        for i in range(9):123            neighbors[0, i] = y + yxi[0][i]124            neighbors[1, i] = x + yxi[1][i]125        isneighbor = torch.ones((9, y.shape[0]), dtype=torch.bool, device=device)126        m0 = masks_padded[neighbors[0, 0], neighbors[1, 0]]127        for i in range(1, 9):128            isneighbor[i] = masks_padded[neighbors[0, i], neighbors[1, i]] == m0129        del m0, masks_padded130        131        ### get center-of-mass within cell132        slices = find_objects(masks)133        centers, ext = get_centers(masks, slices)134        meds_p = torch.from_numpy(centers).to(device).long()135        meds_p += 1  # for padding136 137        ### run diffusion138        n_iter = 2 * ext.max() if niter is None else niter139        mu = _extend_centers_gpu(neighbors, meds_p, isneighbor, shape, n_iter=n_iter,140                                device=device)141        mu = mu.astype("float64")142 143        # new normalization144        mu /= (1e-60 + (mu**2).sum(axis=0)**0.5)145 146        # put into original image147        mu0 = np.zeros((2, Ly0, Lx0))148        mu0[:, y.cpu().numpy() - 1, x.cpu().numpy() - 1] = mu149    else:150        # no masks, return empty flows151        mu0 = np.zeros((2, masks.shape[0], masks.shape[1]))152    return mu0153 154 155 156def flow_error(maski, dP_net, device=None):157    """Error in flows from predicted masks vs flows predicted by network run on image.158 159    This function serves to benchmark the quality of masks. It works as follows:160    1. The predicted masks are used to create a flow diagram.161    2. The mask-flows are compared to the flows that the network predicted.162 163    If there is a discrepancy between the flows, it suggests that the mask is incorrect.164    Masks with flow_errors greater than 0.4 are discarded by default. This setting can be165    changed in Cellpose.eval or CellposeModel.eval.166 167    Args:168        maski (np.ndarray, int): Masks produced from running dynamics on dP_net, where 0=NO masks; 1,2... are mask labels.169        dP_net (np.ndarray, float): ND flows where dP_net.shape[1:] = maski.shape.170 171    Returns:172        A tuple containing (flow_errors, dP_masks): flow_errors (np.ndarray, float): Mean squared error between predicted flows and flows from masks; 173        dP_masks (np.ndarray, float): ND flows produced from the predicted masks.174    """175    if dP_net.shape[1:] != maski.shape:176        print("ERROR: net flow is not same size as predicted masks")177        return178 179    # flows predicted from estimated masks180    dP_masks = masks_to_flows_gpu(maski, device=device)181    # difference between predicted flows vs mask flows182    flow_errors = np.zeros(maski.max())183    for i in range(dP_masks.shape[0]):184        flow_errors += mean((dP_masks[i] - dP_net[i] / 5.)**2, maski,185                            index=np.arange(1,186                                            maski.max() + 1))187 188    return flow_errors, dP_masks189 190 191def steps_interp(dP, inds, niter, device=torch.device("cpu")):192    """ Run dynamics of pixels to recover masks in 2D/3D, with interpolation between pixel values.193 194    Euler integration of dynamics dP for niter steps.195 196    Args:197        p (numpy.ndarray): Array of shape (n_points, 2 or 3) representing the initial pixel locations.198        dP (numpy.ndarray): Array of shape (2, Ly, Lx) or (3, Lz, Ly, Lx) representing the flow field.199        niter (int): Number of iterations to perform.200        device (torch.device, optional): Device to use for computation. Defaults to None.201 202    Returns:203        numpy.ndarray: Array of shape (n_points, 2) or (n_points, 3) representing the final pixel locations.204 205    Raises:206        None207 208    """209    210    shape = dP.shape[1:]211    ndim = len(shape)212    213    pt = torch.zeros((*[1]*ndim, len(inds[0]), ndim), dtype=torch.float32, device=device)214    im = torch.zeros((1, ndim, *shape), dtype=torch.float32, device=device)215    # Y and X dimensions, flipped X-1, Y-1216    # pt is [1 1 1 3 n_points]217    for n in range(ndim):218        if ndim==3:219            pt[0, 0, 0, :, ndim - n - 1] = torch.from_numpy(inds[n]).to(device, dtype=torch.float32)220        else:221            pt[0, 0, :, ndim - n - 1] = torch.from_numpy(inds[n]).to(device, dtype=torch.float32)222        im[0, ndim - n - 1] = torch.from_numpy(dP[n]).to(device, dtype=torch.float32)223    shape = np.array(shape)[::-1].astype("float") - 1  224    225    # normalize pt between  0 and  1, normalize the flow226    for k in range(ndim):227        im[:, k] *= 2. / shape[k]228        pt[..., k] /= shape[k]229 230    # normalize to between -1 and 1231    pt *= 2 232    pt -= 1233    234    # dynamics235    for t in range(niter):236        dPt = torch.nn.functional.grid_sample(im, pt, align_corners=False)237        for k in range(ndim):  #clamp the final pixel locations238            pt[..., k] = torch.clamp(pt[..., k] + dPt[:, k], -1., 1.)239 240    #undo the normalization from before, reverse order of operations241    pt += 1 242    pt *= 0.5243    for k in range(ndim):244        pt[..., k] *= shape[k]245 246    if ndim==3:247        pt = pt[..., [2, 1, 0]].squeeze()248        pt = pt.unsqueeze(0) if pt.ndim==1 else pt 249        return pt.T250    else:251        pt = pt[..., [1, 0]].squeeze()252        pt = pt.unsqueeze(0) if pt.ndim==1 else pt253        return pt.T254 255 256 257def remove_bad_flow_masks(masks, flows, threshold=0.4, device=torch.device("cpu")):258    """Remove masks which have inconsistent flows.259 260    Uses metrics.flow_error to compute flows from predicted masks 261    and compare flows to predicted flows from the network. Discards 262    masks with flow errors greater than the threshold.263 264    Args:265        masks (int, 2D or 3D array): Labelled masks, 0=NO masks; 1,2,...=mask labels,266            size [Ly x Lx] or [Lz x Ly x Lx].267        flows (float, 3D or 4D array): Flows [axis x Ly x Lx] or [axis x Lz x Ly x Lx].268        threshold (float, optional): Masks with flow error greater than threshold are discarded.269            Default is 0.4.270 271    Returns:272        masks (int, 2D or 3D array): Masks with inconsistent flow masks removed,273            0=NO masks; 1,2,...=mask labels, size [Ly x Lx] or [Lz x Ly x Lx].274    """275    device0 = device276    if masks.size > 10000 * 10000 and (device is not None and device.type == "cuda"):277 278        major_version, minor_version = torch.__version__.split(".")[:2]279        torch.cuda.empty_cache()280        if major_version == "1" and int(minor_version) < 10:281            # for PyTorch version lower than 1.10282            def mem_info():283                total_mem = torch.cuda.get_device_properties(device0.index).total_memory284                used_mem = torch.cuda.memory_allocated(device0.index)285                free_mem = total_mem - used_mem286                return total_mem, free_mem287        else:288            # for PyTorch version 1.10 and above289            def mem_info():290                free_mem, total_mem = torch.cuda.mem_get_info(device0.index)291                return total_mem, free_mem292        total_mem, free_mem = mem_info()293        if masks.size * 32 > free_mem:294            dynamics_logger.warning(295                "WARNING: image is very large, not using gpu to compute flows from masks for QC step flow_threshold"296            )297            dynamics_logger.info("turn off QC step with flow_threshold=0 if too slow")298            device0 = torch.device("cpu")299 300    merrors, _ = flow_error(masks, flows, device0)301    badi = 1 + (merrors > threshold).nonzero()[0]302    masks[np.isin(masks, badi)] = 0303    return masks304 305 306def max_pool1d(h, kernel_size=5, axis=1, out=None):307    """ memory efficient max_pool thanks to Mark Kittisopikul 308    309    for stride=1, padding=kernel_size//2, requires odd kernel_size >= 3310 311    """312    if out is None:313        out = h.clone()314    else:315        out.copy_(h)316 317    nd = h.shape[axis]    318    k0 = kernel_size // 2319    for d in range(-k0, k0+1):320        if axis==1:321            mv = out[:, max(-d,0):min(nd-d,nd)]322            hv = h[:, max(d,0):min(nd+d,nd)]323        elif axis==2:324            mv = out[:, :, max(-d,0):min(nd-d,nd)]325            hv = h[:,  :, max(d,0):min(nd+d,nd)]326        elif axis==3:327            mv = out[:, :, :, max(-d,0):min(nd-d,nd)]328            hv = h[:, :,  :, max(d,0):min(nd+d,nd)]329        torch.maximum(mv, hv, out=mv)330    return out331 332def max_pool_nd(h, kernel_size=5):333    """ memory efficient max_pool in 2d or 3d """334    ndim = h.ndim - 1335    hmax = max_pool1d(h, kernel_size=kernel_size, axis=1)336    hmax2 = max_pool1d(hmax, kernel_size=kernel_size, axis=2)337    if ndim==2:338        del hmax339        return hmax2340    else:341        hmax = max_pool1d(hmax2, kernel_size=kernel_size, axis=3, out=hmax)342        del hmax2 343        return hmax344 345def get_masks_torch(pt, inds, shape0, rpad=20, max_size_fraction=0.4):346    """Create masks using pixel convergence after running dynamics.347 348    Makes a histogram of final pixel locations p, initializes masks 349    at peaks of histogram and extends the masks from the peaks so that350    they include all pixels with more than 2 final pixels p. Discards 351    masks with flow errors greater than the threshold. 352 353    Parameters:354        p (float32, 3D or 4D array): Final locations of each pixel after dynamics,355            size [axis x Ly x Lx] or [axis x Lz x Ly x Lx].356        iscell (bool, 2D or 3D array): If iscell is not None, set pixels that are 357            iscell False to stay in their original location.358        rpad (int, optional): Histogram edge padding. Default is 20.359        max_size_fraction (float, optional): Masks larger than max_size_fraction of360            total image size are removed. Default is 0.4.361 362    Returns:363        M0 (int, 2D or 3D array): Masks with inconsistent flow masks removed, 364            0=NO masks; 1,2,...=mask labels, size [Ly x Lx] or [Lz x Ly x Lx].365    """366    367    ndim = len(shape0)368    device = pt.device369    370    rpad = 20371    pt += rpad372    pt = torch.clamp(pt, min=0)373    for i in range(len(pt)):374        pt[i] = torch.clamp(pt[i], max=shape0[i]+rpad-1)375 376    # # add extra padding to make divisible by 5377    # shape = tuple((np.ceil((shape0 + 2*rpad)/5) * 5).astype(int))378    shape = tuple(np.array(shape0) + 2*rpad)379 380    # sparse coo torch381    coo = torch.sparse_coo_tensor(pt, torch.ones(pt.shape[1], device=pt.device, dtype=torch.int), 382                                shape)383    h1 = coo.to_dense()384    del coo385 386    hmax1 = max_pool_nd(h1.unsqueeze(0), kernel_size=5)387    hmax1 = hmax1.squeeze()388    seeds1 = torch.nonzero((h1 - hmax1 > -1e-6) * (h1 > 10))389    del hmax1390    if len(seeds1) == 0:391        dynamics_logger.warning("no seeds found in get_masks_torch - no masks found.")392        return np.zeros(shape0, dtype="uint16")393    394    npts = h1[tuple(seeds1.T)]395    isort1 = npts.argsort()396    seeds1 = seeds1[isort1]397 398    n_seeds = len(seeds1)399    h_slc = torch.zeros((n_seeds, *[11]*ndim), device=seeds1.device)400    for k in range(n_seeds):401        slc = tuple([slice(seeds1[k][j]-5, seeds1[k][j]+6) for j in range(ndim)])402        h_slc[k] = h1[slc]403    del h1404    seed_masks = torch.zeros((n_seeds, *[11]*ndim), device=seeds1.device)405    if ndim==2:406        seed_masks[:,5,5] = 1407    else:408        seed_masks[:,5,5,5] = 1409    410    for iter in range(5):411        seed_masks = max_pool_nd(seed_masks, kernel_size=3)412        seed_masks *= h_slc > 2413    del h_slc 414    seeds_new = [tuple((torch.nonzero(seed_masks[k]) + seeds1[k] - 5).T) 415            for k in range(n_seeds)]416    del seed_masks 417    418    dtype = torch.int32 if n_seeds < 2**16 else torch.int64419    M1 = torch.zeros(shape, dtype=dtype, device=device)420    for k in range(n_seeds):421        M1[seeds_new[k]] = 1 + k422 423    M1 = M1[tuple(pt)]424    M1 = M1.cpu().numpy()425 426    dtype = "uint16" if n_seeds < 2**16 else "uint32"427    M0 = np.zeros(shape0, dtype=dtype)428    M0[inds] = M1429        430    # remove big masks431    uniq, counts = fastremap.unique(M0, return_counts=True)432    big = np.prod(shape0) * max_size_fraction433    bigc = uniq[counts > big]434    if len(bigc) > 0 and (len(bigc) > 1 or bigc[0] != 0):435        M0 = fastremap.mask(M0, bigc)436    fastremap.renumber(M0, in_place=True)  #convenient to guarantee non-skipped labels437    M0 = M0.reshape(tuple(shape0))438    439    return M0440 441 442def resize_and_compute_masks(dP, cellprob, niter=200, cellprob_threshold=0.0,443                             flow_threshold=0.4, do_3D=False, min_size=15,444                             max_size_fraction=0.4, resize=None, device=torch.device("cpu")):445    """Compute masks using dynamics from dP and cellprob, and resizes masks if resize is not None.446 447    Args:448        dP (numpy.ndarray): The dynamics flow field array.449        cellprob (numpy.ndarray): The cell probability array.450        p (numpy.ndarray, optional): The pixels on which to run dynamics. Defaults to None451        niter (int, optional): The number of iterations for mask computation. Defaults to 200.452        cellprob_threshold (float, optional): The threshold for cell probability. Defaults to 0.0.453        flow_threshold (float, optional): The threshold for quality control metrics. Defaults to 0.4.454        interp (bool, optional): Whether to interpolate during dynamics computation. Defaults to True.455        do_3D (bool, optional): Whether to perform mask computation in 3D. Defaults to False.456        min_size (int, optional): The minimum size of the masks. Defaults to 15.457        max_size_fraction (float, optional): Masks larger than max_size_fraction of458            total image size are removed. Default is 0.4.459        resize (tuple, optional): The desired size for resizing the masks. Defaults to None.460        device (torch.device, optional): The device to use for computation. Defaults to torch.device("cpu").461 462    Returns:463        tuple: A tuple containing the computed masks and the final pixel locations.464    """465    mask = compute_masks(dP, cellprob, niter=niter,466                            cellprob_threshold=cellprob_threshold,467                            flow_threshold=flow_threshold, do_3D=do_3D,468                            max_size_fraction=max_size_fraction, 469                            device=device)470 471    if resize is not None:472        dynamics_logger.warning("Resizing is depricated in v4.0.1+")473 474    mask = utils.fill_holes_and_remove_small_masks(mask, min_size=min_size)475 476    return mask477 478 479def compute_masks(dP, cellprob, p=None, niter=200, cellprob_threshold=0.0,480                  flow_threshold=0.4, do_3D=False, min_size=-1,481                  max_size_fraction=0.4, device=torch.device("cpu")):482    """Compute masks using dynamics from dP and cellprob.483 484    Args:485        dP (numpy.ndarray): The dynamics flow field array.486        cellprob (numpy.ndarray): The cell probability array.487        p (numpy.ndarray, optional): The pixels on which to run dynamics. Defaults to None488        niter (int, optional): The number of iterations for mask computation. Defaults to 200.489        cellprob_threshold (float, optional): The threshold for cell probability. Defaults to 0.0.490        flow_threshold (float, optional): The threshold for quality control metrics. Defaults to 0.4.491        interp (bool, optional): Whether to interpolate during dynamics computation. Defaults to True.492        do_3D (bool, optional): Whether to perform mask computation in 3D. Defaults to False.493        min_size (int, optional): The minimum size of the masks. Defaults to 15.494        max_size_fraction (float, optional): Masks larger than max_size_fraction of495            total image size are removed. Default is 0.4.496        device (torch.device, optional): The device to use for computation. Defaults to torch.device("cpu").497 498    Returns:499        tuple: A tuple containing the computed masks and the final pixel locations.500    """501    502    if (cellprob > cellprob_threshold).sum():  #mask at this point is a cell cluster binary map, not labels503        inds = np.nonzero(cellprob > cellprob_threshold)504        if len(inds[0]) == 0:505            dynamics_logger.info("No cell pixels found.")506            shape = cellprob.shape507            mask = np.zeros(shape, "uint16")508            return mask509 510        p_final = steps_interp(dP * (cellprob > cellprob_threshold) / 5., 511                               inds=inds, niter=niter, 512                                device=device)513        if not torch.is_tensor(p_final):514            p_final = torch.from_numpy(p_final).to(device, dtype=torch.int)515        else:516            p_final = p_final.int()517        # calculate masks518        if device.type == "mps":519            p_final = p_final.to(torch.device("cpu"))520        mask = get_masks_torch(p_final, inds, dP.shape[1:], 521                               max_size_fraction=max_size_fraction)522        del p_final523        # flow thresholding factored out of get_masks524        if not do_3D:525            if mask.max() > 0 and flow_threshold is not None and flow_threshold > 0:526                # make sure labels are unique at output of get_masks527                mask = remove_bad_flow_masks(mask, dP, threshold=flow_threshold,528                                             device=device)529 530        if mask.max() < 2**16 and mask.dtype != "uint16":531            mask = mask.astype("uint16")532 533    else:  # nothing to compute, just make it compatible534        dynamics_logger.info("No cell pixels found.")535        shape = cellprob.shape536        mask = np.zeros(cellprob.shape, "uint16")537        return mask538    539    if min_size > 0:540        mask = utils.fill_holes_and_remove_small_masks(mask, min_size=min_size)541 542    if mask.dtype == np.uint32:543        dynamics_logger.warning(544            "more than 65535 masks in image, masks returned as np.uint32")545 546    return mask547