Aligning Spatial Glycomics and IHC images

Author:Andrew Causer

This notebook is a tutorial of how to use goatpy to align IHC images with spatial glycomics data. For more infomation on image alignment with goatpy please visit the Getting Started page.

Similar to using a H&E image, IHC images can be aligned to Glycomics data with goatpy using either a manual or automatic method.

[1]:
import goatpy as gp
import spatialdata_plot
import matplotlib.pyplot as plt
import scanpy as sc
import pandas as pd
import squidpy as sq
from napari_spatialdata import Interactive

Manual Alignment

Manual alignment involves the use of SpatialData-Napari. First we need to contruct two SpatialData objects using goatpy:

  1. Spatial Glycomics Object (MALDI-MSI)

  2. Imaging Data Object (IHC)

Lets do this below:

[2]:
## Create MALDI-MSI SpatialData object
maldi_sd = gp.glyco_spatialdata(imzml_path="./Documents/IHC_test_data/ad_dm_sa_deriv_19pm0001_0204002_reduced_peakonly.imzML")

## Create IHC SpatialData object
ihc_sd = gp.ihc_spatialdata("./Documents/IHC_test_data/DM_22.05.26_Image_2_19PM0001_RedGrnChannel.tif")
Loaded image with shape: (1341, 1508, 3)
Successfully generated SpatialData object

We can visualise the data structure of these two objects:

[3]:
maldi_sd
[3]:
SpatialData object
├── Points
│     └── 'centroids': DataFrame with shape: (<Delayed>, 112) (2D points)
├── Shapes
│     └── 'pixels': GeoDataFrame shape: (117948, 2) (2D shapes)
└── Tables
      └── 'maldi_adata': AnnData (117948, 108)
with coordinate systems:
    ▸ 'global', with elements:
        centroids (Points), pixels (Shapes)
[4]:
ihc_sd
[4]:
SpatialData object
└── Images
      └── 'ihc_image': DataArray[cyx] (3, 1341, 1508)
with coordinate systems:
    ▸ 'global', with elements:
        ihc_image (Images)

Following the same method as H&E manual alignment we must generate a psuedo-image for our MALDI-MSI data:

[5]:
maldi_sd = gp.Add_Pseudo_Image(maldi_sd, "MPI", tables = "maldi_adata", library_id = "Spatial", is_continous=True, cmap = "viridis",img_upscaling=2)
INFO     Transposing `data` of type: <class 'dask.array.core.Array'> to ('c', 'y', 'x').

Now we can run goatpy’s manual alignment GUI to select corresponding landmarks on the pseudo-MSI and IHC image. Below is a screenshot showing the layout of the single window GUI:

[ ]:
viewer, widget = gp.launch_landmark_gui(maldi_sd, ihc_sd, split_view=False,  he_image_key = "ihc_image")

1bfae8ca7cc44335828086970e3de2af

The below section of code is for reproducability. These are the location of 4 landmarks on the MALDI-MSI and IHC coordinate system, respectively.

[6]:
from spatialdata.models import PointsModel
import pandas as pd

maldi_align_points = pd.DataFrame({
    "x": [56.691345,172.349422,577.152689,997.829025,],
    "y": [483.286246,166.483689,322.370662,597.166866]
})

ihc_align_points = pd.DataFrame({
    "x": [129.072657,318.347858,834.765384,1318.743398],
    "y": [669.121976,267.204264,503.214083,870.946993]
})

maldi_sd["maldi_landmarks"] = PointsModel.parse(maldi_align_points)
ihc_sd["ihc_landmarks"] = PointsModel.parse(ihc_align_points)

Following the selection of these landmarks we can run the alignment function.

NOTE: Make sure the correct image keys are used if changed between H&E and IHC images.

[7]:
aligned = gp.align_image_using_landmarks(maldi_sd, ihc_sd, he_image_key = "ihc_image", he_landmark_key = "ihc_landmarks")
[8]:
aligned.pl.render_images("ihc_image") \
       .pl.render_shapes("pixels", color="MPI", size =50) \
       .pl.show("aligned")
