Nuclei segmentation using Cellpose

In this tutorial we show how we can use the anatomical segmentation algorithm Cellpose in squidpy.im.segment for nuclei segmentation.

Cellpose Stringer, Carsen, et al. (2021), (code) is a novel anatomical segmentation algorithm. To use it in this example, we need to install it first via: pip install cellpose. To run the notebook locally, create a conda environment as conda env create -f cellpose_environment.yml using this cellpose_environment.yml, which installs Squidpy and Cellpose.

import numpy as np

import matplotlib.pyplot as plt

import squidpy as sq

Prepare custom segmentation function using Cellpose

Import the Cellpose segmentation model. See https://cellpose.readthedocs.io/en/latest/api.html#cellpose-class.

from cellpose import models

The method parameter of the sq.im.segment method accepts any callable with the signature: numpy.ndarray (height, width, channels) -> numpy.ndarray (height, width[, channels]). Additional model specific arguments will also be passed on. To use the Cellpose model, we define a wrapper that initializes the model, evaluates it and returns the segmentation masks. We can make use of Cellpose specific options by passing on arguments like the minimum number of pixels per mask min_size.

def cellpose(img, min_size=15):
    model = models.Cellpose(model_type="nuclei")
    res, _, _, _ = model.eval(
        img,
        channels=[0, 0],
        diameter=None,
        min_size=min_size,
    )
    return res

Cell segmentation on Visium fluorescence data

Load the image and visualize its channels.

img = sq.datasets.visium_fluo_image_crop()
crop = img.crop_corner(1000, 1000, size=1000)
crop.show(channelwise=True)
../../_images/5734c2de6c64240712f53f4b51df64933d77502fd7a058266fe6801ee2b4ed62.png

Segment the DAPI channel using the cellpose function defined above.

sq.im.segment(img=crop, layer="image", channel=0, method=cellpose)

Plot the DAPI channel of the image crop and the segmentation result.

print(crop)
print(f"Number of segments in crop: {len(np.unique(crop['segmented_custom']))}")

fig, axes = plt.subplots(1, 2, figsize=(10, 20))
crop.show("image", channel=0, ax=axes[0])
_ = axes[0].set_title("DAPI")
crop.show("segmented_custom", cmap="jet", interpolation="none", ax=axes[1])
_ = axes[1].set_title("Cellpose segmentation")
ImageContainer[shape=(1000, 1000), layers=['image', 'segmented_custom']]
Number of segments in crop: 334
../../_images/bb131ddafe36e364a04d0b6412b1f8709504c340d34623a355b450b3038ee5e3.png

The sq.im.segment method will pass any additional arguments to the cellpose function, so we can also filter out segments with less than 200 pixels and compare the results to the segmentation result from above that works with the default of 15 pixels.

sq.im.segment(img=crop, layer="image", channel=0, method=cellpose, min_size=200)

print(crop)
print(f"Number of segments in crop: {len(np.unique(crop['segmented_custom']))}")

fig, axes = plt.subplots(1, 2, figsize=(10, 20))
crop.show("image", channel=0, ax=axes[0])
_ = axes[0].set_title("DAPI")
crop.show("segmented_custom", cmap="jet", interpolation="none", ax=axes[1])
_ = axes[1].set_title("Cellpose segmentation")
ImageContainer[shape=(1000, 1000), layers=['image', 'segmented_custom']]
Number of segments in crop: 188
../../_images/183c56cb6566672ad4b7ff8e916730987200464b23bd1988ecdc578480734f3e.png

Cell segmentation on H&E stained tissue data

For the fluorescence data, we did nuclei segmentation on the DAPI channel simply by just passing on that channel to the Cellpose model. For the H&E images, we will pass on all three channels by calling sq.im.segment() with channel=None and then decide in the Cellpose model on which channel we want to segment using the first element of the channels parameter. Here, we need to be cautious as the numbering is different (0=grayscale, 1=red, 2=green, 3=blue)!

Further, for H&E images we need to invert the color values by setting invert=True. Additionally, we found that the naive usage will recognize only few of the cells and extended the arguments we allow for Cellpose by flow_threshold.

