Align Visium and Xenium Data#

This tutorial demonstrates how to align Visium spatial transcriptomics data to Xenium data using InSituPy. This workflow is useful for integrating data from different spatial technologies on the same tissue sample.

In InSituPy all data is registered to physical units. Therefore, it is important to know the resolution of the images as well as the measuring units of the coordinates of spatial units (here Visium spots) and cells, etc. The resolution of images can be checked in image analysis software such as QuPath or Fiji.

# Enable autoreload for development
%load_ext autoreload
%autoreload 2
# Configure dask to use legacy dataframe implementation (required for SpatialData)
import dask
dask.config.set({'dataframe.query-planning': False})

from insitupy import InSituData, CACHE
from insitupy.datasets import visium_human_breast_cancer
from insitupy.images.io import read_image

Load Xenium Dataset#

# Load Xenium data (adjust path to your data location)
xenium_path = CACHE / "out/demo_insitupy_project"
xenium = InSituData.read(xenium_path)
xenium.load_all()
print(xenium)
InSituData
Method:		Xenium
Slide ID:	0001879
Sample ID:	Replicate 1
Path:		C:\Users\ge37voy\.cache\InSituPy\out\demo_insitupy_project

    ➤ images
       'CD20':     (25778, 35416)
       'HE':       (25778, 35416, 3)
       'HER2':     (25778, 35416)
       'nuclei':   (25778, 35416)
    ➤ cells
       MultiCellData with main layer 'main'
           matrix
               AnnData object with n_obs × n_vars = 156447 × 297
               obs: 'transcript_counts', 'control_probe_counts', 'control_codeword_counts', 'total_counts', 'cell_area', 'nucleus_area', 'n_genes_by_counts', 'n_genes', 'leiden', 'cell_type_dc_sub_final', 'cell_type_publ'
               var: 'gene_ids', 'feature_types', 'genome', 'n_cells_by_counts', 'mean_counts', 'pct_dropout_by_counts', 'total_counts', 'n_cells'
               uns: 'cell_type_dc_sub_final_colors', 'cell_type_publ_colors', 'leiden', 'leiden_colors', 'log1p', 'neighbors', 'pca', 'umap'
               obsm: 'X_pca', 'X_umap', 'annotations', 'regions', 'spatial'
               varm: 'PCs'
               layers: 'counts', 'norm_counts'
               obsp: 'connectivities', 'distances'
           boundaries
               BoundariesData object with 2 entries:
                   cells
                   nuclei annotations
       Demo:	28 annotations, 2 classes ('Stroma', 'Tumor cells')
       demo2:	5 annotations, 3 classes ('Negative', 'Other', 'Positive')
       demo3:	7 annotations, 5 classes ('Immune cells', 'Necrosis', 'Stroma', 'Tumor', 'unclassified')
       Janesick:	18 annotations, 3 classes ('DCIS #1', 'DCIS #2', 'Invasive')
       Katja:	18 annotations, 4 classes ('DCIS', 'DCIS intermediate', 'DCIS with stromal reaction', 'Invasive')
       test:	6 annotations, 1 class ('testclass')
       TestKey:	3 annotations, 1 class ('TestClass')
    ➤ regions
       Demo:	3 regions, 3 classes ('Region 1', 'Region 2', 'Region 3')
       demo_regions:	3 regions, 3 classes ('Region1', 'Region2', 'Region3')
       Katja:	4 regions, 4 classes ('Region 1', 'Region 2', 'Region 3', 'Region 4')
       TMA:	6 regions, 6 classes ('A-1', 'A-2', 'A-3', 'B-1', 'B-2', 'B-3')
    ➤ transcripts
       DataFrame with shape Delayed('int-72f45051-f1a1-41da-b13d-7f8548ed6b65') x 8

Load Visium Dataset#

The Visium dataset is downloaded using the convenience function visium_human_breast_cancer. To download also the fullres image (“Microscope image without fiducial frame (TIFF)”) the option download_fullres needs to be activated. The fullres image is later needed for alignment.

# Download and load Visium human breast cancer dataset
visium = visium_human_breast_cancer(download_fullres=True)

print(visium)
H5 file exists. Checking md5sum...
The h5 file md5sum matches. Download is skipped. To force download set `overwrite=True`.
Spatial directory exists. Download is skipped. To force download set `overwrite=True`.
Full-resolution image exists. Checking md5sum...
The full-resolution image md5sum matches. Download is skipped. To force download set `overwrite=True`.
Visium data structure is ready at C:\Users\ge37voy\.cache\InSituPy\demo_datasets\visium_hbreastcancer\CytAssist_FFPE_Human_Breast_Cancer
Dataset contains:
- filtered_feature_bc_matrix.h5
- spatial/ directory
- CytAssist_FFPE_Human_Breast_Cancer_tissue_image.tif
Adding images...
InSituData
Method:		visium
Slide ID:	slide_id
Sample ID:	sample_id
Path:		None

    ➤ images
       'hires':    (2000, 1809, 3)
       'lowres':   (600, 543, 3)
    ➤ units
       SpatialUnitsData (Type: 'visium')
           .data: 4992 obs × 18085 vars
           .shapes: 4992 geometries
