Skip to content

Cellpose plugin

patchworks.plugins.cellpose.cellpose_fn(model: str = 'cyto3', *, gpu: bool = False, diameter: float | None = None, do_3D: bool = False, channels: list[int] | None = None, channel_axis: int | None = None, voxel_size: dict[str, float] | None = None, intensity_range: 'list | tuple | None' = None, fragments: 'float | None | str' = 'auto', **cellpose_kwargs: Any) -> Callable[[np.ndarray], np.ndarray]

Return a ready-to-use Cellpose function for tile_process.

One-liner convenience wrapper: combines model configuration and function creation into a single call.

Parameters:

Name Type Description Default
model str

Cellpose model type: "cyto3", "cyto2", "nuclei", etc.

'cyto3'
gpu bool

Use GPU for inference.

False
diameter float | None

Expected cell diameter in pixels. None → Cellpose auto-estimates.

None
do_3D bool

Run in 3-D mode. Each tile must contain the full z-stack — use auto_tile_shape_cellpose(do_3D=True) for appropriate tile shapes.

False
channels list[int] | None

Cellpose 3 only. [cytoplasm_channel, nucleus_channel] (1-based, 0 = greyscale). [0, 0] → greyscale. [1, 2] → cyto=ch1, nuc=ch2.

None
channel_axis int | None

Cellpose 4 only. Index of the channel axis in the input array. None → greyscale input.

None
voxel_size dict[str, float] | None

Physical voxel size as {"z": .., "y": .., "x": ..}. When do_3D is set and anisotropy is not already in cellpose_kwargs, it is derived from this via :func:cellpose_anisotropy -- an explicit anisotropy= always wins. The Snakemake workflow passes the image's own calibration automatically; from the API, :func:patchworks.plugins.ome_zarr.read_pixel_size reads it from a store.

None
intensity_range 'list | tuple | None'

(low, high), or one per channel, mapped to 0 and 1 in every tile before Cellpose sees it, with Cellpose's own per-input normalisation turned off. Without it, each tile is stretched from its own percentiles, so neighbouring tiles get different contrast and cells are cut at the seams. Measure it once with :func:patchworks.intensity_range; the workflow does by default.

None
fragments 'float | None | str'

Merge objects smaller than this fraction of a cell of diameter (30 px if not given) into the neighbour they touch most, and drop those touching nothing (:func:patchworks.absorb_fragments), per tile before the halo is trimmed. 3-D Cellpose often splits a cell into a body and slivers, and leaves specks in dim regions. "auto" (default): 0.1 in 3-D when diameter is given (the size it is judged against), else off; None turns it off. 3-D runs also smooth the flows (flow3D_smooth=1) unless told otherwise, Cellpose's own remedy for fragmentation along z.

'auto'
**cellpose_kwargs Any

Extra kwargs forwarded to model.eval() (e.g. flow_threshold, cellprob_threshold, anisotropy).

{}

Returns:

Type Description
Callable[[ndarray], ndarray]

Picklable function ready for tile_process.

Examples:

Greyscale 2-D:

>>> fn = cellpose_fn("cyto3", gpu=True, diameter=30)
>>> result = tile_process("image.zarr", fn, tile_shape=(1, 2048, 2048), overlap=20)

Nuclear segmentation:

>>> fn = cellpose_fn("nuclei", diameter=15)
>>> result = tile_process("image.zarr", fn, channel=1)

3-D with an explicit anisotropy:

>>> fn = cellpose_fn("cyto3", gpu=True, do_3D=True, anisotropy=3.0, diameter=20)
>>> from functools import partial
>>> from patchworks import auto_tile_shape_cellpose, tile_process
>>> tile_fn = partial(auto_tile_shape_cellpose, do_3D=True, use_gpu=True, diameter=20)
>>> result = tile_process("image.zarr", fn, tile_shape=tile_fn, overlap=10)

3-D with anisotropy derived from the image's own calibration:

>>> fn = cellpose_fn("cyto3", gpu=True, do_3D=True, diameter=20,
...                  voxel_size={"z": 0.24, "y": 0.10833, "x": 0.10833})
Source code in src/patchworks/plugins/cellpose.py
def cellpose_fn(
    model: str = "cyto3",
    *,
    gpu: bool = False,
    diameter: float | None = None,
    do_3D: bool = False,
    channels: list[int] | None = None,
    channel_axis: int | None = None,
    voxel_size: dict[str, float] | None = None,
    intensity_range: "list | tuple | None" = None,
    fragments: "float | None | str" = "auto",
    **cellpose_kwargs: Any,
) -> Callable[[np.ndarray], np.ndarray]:
    """Return a ready-to-use Cellpose function for ``tile_process``.

    One-liner convenience wrapper: combines model configuration and function
    creation into a single call.

    Parameters
    ----------
    model:
        Cellpose model type: ``"cyto3"``, ``"cyto2"``, ``"nuclei"``, etc.
    gpu:
        Use GPU for inference.
    diameter:
        Expected cell diameter in pixels. ``None`` → Cellpose auto-estimates.
    do_3D:
        Run in 3-D mode. Each tile must contain the full z-stack — use
        ``auto_tile_shape_cellpose(do_3D=True)`` for appropriate tile shapes.
    channels:
        *Cellpose 3 only.* ``[cytoplasm_channel, nucleus_channel]`` (1-based,
        0 = greyscale). ``[0, 0]`` → greyscale. ``[1, 2]`` → cyto=ch1, nuc=ch2.
    channel_axis:
        *Cellpose 4 only.* Index of the channel axis in the input array.
        ``None`` → greyscale input.
    voxel_size:
        Physical voxel size as ``{"z": .., "y": .., "x": ..}``. When
        ``do_3D`` is set and ``anisotropy`` is not already in
        ``cellpose_kwargs``, it is derived from this via
        :func:`cellpose_anisotropy` -- an explicit ``anisotropy=`` always
        wins. The Snakemake workflow passes the image's own calibration
        automatically; from the API,
        :func:`patchworks.plugins.ome_zarr.read_pixel_size` reads it from a
        store.
    intensity_range:
        ``(low, high)``, or one per channel, mapped to 0 and 1 in every
        tile before Cellpose sees it, with Cellpose's own per-input
        normalisation turned off. Without it, each tile is stretched from
        its own percentiles, so neighbouring tiles get different contrast
        and cells are cut at the seams. Measure it once with
        :func:`patchworks.intensity_range`; the workflow does by default.
    fragments:
        Merge objects smaller than this fraction of a cell of *diameter*
        (30 px if not given) into the neighbour they touch most, and drop
        those touching nothing
        (:func:`patchworks.absorb_fragments`), per tile before the halo is
        trimmed. 3-D Cellpose often splits a cell into a body and slivers,
        and leaves specks in dim regions. ``"auto"`` (default): 0.1 in 3-D
        when *diameter* is given (the size it is judged against), else off;
        ``None`` turns it off. 3-D runs also smooth
        the flows (``flow3D_smooth=1``) unless told otherwise, Cellpose's own
        remedy for fragmentation along z.
    **cellpose_kwargs:
        Extra kwargs forwarded to ``model.eval()``
        (e.g. ``flow_threshold``, ``cellprob_threshold``, ``anisotropy``).

    Returns
    -------
    Callable[[ndarray], ndarray]
        Picklable function ready for ``tile_process``.

    Examples
    --------
    Greyscale 2-D:

    >>> fn = cellpose_fn("cyto3", gpu=True, diameter=30)  # doctest: +SKIP
    >>> result = tile_process("image.zarr", fn, tile_shape=(1, 2048, 2048), overlap=20)  # doctest: +SKIP

    Nuclear segmentation:

    >>> fn = cellpose_fn("nuclei", diameter=15)  # doctest: +SKIP
    >>> result = tile_process("image.zarr", fn, channel=1)  # doctest: +SKIP

    3-D with an explicit anisotropy:

    >>> fn = cellpose_fn("cyto3", gpu=True, do_3D=True, anisotropy=3.0, diameter=20)  # doctest: +SKIP
    >>> from functools import partial  # doctest: +SKIP
    >>> from patchworks import auto_tile_shape_cellpose, tile_process  # doctest: +SKIP
    >>> tile_fn = partial(auto_tile_shape_cellpose, do_3D=True, use_gpu=True, diameter=20)  # doctest: +SKIP
    >>> result = tile_process("image.zarr", fn, tile_shape=tile_fn, overlap=10)  # doctest: +SKIP

    3-D with anisotropy derived from the image's own calibration:

    >>> fn = cellpose_fn("cyto3", gpu=True, do_3D=True, diameter=20,  # doctest: +SKIP
    ...                  voxel_size={"z": 0.24, "y": 0.10833, "x": 0.10833})
    """
    _require_cellpose()
    if do_3D and voxel_size and "anisotropy" not in cellpose_kwargs:
        anisotropy = cellpose_anisotropy(voxel_size)
        if anisotropy is not None:
            logger.info(
                "anisotropy derived from image calibration: %.4g "
                "(z=%.4g, lateral=%.4g)",
                anisotropy,
                voxel_size.get("z"),
                voxel_size.get("x") or voxel_size.get("y"),
            )
            cellpose_kwargs = {**cellpose_kwargs, "anisotropy": anisotropy}
    defaults = applied_defaults(
        do_3D, {**cellpose_kwargs, "fragments": fragments, "diameter": diameter}
    )
    fragments = defaults.pop("fragments", None)
    cellpose_kwargs = {**cellpose_kwargs, **defaults}
    if intensity_range is not None:
        if "normalize" in cellpose_kwargs:
            raise ValueError(
                "give either intensity_range or Cellpose's normalize, not both"
            )
        cellpose_kwargs = {**cellpose_kwargs, "normalize": False}
    cfg = _make_config(
        model, gpu, channels, channel_axis, diameter, do_3D, **cellpose_kwargs
    )
    if intensity_range is not None:
        ranges = np.asarray(intensity_range, dtype=float).reshape(-1, 2)
        cfg["intensity_range"] = ranges.tolist()
    cfg["fragments"] = fragments
    return partial(_run, cellpose_dict=cfg)

