Skip to content

Nuclei-seeded watershed plugin

See Cells from a membrane stain.

patchworks.plugins.watershed.watershed_fn(*, boundary_sigma: Any = 1.0, nuclei_sigma: Any = 1.0, nuclei_threshold: float | None = None, nuclei_min_size: int = 50, foreground: str | float | None = 'otsu', foreground_sigma: Any = 2.0, max_radius_um: float | None = None, compactness: float = 0.0, min_size: int = 0, sigma_units: str = 'px', seeds: str = 'channel', voxel_size: dict[str, float] | None = None) -> Callable[[np.ndarray], np.ndarray]

Return a nuclei-seeded watershed for tile_process.

Parameters:

Name Type Description Default
boundary_sigma Any

Smoothing of the membrane channel before flooding it.

1.0
nuclei_sigma Any

How the nuclei are found: see :func:nuclei_seeds. The threshold is an intensity of the nuclear channel; None picks it by Otsu, per tile.

1.0
nuclei_threshold Any

How the nuclei are found: see :func:nuclei_seeds. The threshold is an intensity of the nuclear channel; None picks it by Otsu, per tile.

1.0
nuclei_min_size Any

How the nuclei are found: see :func:nuclei_seeds. The threshold is an intensity of the nuclear channel; None picks it by Otsu, per tile.

1.0
foreground str | float | None

Tissue mask from the membrane channel: "otsu" (default), an intensity, or None to let cells fill the whole tile.

'otsu'
foreground_sigma str | float | None

Tissue mask from the membrane channel: "otsu" (default), an intensity, or None to let cells fill the whole tile.

'otsu'
max_radius_um float | None

Furthest a cell reaches from its nucleus, in micrometres (needs voxel_size; the workflow passes it). None: no limit.

None
compactness float

