Version Compatibility
Reference examples tested with: Cellpose 4.0+ (cpsam model), anndata 0.10+, matplotlib 3.8+, numpy 1.26+, pandas 2.2+, scanpy 1.10+, scvi-tools 1.1+, squidpy 1.3+, steinbock 0.16+
Before using code patterns, verify installed versions match. If versions differ:
- Python:
pip show <package> then help(module.function) to check signatures
- R:
packageVersion('<pkg>') then ?function_name to verify parameters
- CLI:
<tool> --version then <tool> --help to confirm flags
If code throws ImportError, AttributeError, or TypeError, introspect the installed
package and adapt the example to match the actual API rather than retrying.
Imaging Mass Cytometry Pipeline
"Process my imaging mass cytometry data from images to spatial analysis" -> Orchestrate image preprocessing (steinbock), cell segmentation (Cellpose), phenotyping (FlowSOM/scanpy), spatial neighborhood analysis (squidpy), and tissue community detection.
This is a workflow skill: it owns the chaining decisions and hand-offs, not the internals of any one step.
The governing principle
Segmentation is the largest irreversible error source, and it is spatial: every per-cell number is a mask-bounded pixel average, so a wrong boundary fabricates cell types before any expression QC can see them. The seam ORDER โ and the patient-level unit โ is therefore what decides trustworthiness.
- The comparison frame (panel + segmentation frame + pixel size) is committed once and inherited by every per-cell number. The summed membrane channel encodes a cell-type bias (sum BROADLY-expressed markers, or segmentation under-performs on cell types lacking a strong membrane marker); Mesmer was trained at model_mpp ~0.5 um and rescales the input to it, so passing the wrong pixel size (Mesmer's
image_mpp defaults to None = NO rescaling, assuming the input is already at model resolution โ the true pixel size must be passed explicitly; steinbock's --pixelsize flag wraps image_mpp and defaults to 1.0) rescales cells to the wrong learned size and degrades every boundary. No downstream step recovers a merged or split cell.
- Channel spillover is compensated on PIXELS before segmentation; lateral spillover (REDSEA) runs on the per-cell table AFTER segmentation โ they are DIFFERENT problems. Metal-isotope crosstalk is a pixel-level NNLS correction whose compensated value must be what gets averaged into the per-cell mean (post-aggregation is wrong). REDSEA corrects real signal leaking across shared cell boundaries at ~1 um even with perfect segmentation and zero channel spillover โ it is defined on segmented neighbors, so it must run after segmentation. Running REDSEA pre-segmentation, or channel comp post-aggregation, is a category error.
- The experimental unit is the PATIENT, not the cell or the ROI. Cells and ROIs from one patient are not independent replicates; a cell-level or per-image test over correlated cells is pseudoreplication (reports p~0 for trivial effects). Aggregate to per-patient proportions/summaries, then a mixed model / scCODA. Arcsinh cofactor is 1 for IMC integer ion counts, NOT the suspension-CyTOF 5 (which over-compresses them). Impossible lineage-exclusive co-expression is a segmentation/spillover ALARM, not a hybrid cell type.
Made-once commitments
| Commitment | Consequence inherited downstream |
|---|
| Panel (metal->antibody; membrane-sum channels) | Which channels extract and phenotype; a narrow membrane sum biases segmentation against some cell types |
| Segmentation frame (nuclear + membrane channels) | Every per-cell number (all are mask-bounded pixel averages); the largest irreversible error source |
Pixel size (steinbock --pixelsize / Mesmer image_mpp, ~1.0 um for IMC) | Boundary quality + all spatial distances; the wrong value rescales cells to the wrong learned size |
| Arcsinh cofactor = 1 (IMC), not 5 (CyTOF) | Clustering/phenotyping distances; cofactor 5 over-compresses integer ion counts |
Pipeline Overview
Raw MCD/TIFF Files โโ> Image Processing โโ> Cell Masks
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ imc-pipeline โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 1. Data Preprocessing (spillover, hot px) โ
โ 2. Cell Segmentation (Cellpose/Mesmer) โ
โ 3. Single-cell Quantification โ
โ 4. Clustering & Phenotyping โ
โ 5. Spatial Analysis โ
โ 6. Visualization โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
Cell Types + Spatial Neighborhoods
Decisions Threaded Through This Pipeline
Four reframes govern every stage and are detailed in the depended-on skills: IMC pixels are integer ion COUNTS (arcsinh cofactor 1, not the suspension-CyTOF 5), and spillover is spatial so it must be NNLS-compensated before segmentation; segmentation is the largest irreversible error source, so impossible double-positives are a QC alarm, not biology; a spatial interaction is a hypothesis test whose null silently decides whether the result is real or a density artifact; and the experimental unit is the patient, not the cell, so cross-condition tests aggregate to patients before testing.
Complete steinbock Workflow
Step 1: Setup and Preprocessing
steinbock preprocess imc panel
steinbock preprocess imc images --hpf 50
Step 2: Cell Segmentation
steinbock segment deepcell --pixelsize 1.0 --minmax -o masks
steinbock segment cellpose --minmax -o masks
Step 3: Single-cell Quantification
steinbock measure intensities -o intensities
steinbock measure regionprops -o regionprops
steinbock measure neighbors --type expansion --dmax 15 -o neighbors
Complete Python Workflow
import pandas as pd
import numpy as np
import anndata as ad
import scanpy as sc
import squidpy as sq
from pathlib import Path
data_dir = Path('steinbock_output')
intensities = pd.read_csv(data_dir / 'intensities.csv', index_col=0)
regionprops = pd.read_csv(data_dir / 'regionprops.csv', index_col=0)
neighbors = pd.read_csv(data_dir / 'neighbors.csv')
print(f'Loaded {len(intensities)} cells')
adata = ad.AnnData(X=intensities.values, obs=regionprops, var=pd.DataFrame(index=intensities.columns))
adata.obs['image_id'] = pd.Categorical([idx.rsplit('_', 1)[0] for idx in intensities.index])
adata.obs['cell_id'] = intensities.index
adata.obsm['spatial'] = regionprops[['centroid-0', 'centroid-1']].values
adata.layers['counts'] = adata.X.copy()
adata.X = np.arcsinh(adata.X / )
sc.pp.scale(adata, max_value=)
adata.raw = adata.copy()
sc.pp.pca(adata, n_comps=)
sc.pp.neighbors(adata, n_neighbors=)
sc.tl.umap(adata)
sc.tl.leiden(adata, resolution=)
()
sc.tl.rank_genes_groups(adata, , method=)
marker_genes = sc.get.rank_genes_groups_df(adata, group=)
cluster_annotations = {
: ,
: ,
: ,
: ,
:
}
adata.obs[] = adata.obs[].(cluster_annotations)
sq.gr.spatial_neighbors(adata, coord_type=, delaunay=, library_key=)
sq.gr.nhood_enrichment(adata, cluster_key=)
sq.gr.co_occurrence(adata, cluster_key=)
sq.gr.ripley(adata, cluster_key=, mode=)
matplotlib.pyplot plt
fig, axes = plt.subplots(, , figsize=(, ))
sc.pl.umap(adata, color=, ax=axes[], show=)
sc.pl.umap(adata, color=, ax=axes[], show=)
plt.savefig(, dpi=, bbox_inches=)
fig, ax = plt.subplots(figsize=(, ))
first_image = adata.obs[].iloc[]
sq.pl.spatial_scatter(adata[adata.obs[] == first_image],
color=, shape=, size=, ax=ax)
plt.savefig(, dpi=, bbox_inches=)
sq.pl.nhood_enrichment(adata, cluster_key=)
plt.savefig(, dpi=, bbox_inches=)
statsmodels.formula.api smf
counts = adata.obs.groupby([, , , ], observed=).size().unstack(fill_value=)
image_prop = counts.div(counts.(axis=), axis=).reset_index()
target =
res = smf.mixedlm(, image_prop, groups=image_prop[]).fit()
(res.summary())
adata.write()
()
R Alternative (imcRtools)
library(imcRtools)
library(cytomapper)
library(CATALYST)
spe <- read_steinbock('steinbock_output/')
assay(spe, 'exprs') <- asinh(counts(spe) / 1)
spe <- runDR(spe, features = rownames(spe), assay = 'exprs', dr = 'UMAP')
spe <- cluster(spe, features = rownames(spe), xdim = 10, ydim = 10, maxK = 20)
spe buildSpatialGraphspe img_id type threshold
spe aggregateNeighborsspe colPairName
aggregate_by count_by
spe detectCommunityspe colPairName
size_threshold group_by
plotSpatialspe img_id node_color_by
QC Checkpoints
| Stage | Check | Action if Failed |
|---|
| Preprocessing | No hot pixel streaks | Lower threshold |
| Segmentation | >80% cells detected | Adjust diameter |
| Quantification | All markers extracted | Check panel.csv |
| Clustering | 5-20 clusters | Adjust resolution |
| Spatial | Neighbors detected | Check distance |
Workflow Variants
High-plex Panels (40+ markers)
import scvi
scvi.model.SCVI.setup_anndata(adata, batch_key='image_id')
model = scvi.model.SCVI(adata)
model.train()
adata.obsm['X_scvi'] = model.get_latent_representation()
sc.pp.neighbors(adata, use_rep='X_scvi')
Tumor Microenvironment Analysis
sq.gr.nhood_enrichment(adata, cluster_key='cell_type')
Common Errors
| Symptom | Cause | Fix |
|---|
| Impossible double-positive "hybrid" cell types | Spillover not corrected before phenotyping (channel and/or lateral) | NNLS channel compensation on pixels before segmentation; REDSEA on the per-cell table after; treat lineage-exclusive co-expression as a QC failure until proven |
| Every boundary degraded, cells the wrong size | Wrong pixel size (Mesmer image_mpp defaults None=no rescaling, model trained at ~0.5; steinbock --pixelsize defaults 1.0) | Pass the true acquisition resolution explicitly (~1.0 um for IMC) |
| Macrophages under-captured; biased comparison | Nuclear-expansion segmentation cross-compared with whole-cell data | Never quantitatively compare expansion-segmented vs whole-cell; report the expansion radius; use constrained (not free) dilation |
| p~0 for a trivial effect | Pseudoreplication (cells/ROIs treated as replicates) | Aggregate to per-patient summaries; mixed model with patient random effect / scCODA |
| Markers over-compressed, noise clusters | Arcsinh cofactor 5 used on IMC | Cofactor 1 for IMC integer ion counts |
| Acquisition batch drives the clusters | Batch confounded with / not modeled against condition | Randomize acquisition order; batch-aware clustering (Harmony/scVI) for clustering ONLY; model batch as a covariate; no rescue if batch==condition |
References
- Windhager J, Zanotelli VRT, Schulz D, et al (2023) An end-to-end workflow for multiplexed image processing and analysis. Nature Protocols 18:3565-3613. DOI 10.1038/s41596-023-00881-0. (steinbock.)
- Greenwald NF, Miller G, Moen E, et al (2022) Whole-cell segmentation of tissue images with human-level performance using large-scale data annotation and deep learning. Nature Biotechnology 40:555-565. DOI 10.1038/s41587-021-01094-0. (Mesmer/DeepCell.)
- Bai Y, Zhu B, Rovira-Clave X, et al (2021) Adjacent cell marker lateral spillover compensation and reinforcement for multiplexed images. Frontiers in Immunology 12:652631. DOI 10.3389/fimmu.2021.652631. (REDSEA.)
- Palla G, Spitzer H, Klein M, et al (2022) Squidpy: a scalable framework for spatial omics analysis. Nature Methods 19:171-178. DOI 10.1038/s41592-021-01358-2.
- Hunter B, Nicorescu I, Foster E, et al (2024) OPTIMAL: an OPTimized Imaging Mass cytometry AnaLysis framework for benchmarking segmentation and data exploration. Cytometry Part A 105:36-53. DOI 10.1002/cyto.a.24803. (arcsinh cofactor 1 for IMC.)
Related Skills
- imaging-mass-cytometry/data-preprocessing - Hot pixel, spillover
- imaging-mass-cytometry/cell-segmentation - Cellpose/Mesmer details
- imaging-mass-cytometry/phenotyping - Cluster annotation
- imaging-mass-cytometry/spatial-analysis - Spatial statistics
- imaging-mass-cytometry/differential-analysis - Patient-level cross-condition testing
- imaging-mass-cytometry/interactive-annotation - Manual cell labeling
- imaging-mass-cytometry/quality-metrics - QC metrics
- single-cell/clustering - Clustering methods
- spatial-transcriptomics/spatial-statistics - Related spatial methods