Example Notebook showcasing the different segmentation workflows

This notebook walks you through the different segmentation workflows currently implemented in scPortrait using the same input example. Each segmentation workflow needs to be implemented in a seperate scPortrait project as the segmentation mask is the starting point for all further downstream steps.

[1]:
import os
import pandas as pd
import matplotlib.pyplot as plt
import h5py
import numpy as np
import tifffile

from scportrait.pipeline.project import Project
from scportrait.pipeline.extraction import HDF5CellExtraction
[2]:
#define input images
project_directory = "../../../example_data/example_3/segmentation_workflows"

images = ["../../../example_data/example_3/input_images/Ch1.tif",
          "../../../example_data/example_3/input_images/Ch2.tif",
          "../../../example_data/example_3/input_images/Ch3.tif"]
[3]:
# visualize input images as example
# it is not recommended to execute this block with large input images as it will compute for some time

#read input image to visualize as an example
image = np.array([tifffile.imread(img) for img in images])

def colorize(im, color, clip_percentile=0.0):
    """
    Helper function to create an RGB image from a single-channel image using a
    specific color.
    """
    # Check that we do just have a 2D image
    if im.ndim > 2 and im.shape[2] != 1:
        raise ValueError('This function expects a single-channel image!')

    # Rescale the image according to how we want to display it
    im_scaled = im.astype(np.float32) - np.percentile(im, clip_percentile)
    im_scaled = im_scaled / np.percentile(im_scaled, 100 - clip_percentile)
    im_scaled = np.clip(im_scaled, 0, 1)

    # Need to make sure we have a channels dimension for the multiplication to work
    im_scaled = np.atleast_3d(im_scaled)

    # Reshape the color (here, we assume channels last)
    color = np.asarray(color).reshape((1, 1, -1))
    return im_scaled * color

def generate_composite(images, colors = [(0, 0, 1),(0, 1, 0), (1, 0, 0), (1, 0, 1)], plot = False):
    colorized = []
    for image, color in zip(images, colors):
        image = colorize(image, color, 0.0)
        colorized.append(image)

    if plot:
        for i in colorized:
            plt.figure()
            plt.imshow(i)

    image = colorized[0]
    for i in range(len(colorized)-1):
        image += colorized[i+1]

    return(np.clip(image, 0, 1))

fig, axs = plt.subplots(1, 4, figsize = (20, 5));

for i, _img in enumerate(image):
    axs[i].imshow(_img, cmap='gray')
    axs[i].axis("off")
    axs[i].set_title("Channel {}".format(i+1))

#also plot and visualize input image
img = generate_composite(image)

axs[3].imshow(img)
axs[3].axis("off");
axs[3].set_title("Composite image");

fig.tight_layout()
../../_images/pages_notebooks_example_notebook_segmentation_workflows_3_0.png

WGA Segmentation

This segmentation workflow aims to segment mononucleated cells. Based on a nuclear and cytosolic stain, it first uses a thresholding approach to identify nuclei which are assumed to be the center of each cell. Then in a second step, the center of the identified nuclei are used as a starting point to generate a potential map using the cytosolic stain. This potential map is then used to segment the cytosol using a watershed approach. At the end of the workflow the user obtains both a nuclear and a cytosolic segmentation mask where each cytosol is matched to exactly one nucleus as kann be identified by the matching cell id.

The configuration file for the WGASegmentation method contains many parameters that need to be optimized for your particular dataset. Here is an example configuration:

WGASegmentation:
input_channels: 3
chunk_size: 50 # chunk size for chunked HDF5 storage. is needed for correct caching and high performance reading. should be left at 50.
cache: "."
lower_quantile_normalization:   0.001
upper_quantile_normalization:   0.999
median_filter_size:   4 # Size in pixels
nucleus_segmentation:
    lower_quantile_normalization:   0.01 # quantile normalization of dapi channel before local tresholding. Strong normalization (0.05,0.95) can help with nuclear speckles.
    upper_quantile_normalization:   0.99 # quantile normalization of dapi channel before local tresholding. Strong normalization (0.05,0.95) can help with nuclear speckles.
    median_block: 41 # Size of pixel disk used for median, should be uneven
    median_step: 4
    threshold: 0.2 # threshold above local median for nuclear segmentation
    speckle_kernel: 9 # Erosion followed by Dilation to remove speckels, size in pixels, should be uneven
    peak_footprint: 7 #
    min_distance: 8 # minimum distance between two nucleis in pixel
    dilation: 0 # final dilation of pixel mask
    min_size: 200 # minimum nucleus area in pixel
    max_size: 5000 # maximum nucleus area in pixel
    contact_filter: 0.5 # minimum nucleus contact with background
cytosol_segmentation:
    threshold: 0.15 # treshold above which cytosol is detected
    lower_quantile_normalization: 0.01
    upper_quantile_normalization: 0.99
    erosion: 2 # erosion and dilation are used for speckle removal and shrinking / dilation
    dilation: 7 # for no change in size choose erosion = dilation, for larger cells increase the mask erosion
    min_clip: 0
    max_clip: 0.2
    min_size: 200
    max_size: 30000
chunk_size: 50
filter_masks_size: True
[4]:
from scportrait.pipeline.workflows import WGASegmentation

project_location = f"{project_directory}/WGASegmentation"

project = Project(os.path.abspath(project_location),
                  config_path= "../../../example_data/example_3/config_example3.yml",
                  overwrite=True,
                  debug=False,
                  segmentation_f=WGASegmentation,
                  extraction_f=HDF5CellExtraction,
                  )


project.load_input_from_tif_files(images)
Updating project config file.
INFO     The Zarr backing store has been changed from None the new file path:
         /Users/sophia/Documents/GitHub/SPARCSspatial/example_data/example_3/segmentation_workflows/WGASegmentation
         /sparcs.sdata
/Users/sophia/Documents/GitHub/SPARCSspatial/src/scportrait/pipeline/project.py:106: UserWarning: There is already a directory in the location path
  warnings.warn("There is already a directory in the location path")
[6]:
project.segment()
project.extract()
Channels shape:  (2, 399, 399)
multiprocessing done.

Segmentation Results

[8]:
fig, axs = plt.subplots(1, 2, figsize = (10, 5));
axs[0].imshow(project.sdata["seg_all_nucleus"])
axs[0].axis("off")
axs[0].set_title("Nucleus Segmentation Mask")

axs[1].imshow(project.sdata["seg_all_cytosol"])
axs[1].axis("off")
axs[1].set_title("Cytosol Segmentation Mask")
fig.tight_layout()
../../_images/pages_notebooks_example_notebook_segmentation_workflows_8_0.png

Extraction Results

[9]:
with h5py.File(f"{project_location}/extraction/data/single_cells.h5") as hf:
    index = hf.get("single_cell_index")
    _images = hf.get("single_cell_data")

    n_cells = 4
    fig, axs = plt.subplots(n_cells, 5, figsize = (5*2, n_cells*2))
    labels = ["nucleus mask", "cytosol mask", "nucleus", "cytosol", "additional channel"]

    for i in range(n_cells):
        cell_id = index[i][1]
        image = _images[i]

        for n, _img in enumerate(image):
            axs[i, n].imshow(_img)

            if n == 0:
                axs[i, n].set_ylabel(f"cell {cell_id}", fontsize = 10, rotation = 0, labelpad = 25)
                axs[i, n].xaxis.set_visible(False)
                axs[i, n].tick_params(left=False, labelleft=False)
            else:
                axs[i, n].axis("off")

            if i == 0:
                axs[i, n].set_title(labels[n], fontsize = 10)


    fig.tight_layout()
../../_images/pages_notebooks_example_notebook_segmentation_workflows_10_0.png

DAPI Segmentation