visium
InSituData
Method:		visium
Slide ID:	slide_id
Sample ID:	sample_id
Path:		None

    ➤ images
       'hires':    (2000, 1809, 3)
       'lowres':   (600, 543, 3)
    ➤ units
       SpatialUnitsData (Type: 'visium')
           .data: 4992 obs × 18085 vars
           .shapes: 4992 geometries

Add Full-Resolution Image to Visium#

The full-resolution H&E image needs to be added to the Visium dataset for alignment.

# Path to full-resolution image (adjust to your data location)
fullres_path = CACHE / "demo_datasets/visium_hbreastcancer/CytAssist_FFPE_Human_Breast_Cancer/CytAssist_FFPE_Human_Breast_Cancer_tissue_image.tif"

# Read the full-resolution image
fullres_img, ome_meta, axes, pixel_size = read_image(str(fullres_path))

# Add to Visium dataset
visium.images.add_image(
    image=fullres_img,
    axes=axes,
    pixel_size=pixel_size,
    name="fullres"
)

print(f"Added full-resolution image with shape: {visium.images['fullres'].shape}")
Added full-resolution image with shape: (21571, 19505, 3)

Save Visium Dataset with Full-Resolution Image#

Save the updated Visium dataset before alignment.

# Save Visium dataset
visium_out = CACHE / "out/visium_with_fullres"
visium.saveas(visium_out, overwrite=True)

# Reload to ensure everything is properly saved
visium = InSituData.read(visium_out)
visium.load_all()

print("Visium dataset saved and reloaded successfully")
Saving data to C:\Users\ge37voy\.cache\InSituPy\out\visium_with_fullres
Saved.
Visium dataset saved and reloaded successfully

Perform Alignment#

Align Visium features and images to the Xenium coordinate system using a transformation matrix.

About the Transformation Matrix#

The transformation matrix should be obtained from an external software. In this case the Xenium explorer was used to align the fullres image to the Xenium dataset and export the transformation matrix. Alternatively one could also use Fiji as explained here: https://www.10xgenomics.com/analysis-guides/he-to-xenium-dapi-image-registration-with-fiji. The transformation matrix defines how to map coordinates from the Visium space to the Xenium space.

# Create a copy of Xenium dataset for alignment
xenium_aligned = xenium.copy()

# Path to transformation matrix (adjust to your alignment file location)
transformation_matrix_file = "../../demo_data/transformation_matrix_visium_to_xenium.csv"

# Perform alignment
xenium_aligned.align_units(
    other=visium,
    transformation_matrix=str(transformation_matrix_file),
    source_image_name="fullres",  # Visium image to transform
    reference_image_name="HE",     # Xenium reference image
    transfer_images=True,          # Also transform Visium images
    verbose=True
)

print("\nAlignment completed successfully!")
Transforming and aligning spatial units...
Converted transformation matrix from pixel coordinates (reference: 0.2125 µm/pixel) to physical coordinates.
Applying transformation (in physical coordinates): a=0.0560531105468679, b=1.0044753763751142, d=-1.0044753763751142, e=0.0560531105468679, xoff=-2616.775799489288, yoff=8535.07889349505
Transformed 4992 features.
Spatial units aligned and added to InSituData object.
Transforming and aligning images...
Converted transformation matrix from pixel coordinates (reference: 0.2125 µm/pixel) to physical coordinates.
Applying transformation matrix (in physical coordinates):
[[ 5.60531105e-02  1.00447538e+00 -2.61677580e+03]
 [-1.00447538e+00  5.60531105e-02  8.53507889e+03]]
Transforming image 'fullres' with shape (21571, 19505, 3) -> output size (19505, 21571)
Transformed image 'fullres'
Transforming image 'hires' with shape (2000, 1809, 3) -> output size (1809, 2000)
Transformed image 'hires'
Transforming image 'lowres' with shape (600, 543, 3) -> output size (543, 600)
Transformed image 'lowres'
Transformed 3 images.
Images aligned and added to InSituData object.

Alignment completed successfully!

Save Aligned Dataset#

Save the aligned dataset for further analysis.

# Save aligned dataset
aligned_out = CACHE / "out/xenium_visium_aligned"
xenium_aligned.saveas(aligned_out, overwrite=True)

print(f"Aligned dataset saved to: {aligned_out}")
Saving data to C:\Users\ge37voy\.cache\InSituPy\out\xenium_visium_aligned
Saved.
Aligned dataset saved to: C:\Users\ge37voy\.cache\InSituPy\out\xenium_visium_aligned

Visualize Aligned Data#

Load and visualize the aligned dataset to verify the alignment quality.

# Reload aligned dataset
aligned_out = CACHE / "out/xenium_visium_aligned"
xenium_final = InSituData.read(aligned_out)
xenium_final.load_all()
# Visualize in napari
xenium_final.show()
INFO: Saved color legend to 'C:\Users\ge37voy\Downloads\colorlegend-main-ACTA2.pdf'