DVP-IO x SOPA/Cellpose

DVP-IO x SOPA/Cellpose#

In this tutorial, we will run the cell segmentation pipeline implemented in sopa [BMG+24] to perform cell segmentation of a multi-channel fluorescence image in the .czi format with cellpose. We will use dvp-io to read the image from its native format into the sopa-supported spatialdata format. Subsequently, we will run the sopa cell segmentation pipeline. Finally we will use dvp-io to export the shapes as LMD-compatible .xml file.

Load modules#

This tutorial requires you to have sopa with cellpose support installed in your local environment.

conda create -n dvpio python=3.11 -y && conda activate dvpio
pip install dvp-io
pip install "sopa[cellpose]"
import sopa
from dvpio.read.image import read_czi
from dvpio.write.shapes import write_lmd
import spatialdata_plot  # noqa
import spatialdata as sd
import numpy as np
import matplotlib.pyplot as plt

Load data#

We will use a sample .czi image (3 channels) kindly provided by Sophia Mädler from the scPortrait package [SMW+23] and convert it to a spatialdata object using dvpio.read.image.read_czi. Note that the converted spatialdata object must be written to disk for the rest of the notebook to function. This is because some features of read_czi rely on C modules that cannot be parallelized by Dask, which sopa’s parallelization backend depends on.

## Uncomment to obtain data
# ! wget -O "./data/example.czi" "https://datashare.biochem.mpg.de/s/4jH87DiwvxoM4Qd/download"
sdata = sd.SpatialData()

# Get all 3 channels from czi image
sdata.images["czi"] = read_czi("./data/example.czi", channels=[0, 1, 2])

# Write sdata to disk
sdata.write("./data/scportrait-czi.sdata.zarr")
INFO     The Zarr backing store has been changed from None the new file path: data/scportrait-czi.sdata.zarr
# Re-read sdata object from disk
sdata = sd.read_zarr("./data/scportrait-czi.sdata.zarr")

We can plot the 3 channels. Channel 0 corresponds to the nuclei channel and 1 to the cytosol channel

fig, axs = plt.subplots(1, 4, figsize=(16, 4), sharex=True, sharey=True)

for idx in range(3):
    sdata.pl.render_images("czi", channel=idx).pl.show(ax=axs[idx])


sdata.pl.render_images("czi").pl.show(ax=axs[3])
plt.tight_layout()
INFO     Rasterizing image for faster rendering.                                                                   
INFO     Rasterizing image for faster rendering.                                                                   
INFO     Rasterizing image for faster rendering.                                                                   
INFO     Rasterizing image for faster rendering.
../_images/d03af3d720673845ea26fec8a29bd15e5bba71bb59cb405e4fdb4960ba7bc027.png

Run sopa#

We can now run sopa. Sopa first creates smaller image patches that can be passed to cellpose, using the sopa.make_image_patches method. Subsquently, we can run the cellpose wrapper sopa.segmentation.cellpose. Note that you can also pass your own pretrained models to via the pretrained_model="/path/to/pretrained/model" argument. Here, we are using the default cyto3 model.

sopa.make_image_patches(sdata, patch_width=500, patch_overlap=50, image_key="czi")
# Set dask as parallelization backend
sopa.settings.parallelization_backend = "dask"

sopa.segmentation.cellpose(sdata, channels=[1, 0], diameter=50, model_type="cyto3", image_key="czi")

# Runs in ca. 30s on MacOS/M3
[                                        ] | 0% Completed |  0.4s
[########################################] | 100% Completed | 23.3s

We can inspect the cell segmentation in a smaller patch and we can see that the model performed well on this dataset.

(
    sdata.query.bounding_box(
        axes=("x", "y"),
        min_coordinate=np.array([0, 0]),
        max_coordinate=np.array([600, 600]),
        target_coordinate_system="global",
    )
    .pl.render_images("czi")
    .pl.render_shapes("cellpose_boundaries", fill_alpha=0, outline_alpha=1)
    .pl.show()
)
../_images/cf30aab922f211eef838e736b17f036c0134ec2efc18384d21708142fef9f558.png

Write shapes#

In many cases, we are now interested in exporting the obtained shapes to a Leica Microdissection Microscope. We can use the dvpio.write.write_lmd function to export shapes in a compatible format. The function wraps the py-LMD package [SMW+23].

The function also relies on adding calibration points

sdata
SpatialData object, with associated Zarr store: /Users/lucas-diedrich/Documents/Projects/scverse/spatialdata/spatialdata-io/dvp-io/dvp-io/docs/tutorials/data/scportrait-czi.sdata.zarr
├── Images
│     └── 'czi': DataArray[cyx] (3, 3038, 3040)
└── Shapes
      ├── 'cellpose_boundaries': GeoDataFrame shape: (579, 1) (2D shapes)
      └── 'image_patches': GeoDataFrame shape: (49, 3) (2D shapes)
with coordinate systems:
    ▸ 'global', with elements:
        czi (Images), cellpose_boundaries (Shapes), image_patches (Shapes)
cell_shapes = sdata.shapes["cellpose_boundaries"]

We will also need to select calibration points in the image.

## Uncomment to select calibration points interactively
# interactive = Interactive(sdata)
# interactive.run()

Interactive point selection in napari

## Uncomment to select interactively selected calibration points
# calibration_points = sdata.points["calibration_points_image"]


# Use these points for reproducibility
calibration_points = sd.models.PointsModel.parse(
    np.array([[251.03580019, 151.55537058], [1999.99218284, 36.1721564], [109.75405585, 2953.58610685]])
)

Lastly, we need to define an affine transformation that is used to translate the image coordinates into LMD coordinates.

affine_transformation = np.array([[0, -10, 0], [10, 0, 0], [0, 0, 1]])

We can use this information to write an LMD-compatible xml file that contains the segmented cells as shapes

write_lmd("./data/lmd.xml", cell_shapes, calibration_points, affine_transformation)
[0. 0.]
[     0. -10000.]
[10000.     0.]