This segmentation workflow aims to only segment the nuclei of cells. It uses the same nuclear segmentation method as the WGASegmentation. Here is an example configuration:

DAPISegmentation:
    input_channels: 3
    chunk_size: 50 # chunk size for chunked HDF5 storage. is needed for correct caching and high performance reading. should be left at 50.
    cache: "."
    lower_quantile_normalization:   0.001
    upper_quantile_normalization:   0.999
    median_filter_size:   4 # Size in pixels
    nucleus_segmentation:
        lower_quantile_normalization:   0.01 # quantile normalization of dapi channel before local tresholding. Strong normalization (0.05,0.95) can help with nuclear speckles.
        upper_quantile_normalization:   0.99 # quantile normalization of dapi channel before local tresholding. Strong normalization (0.05,0.95) can help with nuclear speckles.
        median_block: 41 # Size of pixel disk used for median, should be uneven
        median_step: 4
        threshold: 0.2 # threshold above local median for nuclear segmentation
        min_distance: 8 # minimum distance between two nucleis in pixel
        peak_footprint: 7 #
        speckle_kernel: 9 # Erosion followed by Dilation to remove speckels, size in pixels, should be uneven
        dilation: 0 # final dilation of pixel mask
        min_size: 200 # minimum nucleus area in pixel
        max_size: 5000 # maximum nucleus area in pixel
        contact_filter: 0.5 # minimum nucleus contact with background
    chunk_size: 50
    filter_masks_size: False
[4]:
from scportrait.pipeline.workflows import DAPISegmentation

project_location = f"{project_directory}/DAPISegmentation"

project = Project(os.path.abspath(project_location),
                  config_path= "../../../example_data/example_3/config_example3.yml",
                  overwrite=True,
                  debug=False,
                  segmentation_f=DAPISegmentation,
                  extraction_f=HDF5CellExtraction,
                  )

project.load_input_from_tif_files(images)
Updating project config file.
INFO     The Zarr backing store has been changed from None the new file path:
         /Users/sophia/Documents/GitHub/SPARCSspatial/example_data/example_3/segmentation_workflows/DAPISegmentatio
         n/sparcs.sdata
/Users/sophia/Documents/GitHub/SPARCSspatial/src/scportrait/pipeline/project.py:106: UserWarning: There is already a directory in the location path
  warnings.warn("There is already a directory in the location path")
[5]:
project.segment()
project.extract()
(3, 399, 399)
multiprocessing done.

Segmentation Results

[9]:
fig, axs = plt.subplots(1, 1, figsize = (5, 5));
axs.imshow(project.sdata["seg_all_nucleus"])
axs.axis("off")
axs.set_title("Nucleus Segmentation Mask")
fig.tight_layout()
../../_images/pages_notebooks_example_notebook_segmentation_workflows_15_0.png

Extraction Results

[11]:
with h5py.File(f"{project_location}/extraction/data/single_cells.h5") as hf:
    index = hf.get("single_cell_index")
    _images = hf.get("single_cell_data")

    n_cells = 4
    fig, axs = plt.subplots(n_cells, 4, figsize = (5*2, n_cells*2))
    labels = ["nucleus mask", "duplicated nucleus mask", "nucleus", "cytosol", "additional channel"]

    for i in range(n_cells):
        cell_id = index[i][1]
        image = _images[i]

        for n, _img in enumerate(image):
            axs[i, n].imshow(_img)

            if n == 0:
                axs[i, n].set_ylabel(f"cell {cell_id}", fontsize = 10, rotation = 0, labelpad = 25)
                axs[i, n].xaxis.set_visible(False)
                axs[i, n].tick_params(left=False, labelleft=False)
            else:
                axs[i, n].axis("off")

            if i == 0:
                axs[i, n].set_title(labels[n], fontsize = 10)


    fig.tight_layout()
../../_images/pages_notebooks_example_notebook_segmentation_workflows_17_0.png

Cytosol Segmentation Cellpose

