Install any skill in seconds. Free to start, no credit card required.
Get Started Free →End-to-end imaging mass cytometry workflow from raw acquisitions to spatial cell analysis. Orchestrates image preprocessing, segmentation, phenotyping, and spatial statistics. Use when analyzing imaging mass cytometry data end-to-end.
.claude/skills/bio-workflows-imc-pipeline/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-10 | ✗→✓ | ▲ Improved | 54% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 217% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 106% | 0% |
| case-16 | ✗→✓ | ▲ Improved | 92% | 0% |
| case-18 | ✗→✓ | ▲ Improved | -33% | 0% |
<!--
#
#
-->
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 Neighborhoodsbash# Initialize steinbock project steinbock preprocess imc \ --mcd data/*.mcd \ --panel panel.csv \ --output raw/ # Hot pixel filtering steinbock preprocess imc hotpixel \ --input raw/ \ --output img/ \ --threshold 50 # Create nuclear and membrane channels steinbock preprocess mosaic \ --input img/ \ --channels panel.csv \ --output mosaics/
bash# Using Cellpose steinbock segment cellpose \ --input img/ \ --panel panel.csv \ --channel DNA1 DNA2 \ --output masks/ \ --diameter 20 # Alternative: Using Mesmer steinbock segment mesmer \ --input img/ \ --panel panel.csv \ --nuclear DNA1 DNA2 \ --membrane CD45 \ --output masks/
bash# Extract intensities steinbock measure intensities \ --input img/ \ --masks masks/ \ --panel panel.csv \ --output intensities/ # Measure cell properties (area, etc.) steinbock measure regionprops \ --masks masks/ \ --output regionprops/ # Extract neighbor relationships steinbock measure neighbors \ --masks masks/ \ --output neighbors/ \ --distance 15
pythonimport pandas as pd import numpy as np import anndata as ad import scanpy as sc import squidpy as sq from pathlib import Path # === 1. LOAD DATA === 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') # === 2. CREATE ANNDATA === adata = ad.AnnData(X=intensities.values, obs=regionprops, var=pd.DataFrame(index=intensities.columns)) adata.obs['image_id'] = [idx.split('_')[0] for idx in intensities.index] adata.obs['cell_id'] = intensities.index # Add spatial coordinates adata.obsm['spatial'] = regionprops[['centroid_y', 'centroid_x']].values # === 3. PREPROCESSING === # Arcsinh transform (cofactor 5 for IMC) adata.X = np.arcsinh(adata.X / 5) # Scale for clustering sc.pp.scale(adata, max_value=10) adata.raw = adata.copy() # === 4. DIMENSIONALITY REDUCTION === sc.pp.pca(adata, n_comps=20) sc.pp.neighbors(adata, n_neighbors=15) sc.tl.umap(adata) # === 5. CLUSTERING === sc.tl.leiden(adata, resolution=0.8) print(f'Found {adata.obs["leiden"].nunique()} clusters') # === 6. PHENOTYPING === # Marker expression per cluster sc.tl.rank_genes_groups(adata, 'leiden', method='wilcoxon') marker_genes = sc.get.rank_genes_groups_df(adata, group=None) # Annotate clusters based on markers cluster_annotations = { '0': 'T cells', '1': 'Macrophages', '2': 'Tumor', '3': 'B cells', '4': 'Stromal' } adata.obs['cell_type'] = adata.obs['leiden'].map(cluster_annotations) # === 7. SPATIAL ANALYSIS === # Build spatial graph sq.gr.spatial_neighbors(adata, coord_type='generic', delaunay=True) # Neighborhood enrichment sq.gr.nhood_enrichment(adata, cluster_key='cell_type') # Co-occurrence analysis sq.gr.co_occurrence(adata, cluster_key='cell_type') # Ripley's statistics sq.gr.ripley(adata, cluster_key='cell_type', mode='L') # === 8. VISUALIZATION === import matplotlib.pyplot as plt # UMAP by cell type fig, axes = plt.subplots(1, 2, figsize=(14, 5)) sc.pl.umap(adata, color='cell_type', ax=axes[0], show=False) sc.pl.umap(adata, color='leiden', ax=axes[1], show=False) plt.savefig('umap_celltypes.png', dpi=150, bbox_inches='tight') # Spatial plot fig, ax = plt.subplots(figsize=(10, 10)) sq.pl.spatial_scatter(adata[adata.obs['image_id'] == 'image1'], color='cell_type', shape=None, size=10, ax=ax) plt.savefig('spatial_celltypes.png', dpi=150, bbox_inches='tight') # Neighborhood enrichment heatmap sq.pl.nhood_enrichment(adata, cluster_key='cell_type') plt.savefig('neighborhood_enrichment.png', dpi=150, bbox_inches='tight') # === 9. DIFFERENTIAL ANALYSIS === # Compare conditions adata.obs['condition'] = adata.obs['image_id'].map({ 'image1': 'Control', 'image2': 'Control', 'image3': 'Treatment', 'image4': 'Treatment' }) # Cell type proportions proportions = adata.obs.groupby(['image_id', 'condition', 'cell_type']).size().unstack(fill_value=0) proportions = proportions.div(proportions.sum(axis=1), axis=0) # Save results adata.write('imc_analysis.h5ad') proportions.to_csv('cell_type_proportions.csv') print('Analysis complete!')
rlibrary(imcRtools) library(cytomapper) library(CATALYST) # Read steinbock output spe <- read_steinbock('steinbock_output/') # Transform assay(spe, 'exprs') <- asinh(counts(spe) / 5) # Cluster spe <- runDR(spe, features = rownames(spe), exprs_values = 'exprs', dr = 'UMAP') spe <- cluster(spe, features = rownames(spe), exprs_values = 'exprs', xdim = 10, ydim = 10, maxK = 20) # Spatial analysis spe <- buildSpatialGraph(spe, img_id = 'image_id', type = 'expansion', threshold = 20) spe <- aggregateNeighbors(spe, colPairName = 'neighborhood', by = 'cluster_id') # Spatial context cn <- detectCommunity(spe, colPairName = 'neighborhood', size_threshold = 10, group_by = 'image_id') # Plot plotSpatial(spe, img_id = 'image1', node_color_by = 'cluster_id')
| 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 |
python# Use batch-aware clustering 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')
python# Spatial interactions with tumor tumor_cells = adata[adata.obs['cell_type'] == 'Tumor'].obs_names sq.gr.ligrec(adata, cluster_key='cell_type', source_groups=['Tumor'], target_groups=['T cells', 'Macrophages'])
<!-- AUTHOR_SIGNATURE: 9a7f3c2e-MD-BABU-MIA-2026-MSSM-SECURE -->
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 12,672 | 9,097 | -28% | 1 | 1 | 0% | 2,296 | 4,290 | +87% | 0 | 0 | — |
case-10 | fail→pass | 9,135 | 3,649 | -60% | 1 | 1 | 0% | 2,002 | 3,088 | +54% | 0 | 0 | — |
case-15 | pass→pass | 6,506 | 2,147 | -67% | 1 | 1 | 0% | 816 | 2,840 | +248% | 0 | 0 | — |
case-02 | fail→fail | 16,218 | 18,816 | +16% | 1 | 1 | 0% | 3,401 | 6,678 | +96% | 0 | 0 | — |
case-03 | pass→pass | 9,101 | 5,568 | -39% | 1 | 1 | 0% | 1,658 | 3,405 | +105% | 0 | 0 | — |
case-04 | pass→pass | 6,113 | 5,591 | -9% | 1 | 1 | 0% | 1,208 | 3,568 | +195% | 0 | 0 | — |
case-05 | pass→pass | 11,369 | 8,633 | -24% | 1 | 1 | 0% | 2,194 | 3,965 | +81% | 0 | 0 | — |
case-06 | pass→pass | 11,343 | 4,054 | -64% | 1 | 1 | 0% | 2,109 | 3,233 | +53% | 0 | 0 | — |
case-07 | pass→pass | 5,934 | 3,851 | -35% | 1 | 1 | 0% | 1,082 | 3,148 | +191% | 0 | 0 | — |
case-08 | fail→pass | 5,330 | 2,716 | -49% | 1 | 1 | 0% | 934 | 2,957 | +217% | 0 | 0 | — |
case-09 | pass→pass | 8,576 | 3,615 | -58% | 1 | 1 | 0% | 1,616 | 3,143 | +94% | 0 | 0 | — |
case-11 | pass→pass | 10,586 | 5,928 | -44% | 1 | 1 | 0% | 1,996 | 3,539 | +77% | 0 | 0 | — |
case-12 | pass→pass | 9,017 | 5,400 | -40% | 1 | 1 | 0% | 1,299 | 3,340 | +157% | 0 | 0 | — |
case-13 | pass→pass | 6,074 | 3,581 | -41% | 1 | 1 | 0% | 1,219 | 3,138 | +157% | 0 | 0 | — |
case-14 | fail→pass | 8,176 | 3,103 | -62% | 1 | 1 | 0% | 1,444 | 2,969 | +106% | 0 | 0 | — |
case-16 | fail→pass | 11,284 | 3,718 | -67% | 1 | 1 | 0% | 1,643 | 3,150 | +92% | 0 | 0 | — |
case-17 | pass→pass | 6,120 | 4,000 | -35% | 1 | 1 | 0% | 1,250 | 3,227 | +158% | 0 | 0 | — |
case-18 | fail→pass | 22,443 | 2,484 | -89% | 1 | 1 | 0% | 4,378 | 2,940 | -33% | 0 | 0 | — |
case-19 | pass→pass | 6,995 | 6,321 | -10% | 1 | 1 | 0% | 1,293 | 3,375 | +161% | 0 | 0 | — |
case-20 | pass→pass | 8,094 | 10,917 | +35% | 1 | 1 | 0% | 1,618 | 4,885 | +202% | 0 | 0 | — |
case-21 | pass→pass | 12,954 | 14,310 | +10% | 1 | 1 | 0% | 2,775 | 5,540 | +100% | 0 | 0 | — |
case-22 | pass→pass | 15,395 | 17,605 | +14% | 1 | 1 | 0% | 3,267 | 6,079 | +86% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +23 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 7/24/2026 | 0% |
Other measured skills in the registry, with their headline benchmark lift.