%matplotlib inline
Use z-stacks with ImageContainer
In this example we showcase how to use z-stacks with squidpy.im.ImageContainer
It is possible to acquire several consecutive image slices from the same tissue.
Squidpy’s ImageContainer supports storing, processing, and visualization of these z-stacks.
Here, we use the Visium 10x mouse brain sagittal slices as an example of a z-stack image with two Z dimensions.
We will use the “hires” images contained in the anndata.AnnData object, but you could also use the
original resolution tiff images in the ImageContainer.
Import libraries and load individual image sections
import anndata as ad
import scanpy as sc
import squidpy as sq
library_ids = [
"V1_Mouse_Brain_Sagittal_Posterior",
"V1_Mouse_Brain_Sagittal_Posterior_Section_2",
]
adatas, imgs = [], []
use_hires_tiff = False
for library_id in library_ids:
adatas.append(sc.datasets.visium_sge(library_id, include_hires_tiff=use_hires_tiff))
adatas[-1].var_names_make_unique()
if use_hires_tiff:
imgs.append(
sq.im.ImageContainer(
adatas[-1].uns["spatial"][library_id]["metadata"]["source_image_path"]
)
)
else:
# as we are using a scaled image, we need to specify a scalefactor
# to allow correct mapping to adata.obsm['spatial']
imgs.append(
sq.im.ImageContainer(
adatas[-1].uns["spatial"][library_id]["images"]["hires"],
scale=adatas[-1].uns["spatial"][library_id]["scalefactors"][
"tissue_hires_scalef"
],
)
)
100%|██████████| 9.26M/9.26M [00:01<00:00, 4.96MB/s]
100%|██████████| 20.1M/20.1M [00:05<00:00, 3.57MB/s]
100%|██████████| 9.26M/9.26M [00:01<00:00, 5.00MB/s]
100%|██████████| 19.0M/19.0M [00:04<00:00, 4.66MB/s]
Concatenate per-section data to a z-stack
To allow mapping from observations in adata to the correct Z dimension in img,
we will store a library_id column in adata.obs and associate each library_id
to a Z dimension in the ImageContainer.
For this, we will use anndata.concat() with uns_merge = only
(to ensure that uns entries are correctly concatenated),
label = 'library_id' and keys = library_ids (to create the necessary column in adata.obs.
To concatenate the individual squidpy.im.ImageContainer,
we will use squidpy.im.ImageContainer.concat(), specifying
library_ids = library_ids for associating each image with the correct observations in adata.
adata = ad.concat(
adatas, uns_merge="only", label="library_id", keys=library_ids, index_unique="-"
)
img = sq.im.ImageContainer.concat(imgs, library_ids=library_ids)
adata now contains a library_id column in adata.obs, which maps observations to a unique library_id.
print(adata)
adata.obs
AnnData object with n_obs × n_vars = 6644 × 32285
obs: 'in_tissue', 'array_row', 'array_col', 'library_id'
uns: 'spatial'
obsm: 'spatial'
| in_tissue | array_row | array_col | library_id | |
|---|---|---|---|---|
| AAACAAGTATCTCCCA-1-V1_Mouse_Brain_Sagittal_Posterior | 1 | 50 | 102 | V1_Mouse_Brain_Sagittal_Posterior |
| AAACACCAATAACTGC-1-V1_Mouse_Brain_Sagittal_Posterior | 1 | 59 | 19 | V1_Mouse_Brain_Sagittal_Posterior |
| AAACAGAGCGACTCCT-1-V1_Mouse_Brain_Sagittal_Posterior | 1 | 14 | 94 | V1_Mouse_Brain_Sagittal_Posterior |
| AAACAGCTTTCAGAAG-1-V1_Mouse_Brain_Sagittal_Posterior | 1 | 43 | 9 | V1_Mouse_Brain_Sagittal_Posterior |
| AAACAGGGTCTATATT-1-V1_Mouse_Brain_Sagittal_Posterior | 1 | 47 | 13 | V1_Mouse_Brain_Sagittal_Posterior |
| ... | ... | ... | ... | ... |
| TTGTTGTGTGTCAAGA-1-V1_Mouse_Brain_Sagittal_Posterior_Section_2 | 1 | 31 | 77 | V1_Mouse_Brain_Sagittal_Posterior_Section_2 |
| TTGTTTCACATCCAGG-1-V1_Mouse_Brain_Sagittal_Posterior_Section_2 | 1 | 58 | 42 | V1_Mouse_Brain_Sagittal_Posterior_Section_2 |
| TTGTTTCATTAGTCTA-1-V1_Mouse_Brain_Sagittal_Posterior_Section_2 | 1 | 60 | 30 | V1_Mouse_Brain_Sagittal_Posterior_Section_2 |
| TTGTTTCCATACAACT-1-V1_Mouse_Brain_Sagittal_Posterior_Section_2 | 1 | 45 | 27 | V1_Mouse_Brain_Sagittal_Posterior_Section_2 |
| TTGTTTGTATTACACG-1-V1_Mouse_Brain_Sagittal_Posterior_Section_2 | 1 | 73 | 41 | V1_Mouse_Brain_Sagittal_Posterior_Section_2 |
6644 rows × 4 columns
img contains the 2D images concatenated along the Z dimension in one image layer.
The Z dimensions are named the same as the library_id’s in adata to allow a mapping from adata to img.
print(img["image"].z)
img
<xarray.DataArray 'z' (z: 2)>
array(['V1_Mouse_Brain_Sagittal_Posterior',
'V1_Mouse_Brain_Sagittal_Posterior_Section_2'], dtype='<U43')
Coordinates:
* z (z) <U43 'V1_Mouse_Brain_Sagittal_Posterior' 'V1_Mouse_Brain_Sag...
image: y (1998), x (2000), z (2), channels (3)
It is also possible to initialize the ImageContainer with images that already contain the Z dimension.
In this case you need to specify the library_id argument in the constructor.
In addition, you might want to set dims to the correct ordering of dimensions manually for more control.
arr = img["image"].values
print(arr.shape)
img2 = sq.im.ImageContainer(
arr, library_id=library_ids, dims=("y", "x", "z", "channels")
)
img2
(1998, 2000, 2, 3)
image: y (1998), x (2000), z (2), channels (3)
Generally, an ImageContainer with more than one Z dimension can be used in the same way as an ImageContainer
with only one Z dimension.
In addition, we can specify library_id to cropping, pre-processing,
and segmentation functions if we’d like to only process a specific library_id.
Visualization
For using squidpy.pl.spatial_scatter(), subset the adata to the desired library_id.
library_id = library_ids[0]
sq.pl.spatial_scatter(
adata[adata.obs["library_id"] == library_id],
library_id=library_id,
color="in_tissue",
)
squidpy.im.ImageContainer.show() works with z-stacks out of the box, by plotting them as separate images.
Additionally, you can specify a library_id if you only want to plot one Z dimension.
img.show()
Interactive visualization of z-stacks is also possible.
The Napari viewer will have a slider at the bottom, allowing you to choose the Z dimension to display.
The adata observations are automatically updated to the current Z dimension.
When calling img.interactive just specify library_key as the column name in adata.obs
which maps from observations to library_ids
.. code-block:: python
img.interactive(adata, library_key=’library_id’)
Cropping
By default, the cropping functions will crop all Z dimensions.
crop = img.crop_corner(500, 1000, size=500)
crop.show()
You can also specify library_id, as either a single or multiple Z dimensions to crop.
img.crop_corner(500, 1000, size=500, library_id=library_ids[0]).show()
Processing and segmenting
Let us smooth the image.
When not specifying a library_id, squidpy.im.process() treats the image as a 3D volume.
As we would like to smooth only in x and y dimensions, and not in z, we need so specify a per-dimension sigma.
The internal dimensions of the image are y, x, z, channels, as you can check with crop['image'].dims.
Therefore, to only smooth in x and y, we need to specify sigma = [10, 10, 0, 0].
sq.im.process(
img, layer="image", method="smooth", sigma=[10, 10, 0, 0], layer_added="smooth1"
)
img.show("smooth1")
Now, let us just smooth one library_id.
Specifying library_id means that the processing function will process each Z dimension separately.
This means that now the dimensions of the processed image are y, x, channels (with z removed), meaning that
we have to update sigma accordingly.
If the number of channels does not change due to the processing, squidpy.im.process() implies the identity
function for non-processed Z dimensions.
sq.im.process(
img,
layer="image",
method="smooth",
sigma=10,
layer_added="smooth2",
library_id=library_ids[0],
)
img.show("smooth2")
None, only the first library_id is smoothed.
For the second, the original image was used.
If the processing function changes the number of dimensions, non-processed Z dimensions will contain 0.
Let’s see this behavior with using method = 'gray', which moves from 3 channels (RGB) to one channel (gray).
sq.im.process(
img, layer="image", method="gray", layer_added="gray", library_id=library_ids[0]
)
img.show("gray", cmap="gray")
WARNING: Function changed the number of channels, cannot use identity for library ids `['V1_Mouse_Brain_Sagittal_Posterior_Section_2']`. Replacing with 0
squidpy.im.segment() works in the same way, just specify library_id if you only wish to
segment specific Z dimensions.
Feature calculation
Calculating features from z-stack images is straight forward as well.
With more than one Z dimension, we just need to specify the column name in adata.obs
which contains the mapping from observations to library_ids
to allow the function to extract the features from the correct Z dimension.
As of now, features can only be extracted on 2D, meaning from the Z dimension that the current spot is located on.
The following call extracts features for each observation in adata, automatically choosing the correct
Z dimension in img.
adata_crop = crop.subset(adata) # subset adata to the image crop
sq.im.calculate_image_features(
adata_crop,
crop,
library_id="library_id",
layer="image",
features="summary",
n_jobs=4,
)
adata_crop.obsm["img_features"]
100%|██████████| 774/774 [00:14<00:00, 54.34/s]
| summary_ch-0_quantile-0.9 | summary_ch-0_quantile-0.5 | summary_ch-0_quantile-0.1 | summary_ch-0_mean | summary_ch-0_std | summary_ch-1_quantile-0.9 | summary_ch-1_quantile-0.5 | summary_ch-1_quantile-0.1 | summary_ch-1_mean | summary_ch-1_std | summary_ch-2_quantile-0.9 | summary_ch-2_quantile-0.5 | summary_ch-2_quantile-0.1 | summary_ch-2_mean | summary_ch-2_std | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| AAACAGAGCGACTCCT-1-V1_Mouse_Brain_Sagittal_Posterior | 0.721569 | 0.670588 | 0.542745 | 0.647495 | 0.074835 | 0.725490 | 0.611765 | 0.247059 | 0.517943 | 0.209248 | 0.729412 | 0.674510 | 0.549020 | 0.652933 | 0.074534 |
| AAACGAGACGGTTGAT-1-V1_Mouse_Brain_Sagittal_Posterior | 0.450980 | 0.309804 | 0.200000 | 0.317769 | 0.095004 | 0.360784 | 0.270588 | 0.184314 | 0.269996 | 0.069024 | 0.576471 | 0.509804 | 0.462745 | 0.515190 | 0.045777 |
| AAATTACCTATCGATG-1-V1_Mouse_Brain_Sagittal_Posterior | 0.680784 | 0.611765 | 0.487843 | 0.599930 | 0.075161 | 0.517647 | 0.462745 | 0.379608 | 0.455529 | 0.055273 | 0.692549 | 0.650980 | 0.596078 | 0.647303 | 0.041485 |
| AACAGGAAATCGAATA-1-V1_Mouse_Brain_Sagittal_Posterior | 0.658824 | 0.603922 | 0.511373 | 0.594231 | 0.057229 | 0.521569 | 0.466667 | 0.393725 | 0.462379 | 0.048675 | 0.678431 | 0.643137 | 0.584314 | 0.635939 | 0.035439 |
| AACATATCAACTGGTG-1-V1_Mouse_Brain_Sagittal_Posterior | 0.586667 | 0.360784 | 0.211765 | 0.385795 | 0.140289 | 0.447059 | 0.301961 | 0.185882 | 0.308392 | 0.098326 | 0.645490 | 0.533333 | 0.444706 | 0.540601 | 0.071795 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| TTGGGACACTGCCCGC-1-V1_Mouse_Brain_Sagittal_Posterior_Section_2 | 0.635294 | 0.580392 | 0.480000 | 0.564357 | 0.063804 | 0.522353 | 0.466667 | 0.397647 | 0.464401 | 0.049290 | 0.674510 | 0.631373 | 0.580392 | 0.627468 | 0.038752 |
| TTGGGCGGCGGTTGCC-1-V1_Mouse_Brain_Sagittal_Posterior_Section_2 | 0.643137 | 0.592157 | 0.502745 | 0.582867 | 0.055017 | 0.556863 | 0.505882 | 0.435294 | 0.500200 | 0.047835 | 0.666667 | 0.631373 | 0.581961 | 0.626580 | 0.033794 |
| TTGTAAGGCCAGTTGG-1-V1_Mouse_Brain_Sagittal_Posterior_Section_2 | 0.670588 | 0.627451 | 0.537255 | 0.615948 | 0.055211 | 0.602353 | 0.556863 | 0.483922 | 0.548758 | 0.047661 | 0.694118 | 0.662745 | 0.596078 | 0.652636 | 0.036439 |
| TTGTTCAGTGTGCTAC-1-V1_Mouse_Brain_Sagittal_Posterior_Section_2 | 0.647059 | 0.584314 | 0.476078 | 0.571329 | 0.064511 | 0.545098 | 0.494118 | 0.419608 | 0.489499 | 0.048448 | 0.674510 | 0.639216 | 0.592157 | 0.636148 | 0.032888 |
| TTGTTGTGTGTCAAGA-1-V1_Mouse_Brain_Sagittal_Posterior_Section_2 | 0.662745 | 0.623529 | 0.510588 | 0.607930 | 0.060086 | 0.623529 | 0.568627 | 0.480000 | 0.560070 | 0.054016 | 0.682353 | 0.654902 | 0.607843 | 0.648872 | 0.030845 |
774 rows × 15 columns
The calculated features can now be used in downstream Scanpy analyses, by e.g. using all Z dimensions to cluster spots based on image features and gene features.
Here, we cluster genes and calculated features using a standard Scanpy workflow.
sc.pp.normalize_total(adata_crop, inplace=True)
sc.pp.log1p(adata_crop)
sc.pp.pca(adata_crop)
sc.pp.neighbors(adata_crop)
sc.tl.leiden(adata_crop)
sc.pp.neighbors(adata_crop, use_rep="img_features", key_added="neigh_features")
sc.tl.leiden(adata_crop, neighbors_key="neigh_features", key_added="leiden_features")
Visualize the result interactively using Napari, or statically using squidpy.pl.spatial_scatter():
.. code-block:: python
img.interactive(adata, library_key=’library_id’)
sq.pl.spatial_scatter(
adata_crop,
library_key="library_id",
crop_coord=(5700, 2700, 9000, 6000),
library_id=library_ids[0],
color=["leiden", "leiden_features"],
)
sq.pl.spatial_scatter(
adata_crop,
library_key="library_id",
crop_coord=(5700, 3500, 9000, 6000),
library_id=library_ids[1],
color=["leiden", "leiden_features"],
)