This method uses a deep learning based segmentation approach and should optimally be run using a GPU as it is otherwise very slow. During this segmentation workflow two different cellpose models are run. The first is used to segment the nuclei and the second one is used to segment the cytosols. Afterwards the IDs in the nucleus and cytosol mask are matched together.

Here is an example configuration:

CytosolSegmentationCellpose:
    input_channels: 2
    output_masks: 2
    shard_size: 120000000 # maxmimum number of pixel per tile
    overlap_px: 100
    nGPUs: 1
    chunk_size: 50 # chunk size for chunked HDF5 storage. is needed for correct caching and high performance reading. should be left at 50.
    threads: 1 # number of shards / tiles segmented at the same size. should be adapted to the maximum amount allowed by memory.
    cache: "."
    lower_quantile_normalization:   0.001
    upper_quantile_normalization:   0.999
    median_filter_size: 6 # Size in pixels
    nucleus_segmentation:
        model: "nuclei"
    cytosol_segmentation:
        model: "cyto2"
    chunk_size: 50
    match_masks: True
    filtering_threshold_mask_matching: 0.95
    filter_masks_size: False
[4]:
from scportrait.pipeline.workflows import CytosolSegmentationCellpose

project_location = f"{project_directory}/CytosolSegmentationCellpose"

project = Project(os.path.abspath(project_location),
                  config_path= "../../../example_data/example_3/config_example3.yml",
                  overwrite=True,
                  debug=False,
                  segmentation_f=CytosolSegmentationCellpose,
                  extraction_f=HDF5CellExtraction,
                  )

project.load_input_from_tif_files(images)
Updating project config file.
INFO     The Zarr backing store has been changed from None the new file path:
         /Users/sophia/Documents/GitHub/SPARCSspatial/example_data/example_3/segmentation_workflows/CytosolSegmenta
         tionCellpose/sparcs.sdata
/Users/sophia/Documents/GitHub/SPARCSspatial/src/scportrait/pipeline/project.py:106: UserWarning: There is already a directory in the location path
  warnings.warn("There is already a directory in the location path")
[9]:
project.cyto_seg_status
[9]:
True
[5]:
project.segment()
project.extract()
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[5], line 2
      1 project.segment()
----> 2 project.extract()

File ~/Documents/GitHub/SPARCSspatial/src/scportrait/pipeline/project.py:1120, in Project.extract(self, partial, n_cells, overwrite)
   1117 self._check_sdata_status()
   1119 if not self.nuc_seg_status or self.cyto_seg_status:
-> 1120     raise ValueError(
   1121         "No nucleus or cytosol segmentation loaded. Please load a segmentation first."
   1122     )
   1124 # setup overwrite if specified in call
   1125 if overwrite is not None:

ValueError: No nucleus or cytosol segmentation loaded. Please load a segmentation first.

Segmentation Results

[ ]:
with h5py.File(f"{project_location}/segmentation/segmentation.h5") as hf:
    segmentation = hf.get("labels")

    fig, axs = plt.subplots(1, 2, figsize = (10, 5));
    axs[0].imshow(segmentation[0])
    axs[0].axis("off")
    axs[0].set_title("Nucleus Segmentation Mask")

    axs[1].imshow(segmentation[1])
    axs[1].axis("off")
    axs[1].set_title("Cytosol Segmentation Mask")
    fig.tight_layout()
../../_images/pages_notebooks_example_notebook_segmentation_workflows_23_0.png

Extraction Results

[ ]:
with h5py.File(f"{project_location}/extraction/data/single_cells.h5") as hf:
    index = hf.get("single_cell_index")
    _images = hf.get("single_cell_data")

    n_cells = 4
    fig, axs = plt.subplots(n_cells, 5, figsize = (5*2, n_cells*2))
    labels = ["nucleus mask", "cytosol mask", "nucleus", "cytosol", "additional channel"]

    for i in range(n_cells):
        cell_id = index[i][1]
        image = _images[i]

        for n, _img in enumerate(image):
            axs[i, n].imshow(_img)

            if n == 0:
                axs[i, n].set_ylabel(f"cell {cell_id}", fontsize = 10, rotation = 0, labelpad = 25)
                axs[i, n].xaxis.set_visible(False)
                axs[i, n].tick_params(left=False, labelleft=False)
            else:
                axs[i, n].axis("off")

            if i == 0:
                axs[i, n].set_title(labels[n], fontsize = 10)


    fig.tight_layout()