INFO     Using 'datashader' backend with 'None' as reduction method to speed up plotting. Depending on the
         reduction method, the value range of the plot might change. Set method to 'matplotlib' to disable this
         behaviour.
INFO     Using the datashader reduction "mean". "max" will give an output very close to the matplotlib result.
../../_images/guides_Tutorials_ihc_alignment_16_1.png

From the figure above we can see the alignment has worked well.

Automatic Alignment

Similar to H&E, we can also run automatic alignment with the IHC image. Lets run this below, providing the .imzML, IHC image and peaks list file path.

[9]:
sdata = gp.load_and_align(imzml_path="./Documents/IHC_test_data/ad_dm_sa_deriv_19pm0001_0204002_reduced_peakonly.imzML",
               ihc_path="./Documents/IHC_test_data/DM_22.05.26_Image_2_19PM0001_RedGrnChannel.tif",
               peaks_path="./Documents/IHC_test_data/Sample 19M0001 glycan list.csv")
[0.23GB] Loading peaks ...
[0.24GB]   174 peaks
[0.24GB]   IHC pixel size from ImageJ TIFF tags (ResolutionUnit=1, unit=µm): 7.8000 um/px  (XResolution=0.128205 px/µm)
[0.24GB]   IHC native pixel size: 7.8000 um/px
[0.29GB]   MALDI pixel size from imzML metadata: 20.0 um/px
[0.25GB]   Validated: ref thumbnail (588x523 px) >= MALDI (544x492 px)
[0.25GB]   maldi_pixel_um=20.0  ref_pixel_um=7.8000
[0.25GB] Computing MALDI crop offsets ...
[0.30GB]   Crop: row=0, col=0
[0.70GB] Loading 174 ion images (chunk=10) with 0.1 Da tolerance per peak ...
[0.44GB]   Peaks 1-10 / 174
[0.44GB]   Peaks 11-20 / 174
[0.45GB]   Peaks 21-30 / 174
[0.46GB]   Peaks 31-40 / 174
[0.44GB]   Peaks 41-50 / 174
[0.43GB]   Peaks 51-60 / 174
[0.44GB]   Peaks 61-70 / 174
[0.43GB]   Peaks 71-80 / 174
[0.43GB]   Peaks 81-90 / 174
[0.43GB]   Peaks 91-100 / 174
[0.46GB]   Peaks 101-110 / 174
[0.43GB]   Peaks 111-120 / 174
[0.46GB]   Peaks 121-130 / 174
[0.32GB]   Peaks 131-140 / 174
[0.32GB]   Peaks 141-150 / 174
[0.29GB]   Peaks 151-160 / 174
[0.31GB]   Peaks 161-170 / 174
[0.32GB]   Peaks 171-174 / 174
[0.35GB]   spectra_all: (492, 544, 174)  (186 MB)
[0.35GB] Preparing MALDI template ...
[0.35GB]   MALDI grayscale: (492, 544)  mean=0.192
[0.74GB] Loading IHC at 20.0 um/px ...
[0.74GB]   Loading IHC: ./Documents/IHC_test_data/DM_22.05.26_Image_2_19PM0001_RedGrnChannel.tif
[0.74GB]   Raw array shape: (1341, 1508, 3)  dtype=uint8  axes='YXS'
[0.74GB]   Normalised to (C,H,W): (3, 1341, 1508)
[0.74GB]   Channel names: ['CF_405', 'CF_488', 'CF_561']
[0.74GB]   Auto-selected registration channel 1 ('CF_488', mean=13.4)
[0.74GB]   Resizing: (1341, 1508) -> (523, 588)  scale=0.3900  (7.800 -> 20.000 um/px)
[0.76GB]   IHC at reg resolution: (3, 523, 588)  20.000 um/px  (0.9 MB)
[0.76GB] Preparing IHC registration image ...
[0.76GB]   IHC grayscale: (523, 588)  mean=0.053  (invert=False)
[0.76GB] Running registration ...
[0.76GB]   Coarse: 24 rotations (0-360 step 15) ...
[0.77GB]       0.0  score=0.5745
[0.78GB]      15.0  score=0.4680
[0.80GB]      30.0  score=0.4026
[0.81GB]      45.0  score=0.3938
[0.82GB]      60.0  score=0.3977
[0.83GB]      75.0  score=0.3542
[0.83GB]      90.0  score=0.3256
[0.83GB]     105.0  score=0.3269
[0.84GB]     120.0  score=0.3869
[0.85GB]     135.0  score=0.3855
[0.86GB]     150.0  score=0.3698
[0.86GB]     165.0  score=0.3843
[0.86GB]     180.0  score=0.3616
[0.87GB]     195.0  score=0.3575
[0.87GB]     210.0  score=0.3397
[0.88GB]     225.0  score=0.3499
[0.88GB]     240.0  score=0.3742
[0.88GB]     255.0  score=0.3260
[0.88GB]     270.0  score=0.2170
[0.88GB]     285.0  score=0.2460
[0.88GB]     300.0  score=0.2588
[0.88GB]     315.0  score=0.2950
[0.89GB]     330.0  score=0.3452
[0.89GB]     345.0  score=0.4896
[0.89GB]   Best coarse: 0.0  score=0.5745
[0.89GB]   Fine: 10 rotations (+-5.0 step 1.0) ...
[0.89GB]      -5.0  score=0.6059
[0.89GB]      -4.0  score=0.6262
[0.89GB]      -3.0  score=0.6384
[0.89GB]      -2.0  score=0.6277
[0.89GB]      -1.0  score=0.6122
[0.89GB]       1.0  score=0.5813
[0.89GB]       2.0  score=0.5682
[0.89GB]       3.0  score=0.5584
[0.89GB]       4.0  score=0.5481
[0.89GB]       5.0  score=0.5374
[0.89GB]   Final: -3.0  score=0.6384  offset=(123, 116)
[0.89GB] Building output canvas ...
[0.89GB]   IHC canvas (cv2): 764x703  channels=3  pr=75, pc=75  rotation=-3.0
[0.89GB] Building SpatialData ...
[0.99GB]   H&E upscaled 10x: 7640x7030  (161 MB)
[1.42GB]   IHC upscaled 10x: (3, 7030, 7640)  (161 MB)
[1.41GB]   IHC image stored: channels=['CF_405', 'CF_488', 'CF_561']
[1.43GB]   Transform stored: mode=ihc  rotation=-3.0  ref_reg_size=[523, 588]  canvas_placement=[75, 75]
[1.43GB] Done.
[10]:
sdata
[10]:
SpatialData object
├── Images
│     └── 'ihc_image': DataArray[cyx] (3, 7030, 7640)
├── Points
│     └── 'centroids': DataFrame with shape: (<Delayed>, 3) (2D points)
├── Shapes
│     └── 'pixels': GeoDataFrame shape: (267648, 2) (2D shapes)
└── Tables
      └── 'maldi_adata': AnnData (267648, 174)
