spatial-transcriptomics/image-analysis/SKILL.md
Segments cells/nuclei and extracts image features from imaging spatial transcriptomics (Xenium, MERFISH/MERSCOPE, CosMx) and H&E/IF tissue images using Cellpose, StarDist, Baysor, and Squidpy. Use when choosing a segmentation strategy (DAPI nucleus + expansion vs membrane-stain whole-cell vs transcript-aware Baysor/proseg vs segmentation-free SSAM) given the available stain; judging whether transcript spillover is fabricating false co-expression and short-range cell-cell signal; and deciding whether the derived cell-by-gene matrix is trustworthy before downstream typing, DE, or ligand-receptor analysis.
npx skillsauth add GPTomics/bioSkills bio-spatial-transcriptomics-image-analysisInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Reference examples tested with: squidpy 1.7+, scanpy 1.10+, scikit-image 0.22+, numpy 1.26+, pandas 2.2+, cellpose 4.0+ (CLI tools: Baysor 0.6+, proseg 1.0+)
Before using code patterns, verify installed versions match. If versions differ:
pip show <package> then help(module.function) to check signaturesIf code throws ImportError, AttributeError, or TypeError, introspect the installed package and adapt the example to match the actual API rather than retrying.
"Segment cells from my imaging data" -> Draw cell/nucleus boundaries on an image (or on the transcript point cloud) and assign each molecule to one cell, producing a cell-by-gene matrix.
cellpose.models.CellposeModel().eval(), StarDist for nuclei, squidpy.im.segment() (watershed baseline)"Extract image features for my spots" -> Summarize pixel intensity/texture under each Visium spot (a DIFFERENT operation from segmentation -- no cell boundaries are drawn).
squidpy.im.calculate_image_features() (summary, histogram, texture/GLCM)Keep these two operations distinct. squidpy.im feature extraction (texture/summary on H&E) describes the image patch under a spot for domain detection. Segmentation manufactures the cells themselves. This skill leads with segmentation because that is the dominant error source; feature extraction is a downstream convenience.
In imaging spatial data the cell is a segmentation HYPOTHESIS, not an observation. The raw data are images plus a table of decoded molecules with x, y (and sometimes z); there is no native cell. A cell-by-gene matrix exists only AFTER an algorithm draws boundaries and assigns each molecule to one cell or to background. Every row of that matrix is produced by the segmentation step, which is the dominant, irreducible upstream error source for every imaging platform -- it confounds typing, DE, and communication downstream (Mitchel et al. 2026 Nat Genet 58:434, who find segmentation errors "dominate the results" for context-dependent DE and ligand-receptor inference).
Segmentation fails three ways, and each fabricates a specific downstream lie:
The cascade is worst for exactly the analyses people prize. A T cell abutting epithelium picks up keratin spillover -> false marker co-expression -> a spurious "transitional/hybrid" state that is pure artifact; rare cells in dense parenchyma (TILs, neutrophils) are swamped by neighbor spillover and lost. Crucially, distance-dependent spillover FABRICATES the short-range co-localization that ligand-receptor and cell-cell communication tools detect -- so an L-R "hit" between two touching types can be a pure segmentation artifact (a circularity; see spatial-communication). Treat the derived matrix as PROVISIONAL and run a contamination QC step before trusting any single-cell-resolution claim.
The first question is not "which tool" but "what boundary signal do I have?" A nuclear stain says where the nucleus is; a membrane/boundary stain says where the cell ENDS. Whole-cell segmentation is boundary-finding, and DAPI carries no boundary information -- so DAPI-only whole-cell is always inference (expansion). Adding a membrane/boundary stain converts that inference into measurement, and is the single highest-leverage change available -- it beats swapping algorithms on DAPI-only data.
| Tool | Class | Input signal | Best when | Fails / weak when |
|------|-------|--------------|-----------|-------------------|
| StarDist | image, star-convex polygons | DAPI, 1 channel | round, crowded NUCLEI; fast | non-convex shapes (whole cells, neurons) cannot be represented; not a whole-cell tool |
| Cellpose | image, DL generalist | 1-2 ch (nucleus +/- membrane) | generalist; 2-channel nucleus+membrane = true whole-cell; retrainable | no transcript-only mode; over/under-segments on DAPI-only without a membrane channel |
| Watershed (squidpy.im.segment) | classic flooding | DAPI seeds + intensity | fast baseline; seeded splitting of touching nuclei | over-segments textured nuclei; seed/threshold-sensitive; no shape prior |
| Mesmer / DeepCell | image, DL whole-cell (TissueNet) | 2 ch: nuclear + membrane | any platform WITH a membrane stain (also CODEX/MIBI/IMC); human-level whole-cell | needs a membrane channel; DAPI-only -> nuclear only |
| Baysor | transcript MRF+EM, optional prior | molecule table (+ optional DAPI) | refining/replacing image segmentation by transcriptional composition; recovers cells images miss; runs with or without a prior | sparse/low-plex panel + no prior -> unstable; compute-heavy |
| proseg | transcript, cell-simulation membership | molecule table | transcript-only whole-cell WITHOUT a membrane stain; fast; recovers hard immune cells | sparse-panel limits of all transcript-only methods; newer/less battle-tested |
| SSAM / ClusterMap | segmentation-FREE molecule density | molecule table | cell-type/domain MAPPING when boundaries are hopeless; recovers low-density types | produce NO cell objects -> no per-cell composition, counts, or neighbor graph |
The ladder of trust runs: DAPI-only StarDist/Cellpose-nuclei (accept nuclear sensitivity loss or expansion bias) < membrane-stain whole-cell Mesmer or 2-channel Cellpose < transcript-aware Baysor/proseg (molecules, not a fixed radius, set boundaries) < segmentation-free SSAM/ClusterMap (best mapping, but the cell unit is lost -- no per-cell matrix, neighbor graph, or QC). Methods evolve fast here; verify current best practice against the latest benchmarks before committing.
Nuclear (DAPI) segmentation is robust because nuclei are round, separated, and high-contrast -- but the nucleus holds only a minority of mRNA, so cytoplasmic transcripts fall OUTSIDE the mask and are discarded (large sensitivity loss) or must be reassigned. The cheap substitute is nucleus expansion: dilate each nuclear mask by a fixed radius until it hits a neighbor. This assumes round, equal-sized, isotropically-arranged cells -- false for almost all tissue. In dense tissue expanded disks collide and partition intercellular space by a Voronoi-like rule unrelated to true membranes (the worst region for spillover); elongated or large-cytoplasm cells (neurons, muscle, glia, macrophages) are badly served -- a fixed disk captures none of their projections and steals neighbors' transcripts. No single radius is correct for a heterogeneous tissue. Expansion is a baseline, not a solution.
Xenium makes this concrete: XOA v1.0-1.9 used DAPI + 15 um nucleus expansion; v2.0+ cut the default to 5 um "for improved accuracy" -- an admission that 15 um over-assigned in dense tissue. The vendor changed the answer, so do not treat any expansion radius as ground truth. Note that Cellpose on Xenium is a community path via Xenium Ranger import-segmentation, NOT the built-in XOA default -- do not conflate them.
Goal: Produce instance masks (one integer label per cell) from a nuclear-stain image as the starting cell hypotheses.
Approach: Run Cellpose's generalist model; in v4 (Cellpose-SAM) there is one model, channels are no longer an input, and diameter is optional because the model is size-invariant. With a membrane channel available, pass it as a second channel for true whole-cell masks instead of nuclei + expansion.
from cellpose import models
model = models.CellposeModel(gpu=False) # v4 Cellpose-SAM single generalist model; v3 used models.Cellpose(model_type='nuclei')
masks, flows, styles = model.eval(dapi_image, diameter=None) # v4 returns 3 values + drops channels=; v3 returned masks, flows, styles, diams and took channels=[0,0]
# masks: integer label image; 0 = background, 1..N = cells. This is a HYPOTHESIS, not ground truth.
n_cells = int(masks.max())
DAPI-only masks are nuclei. Approximating whole cells without a membrane stain requires expansion (round-cell bias above) or a transcript-aware method. With a membrane/boundary channel, stacking [nucleus, membrane] and passing both lets Cellpose-SAM use the first channels in any order -- that converts boundary inference into measurement.
Goal: Recover cells that image-based nuclear segmentation drops (small, irregular, immune) by letting transcript composition and density define boundaries.
Approach: Run Baysor or proseg on the per-molecule table (x, y, gene). These are CLI tools; the molecule table is the source of truth and the only object that permits re-segmentation. Optionally seed Baysor with the vendor nuclear masks as a prior.
# Baysor: molecule-table segmentation; -m = min transcripts/cell, -s = expected cell scale (um), :gene names the gene column
baysor run -x x_location -y y_location -g feature_name -m 30 -s 10 \
--prior-segmentation-confidence 0.5 transcripts.csv nucleus_id
# proseg: transcript-only whole-cell, reads Xenium/CosMx/MERSCOPE molecule tables directly
proseg --xenium transcripts.csv.gz --output-counts counts.csv.gz --output-cell-polygons cells.geojson
Baysor and proseg output per-molecule cell assignments -> rebuild a cell-by-gene matrix from those. Where the panel is sparse and no membrane stain exists, all transcript-only methods become unstable -- check cell-yield and size distributions against the image before trusting them.
Goal: Detect the segmentation failure modes (over/under-segmentation, spillover) BEFORE they propagate into typing and communication results.
Approach: Treat the matrix as provisional. Inspect cell-size and transcripts-per-cell distributions (bimodality flags merged doublets or fragments), check for impossible co-expression of mutually exclusive lineage markers (a spillover signature), and where possible run a dedicated contamination tool.
import numpy as np
counts = np.asarray(adata.X.sum(axis=1)).ravel() # transcripts per cell
area = adata.obs['cell_area'].to_numpy() # from the segmentation polygons
# Over-segmentation: a spike of tiny, low-count fragments. Under-segmentation: a tail of huge, high-count "cells".
print('transcripts/cell pct [5,50,95]:', np.percentile(counts, [5, 50, 95]))
print('cell area pct [5,50,95]:', np.percentile(area, [5, 50, 95]))
# Spillover signature: cells co-expressing markers of two mutually exclusive lineages (e.g. epithelial KRT + T-cell CD3).
# Distance-dependent -> these false double-positives concentrate at heterotypic boundaries.
epi = np.asarray(adata[:, 'EPCAM'].X).ravel() > 0
tcell = np.asarray(adata[:, 'CD3E'].X).ravel() > 0
print('suspicious EPCAM+CD3E+ cells:', int((epi & tcell).sum()))
Dedicated correction/QC tools target the distance-dependent contamination directly: SPLIT and neighborhood factorization (Mitchel et al. 2026) for contamination, FastReseg for transcript-based re-segmentation (CosMx), and ovrlpy for vertical/z-collapse doublets. SOPA runs Cellpose and Baysor on the same data with patch-based conflict resolution. Run a contamination step before any rare-state, hybrid-state, or short-range L-R claim.
Goal: Summarize the tissue image under each Visium spot (intensity, texture) to augment expression-based spatial-domain detection.
Approach: Wrap the image in a Squidpy ImageContainer and call calculate_image_features. This draws no cell boundaries -- it describes the pixel patch under each spot. Pass layer='image' explicitly if a segmentation layer already exists on the container.
import squidpy as sq
img = sq.datasets.visium_hne_image_crop() # ImageContainer; pair with the matching adata
adata = sq.datasets.visium_hne_adata_crop()
sq.im.calculate_image_features(adata, img, layer='image', features=['summary', 'texture'],
key_added='img_features', n_jobs=1, show_progress_bar=False)
# texture = GLCM (contrast, homogeneity, correlation, ASM); summary = per-channel intensity stats
feats = adata.obsm['img_features'] # rows = spots, columns = features
Watershed via sq.im.segment(img, layer='image', method='watershed') is a fast classical baseline that over-segments H&E; use it for a quick look, not for production cell calling. Morphology per mask comes from skimage.measure.regionprops_table (area, eccentricity, solidity).
| Symptom | Cause | Fix |
|---------|-------|-----|
| Many tiny, low-count "cells" | Over-segmentation split single cells | Lower model sensitivity / raise min_size; check cell-area histogram for a fragment spike; prefer a learned model over watershed |
| Cluster of cells co-expressing exclusive lineage markers (KRT + CD3) | Transcript spillover fabricating co-expression at heterotypic boundaries | Run a contamination QC (SPLIT, neighborhood factorization); re-segment with a membrane stain or Baysor/proseg; do not interpret as a "hybrid state" |
| A short-range ligand-receptor hit between two touching types | Distance-dependent spillover manufactures the short-range co-localization (circularity) | Validate against segmentation quality; constrain L-R by distance; treat as hypothesis (see spatial-communication) |
| Standard doublet detector finds almost nothing, yet merged cells exist | Spatial doublets are neighbor merges, not random pairs the detector simulates | Inspect transcripts/cell and area tails; re-segment; do not rely on Scrublet/DoubletFinder for imaging merges |
| Cytoplasmic markers nearly absent from every cell | Nucleus-only mask discarded cytoplasmic mRNA | Expand the mask, add a membrane stain, or use a transcript-aware method |
| Sharp drop in transcripts/cell after a vendor software update | Xenium expansion default cut 15 um -> 5 um (v2.0) | Expected; the smaller radius assigns fewer (and fewer mis-assigned) transcripts -- re-run downstream, do not "fix" |
| model.eval returns 3 values but code unpacks 4 | Cellpose v4 dropped diams and the channels= argument | Unpack masks, flows, styles; remove channels=; diameter is optional in v4 |
| Unable to determine which layer to use | A segmentation layer was added, so the container has >1 layer | Pass layer='image' to calculate_image_features / segment |
| Transcript-only segmentation yields implausible cell shapes/yield | Sparse/low-plex panel with no prior -> Baysor/proseg unstable | Add a nuclear prior; compare yield + size to the image; fall back to image segmentation |
tools
End-to-end CLIP-seq pipeline from FASTQ to ENCODE-compliant binding sites, single-nucleotide crosslink maps, annotation, motifs, and (optionally) differential binding. Use when running the full Yeo lab eCLIP / iCLIP / iCLIP2 / iCLIP3 / irCLIP / PAR-CLIP analysis with SMInput control, protocol-specific UMI extraction, ENCODE STAR parameters, CLIPper or Skipper peak calling with stringent log2 FC and -log10 p thresholds, IDR rescue and self-consistency QC, and downstream motif registration with mCross or PEKA.
development
Detect, date, and contextualize whole-genome duplication (WGD / paleopolyploidy) events using wgd v2 (Chen et al 2024), KsRates (Sensalari 2022 substitution-rate-corrected Ks dating), DupGen_finder (Qiao 2019), MAPS (Li 2018 phylogenomic), POInT (Conant 2008 ordered-block), SLEDGe (2024 ML-based), Whale.jl (Bayesian DL+WGD), and synteny-anchored paranome construction. Use when identifying ancient polyploidy from Ks distributions and synteny block analysis, positioning WGD events relative to speciation, distinguishing tandem from segmental from WGD duplications, dating the 2R/3R vertebrate / fish / salmonid WGDs, building paranome and Ks-age mixture models, applying KsRates substitution-rate correction across lineages, or testing alternative biased-fractionation / dosage-balance models post-WGD.
tools
Build whole-genome alignments using Progressive Cactus (Armstrong 2020 reference-free clade-level WGA), Minigraph-Cactus (Hickey 2024 pangenome-aware), LASTZ chain/net (UCSC pipeline), MUMmer4 (Marçais 2018 pairwise), minimap2 -x asm5/10/20 (Li 2018 fast pairwise), AnchorWave (Song 2022 WGD-aware), and Mauve / progressiveMauve (bacterial). Operates the HAL toolkit (Hickey 2013) for downstream extraction including halSynteny, halLiftover, halBranchMutations, and hal2maf. Use when constructing multi-species alignments for comparative-annotation projection (TOGA), synteny detection, conservation analyses (phyloP / PhastCons), or pangenome graph construction; selecting between reference-free (Cactus) and reference-anchored (LASTZ chains/nets) approaches; tuning sensitivity for closely vs distantly related genomes; or producing HAL files for genome-wide downstream tools.
development
Detect syntenic blocks and structural rearrangements between genomes using MCScanX (Wang 2012), JCVI/MCScan (Tang 2008 Python), GENESPACE (Lovell 2022) for orthology-anchored riparian visualization, SyRI for structural variation, AnchorWave for sequence-level synteny, i-ADHoRe 3.0 for highly diverged species, SynNet for synteny networks, and ntSynt for multi-genome macrosynteny. Use when identifying collinear gene blocks across species, distinguishing macrosynteny from microsynteny, detecting inversions/translocations/duplications, anchoring orthology in WGD lineages, producing publication riparian plots, computing synteny block age via Ks (cross-references whole-genome-duplication), or running synteny-aware ortholog inference in polyploids.