squidpy.experimental.im.calculate_image_features

squidpy.experimental.im.calculate_image_features(sdata, *, image_key=None, labels_key=None, shapes_key=None, scale=None, channels=None, features=None, tile_size=1024, align_mode='strict', key_added=None, invalid_as_zero=False, drop_constant_features=True, n_jobs=1, inplace=True)[source]

Calculate per-cell features from segmentation masks.

Uses cp_measure for CellProfiler-derived features, scikit-image regionprops for morphological/intensity features, and squidpy-specific per-cell metrics (summary statistics, GLCM texture, intensity histograms). Large images are automatically tiled into tile_size x tile_size chunks with overlap so that every cell is fully contained in exactly one tile.

Parameters:
  • sdata (SpatialData) – SpatialData object.

  • image_key (str | None) – Key in sdata.images. Optional: required only for intensity / squidpy / cp_measure features (and for shapes_key). Morphology-only runs need no image.

  • labels_key (str | None) – Key in sdata.labels with segmentation masks.

  • shapes_key (str | None) – Key in sdata.shapes (rasterized to labels internally).

  • scale (str | None) – Scale level for multi-scale data.

  • channels (list[str] | None) – Subset of channel names to use, matching those returned by spatialdata.models.get_channel_names(). None uses all channels. Integer indices are not accepted – always pass names.

  • features (list[str] | str | None) –

    Which features to compute. None (the default) computes all features across every backend; because that set includes intensity / texture features, an image is required (a clear error is raised if image_key is missing). Otherwise a list of flag strings from three groups:

    • cp_measure"cp_measure:intensity", "cp_measure:sizeshape", "cp_measure:texture", "cp_measure:granularity", "cp_measure:zernike", "cp_measure:feret", "cp_measure:radial", "cp_measure:correlation" (or a single correlation kind via "cp_measure:correlation_<pearson|costes|manders_fold|rwc>"). Columns keep cp_measure’s native CellProfiler names (e.g. Area, Intensity_MeanIntensity__<channel>). Correlation features need an image with >=2 channels.

    • skimage regionprops"skimage:morphology" (all shape props, from the mask alone) or "skimage:morphology:<prop>" for one (e.g. area); "skimage:intensity" (all per-channel intensity props, needs an image) or "skimage:intensity:<prop>" for one (e.g. intensity_mean). Morphology columns use skimage’s native names (area, centroid-0); intensity columns are suffixed with the channel name.

    • squidpy per-cell"squidpy:summary" (per-channel mean / std / min / max), "squidpy:texture" (per-channel GLCM contrast, dissimilarity, homogeneity, energy, ASM, correlation), and "squidpy:histogram" (per-channel intensity histogram).

    If a request computes both "cp_measure:sizeshape" and skimage morphology props, the overlapping skimage props (which cp_measure reproduces identically) are dropped to avoid duplicate columns; only the skimage-only props (centroid_local, feret_diameter_max) are kept. cp_measure computes its groups all-or-nothing, so it wins.

  • tile_size (int) – Side length of the tiling grid (pixels).

  • align_mode (Literal['strict', 'rasterize']) –

    How to handle image/labels whose pixel grids do not match (via their SpatialData transformations).

    • "strict" (default): require the relative transform between image and labels to be identity or an integer-pixel translation; raise otherwise with a hint pointing to spatialdata.rasterize().

    • "rasterize": resample labels onto the image pixel grid using spatialdata.rasterize() when not pixel-aligned (logs a warning because this materializes the full label grid). Not supported for multiscale labels under a non-integer transform – pre-align instead.

    Cells falling outside the image/labels overlap are dropped (logged at INFO).

  • key_added (str | None) – Key under which to store the result in sdata.tables. If None (default), the key is derived from the region and, when an image is used, the image key: f"morphology_{labels_key or shapes_key}_{image_key}" (or f"morphology_{labels_key or shapes_key}" for a morphology-only run). This keeps per-region / per-image runs from clobbering each other.

  • invalid_as_zero (bool) – If True, replace inf/NaN with zero. False (default) keeps them, so undefined features stay distinguishable from genuine zeros.

  • drop_constant_features (bool) – If True (default), drop zero-variance feature columns (they break scaling/PCA downstream). Applied per call and skipped for a single cell; when comparing samples, prefer dropping constants on the concatenated table so column sets stay aligned.

  • n_jobs (int) – Number of worker processes for tile featurization (-1 uses all cores). 1 runs serially in-process. cp_measure is GIL-bound, so n_jobs > 1 uses a dask.distributed.LocalCluster; if an active dask.distributed.Client is already in scope it is used instead and n_jobs is ignored.

  • inplace (bool) – If True, store result in sdata.tables. Otherwise return it.

Notes

Cells dropped during alignment (fully/partially outside the image) and constant features removed by drop_constant_features are logged at WARNING level.

With n_jobs > 1 a LocalCluster is started, which spawns worker processes. On macOS/Windows (spawn start method) the calling code must be guarded by if __name__ == "__main__": (the standard Python multiprocessing requirement), or run from a notebook. For repeated calls or multi-node scale-out, create and reuse your own Client so the worker pool is started once. Worker BLAS/OpenMP threads are pinned to 1 to avoid oversubscription.

Return type:

AnnData | None

Returns:

AnnData when inplace=False, otherwise None.

Examples

>>> import squidpy as sq
>>> sq.experimental.im.calculate_image_features(
...     sdata,
...     image_key="image",
...     labels_key="cells",
...     features=["cp_measure:sizeshape", "skimage:morphology", "squidpy:summary"],
... )

All features across every backend (needs an image):

>>> sq.experimental.im.calculate_image_features(
...     sdata, image_key="image", labels_key="cells", features=None
... )

Morphology-only needs no image:

>>> sq.experimental.im.calculate_image_features(
...     sdata, labels_key="cells", features=["skimage:morphology:area"]
... )

The per-cell table is stored in sdata.tables["morphology_cells"] (the key is derived from the region/image keys; see key_added).