with coordinate systems:
    ▸ 'global', with elements:
        ihc_image (Images), centroids (Points), pixels (Shapes)
[11]:
sdata.pl.render_images("ihc_image") \
     .pl.render_shapes("pixels", color="MPI", fill_alpha = 0.8) \
     .pl.show()
INFO     Rasterizing image for faster rendering.
INFO     Using 'datashader' backend with 'None' as reduction method to speed up plotting. Depending on the
         reduction method, the value range of the plot might change. Set method to 'matplotlib' to disable this
         behaviour.
INFO     Using the datashader reduction "mean". "max" will give an output very close to the matplotlib result.
../../_images/guides_Tutorials_ihc_alignment_21_1.png

Spatially Informed Clustering (GraphPCA)

goatpy has also integrated the use of GraphPCA spatial clustering. Spatial infomation can often improve clustering compared to non-spatial methods (i.e. leiden).

Below we will run spatial_clustering and visualise the results overlayed with the IHC image:

[12]:
sdata = gp.graphpca_spatialdata(sdata, tables= "maldi_adata",
    library_id= 'spatial',
    n_components = 50,
    n_neighbors = 15,
    alpha = 0.5)

sdata = gp.get_kmean_clusters(sdata, tables= "maldi_adata",n_clusters = 12)
[13]:
fig, axes = plt.subplots(1, 2, figsize=(10, 5))

