CoolFace
Apppublic

VisionLanguageGroup/MicroscopyMatching

sourceHugging Faceupdated 2mo agoView on Hugging Face
0likes
transforms.py1018 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 logging5 6import cv27import numpy as np8import torch9from scipy.ndimage import gaussian_filter1d10from torch.fft import fft2, fftshift, ifft211 12transforms_logger = logging.getLogger(__name__)13 14 15def _taper_mask(ly=224, lx=224, sig=7.5):16    """17    Generate a taper mask.18 19    Args:20        ly (int): The height of the mask. Default is 224.21        lx (int): The width of the mask. Default is 224.22        sig (float): The sigma value for the tapering function. Default is 7.5.23 24    Returns:25        numpy.ndarray: The taper mask.26 27    """28    bsize = max(224, max(ly, lx))29    xm = np.arange(bsize)30    xm = np.abs(xm - xm.mean())31    mask = 1 / (1 + np.exp((xm - (bsize / 2 - 20)) / sig))32    mask = mask * mask[:, np.newaxis]33    mask = mask[bsize // 2 - ly // 2:bsize // 2 + ly // 2 + ly % 2,34                bsize // 2 - lx // 2:bsize // 2 + lx // 2 + lx % 2]35    return mask36 37 38def unaugment_tiles(y):39    """Reverse test-time augmentations for averaging (includes flipping of flowsY and flowsX).40 41    Args:42        y (float32): Array of shape (ntiles_y, ntiles_x, chan, Ly, Lx) where chan = (flowsY, flowsX, cell prob).43 44    Returns:45        float32: Array of shape (ntiles_y, ntiles_x, chan, Ly, Lx).46 47    """48    for j in range(y.shape[0]):49        for i in range(y.shape[1]):50            if j % 2 == 0 and i % 2 == 1:51                y[j, i] = y[j, i, :, ::-1, :]52                y[j, i, 0] *= -153            elif j % 2 == 1 and i % 2 == 0:54                y[j, i] = y[j, i, :, :, ::-1]55                y[j, i, 1] *= -156            elif j % 2 == 1 and i % 2 == 1:57                y[j, i] = y[j, i, :, ::-1, ::-1]58                y[j, i, 0] *= -159                y[j, i, 1] *= -160    return y61 62 63def average_tiles(y, ysub, xsub, Ly, Lx):64    """65    Average the results of the network over tiles.66 67    Args:68        y (float): Output of cellpose network for each tile. Shape: [ntiles x nclasses x bsize x bsize]69        ysub (list): List of arrays with start and end of tiles in Y of length ntiles70        xsub (list): List of arrays with start and end of tiles in X of length ntiles71        Ly (int): Size of pre-tiled image in Y (may be larger than original image if image size is less than bsize)72        Lx (int): Size of pre-tiled image in X (may be larger than original image if image size is less than bsize)73 74    Returns:75        yf (float32): Network output averaged over tiles. Shape: [nclasses x Ly x Lx]76    """77    Navg = np.zeros((Ly, Lx))78    yf = np.zeros((y.shape[1], Ly, Lx), np.float32)79    # taper edges of tiles80    mask = _taper_mask(ly=y.shape[-2], lx=y.shape[-1])81    for j in range(len(ysub)):82        yf[:, ysub[j][0]:ysub[j][1], xsub[j][0]:xsub[j][1]] += y[j] * mask83        Navg[ysub[j][0]:ysub[j][1], xsub[j][0]:xsub[j][1]] += mask84    yf /= Navg85    return yf86 87 88def make_tiles(imgi, bsize=224, augment=False, tile_overlap=0.1):89    """Make tiles of image to run at test-time.90 91    Args:92        imgi (np.ndarray): Array of shape (nchan, Ly, Lx) representing the input image.93        bsize (int, optional): Size of tiles. Defaults to 224.94        augment (bool, optional): Whether to flip tiles and set tile_overlap=2. Defaults to False.95        tile_overlap (float, optional): Fraction of overlap of tiles. Defaults to 0.1.96 97    Returns:98        A tuple containing (IMG, ysub, xsub, Ly, Lx):99        IMG (np.ndarray): Array of shape (ntiles, nchan, bsize, bsize) representing the tiles.100        ysub (list): List of arrays with start and end of tiles in Y of length ntiles.101        xsub (list): List of arrays with start and end of tiles in X of length ntiles.102        Ly (int): Height of the input image.103        Lx (int): Width of the input image.104    """105    nchan, Ly, Lx = imgi.shape106    if augment:107        bsize = np.int32(bsize)108        # pad if image smaller than bsize109        if Ly < bsize:110            imgi = np.concatenate((imgi, np.zeros((nchan, bsize - Ly, Lx))), axis=1)111            Ly = bsize112        if Lx < bsize:113            imgi = np.concatenate((imgi, np.zeros((nchan, Ly, bsize - Lx))), axis=2)114        Ly, Lx = imgi.shape[-2:]115        116        # tiles overlap by half of tile size117        ny = max(2, int(np.ceil(2. * Ly / bsize)))118        nx = max(2, int(np.ceil(2. * Lx / bsize)))119        ystart = np.linspace(0, Ly - bsize, ny).astype(int)120        xstart = np.linspace(0, Lx - bsize, nx).astype(int)121 122        ysub = []123        xsub = []124 125        # flip tiles so that overlapping segments are processed in rotation126        IMG = np.zeros((len(ystart), len(xstart), nchan, bsize, bsize), np.float32)127        for j in range(len(ystart)):128            for i in range(len(xstart)):129                ysub.append([ystart[j], ystart[j] + bsize])130                xsub.append([xstart[i], xstart[i] + bsize])131                IMG[j, i] = imgi[:, ysub[-1][0]:ysub[-1][1], xsub[-1][0]:xsub[-1][1]]132                # flip tiles to allow for augmentation of overlapping segments133                if j % 2 == 0 and i % 2 == 1:134                    IMG[j, i] = IMG[j, i, :, ::-1, :]135                elif j % 2 == 1 and i % 2 == 0:136                    IMG[j, i] = IMG[j, i, :, :, ::-1]137                elif j % 2 == 1 and i % 2 == 1:138                    IMG[j, i] = IMG[j, i, :, ::-1, ::-1]139    else:140        tile_overlap = min(0.5, max(0.05, tile_overlap))141        bsizeY, bsizeX = min(bsize, Ly), min(bsize, Lx)142        bsizeY = np.int32(bsizeY)143        bsizeX = np.int32(bsizeX)144        # tiles overlap by 10% tile size145        ny = 1 if Ly <= bsize else int(np.ceil((1. + 2 * tile_overlap) * Ly / bsize))146        nx = 1 if Lx <= bsize else int(np.ceil((1. + 2 * tile_overlap) * Lx / bsize))147        ystart = np.linspace(0, Ly - bsizeY, ny).astype(int)148        xstart = np.linspace(0, Lx - bsizeX, nx).astype(int)149 150        ysub = []151        xsub = []152        IMG = np.zeros((len(ystart), len(xstart), nchan, bsizeY, bsizeX), np.float32)153        for j in range(len(ystart)):154            for i in range(len(xstart)):155                ysub.append([ystart[j], ystart[j] + bsizeY])156                xsub.append([xstart[i], xstart[i] + bsizeX])157                IMG[j, i] = imgi[:, ysub[-1][0]:ysub[-1][1], xsub[-1][0]:xsub[-1][1]]158 159    return IMG, ysub, xsub, Ly, Lx160 161 162def normalize99(Y, lower=1, upper=99, copy=True, downsample=False):163    """164    Normalize the image so that 0.0 corresponds to the 1st percentile and 1.0 corresponds to the 99th percentile.165 166    Args:167        Y (ndarray): The input image (for downsample, use [Ly x Lx] or [Lz x Ly x Lx]).168        lower (int, optional): The lower percentile. Defaults to 1.169        upper (int, optional): The upper percentile. Defaults to 99.170        copy (bool, optional): Whether to create a copy of the input image. Defaults to True.171        downsample (bool, optional): Whether to downsample image to compute percentiles. Defaults to False.172 173    Returns:174        ndarray: The normalized image.175    """176    X = Y.copy() if copy else Y177    X = X.astype("float32") if X.dtype!="float64" and X.dtype!="float32" else X178    if downsample and X.size > 224**3:179        nskip = [max(1, X.shape[i] // 224) for i in range(X.ndim)]180        nskip[0] = max(1, X.shape[0] // 50) if X.ndim == 3 else nskip[0]181        slc = tuple([slice(0, X.shape[i], nskip[i]) for i in range(X.ndim)])182        x01 = np.percentile(X[slc], lower)183        x99 = np.percentile(X[slc], upper)184    else:185        x01 = np.percentile(X, lower)186        x99 = np.percentile(X, upper)187    if x99 - x01 > 1e-3:188        X -= x01 189        X /= (x99 - x01)190    else:191        X[:] = 0192    return X193 194 195def normalize99_tile(img, blocksize=100, lower=1., upper=99., tile_overlap=0.1,196                     norm3D=False, smooth3D=1, is3D=False):197    """Compute normalization like normalize99 function but in tiles.198 199    Args:200        img (numpy.ndarray): Array of shape (Lz x) Ly x Lx (x nchan) containing the image.201        blocksize (float, optional): Size of tiles. Defaults to 100.202        lower (float, optional): Lower percentile for normalization. Defaults to 1.0.203        upper (float, optional): Upper percentile for normalization. Defaults to 99.0.204        tile_overlap (float, optional): Fraction of overlap of tiles. Defaults to 0.1.205        norm3D (bool, optional): Use same tiled normalization for each z-plane. Defaults to False.206        smooth3D (int, optional): Smoothing factor for 3D normalization. Defaults to 1.207        is3D (bool, optional): Set to True if image is a 3D stack. Defaults to False.208 209    Returns:210        numpy.ndarray: Normalized image array of shape (Lz x) Ly x Lx (x nchan).211    """212    is1c = True if img.ndim == 2 or (is3D and img.ndim == 3) else False213    is3D = True if img.ndim > 3 or (is3D and img.ndim == 3) else False214    img = img[..., np.newaxis] if is1c else img215    img = img[np.newaxis, ...] if img.ndim == 3 else img216    Lz, Ly, Lx, nchan = img.shape217 218    tile_overlap = min(0.5, max(0.05, tile_overlap))219    blocksizeY, blocksizeX = min(blocksize, Ly), min(blocksize, Lx)220    blocksizeY = np.int32(blocksizeY)221    blocksizeX = np.int32(blocksizeX)222    # tiles overlap by 10% tile size223    ny = 1 if Ly <= blocksize else int(np.ceil(224        (1. + 2 * tile_overlap) * Ly / blocksize))225    nx = 1 if Lx <= blocksize else int(np.ceil(226        (1. + 2 * tile_overlap) * Lx / blocksize))227    ystart = np.linspace(0, Ly - blocksizeY, ny).astype(int)228    xstart = np.linspace(0, Lx - blocksizeX, nx).astype(int)229    ysub = []230    xsub = []231    for j in range(len(ystart)):232        for i in range(len(xstart)):233            ysub.append([ystart[j], ystart[j] + blocksizeY])234            xsub.append([xstart[i], xstart[i] + blocksizeX])235 236    x01_tiles_z = []237    x99_tiles_z = []238    for z in range(Lz):239        IMG = np.zeros((len(ystart), len(xstart), blocksizeY, blocksizeX, nchan),240                       "float32")241        k = 0242        for j in range(len(ystart)):243            for i in range(len(xstart)):244                IMG[j, i] = img[z, ysub[k][0]:ysub[k][1], xsub[k][0]:xsub[k][1], :]245                k += 1246        x01_tiles = np.percentile(IMG, lower, axis=(-3, -2))247        x99_tiles = np.percentile(IMG, upper, axis=(-3, -2))248 249        # fill areas with small differences with neighboring squares250        to_fill = np.zeros(x01_tiles.shape[:2], "bool")251        for c in range(nchan):252            to_fill = x99_tiles[:, :, c] - x01_tiles[:, :, c] < +1e-3253            if to_fill.sum() > 0 and to_fill.sum() < x99_tiles[:, :, c].size:254                fill_vals = np.nonzero(to_fill)255                fill_neigh = np.nonzero(~to_fill)256                nearest_neigh = (257                    (fill_vals[0] - fill_neigh[0][:, np.newaxis])**2 +258                    (fill_vals[1] - fill_neigh[1][:, np.newaxis])**2).argmin(axis=0)259                x01_tiles[fill_vals[0], fill_vals[1],260                          c] = x01_tiles[fill_neigh[0][nearest_neigh],261                                         fill_neigh[1][nearest_neigh], c]262                x99_tiles[fill_vals[0], fill_vals[1],263                          c] = x99_tiles[fill_neigh[0][nearest_neigh],264                                         fill_neigh[1][nearest_neigh], c]265            elif to_fill.sum() > 0 and to_fill.sum() == x99_tiles[:, :, c].size:266                x01_tiles[:, :, c] = 0267                x99_tiles[:, :, c] = 1268        x01_tiles_z.append(x01_tiles)269        x99_tiles_z.append(x99_tiles)270 271    x01_tiles_z = np.array(x01_tiles_z)272    x99_tiles_z = np.array(x99_tiles_z)273    # do not smooth over z-axis if not normalizing separately per plane274    for a in range(2):275        x01_tiles_z = gaussian_filter1d(x01_tiles_z, 1, axis=a)276        x99_tiles_z = gaussian_filter1d(x99_tiles_z, 1, axis=a)277    if norm3D:278        smooth3D = 1 if smooth3D == 0 else smooth3D279        x01_tiles_z = gaussian_filter1d(x01_tiles_z, smooth3D, axis=a)280        x99_tiles_z = gaussian_filter1d(x99_tiles_z, smooth3D, axis=a)281 282    if not norm3D and Lz > 1:283        x01 = np.zeros((len(x01_tiles_z), Ly, Lx, nchan), "float32")284        x99 = np.zeros((len(x01_tiles_z), Ly, Lx, nchan), "float32")285        for z in range(Lz):286            x01_rsz = cv2.resize(x01_tiles_z[z], (Lx, Ly),287                                 interpolation=cv2.INTER_LINEAR)288            x01[z] = x01_rsz[..., np.newaxis] if nchan == 1 else x01_rsz289            x99_rsz = cv2.resize(x99_tiles_z[z], (Lx, Ly),290                                 interpolation=cv2.INTER_LINEAR)291            x99[z] = x99_rsz[..., np.newaxis] if nchan == 1 else x01_rsz292        if (x99 - x01).min() < 1e-3:293            raise ZeroDivisionError(294                "cannot use norm3D=False with tile_norm, sample is too sparse; set norm3D=True or tile_norm=0"295            )296    else:297        x01 = cv2.resize(x01_tiles_z.mean(axis=0), (Lx, Ly),298                         interpolation=cv2.INTER_LINEAR)299        x99 = cv2.resize(x99_tiles_z.mean(axis=0), (Lx, Ly),300                         interpolation=cv2.INTER_LINEAR)301        if x01.ndim < 3:302            x01 = x01[..., np.newaxis]303            x99 = x99[..., np.newaxis]304 305    if is1c:306        img, x01, x99 = img.squeeze(), x01.squeeze(), x99.squeeze()307    elif not is3D:308        img, x01, x99 = img[0], x01[0], x99[0]309 310    # normalize311    img -= x01 312    img /= (x99 - x01)313 314    return img315 316 317def gaussian_kernel(sigma, Ly, Lx, device=torch.device("cpu")):318    """319    Generates a 2D Gaussian kernel.320 321    Args:322        sigma (float): Standard deviation of the Gaussian distribution.323        Ly (int): Number of pixels in the y-axis.324        Lx (int): Number of pixels in the x-axis.325        device (torch.device, optional): Device to store the kernel tensor. Defaults to torch.device("cpu").326 327    Returns:328        torch.Tensor: 2D Gaussian kernel tensor.329 330    """331    y = torch.linspace(-Ly / 2, Ly / 2 + 1, Ly, device=device)332    x = torch.linspace(-Ly / 2, Ly / 2 + 1, Lx, device=device)333    y, x = torch.meshgrid(y, x, indexing="ij")334    kernel = torch.exp(-(y**2 + x**2) / (2 * sigma**2))335    kernel /= kernel.sum()336    return kernel337 338 339def smooth_sharpen_img(img, smooth_radius=6, sharpen_radius=12,340                       device=torch.device("cpu"), is3D=False):341    """Sharpen blurry images with surround subtraction and/or smooth noisy images.342 343    Args:344        img (float32): Array that's (Lz x) Ly x Lx (x nchan).345        smooth_radius (float, optional): Size of gaussian smoothing filter, recommended to be 1/10-1/4 of cell diameter346            (if also sharpening, should be 2-3x smaller than sharpen_radius). Defaults to 6.347        sharpen_radius (float, optional): Size of gaussian surround filter, recommended to be 1/8-1/2 of cell diameter348            (if also smoothing, should be 2-3x larger than smooth_radius). Defaults to 12.349        device (torch.device, optional): Device on which to perform sharpening.350            Will be faster on GPU but need to ensure GPU has RAM for image. Defaults to torch.device("cpu").351        is3D (bool, optional): If image is 3D stack (only necessary to set if img.ndim==3). Defaults to False.352 353    Returns:354        img_sharpen (float32): Array that's (Lz x) Ly x Lx (x nchan).355    """356    img_sharpen = torch.from_numpy(img.astype("float32")).to(device)357    shape = img_sharpen.shape358 359    is1c = True if img_sharpen.ndim == 2 or (is3D and img_sharpen.ndim == 3) else False360    is3D = True if img_sharpen.ndim > 3 or (is3D and img_sharpen.ndim == 3) else False361    img_sharpen = img_sharpen.unsqueeze(-1) if is1c else img_sharpen362    img_sharpen = img_sharpen.unsqueeze(0) if img_sharpen.ndim == 3 else img_sharpen363    Lz, Ly, Lx, nchan = img_sharpen.shape364 365    if smooth_radius > 0:366        kernel = gaussian_kernel(smooth_radius, Ly, Lx, device=device)367        if sharpen_radius > 0:368            kernel += -1 * gaussian_kernel(sharpen_radius, Ly, Lx, device=device)369    elif sharpen_radius > 0:370        kernel = -1 * gaussian_kernel(sharpen_radius, Ly, Lx, device=device)371        kernel[Ly // 2, Lx // 2] = 1372 373    fhp = fft2(kernel)374    for z in range(Lz):375        for c in range(nchan):376            img_filt = torch.real(ifft2(377                fft2(img_sharpen[z, :, :, c]) * torch.conj(fhp)))378            img_filt = fftshift(img_filt)379            img_sharpen[z, :, :, c] = img_filt380 381    img_sharpen = img_sharpen.reshape(shape)382    return img_sharpen.cpu().numpy()383 384 385def move_axis(img, m_axis=-1, first=True):386    """ move axis m_axis to first or last position """387    if m_axis == -1:388        m_axis = img.ndim - 1389    m_axis = min(img.ndim - 1, m_axis)390    axes = np.arange(0, img.ndim)391    if first:392        axes[1:m_axis + 1] = axes[:m_axis]393        axes[0] = m_axis394    else:395        axes[m_axis:-1] = axes[m_axis + 1:]396        axes[-1] = m_axis397    img = img.transpose(tuple(axes))398    return img399 400 401 402def convert_image(x, channel_axis=None, z_axis=None, do_3D=False):403    """Converts the image to have the z-axis first, channels last. Image will be converted to 3 channels if it is not already.404    If more than 3 channels are provided, only the first 3 channels will be used. 405 406    Accepts: 407        - 2D images with no channel dimension: `z_axis` and `channel_axis` must be `None`408        - 2D images with channel dimension: `channel_axis` will be guessed between first or last axis, can also specify `channel_axis`. `z_axis` must be `None`409        - 3D images with or without channels: 410 411    Args:412        x (numpy.ndarray or torch.Tensor): The input image.413        channel_axis (int or None): The axis of the channels in the input image. If None, the axis is determined automatically.414        z_axis (int or None): The axis of the z-dimension in the input image. If None, the axis is determined automatically.415        do_3D (bool): Whether to process the image in 3D mode. Defaults to False.416 417    Returns:418        numpy.ndarray: The converted image.419 420    Raises:421        ValueError: If the input image is 2D and do_3D is True.422        ValueError: If the input image is 4D and do_3D is False.423    """424 425    # check if image is a torch array instead of numpy array, convert to numpy426    ndim = x.ndim427    if torch.is_tensor(x):428        transforms_logger.warning("torch array used as input, converting to numpy")429        x = x.cpu().numpy()430 431    # should be 2D432    if z_axis is not None and not do_3D:433        raise ValueError("2D image provided, but z_axis is not None. Set z_axis=None to process 2D images of ndim=2 or 3.")434 435    if ndim == 4 and not do_3D:436        raise ValueError("3D input image provided, but do_3D is False. Set do_3D=True to process 3D images. ndims=4")437 438    439    ######################## 2D reshaping ########################440    # if user specifies channel axis, return early441    if channel_axis is not None:442        if ndim == 2:443            raise ValueError("2D image provided, but channel_axis is not None. Set channel_axis=None to process 2D images of ndim=2.")444        445        # Put channel axis last:446        # Find the indices of the dims that need to be put in dim 0 and 1447        n_channels = x.shape[channel_axis]448        x_shape_dims = list(x.shape)449        del x_shape_dims[channel_axis]450        dimension_indicies = [i for i in range(x.ndim)]451        del dimension_indicies[channel_axis]452 453        x = x.transpose((dimension_indicies[0], dimension_indicies[1], channel_axis))454 455        if n_channels != 3:456            x_chans_to_copy = min(3, n_channels)457 458            if n_channels > 3: 459                transforms_logger.warning("more than 3 channels provided, only segmenting on first 3 channels")460                x = x[..., :x_chans_to_copy]461            else: 462                x_out = np.zeros((x_shape_dims[0], x_shape_dims[1], 3), dtype=x.dtype)463                x_out[..., :x_chans_to_copy] = x[...]464                x = x_out465                del x_out466 467        return x468 469    # do image padding and channel conversion470    if ndim == 2:471        # grayscale image, make 3 channels472        x_out = np.zeros((x.shape[0], x.shape[1], 3), dtype=x.dtype)473        x_out[..., 0] = x474        x = x_out475        del x_out476    elif ndim == 3:477        # assume 2d with channels478        # find dim with smaller size between first and last dims479        move_channel_axis = x.shape[0] < x.shape[2]480        if move_channel_axis:481            x = x.transpose((1, 2, 0))482 483        # zero padding up to 3 channels: 484        num_channels = x.shape[-1]485        if num_channels > 3: 486            transforms_logger.warning("Found more than 3 channels, only using first 3")487            num_channels = 3488        x_out = np.zeros((x.shape[0], x.shape[1], 3), dtype=x.dtype)489        x_out[..., :num_channels] = x[..., :num_channels]490        x = x_out491        del x_out492    else:493        # something is wrong: yell494        expected_shapes = "2D (H, W), 3D (H, W, C), or 4D (Z, H, W, C)"495        transforms_logger.critical(f"ERROR: Unexpected image shape: {str(x.shape)}. Expected shapes: {expected_shapes}")496        raise ValueError(f"ERROR: Unexpected image shape: {str(x.shape)}. Expected shapes: {expected_shapes}")497 498    return x499    500 501def normalize_img(img, normalize=True, norm3D=True, invert=False, lowhigh=None,502                  percentile=(1., 99.), sharpen_radius=0, smooth_radius=0,503                  tile_norm_blocksize=0, tile_norm_smooth3D=1, axis=-1):504    """Normalize each channel of the image with optional inversion, smoothing, and sharpening.505 506    Args:507        img (ndarray): The input image. It should have at least 3 dimensions.508            If it is 4-dimensional, it assumes the first non-channel axis is the Z dimension.509        normalize (bool, optional): Whether to perform normalization. Defaults to True.510        norm3D (bool, optional): Whether to normalize in 3D. If True, the entire 3D stack will511            be normalized per channel. If False, normalization is applied per Z-slice. Defaults to False.512        invert (bool, optional): Whether to invert the image. Useful if cells are dark instead of bright.513            Defaults to False.514        lowhigh (tuple or ndarray, optional): The lower and upper bounds for normalization.515            Can be a tuple of two values (applied to all channels) or an array of shape (nchan, 2)516            for per-channel normalization. Incompatible with smoothing and sharpening.517            Defaults to None.518        percentile (tuple, optional): The lower and upper percentiles for normalization. If provided, it should be519            a tuple of two values. Each value should be between 0 and 100. Defaults to (1.0, 99.0).520        sharpen_radius (int, optional): The radius for sharpening the image. Defaults to 0.521        smooth_radius (int, optional): The radius for smoothing the image. Defaults to 0.522        tile_norm_blocksize (int, optional): The block size for tile-based normalization. Defaults to 0.523        tile_norm_smooth3D (int, optional): The smoothness factor for tile-based normalization in 3D. Defaults to 1.524        axis (int, optional): The channel axis to loop over for normalization. Defaults to -1.525 526    Returns:527        ndarray: The normalized image of the same size.528 529    Raises:530        ValueError: If the image has less than 3 dimensions.531        ValueError: If the provided lowhigh or percentile values are invalid.532        ValueError: If the image is inverted without normalization.533 534    """535    if img.ndim < 3:536        error_message = "Image needs to have at least 3 dimensions"537        transforms_logger.critical(error_message)538        raise ValueError(error_message)539 540    img_norm = img if img.dtype=="float32" else img.astype(np.float32)541    if axis != -1 and axis != img_norm.ndim - 1:542        img_norm = np.moveaxis(img_norm, axis, -1)  # Move channel axis to last543 544    nchan = img_norm.shape[-1]545 546    # Validate and handle lowhigh bounds547    if lowhigh is not None:548        lowhigh = np.array(lowhigh)549        if lowhigh.shape == (2,):550            lowhigh = np.tile(lowhigh, (nchan, 1))  # Expand to per-channel bounds551        elif lowhigh.shape != (nchan, 2):552            error_message = "`lowhigh` must have shape (2,) or (nchan, 2)"553            transforms_logger.critical(error_message)554            raise ValueError(error_message)555 556    # Validate percentile557    if percentile is None:558        percentile = (1.0, 99.0)559    elif not (0 <= percentile[0] < percentile[1] <= 100):560        error_message = "Invalid percentile range, should be between 0 and 100"561        transforms_logger.critical(error_message)562        raise ValueError(error_message)563 564    # Apply normalization based on lowhigh or percentile565    cgood = np.zeros(nchan, "bool")566    if lowhigh is not None:567        for c in range(nchan):568            lower = lowhigh[c, 0]569            upper = lowhigh[c, 1]570            img_norm[..., c] -= lower 571            img_norm[..., c] /= (upper - lower)572            cgood[c] = True573    else:574        # Apply sharpening and smoothing if specified575        if sharpen_radius > 0 or smooth_radius > 0:576            img_norm = smooth_sharpen_img(577                img_norm, sharpen_radius=sharpen_radius, smooth_radius=smooth_radius578            )579 580        # Apply tile-based normalization or standard normalization581        if tile_norm_blocksize > 0:582            img_norm = normalize99_tile(583                img_norm,584                blocksize=tile_norm_blocksize,585                lower=percentile[0],586                upper=percentile[1],587                smooth3D=tile_norm_smooth3D,588                norm3D=norm3D,589            )590            cgood[:] = True591        elif normalize:592            if img_norm.ndim == 3 or norm3D:  # i.e. if YXC, or ZYXC with norm3D=True593                for c in range(nchan):594                    if np.ptp(img_norm[..., c]) > 0.:595                        img_norm[..., c] = normalize99(596                            img_norm[..., c],597                            lower=percentile[0],598                            upper=percentile[1],599                            copy=False, downsample=True,600                        )601                        cgood[c] = True602            else:  # i.e. if ZYXC with norm3D=False then per Z-slice603                for z in range(img_norm.shape[0]):604                    for c in range(nchan):605                        if np.ptp(img_norm[z, ..., c]) > 0.:606                            img_norm[z, ..., c] = normalize99(607                                img_norm[z, ..., c],608                                lower=percentile[0],609                                upper=percentile[1],610                                copy=False, downsample=True,611                            )612                            cgood[c] = True613 614 615    if invert:616        if lowhigh is not None or tile_norm_blocksize > 0 or normalize:617            for c in range(nchan):618                if cgood[c]:619                    img_norm[..., c] = 1 - img_norm[..., c]620        else:621            error_message = "Cannot invert image without normalization"622            transforms_logger.critical(error_message)623            raise ValueError(error_message)624 625    # Move channel axis back to the original position626    if axis != -1 and axis != img_norm.ndim - 1:627        img_norm = np.moveaxis(img_norm, -1, axis)628 629    # The transformer can get confused if a channel is all 1's instead of all 0's:630    for i, chan_did_normalize in enumerate(cgood):631        if not chan_did_normalize:632            if img_norm.ndim == 3:633                img_norm[:, :, i] = 0634            if img_norm.ndim == 4:635                img_norm[:, :, :, i] = 0636 637    return img_norm638 639def resize_safe(img, Ly, Lx, interpolation=cv2.INTER_LINEAR):640    """OpenCV resize function does not support uint32.641 642    This function converts the image to float32 before resizing and then converts it back to uint32. Not safe!643    References issue: https://github.com/MouseLand/cellpose/issues/937644 645    Implications:646    * Runtime: Runtime increases by 5x-50x due to type casting. However, with resizing being very efficient, this is not647    a big issue. A 10,000x10,000 image takes 0.47s instead of 0.016s to cast and resize on 32 cores on GPU.648    * Memory: However, memory usage increases. Not tested by how much.649 650    Args:651        img (ndarray): Image of size [Ly x Lx].652        Ly (int): Desired height of the resized image.653        Lx (int): Desired width of the resized image.654        interpolation (int, optional): OpenCV interpolation method. Defaults to cv2.INTER_LINEAR.655 656    Returns:657        ndarray: Resized image of size [Ly x Lx].658 659    """660 661    # cast image662    cast = img.dtype == np.uint32663    if cast:664        img = img.astype(np.float32)665 666    # resize667    img = cv2.resize(img, (Lx, Ly), interpolation=interpolation)668 669    # cast back670    if cast:671        img = img.round().astype(np.uint32)672 673    return img674 675 676def resize_image(img0, Ly=None, Lx=None, rsz=None, interpolation=cv2.INTER_LINEAR,677                 no_channels=False):678    """Resize image for computing flows / unresize for computing dynamics.679 680    Args:681        img0 (ndarray): Image of size [Y x X x nchan] or [Lz x Y x X x nchan] or [Lz x Y x X].682        Ly (int, optional): Desired height of the resized image. Defaults to None.683        Lx (int, optional): Desired width of the resized image. Defaults to None.684        rsz (float, optional): Resize coefficient(s) for the image. If Ly is None, rsz is used. Defaults to None.685        interpolation (int, optional): OpenCV interpolation method. Defaults to cv2.INTER_LINEAR.686        no_channels (bool, optional): Flag indicating whether to treat the third dimension as a channel.687            Defaults to False.688 689    Returns:690        ndarray: Resized image of size [Ly x Lx x nchan] or [Lz x Ly x Lx x nchan].691 692    Raises:693        ValueError: If Ly is None and rsz is None.694 695    """696    if Ly is None and rsz is None:697        error_message = "must give size to resize to or factor to use for resizing"698        transforms_logger.critical(error_message)699        raise ValueError(error_message)700 701    if Ly is None:702        # determine Ly and Lx using rsz703        if not isinstance(rsz, list) and not isinstance(rsz, np.ndarray):704            rsz = [rsz, rsz]705        if no_channels:706            Ly = int(img0.shape[-2] * rsz[-2])707            Lx = int(img0.shape[-1] * rsz[-1])708        else:709            Ly = int(img0.shape[-3] * rsz[-2])710            Lx = int(img0.shape[-2] * rsz[-1])711 712    # no_channels useful for z-stacks, so the third dimension is not treated as a channel713    # but if this is called for grayscale images, they first become [Ly,Lx,2] so ndim=3 but714    if (img0.ndim > 2 and no_channels) or (img0.ndim == 4 and not no_channels):715        if Ly == 0 or Lx == 0:716            raise ValueError(717                "anisotropy too high / low -- not enough pixels to resize to ratio")718        for i, img in enumerate(img0):719            imgi = resize_safe(img, Ly, Lx, interpolation=interpolation)720            if i==0:721                if no_channels:722                    imgs = np.zeros((img0.shape[0], Ly, Lx), imgi.dtype)723                else:724                    imgs = np.zeros((img0.shape[0], Ly, Lx, img0.shape[-1]), imgi.dtype)725            imgs[i] = imgi if imgi.ndim > 2 or no_channels else imgi[..., np.newaxis]726    else:727        imgs = resize_safe(img0, Ly, Lx, interpolation=interpolation)728    return imgs729 730def get_pad_yx(Ly, Lx, div=16, extra=1, min_size=None):731    if min_size is None or Ly >= min_size[-2]:732        Lpad = int(div * np.ceil(Ly / div) - Ly)733    else:734        Lpad = min_size[-2] - Ly735    ypad1 = extra * div // 2 + Lpad // 2736    ypad2 = extra * div // 2 + Lpad - Lpad // 2737    if min_size is None or Lx >= min_size[-1]:738        Lpad = int(div * np.ceil(Lx / div) - Lx)739    else:740        Lpad = min_size[-1] - Lx741    xpad1 = extra * div // 2 + Lpad // 2742    xpad2 = extra * div // 2 + Lpad - Lpad // 2743 744    return ypad1, ypad2, xpad1, xpad2745 746 747def pad_image_ND(img0, div=16, extra=1, min_size=None, zpad=False):748    """Pad image for test-time so that its dimensions are a multiple of 16 (2D or 3D).749 750    Args:751        img0 (ndarray): Image of size [nchan (x Lz) x Ly x Lx].752        div (int, optional): Divisor for padding. Defaults to 16.753        extra (int, optional): Extra padding. Defaults to 1.754        min_size (tuple, optional): Minimum size of the image. Defaults to None.755 756    Returns:757        A tuple containing (I, ysub, xsub) or (I, ysub, xsub, zsub), I is padded image, -sub are ranges of pixels in the padded image corresponding to img0.758            759    """760    Ly, Lx = img0.shape[-2:]761    ypad1, ypad2, xpad1, xpad2 = get_pad_yx(Ly, Lx, div=div, extra=extra, min_size=min_size)762 763    if img0.ndim > 3:764        if zpad:765            Lpad = int(div * np.ceil(img0.shape[-3] / div) - img0.shape[-3])766            zpad1 = extra * div // 2 + Lpad // 2767            zpad2 = extra * div // 2 + Lpad - Lpad // 2768        else:769            zpad1, zpad2 = 0, 0770        pads = np.array([[0, 0], [zpad1, zpad2], [ypad1, ypad2], [xpad1, xpad2]])771    else:772        pads = np.array([[0, 0], [ypad1, ypad2], [xpad1, xpad2]])773 774    I = np.pad(img0, pads, mode="constant")775 776    ysub = np.arange(ypad1, ypad1 + Ly)777    xsub = np.arange(xpad1, xpad1 + Lx)778    if zpad:779        zsub = np.arange(zpad1, zpad1 + img0.shape[-3])780        return I, ysub, xsub, zsub781    else:782        return I, ysub, xsub783 784 785def random_rotate_and_resize(X, Y=None, scale_range=1., xy=(224, 224), do_3D=False,786                             zcrop=48, do_flip=True, rotate=True, rescale=None, unet=False,787                             random_per_image=True):788    """Augmentation by random rotation and resizing.789 790    Args:791        X (list of ND-arrays, float): List of image arrays of size [nchan x Ly x Lx] or [Ly x Lx].792        Y (list of ND-arrays, float, optional): List of image labels of size [nlabels x Ly x Lx] or [Ly x Lx].793            The 1st channel of Y is always nearest-neighbor interpolated (assumed to be masks or 0-1 representation).794            If Y.shape[0]==3 and not unet, then the labels are assumed to be [cell probability, Y flow, X flow].795            If unet, second channel is dist_to_bound. Defaults to None.796        scale_range (float, optional): Range of resizing of images for augmentation.797            Images are resized by (1-scale_range/2) + scale_range * np.random.rand(). Defaults to 1.0.798        xy (tuple, int, optional): Size of transformed images to return. Defaults to (224,224).799        do_flip (bool, optional): Whether or not to flip images horizontally. Defaults to True.800        rotate (bool, optional): Whether or not to rotate images. Defaults to True.801        rescale (array, float, optional): How much to resize images by before performing augmentations. Defaults to None.802        unet (bool, optional): Whether or not to use unet. Defaults to False.803        random_per_image (bool, optional): Different random rotate and resize per image. Defaults to True.804 805    Returns:806        A tuple containing (imgi, lbl, scale): imgi (ND-array, float): Transformed images in array [nimg x nchan x xy[0] x xy[1]]; 807        lbl (ND-array, float): Transformed labels in array [nimg x nchan x xy[0] x xy[1]]; 808        scale (array, float): Amount each image was resized by.809    """810    scale_range = max(0, min(2, float(scale_range))) if scale_range is not None else scale_range811    nimg = len(X)812    if X[0].ndim > 2:813        nchan = X[0].shape[0]814    else:815        nchan = 1816    # if do_3D and X[0].ndim > 3:817    #     shape = (zcrop, xy[0], xy[1])818    # else:819    shape = (xy[0], xy[1])820    imgi = np.zeros((nimg, nchan, *shape), "float32")821 822    lbl = []823    if Y is not None:824        if Y[0].ndim > 2:825            nt = Y[0].shape[0]826        else:827            nt = 1828        lbl = np.zeros((nimg, nt, *shape), np.float32)829 830    scale = np.ones(nimg, np.float32)831 832    for n in range(nimg):833 834        if random_per_image or n == 0:835            Ly, Lx = X[n].shape[-2:]836            # generate random augmentation parameters837            flip = np.random.rand() > .5838            theta = np.random.rand() * np.pi * 2 if rotate else 0.839            if scale_range is None:840                scale[n] = 2 ** (4 * np.random.rand() - 2)841            else:842                scale[n] =  (1 - scale_range / 2) + scale_range * np.random.rand()843            if rescale is not None:844                scale[n] *= 1. / rescale[n]845            dxy = np.maximum(0, np.array([Lx * scale[n] - xy[1],846                                          Ly * scale[n] - xy[0]]))847            dxy = (np.random.rand(2,) - .5) * dxy848 849            # create affine transform850            cc = np.array([Lx / 2, Ly / 2])851            cc1 = cc - np.array([Lx - xy[1], Ly - xy[0]]) / 2 + dxy852            pts1 = np.float32([cc, cc + np.array([1, 0]), cc + np.array([0, 1])])853            pts2 = np.float32([854                cc1,855                cc1 + scale[n] * np.array([np.cos(theta), np.sin(theta)]),856                cc1 + scale[n] *857                np.array([np.cos(np.pi / 2 + theta),858                          np.sin(np.pi / 2 + theta)])859            ])860            M = cv2.getAffineTransform(pts1, pts2)861 862        img = X[n].copy()863        if Y is not None:864            labels = Y[n].copy()865            if labels.ndim < 3:866                labels = labels[np.newaxis, :, :]867 868        if do_flip:869            if flip:870                img = img[..., ::-1]871                if Y is not None:872                    labels = labels[..., ::-1]873                    if nt > 1 and not unet:874                        labels[-1] = -labels[-1]875 876        for k in range(nchan):877            I = cv2.warpAffine(img[k], M, (xy[1], xy[0]), flags=cv2.INTER_LINEAR)878            imgi[n, k] = I879 880        if Y is not None:881            for k in range(nt):882                flag = cv2.INTER_NEAREST if k < nt-2 else cv2.INTER_LINEAR883                lbl[n, k] = cv2.warpAffine(labels[k], M, (xy[1], xy[0]), flags=flag)884 885            if nt > 1 and not unet:886                v1 = lbl[n, -1].copy()887                v2 = lbl[n, -2].copy()888                lbl[n, -2] = (-v1 * np.sin(-theta) + v2 * np.cos(-theta))889                lbl[n, -1] = (v1 * np.cos(-theta) + v2 * np.sin(-theta))890 891    return imgi, lbl, scale892 893 894def random_rotate_and_resize_with_feat(X, Y=None, feat=None, scale_range=1., xy=(224, 224), do_3D=False,895                             zcrop=48, do_flip=True, rotate=True, rescale=None, unet=False,896                             random_per_image=True):897    """Augmentation by random rotation and resizing.898 899    Args:900        X (list of ND-arrays, float): List of image arrays of size [nchan x Ly x Lx] or [Ly x Lx].901        Y (list of ND-arrays, float, optional): List of image labels of size [nlabels x Ly x Lx] or [Ly x Lx].902            The 1st channel of Y is always nearest-neighbor interpolated (assumed to be masks or 0-1 representation).903            If Y.shape[0]==3 and not unet, then the labels are assumed to be [cell probability, Y flow, X flow].904            If unet, second channel is dist_to_bound. Defaults to None.905        scale_range (float, optional): Range of resizing of images for augmentation.906            Images are resized by (1-scale_range/2) + scale_range * np.random.rand(). Defaults to 1.0.907        xy (tuple, int, optional): Size of transformed images to return. Defaults to (224,224).908        do_flip (bool, optional): Whether or not to flip images horizontally. Defaults to True.909        rotate (bool, optional): Whether or not to rotate images. Defaults to True.910        rescale (array, float, optional): How much to resize images by before performing augmentations. Defaults to None.911        unet (bool, optional): Whether or not to use unet. Defaults to False.912        random_per_image (bool, optional): Different random rotate and resize per image. Defaults to True.913 914    Returns:915        A tuple containing (imgi, lbl, scale): imgi (ND-array, float): Transformed images in array [nimg x nchan x xy[0] x xy[1]]; 916        lbl (ND-array, float): Transformed labels in array [nimg x nchan x xy[0] x xy[1]]; 917        scale (array, float): Amount each image was resized by.918    """919    scale_range = max(0, min(2, float(scale_range))) if scale_range is not None else scale_range920    nimg = len(X)921    if X[0].ndim > 2:922        nchan = X[0].shape[0]923    else:924        nchan = 1925    shape = (xy[0], xy[1])926    imgi = np.zeros((nimg, nchan, *shape), "float32")927 928    lbl = []929    if Y is not None:930        if Y[0].ndim > 2:931            nt = Y[0].shape[0]932        else:933            nt = 1934        lbl = np.zeros((nimg, nt, *shape), np.float32)935    936    if feat is not None:937        if feat[0].ndim > 2:938            nf = feat[0].shape[0]939        else:940            nf = 1941        feat_out = np.zeros((nimg, nf, *shape), "float32")942 943    scale = np.ones(nimg, np.float32)944 945    for n in range(nimg):946 947        if random_per_image or n == 0:948            Ly, Lx = X[n].shape[-2:]949            # generate random augmentation parameters950            flip = np.random.rand() > .5951            theta = np.random.rand() * np.pi * 2 if rotate else 0.952            if scale_range is None:953                scale[n] = 2 ** (4 * np.random.rand() - 2)954            else:955                scale[n] =  (1 - scale_range / 2) + scale_range * np.random.rand()956            if rescale is not None:957                scale[n] *= 1. / rescale[n]958            dxy = np.maximum(0, np.array([Lx * scale[n] - xy[1],959                                          Ly * scale[n] - xy[0]]))960            dxy = (np.random.rand(2,) - .5) * dxy961 962            # create affine transform963            cc = np.array([Lx / 2, Ly / 2])964            cc1 = cc - np.array([Lx - xy[1], Ly - xy[0]]) / 2 + dxy965            pts1 = np.float32([cc, cc + np.array([1, 0]), cc + np.array([0, 1])])966            pts2 = np.float32([967                cc1,968                cc1 + scale[n] * np.array([np.cos(theta), np.sin(theta)]),969                cc1 + scale[n] *970                np.array([np.cos(np.pi / 2 + theta),971                          np.sin(np.pi / 2 + theta)])972            ])973            M = cv2.getAffineTransform(pts1, pts2)974 975        img = X[n].copy()976        if Y is not None:977            labels = Y[n].copy()978            if labels.ndim < 3:979                labels = labels[np.newaxis, :, :]980        if feat is not None:981            feats = feat[n].copy()982            if feats.ndim < 3:983                feats = feats[np.newaxis, :, :]984 985        if do_flip:986            if flip:987                img = img[..., ::-1]988                if Y is not None:989                    labels = labels[..., ::-1]990                    if nt > 1 and not unet:991                        labels[-1] = -labels[-1]992                if feat is not None:993                    feats = feats[..., ::-1]994 995 996        for k in range(nchan):997            I = cv2.warpAffine(img[k], M, (xy[1], xy[0]), flags=cv2.INTER_LINEAR)998            imgi[n, k] = I999 1000        if Y is not None:1001            for k in range(nt):1002                flag = cv2.INTER_NEAREST if k < nt-2 else cv2.INTER_LINEAR1003                lbl[n, k] = cv2.warpAffine(labels[k], M, (xy[1], xy[0]), flags=flag)1004 1005            if nt > 1 and not unet:1006                v1 = lbl[n, -1].copy()1007                v2 = lbl[n, -2].copy()1008                lbl[n, -2] = (-v1 * np.sin(-theta) + v2 * np.cos(-theta))1009                lbl[n, -1] = (v1 * np.cos(-theta) + v2 * np.sin(-theta))1010        1011        if feat is not None:1012            for k in range(nf):1013                feat_out[n, k] = cv2.warpAffine(feats[k], M, (xy[1], xy[0]), flags=cv2.INTER_LINEAR)1014 1015 1016 1017    return imgi, lbl, feat_out, scale1018