| # Cell Deconvolution with Tangram |
|
|
| Deconvolve spatial spots into individual cells using Tangram's constrained mapping mode with cell segmentation. |
|
|
| ## When to Use |
|
|
| - Spatial technology has multiple cells per spot (Visium, Slide-seq) |
| - You have histology images with cell segmentation |
| - Goal: Assign cell types to individual segmented cells |
|
|
| --- |
|
|
| ## Requirements |
|
|
| Before running deconvolution, you need: |
|
|
| 1. **scRNA-seq reference** with cell type annotations |
| 2. **Spatial data** with: |
| - Gene expression |
| - Cell segmentation from histology (via Squidpy) |
|
|
| --- |
|
|
| ## Workflow Overview |
|
|
| 1. Segment cells from histology image (Squidpy) |
| 2. Calculate image features |
| 3. Preprocess for Tangram |
| 4. Run constrained mapping |
| 5. Assign cell types to segments |
| 6. Visualize deconvolved cells |
|
|
| --- |
|
|
| ## Step 1: Cell Segmentation (Squidpy) |
|
|
| If not already done, segment cells from histology: |
|
|
| ```python |
| import squidpy as sq |
| import scanpy as sc |
| |
| # Load spatial data with image |
| adata_sp = sc.read_h5ad("spatial.h5ad") |
| img = sq.datasets.visium_fluo_image_crop() # or load your image |
| |
| # Smooth image |
| sq.im.process(img=img, layer="image", method="smooth") |
| |
| # Segment nuclei |
| sq.im.segment( |
| img=img, |
| layer="image_smooth", |
| method="watershed", |
| channel=0, # DAPI channel |
| ) |
| |
| # Calculate features (cell counts per spot) |
| sq.im.calculate_image_features( |
| adata_sp, |
| img, |
| layer="image", |
| features="segmentation", |
| features_kwargs={ |
| "segmentation": { |
| "label_layer": "segmented_watershed", |
| "props": ["label", "centroid"], |
| } |
| }, |
| mask_circle=True, |
| ) |
| |
| # Check cell counts |
| print(adata_sp.obs["cell_count"].describe()) |
| adata_sp.write_h5ad("spatial_with_segmentation.h5ad") |
| ``` |
|
|
| --- |
|
|
| ## Step 2: Visualize Segmentation |
|
|
| Verify segmentation quality: |
|
|
| ```python |
| import matplotlib.pyplot as plt |
| |
| sc.pl.spatial(adata_sp, color="cell_count", title="Cells per Spot") |
| plt.savefig("cell_counts.png") |
| |
| # Check total cells |
| total_cells = adata_sp.obsm["image_features"]["segmentation_label"].sum() |
| print(f"Total segmented cells: {total_cells}") |
| ``` |
|
|
| --- |
|
|
| ## Step 3: Preprocess for Tangram |
|
|
| **Tool**: `tangram_preprocess` |
|
|
| ``` |
| tangram_preprocess( |
| adata_sc_path="scrna.h5ad", |
| adata_sp_path="spatial_with_segmentation.h5ad", |
| marker_genes="auto", |
| cell_type_key="cell_type" |
| ) |
| ``` |
|
|
| --- |
|
|
| ## Step 4: Constrained Mapping |
|
|
| Run Tangram in constrained mode to fit exactly the number of segmented cells: |
|
|
| ```python |
| import scanpy as sc |
| import tangram as tg |
| import numpy as np |
| |
| # Load preprocessed data |
| adata_sc = sc.read_h5ad("experiments/tangram_sc_prep.h5ad") |
| adata_sp = sc.read_h5ad("experiments/tangram_sp_prep.h5ad") |
| |
| # Get cell counts from segmentation |
| cell_counts = adata_sp.obsm["image_features"]["segmentation_label"] |
| target_count = int(cell_counts.sum()) |
| density_prior = np.array(cell_counts) / cell_counts.sum() |
| |
| print(f"Target cell count: {target_count}") |
| |
| # Constrained mapping |
| ad_map = tg.map_cells_to_space( |
| adata_sc, |
| adata_sp, |
| mode="constrained", |
| target_count=target_count, |
| density_prior=density_prior, |
| num_epochs=1000, |
| device="cuda:0", # GPU recommended |
| ) |
| |
| ad_map.write_h5ad("experiments/tangram_constrained_mapping.h5ad") |
| ``` |
|
|
| --- |
|
|
| ## Step 5: Assign Cell Types to Segments |
|
|
| ```python |
| # Create segment dataframe |
| tg.create_segment_cell_df(adata_sp) |
| |
| # Count cell types per spot |
| tg.count_cell_annotations( |
| ad_map, |
| adata_sc, |
| adata_sp, |
| annotation="cell_type", |
| ) |
| |
| # Deconvolve to individual cells |
| adata_segment = tg.deconvolve_cell_annotations(adata_sp) |
| adata_segment.write_h5ad("experiments/deconvolved_cells.h5ad") |
| |
| print(f"Deconvolved {adata_segment.shape[0]} cells") |
| print(adata_segment.obs["cluster"].value_counts()) |
| ``` |
|
|
| --- |
|
|
| ## Step 6: Visualize Deconvolved Cells |
|
|
| ```python |
| import matplotlib.pyplot as plt |
| |
| fig, ax = plt.subplots(figsize=(12, 12)) |
| sc.pl.spatial( |
| adata_segment, |
| color="cluster", |
| size=0.4, |
| show=False, |
| frameon=False, |
| alpha_img=0.2, |
| ax=ax, |
| ) |
| plt.title("Deconvolved Cell Types") |
| plt.savefig("deconvolved_spatial.png", dpi=200) |
| ``` |
|
|
| --- |
|
|
| ## Output Files |
|
|
| - `spatial_with_segmentation.h5ad` - Spatial data with cell counts |
| - `tangram_constrained_mapping.h5ad` - Constrained mapping result |
| - `deconvolved_cells.h5ad` - Individual cell annotations |
| - Visualization plots |
|
|
| --- |
|
|
| ## Interpreting Results |
|
|
| The deconvolved AnnData contains: |
| - `obs['cluster']`: Assigned cell type for each segmented cell |
| - `obs['x']`, `obs['y']`: Spatial coordinates of cell centroids |
| - `obsm['spatial']`: Spatial coordinates array |
|
|
| **Note**: Deconvolution assigns cell types probabilistically based on the mapping. Cells are assigned to the most likely type given the spot's expression profile. |
|
|
| --- |
|
|
| ## Tips |
|
|
| 1. **Segmentation quality matters**: Poor segmentation = poor deconvolution |
| 2. **Use GPU**: Constrained mode is computationally intensive |
| 3. **More epochs**: Use 1000+ epochs for constrained mode |
| 4. **Validate patterns**: Check that deconvolved cell types match expected tissue architecture |
| 5. **Compare to spot-level**: Deconvolved proportions should roughly match spot-level predictions |
|
|