sdata.pl.render_images("ihc_image") \
     .pl.show(ax=axes[0], title="IHC Image")

sdata.pl.render_images("ihc_image") \
     .pl.render_shapes("pixels",size = 3,fill_alpha = 0.5,color="GPCA_clusters") \
     .pl.show(ax=axes[1], title="Spatial Clustering")

plt.tight_layout()
plt.show()

INFO     Rasterizing image for faster rendering.
INFO     Rasterizing image for faster rendering.
INFO     Using 'datashader' backend with 'None' as reduction method to speed up plotting. Depending on the
         reduction method, the value range of the plot might change. Set method to 'matplotlib' to disable this
         behaviour.
../../_images/guides_Tutorials_ihc_alignment_24_1.png

Align Additional Image Layers

This process can be repated to align additional image layers such as both IHC and H&E. Once aligning the first image to the MALDI-MSI (with either the manual or automatic alignment method), we can use the manual landmark based alignment to align any additional images.

Below we will align the H&E image to the aligned IHC data object:

[14]:
he_sd = gp.he_spatialdata( "./Documents/IHC_test_data/HnE_Image_64.vsi - 40x_BF_01.jpg")

viewer, widget = gp.launch_landmark_gui(sdata, he_sd, split_view=False, maldi_image_key = "ihc_image", he_image_key = "he_image")
./goatpy/goatpy/he_image.py:30: UserWarning: sopa.io.wsi failed! Try running `pip install openslide`. Falling back to PIL method.
  warnings.warn(f"sopa.io.wsi failed! Try running `pip install {backend}`. Falling back to PIL method. ")
Available MALDI scale levels:
  scale0: (3, 7030, 7640)
Available H&E scale levels:
  scale0: (3, 8076, 8899)

No maldi_scale_level specified — defaulting to 'scale0'.

Launching GUI with MALDI=scale0, H&E=scale0
[LandmarkGUI] MALDI display level=scale0, scale_factor=1.000  |  H&E display level=scale0, scale_factor=1.000
INFO: Saved 4 landmark pairs (scaled to full resolution)
[15]:
maldi_align_points = pd.DataFrame({
    "x": [2022.968467,3141.007734,5635.143076,3378.290817],
    "y": [2015.704049,2654.583630,3650.106883,5073.061192]
})

he_align_points = pd.DataFrame({
    "x": [1424.905810,3255.734239,7221.517791,3632.379621],
    "y": [1343.220173,2316.813290,4056.500643,6329.981808]
})

sdata["maldi_landmarks"] = PointsModel.parse(maldi_align_points)
he_sd["he_landmarks"] = PointsModel.parse(he_align_points)
[16]:
aligned = gp.align_image_using_landmarks(sdata, he_sd, maldi_image_key = "ihc_image", he_image_key = "he_image", he_landmark_key = "he_landmarks")
[17]:
aligned
[17]:
SpatialData object
├── Images
│     ├── 'he_image': DataArray[cyx] (3, 8076, 8899)
│     └── 'ihc_image': DataArray[cyx] (3, 7030, 7640)
├── Points
│     ├── 'centroids': DataFrame with shape: (<Delayed>, 3) (2D points)
│     ├── 'he_landmarks': DataFrame with shape: (<Delayed>, 2) (2D points)
│     └── 'maldi_landmarks': DataFrame with shape: (<Delayed>, 2) (2D points)
├── Shapes
│     └── 'pixels': GeoDataFrame shape: (267648, 2) (2D shapes)
└── Tables
      └── 'maldi_adata': AnnData (267648, 174)