patchworks.plugins.cellpose.cellpose_anisotropy(voxel_size: dict[str, float]) -> float | None

Cellpose's anisotropy (z voxel size / lateral voxel size) from a calibration.

do_3D assumes isotropic voxels unless told otherwise: without this, Cellpose builds its 3-D flow-field consensus across z-planes it thinks are spaced the same as the xy pixels, which for real data rarely holds and fragments or distorts objects across z. Getting this wrong does not fail loudly -- it just produces a subtly (or not so subtly) wrong segmentation, which is why deriving it beats retyping it.

Parameters:

Name Type Description Default
voxel_size dict

Image calibration, e.g. from :func:patchworks.plugins.ome_zarr.read_pixel_size. Needs z and x (or y) for the lateral size.

required

Returns:

Type Description
float or None

z / lateral, or None if the calibration lacks what is needed.

Examples:

>>> cellpose_anisotropy({"z": 0.24, "y": 0.10833, "x": 0.10833})
2.215452783162559
Source code in src/patchworks/plugins/cellpose.py
def cellpose_anisotropy(voxel_size: dict[str, float]) -> float | None:
    """Cellpose's ``anisotropy`` (z voxel size / lateral voxel size) from a calibration.

    ``do_3D`` assumes isotropic voxels unless told otherwise: without this,
    Cellpose builds its 3-D flow-field consensus across z-planes it thinks
    are spaced the same as the xy pixels, which for real data rarely holds
    and fragments or distorts objects across z. Getting this wrong does not
    fail loudly -- it just produces a subtly (or not so subtly) wrong
    segmentation, which is why deriving it beats retyping it.

    Parameters
    ----------
    voxel_size : dict
        Image calibration, e.g. from
        :func:`patchworks.plugins.ome_zarr.read_pixel_size`. Needs ``z`` and
        ``x`` (or ``y``) for the lateral size.

    Returns
    -------
    float or None
        ``z / lateral``, or None if the calibration lacks what is needed.

    Examples
    --------
    >>> cellpose_anisotropy({"z": 0.24, "y": 0.10833, "x": 0.10833})
    2.215452783162559
    """
    lateral = voxel_size.get("x") or voxel_size.get("y")
    z = voxel_size.get("z")
    if not lateral or not z:
        return None
    return z / lateral