0 favours round cells where a wall is missing (skimage's watershed). Small values (0.001--0.01) are typical.

0.0
min_size int

Drop cells smaller than this many voxels.

0
sigma_units str

"px" (default) or "um" for every sigma above.

'px'
seeds str

"channel" (default): the tile's second channel is a nuclear stain, and the nuclei are found in it (the nuclei_* options). "labels": it is a label image -- the workflow's seed_labels, e.g. Cellpose's nuclei -- and each of its objects seeds one cell, as it is. The workflow sets this itself when seed_labels is used.

'channel'
voxel_size dict[str, float] | None

{"z": .., "y": .., "x": ..} in micrometres.

None

Returns:

Type Description
Callable[[ndarray], ndarray]

Picklable (2, [z,] y, x) -> ([z,] y, x) labeller.

Source code in src/patchworks/plugins/watershed.py
def watershed_fn(
    *,
    boundary_sigma: Any = 1.0,
    nuclei_sigma: Any = 1.0,
    nuclei_threshold: float | None = None,
    nuclei_min_size: int = 50,
    foreground: str | float | None = "otsu",
    foreground_sigma: Any = 2.0,
    max_radius_um: float | None = None,
    compactness: float = 0.0,
    min_size: int = 0,
    sigma_units: str = "px",
    seeds: str = "channel",
    voxel_size: dict[str, float] | None = None,
) -> Callable[[np.ndarray], np.ndarray]:
    """Return a nuclei-seeded watershed for ``tile_process``.

    Parameters
    ----------
    boundary_sigma :
        Smoothing of the membrane channel before flooding it.
    nuclei_sigma, nuclei_threshold, nuclei_min_size :
        How the nuclei are found: see :func:`nuclei_seeds`. The threshold is
        an intensity of the nuclear channel; ``None`` picks it by Otsu, per
        tile.
    foreground, foreground_sigma :
        Tissue mask from the membrane channel: ``"otsu"`` (default), an
        intensity, or ``None`` to let cells fill the whole tile.
    max_radius_um :
        Furthest a cell reaches from its nucleus, in micrometres (needs
        *voxel_size*; the workflow passes it). ``None``: no limit.
    compactness :
        > 0 favours round cells where a wall is missing (skimage's
        ``watershed``). Small values (0.001--0.01) are typical.
    min_size :
        Drop cells smaller than this many voxels.
    sigma_units :
        ``"px"`` (default) or ``"um"`` for every sigma above.
    seeds :
        ``"channel"`` (default): the tile's second channel is a nuclear
        stain, and the nuclei are found in it (the ``nuclei_*`` options).
        ``"labels"``: it is a label image -- the workflow's ``seed_labels``,
        e.g. Cellpose's nuclei -- and each of its objects seeds one cell, as
        it is. The workflow sets this itself when ``seed_labels`` is used.
    voxel_size :
        ``{"z": .., "y": .., "x": ..}`` in micrometres.

    Returns
    -------
    Callable[[ndarray], ndarray]
        Picklable ``(2, [z,] y, x) -> ([z,] y, x)`` labeller.
    """
    if sigma_units not in ("px", "um"):
        raise ValueError(
            f'sigma_units must be "px" or "um", got {sigma_units!r}'
        )
    if sigma_units == "um" and not voxel_size:
        raise ValueError('sigma_units="um" needs voxel_size')
    if max_radius_um is not None and not voxel_size:
        raise ValueError(
            "max_radius_um needs voxel_size (the image calibration)"
        )
    if not (
        foreground in FOREGROUND_MODES or isinstance(foreground, (int, float))
    ):
        raise ValueError(
            f'foreground must be "otsu", a number or null, got {foreground!r}'
        )
    if seeds not in SEED_MODES:
        raise ValueError(f"seeds must be one of {SEED_MODES}, got {seeds!r}")
    cfg = dict(
        boundary_sigma=boundary_sigma,
        nuclei_sigma=nuclei_sigma,
        nuclei_threshold=nuclei_threshold,
        nuclei_min_size=nuclei_min_size,
        foreground=foreground,
        foreground_sigma=foreground_sigma,
        max_radius_um=max_radius_um,
        compactness=compactness,
        min_size=min_size,
        sigma_units=sigma_units,
        seeds=seeds,
        voxel_size=voxel_size,
    )
    return partial(_run, cfg=cfg)

patchworks.plugins.watershed.nuclei_seeds(nuclei: np.ndarray, *, sigma: Any = 1.0, threshold: float | None = None, min_size: int = 50) -> np.ndarray

Label the nuclei of a nuclear-dye image, to seed the cells with.

Smooth, threshold (Otsu unless threshold is given, in the image's own intensity units), fill holes, drop specks below min_size voxels and label what is left.

Two touching nuclei come out as one seed, so the two cells around them as one cell: raise threshold if that happens, or give the seeds from a nuclei model (:func:seeded_watershed takes any label image).

Source code in src/patchworks/plugins/watershed.py
def nuclei_seeds(
    nuclei: np.ndarray,
    *,
    sigma: Any = 1.0,
    threshold: float | None = None,
    min_size: int = 50,
) -> np.ndarray:
    """Label the nuclei of a nuclear-dye image, to seed the cells with.

    Smooth, threshold (Otsu unless *threshold* is given, in the image's own
    intensity units), fill holes, drop specks below *min_size* voxels and
    label what is left.

    Two touching nuclei come out as one seed, so the two cells around them
    as one cell: raise *threshold* if that happens, or give the seeds from a
    nuclei model (:func:`seeded_watershed` takes any label image).
    """
    from scipy import ndimage as ndi
    from skimage.filters import threshold_otsu
    from skimage.measure import label

    smooth = ndi.gaussian_filter(np.asarray(nuclei, "float32"), sigma)
    if threshold is None:
        threshold = (
            float(threshold_otsu(smooth))
            if smooth.max() > smooth.min()
            else float("inf")
        )
    mask = smooth > threshold
    mask = ndi.binary_fill_holes(mask)
    return _drop_small(label(mask).astype("int32"), min_size)

patchworks.plugins.watershed.foreground_mask(membrane: np.ndarray, seeds: np.ndarray, *, foreground: str | float | None = 'otsu', sigma: Any = 2.0, max_radius: tuple[float, ...] | None = None) -> np.ndarray | None

Where cells may grow: tissue and/or near a seed. None: anywhere.

foreground "otsu" (or an intensity) keeps what the smoothed membrane signal covers, holes filled -- the cell interiors are dark in a membrane stain but enclosed by it. max_radius (pixels per axis) keeps what lies within that distance of a nucleus, so a cell on the tissue's edge stops instead of flooding the empty space beyond it.

Source code in src/patchworks/plugins/watershed.py
def foreground_mask(
    membrane: np.ndarray,
    seeds: np.ndarray,
    *,
    foreground: str | float | None = "otsu",
    sigma: Any = 2.0,
    max_radius: tuple[float, ...] | None = None,
) -> np.ndarray | None:
    """Where cells may grow: tissue and/or near a seed. ``None``: anywhere.

    *foreground* ``"otsu"`` (or an intensity) keeps what the smoothed
    membrane signal covers, holes filled -- the cell interiors are dark in a
    membrane stain but enclosed by it. *max_radius* (pixels per axis) keeps
    what lies within that distance of a nucleus, so a cell on the tissue's
    edge stops instead of flooding the empty space beyond it.
    """
    from scipy import ndimage as ndi
    from skimage.filters import threshold_otsu

    mask = None
    if foreground is not None:
        smooth = ndi.gaussian_filter(np.asarray(membrane, "float32"), sigma)
        if foreground == "otsu":
            thr = (
                float(threshold_otsu(smooth))
                if smooth.max() > smooth.min()
                else float("inf")
            )
        else:
            thr = float(foreground)
        mask = smooth > thr
        # Interiors: in 3-D and plane by plane, since a cell cut by the tile
        # edge is enclosed in its planes but open to the border in 3-D.
        mask = ndi.binary_fill_holes(mask)
        if mask.ndim == 3:
            for z in range(mask.shape[0]):
                mask[z] = ndi.binary_fill_holes(mask[z])
    if max_radius is not None:
        # Distance in units of the radius per axis: <= 1 means within reach.
        near = (
            ndi.distance_transform_edt(
                seeds == 0, sampling=[1.0 / r for r in max_radius]
            )
            <= 1.0
        )
        mask = near if mask is None else (mask & near)
    if mask is not None:
        mask |= seeds > 0
    return mask

patchworks.plugins.watershed.seeded_watershed(boundaries: np.ndarray, seeds: np.ndarray, *, mask: np.ndarray | None = None, compactness: float = 0.0, min_size: int = 0) -> np.ndarray

Flood boundaries (high on cell walls) from the labelled seeds.

Every seed becomes exactly one cell; with mask, cells stop at its edge. compactness > 0 favours rounder cells where a wall is missing. Cells smaller than min_size voxels are dropped (a seed in a speck of tissue).

Source code in src/patchworks/plugins/watershed.py
def seeded_watershed(
    boundaries: np.ndarray,
    seeds: np.ndarray,
    *,
    mask: np.ndarray | None = None,
    compactness: float = 0.0,
    min_size: int = 0,
) -> np.ndarray:
    """Flood *boundaries* (high on cell walls) from the labelled *seeds*.

    Every seed becomes exactly one cell; with *mask*, cells stop at its
    edge. *compactness* > 0 favours rounder cells where a wall is missing.
    Cells smaller than *min_size* voxels are dropped (a seed in a speck of
    tissue).
    """
    from skimage.segmentation import watershed

    labels = watershed(
        np.asarray(boundaries, "float32"),
        markers=seeds,
        mask=mask,
        compactness=compactness,
    ).astype("int32")
    return _drop_small(labels, min_size)