with coordinate systems:
    ▸ 'aligned', with elements:
        he_image (Images), ihc_image (Images), centroids (Points), he_landmarks (Points), maldi_landmarks (Points), pixels (Shapes)
    ▸ 'global', with elements:
        he_image (Images), ihc_image (Images), centroids (Points), he_landmarks (Points), maldi_landmarks (Points), pixels (Shapes)
[18]:
fig, axes = plt.subplots(1, 2, figsize=(10, 5))

aligned.pl.render_images("ihc_image") \
     .pl.render_shapes("pixels",size = 3,fill_alpha = 0.3,color="GPCA_clusters") \
     .pl.show("aligned", ax=axes[0], title="IHC + Spatial Clustering")

aligned.pl.render_images("he_image") \
     .pl.render_shapes("pixels",size = 3,fill_alpha = 0.3,color="GPCA_clusters") \
     .pl.show("aligned", ax=axes[1], title="H&E + Spatial Clustering")

plt.tight_layout()
plt.show()
INFO     Rasterizing image for faster rendering.
INFO     Using 'datashader' backend with 'None' as reduction method to speed up plotting. Depending on the
         reduction method, the value range of the plot might change. Set method to 'matplotlib' to disable this
         behaviour.
INFO     Rasterizing image for faster rendering.
INFO     Using 'datashader' backend with 'None' as reduction method to speed up plotting. Depending on the
         reduction method, the value range of the plot might change. Set method to 'matplotlib' to disable this
         behaviour.
../../_images/guides_Tutorials_ihc_alignment_30_1.png

This object can now be used to run downstream analysis using goatpy or any other package:

Tutorial Session Info