../../_images/pages_notebooks_example_notebook_segmentation_workflows_25_0.png

DAPI Segmentation Cellpose

This method uses a deep learning based segmentation approach and should optimally be run using a GPU as it is otherwise very slow. It only performs a nuclear segmentation using one cellpose model.

Here is an example configuration:

DAPISegmentationCellpose:
    input_channels: 2
    output_masks: 2
    shard_size: 120000000 # maxmimum number of pixel per tile
    overlap_px: 100
    nGPUs: 1
    chunk_size: 50 # chunk size for chunked HDF5 storage. is needed for correct caching and high performance reading. should be left at 50.
    threads: 1 # number of shards / tiles segmented at the same size. should be adapted to the maximum amount allowed by memory.
    cache: "."
    lower_quantile_normalization:   0.001
    upper_quantile_normalization:   0.999
    median_filter_size: 6 # Size in pixels
    nucleus_segmentation:
        model: "nuclei"
    chunk_size: 50
    filter_masks_size: False
[ ]:
from scportrait.pipeline.workflows import DAPISegmentationCellpose

project_location = f"{project_directory}/DAPISegmentationCellpose"

project = SpatialProject(os.path.abspath(project_location),
                         config_path= "../../../example_data/example_3/config_example3.yml",
                         overwrite=True,
                         debug=False,
                         segmentation_f=DAPISegmentationCellpose,
                         extraction_f=HDF5CellExtraction,
                        )

project.load_input_from_tif_files(images)
modifying config
/Users/sophia/Documents/GitHub/SPARCSspatial/src/scportrait/pipeline/project.py:154: UserWarning: There is already a directory in the location path
  warnings.warn("There is already a directory in the location path")
[ ]:
project.segment()
project.extract()
multiprocessing done.

Segmentation Results

[ ]:
with h5py.File(f"{project_location}/segmentation/segmentation.h5") as hf:
    segmentation = hf.get("labels")

    fig, axs = plt.subplots(1, 2, figsize = (10, 5));
    axs[0].imshow(segmentation[0])
    axs[0].axis("off")
    axs[0].set_title("Nucleus Segmentation Mask")

    axs[1].imshow(segmentation[1])
    axs[1].axis("off")
    axs[1].set_title("Cytosol Segmentation Mask")
    fig.tight_layout()
../../_images/pages_notebooks_example_notebook_segmentation_workflows_30_0.png

Extraction Results

[ ]:
with h5py.File(f"{project_location}/extraction/data/single_cells.h5") as hf:
    index = hf.get("single_cell_index")
    images = hf.get("single_cell_data")

    n_cells = 4
    fig, axs = plt.subplots(n_cells, 5, figsize = (5*2, n_cells*2))
    labels = ["nucleus mask", "cytosol mask", "nucleus", "cytosol", "additional channel"]

    for i in range(n_cells):
        cell_id = index[i][1]
        image = images[i]

        for n, _img in enumerate(image):
            axs[i, n].imshow(_img)

            if n == 0:
                axs[i, n].set_ylabel(f"cell {cell_id}", fontsize = 10, rotation = 0, labelpad = 25)
                axs[i, n].xaxis.set_visible(False)
                axs[i, n].tick_params(left=False, labelleft=False)
            else:
                axs[i, n].axis("off")

            if i == 0:
                axs[i, n].set_title(labels[n], fontsize = 10)


    fig.tight_layout()
../../_images/pages_notebooks_example_notebook_segmentation_workflows_32_0.png