def cellpose_he(img, min_size=15, flow_threshold=0.4, channel_cellpose=0):
    model = models.Cellpose(model_type="nuclei")
    res, _, _, _ = model.eval(
        img,
        channels=[channel_cellpose, 0],
        diameter=None,
        min_size=min_size,
        invert=True,
        flow_threshold=flow_threshold,
    )
    return res
img = sq.datasets.visium_hne_image_crop()
crop = img.crop_corner(0, 0, size=1000)
crop.show("image", channelwise=True)
../../_images/361c8e8db83cb4b5f21215c436b29ce84fcaa51596424651c5aef0b321285fc0.png

For the H&E image we start by testing the segmentation on only the blue channel (image:0).

sq.im.segment(
    img=crop, layer="image", channel=None, method=cellpose_he, channel_cellpose=1
)

print(crop)
print(f"Number of segments in crop: {len(np.unique(crop['segmented_custom']))}")

fig, axes = plt.subplots(1, 2, figsize=(10, 20))
crop.show("image", channel=None, ax=axes[0])
_ = axes[0].set_title("H&E")
crop.show("segmented_custom", cmap="jet", interpolation="none", ax=axes[1])
_ = axes[1].set_title("Cellpose segmentation")
ImageContainer[shape=(1000, 1000), layers=['image', 'segmented_custom']]
Number of segments in crop: 647
../../_images/1389164e93f3c516518479f27c0fe2e7e9a6763a196124188bae67b8e036cf8b.png

We clearly see that we should increase sensitivity and test flow_threshold=0.8 resulting in significantly more detected nuclei.

sq.im.segment(
    img=crop,
    layer="image",
    channel=None,
    method=cellpose_he,
    flow_threshold=0.8,
    channel_cellpose=1,
)

print(crop)
print(f"Number of segments in crop: {len(np.unique(crop['segmented_custom']))}")

fig, axes = plt.subplots(1, 2, figsize=(10, 20))
crop.show("image", channel=None, ax=axes[0])
_ = axes[0].set_title("H&E")
crop.show("segmented_custom", cmap="jet", interpolation="none", ax=axes[1])
_ = axes[1].set_title("Cellpose segmentation")
ImageContainer[shape=(1000, 1000), layers=['image', 'segmented_custom']]
Number of segments in crop: 902
../../_images/5bc25a12a6f3b9e8d8fc9e3834add3a8177c5425d4bec9a3dcdf656fd6f3c13f.png

We can also check whether using all channels improves nuclei detection by segmenting on the grayscale image (channel_cellpose=0).

sq.im.segment(
    img=crop, layer="image", channel=None, method=cellpose_he, channel_cellpose=0
)

print(crop)
print(f"Number of segments in crop: {len(np.unique(crop['segmented_custom']))}")

fig, axes = plt.subplots(1, 2, figsize=(10, 20))
crop.show("image", channel=None, ax=axes[0])
_ = axes[0].set_title("H&E")
crop.show("segmented_custom", cmap="jet", interpolation="none", ax=axes[1])
_ = axes[1].set_title("Cellpose segmentation")
ImageContainer[shape=(1000, 1000), layers=['image', 'segmented_custom']]
Number of segments in crop: 449
../../_images/bf871633ec2cce9b75e4a3293cabc8a9eef74365ec5c3b14e1217dd86bbdf265.png

And again with increased flow_threshold=0.8.

sq.im.segment(
    img=crop,
    layer="image",
    channel=None,
    method=cellpose_he,
    flow_threshold=0.8,
    channel_cellpose=0,
)

print(crop)
print(f"Number of segments in crop: {len(np.unique(crop['segmented_custom']))}")

fig, axes = plt.subplots(1, 2, figsize=(10, 20))
crop.show("image", channel=None, ax=axes[0])
_ = axes[0].set_title("H&E")
crop.show("segmented_custom", cmap="jet", interpolation="none", ax=axes[1])
_ = axes[1].set_title("Cellpose segmentation")
ImageContainer[shape=(1000, 1000), layers=['image', 'segmented_custom']]
Number of segments in crop: 689
../../_images/67aaa2f28deada4f3f4c3bd56320f8b9f5b2cb16d3829f0195c90be635c3774b.png