[19]:
from session_info import show
show()
[19]:
Click to view session information
-----
anndata             0.11.4
debugpy             1.8.20
geopandas           1.1.2
goatpy              0.1.0
ipykernel           6.31.0
matplotlib          3.10.8
napari              0.6.6
napari_spatialdata  0.6.0
pandas              2.3.3
scanpy              1.11.5
session_info        v1.0.1
spatialdata         0.5.0
spatialdata_plot    0.2.12
squidpy             1.6.5
-----
Click to view modules imported as dependencies
81d243bd2c585b0f4821__mypyc NA
PIL                         12.1.1
PyQt5                       NA
ac51d50a4f4b6d748b8c__mypyc NA
annotated_types             0.7.0
app_model                   0.4.0
appdirs                     1.4.4
appnope                     0.1.4
asciitree                   NA
asttokens                   NA
attr                        25.4.0
attrs                       25.4.0
babel                       2.18.0
backports                   NA
bermuda                     NA
cachey                      0.2.1
certifi                     2026.02.25
charset_normalizer          3.4.5
click                       8.3.1
cloudpickle                 3.1.2
colorama                    0.4.6
comm                        0.2.3
cv2                         4.13.0
cycler                      0.12.1
cython_runtime              NA
dask                        2024.11.2
dask_image                  NA
datashader                  0.18.2
dateutil                    2.9.0.post0
decorator                   5.2.1
distributed                 2024.11.2
docrep                      0.3.2
docstring_parser            NA
exceptiongroup              1.3.1
executing                   2.2.1
flexcache                   0.3
flexparser                  0.4
fontTools                   4.61.1
fsspec                      2026.2.0
h5py                        3.16.0
heapdict                    NA
hsluv                       5.0.4
idna                        3.11
igraph                      1.0.0
imagecodecs                 2025.3.30
imageio                     2.37.2
importlib_metadata          NA
in_n_out                    0.2.1
jaraco                      NA
jedi                        0.19.2
jinja2                      3.1.6
joblib                      1.5.3
jsonschema                  4.26.0
jsonschema_specifications   NA
kiwisolver                  1.4.9
lazy_loader                 0.5
legacy_api_wrap             NA
leidenalg                   0.11.0
llvmlite                    0.46.0
locket                      NA
loguru                      0.7.3
lxml                        6.1.1
magicgui                    0.10.1
markupsafe                  3.0.3
matplotlib_inline           0.2.1
matplotlib_scalebar         0.9.0
metaspace                   2.0.9
metaspace_converter         NA
more_itertools              10.8.0
mpl_toolkits                NA
msgpack                     1.1.2
multipledispatch            0.6.0
multiscale_spatial_image    2.0.3
napari_builtins             0.6.6
napari_plugin_engine        0.2.1
natsort                     8.4.0
networkx                    3.4.2
npe2                        0.8.1
numba                       0.64.0
numcodecs                   0.13.1
numpy                       2.2.6
numpydoc                    1.10.0
ome_zarr                    NA
openslide                   1.4.3
openslide_bin               4.0.0.12
packaging                   26.0
parso                       0.8.6
patsy                       1.0.2
pexpect                     4.9.0
pims                        0.7
pint                        0.24.4
pkg_resources               NA
platformdirs                4.9.4
plotly                      6.6.0
prompt_toolkit              3.0.52
psutil                      7.2.2
psygnal                     0.15.1
ptyprocess                  0.7.0
pure_eval                   0.2.3
pyarrow                     21.0.0
pyct                        0.6.0
pydantic                    2.12.5
pydantic_compat             0.1.2
pydantic_core               2.41.5
pydantic_extra_types        2.11.0
pydev_ipython               NA
pydevconsole                NA
pydevd                      3.2.3
pydevd_file_utils           NA
pydevd_plugins              NA
pydevd_tracing              NA
pygments                    2.19.2
pyimzml                     1.5.5
pyparsing                   3.3.2
pyproj                      3.7.1
pyqtgraph                   0.14.0
pytz                        2026.1.post1
qtpy                        2.4.3
referencing                 NA
requests                    2.32.5
rich                        NA
rpds                        NA
scipy                       1.15.3
seaborn                     0.13.2
shapely                     2.1.2
sip                         NA
six                         1.17.0
skimage                     0.25.2
sklearn                     1.7.2
slicerator                  1.1.0
sopa                        2.1.11
sortedcontainers            2.4.0
spatial_image               1.2.3
sphinxcontrib               NA
stack_data                  0.6.3
statsmodels                 0.14.6
superqt                     0.8.0
tblib                       3.2.2
texttable                   1.7.0
threadpoolctl               3.6.0
tifffile                    2025.5.10
tlz                         1.1.0
toolz                       1.1.0
torch                       2.10.0
torchgen                    NA
tornado                     6.5.4
tqdm                        4.67.3
traitlets                   5.14.3
triangle                    20250106
typing_extensions           NA
typing_inspection           NA
urllib3                     2.6.3
validators                  0.35.0
vispy                       0.15.2
vscode                      NA
wcwidth                     0.6.0
wrapt                       2.1.2
xarray                      2025.6.1
xarray_dataclass            3.0.0
xarray_schema               0.0.3
xrspatial                   0.5.3
yaml                        6.0.3
zarr                        2.18.3
zict                        3.0.0
zipp                        NA
zmq                         27.1.0
zoneinfo                    NA
-----
IPython             8.38.0
jupyter_client      8.8.0
jupyter_core        5.9.1
-----
Python 3.10.19 | packaged by conda-forge | (main, Oct 22 2025, 22:46:49) [Clang 19.1.7 ]
macOS-26.3.1-arm64-arm-64bit
-----
Session information updated at 2026-05-30 19:32