Atlas Segmentation#

This notebook demonstrates how to perform basic atlas-based segmentation using PlatiPy.

Import Modules#

The following cell imports the modules needed for this example.

[1]:
# Check if platipy is installed, if not install it.
try:
    import platipy
except:
    !pip install platipy
    import platipy

# The ImageVisualiser class
from platipy.imaging import ImageVisualiser

# Function to grab some test data
from platipy.imaging.tests.data import get_lung_nifti

# Usual suspects
import numpy as np
import SimpleITK as sitk
import pandas as pd
from pathlib import Path
import matplotlib.pyplot as plt

# The platipy functions we'll be using in this example
from platipy.imaging.projects.multiatlas.run import run_segmentation, MUTLIATLAS_SETTINGS_DEFAULTS
from platipy.imaging.registration.deformable import fast_symmetric_forces_demons_registration
from platipy.imaging.registration.utils import apply_transform
from platipy.imaging.registration.linear import linear_registration
from platipy.imaging.visualisation.comparison import contour_comparison

Download Test Data#

Some Lung test data from LCTSC is fetched here for use in this example notebook.

[2]:
input_directory = get_lung_nifti()

Single Atlas Segmentation#

Here we will define one test case and one atlas case to perform single atlas segmentation.

[3]:
atlas_case = "LCTSC-Test-S1-101"
test_case = "LCTSC-Test-S1-201"

Read data#

Let’s load the images and contours for our test and atlas case

[4]:
def read_image_and_contours(case_id):

    pat_directory = input_directory.joinpath(case_id)

    # Read in the CT image
    ct_filename = next(pat_directory.glob("**/IMAGES/*.nii.gz"))
    ct_image = sitk.ReadImage(ct_filename.as_posix())

    # Read in the RTStruct contours as binary masks
    contour_filename_list = list(pat_directory.glob("**/STRUCTURES/*.nii.gz"))

    contours = {}
    for contour_filename in contour_filename_list:
        _name = contour_filename.name.split(".nii.gz")[0].split("RTSTRUCT_")[-1]

        contours[_name] = sitk.ReadImage(contour_filename.as_posix())

    return ct_image, contours

img_ct_test, contours_test = read_image_and_contours(test_case)
img_ct_atlas, contours_atlas = read_image_and_contours(atlas_case)

Visualise data#

Now we’ll prepare some figures to view this data.

[5]:
vis = ImageVisualiser(img_ct_test)
vis.add_contour(contours_test)
fig = vis.show()
../_images/_examples_atlas_segmentation_9_0.png
[6]:
vis = ImageVisualiser(img_ct_atlas)
vis.add_contour(contours_atlas)
fig = vis.show()
../_images/_examples_atlas_segmentation_10_0.png

Linear (Rigid) Registration#

Next we perform a linear registration, setting our test image as the fixed image and aligning our atlas image to it. The aligned image is the visualised by overlaying it with the test image to help visually inspect the quality of the registration.

[7]:
img_ct_atlas_reg_linear, tfm_linear = linear_registration(
    fixed_image = img_ct_test,
    moving_image = img_ct_atlas,
    reg_method='similarity',
    metric='mean_squares',
    optimiser='gradient_descent',
    shrink_factors=[8, 4, 2],
    smooth_sigmas=[4, 2, 0],
    sampling_rate=1.0,
    number_of_iterations=50,
)

vis = ImageVisualiser(img_ct_test)
vis.add_comparison_overlay(img_ct_atlas_reg_linear)
fig = vis.show()
../_images/_examples_atlas_segmentation_12_0.png

Deformable Registration#

Assuming the linear registration went well, we now us deformable image registration to better align the atlas image to the test image. Again the resulting alignment is visualised.

[8]:

img_ct_atlas_reg_dir, tfm_dir, dvf = fast_symmetric_forces_demons_registration( img_ct_test, img_ct_atlas_reg_linear, ncores=4, isotropic_resample=True, resolution_staging=[8], iteration_staging=[20], ) vis = ImageVisualiser(img_ct_test) vis.add_comparison_overlay(img_ct_atlas_reg_dir) fig = vis.show()
../_images/_examples_atlas_segmentation_14_0.png

Propagate Contours#

Now that our images are aligned, we can combined the linear and the deformable registration and use this to propagate the contours from our atlas image to our test image.

[9]:
tfm_combined = sitk.CompositeTransform((tfm_linear, tfm_dir))

# apply to the contours
contours_atlas_reg_dir = {}
for s in contours_atlas:
  contours_atlas_reg_dir[s] = apply_transform(
      contours_atlas[s],
      reference_image=img_ct_test,
      transform=tfm_combined
    )

Contour Comparison#

Then we can compare our automatically generated (propagated) contours with our ground truth (manual) contours on our test case. To to this we use platipy’s built-in contour_comparison function. This produces a visualisation was well as a pandas DataFrame with the quantitative metrics.

[10]:
fig, df_sas = contour_comparison(
    img = img_ct_test,
    contour_dict_a = contours_test,
    contour_dict_b = contours_atlas_reg_dir,
    contour_label_a = "MANUAL",
    contour_label_b = "AUTOMATIC",
    title='Atlas-based Segmentation',
    subtitle='',
    subsubtitle='',
)

df_sas
[10]:
STRUCTURE DSC MDA_mm HD_mm VOL_MANUAL_cm3 VOL_AUTOMATIC_cm3
0 SPINALCORD 0.639930 3.858865 41.614619 54.210663 33.891678
0 LUNG_L 0.897254 1.832565 19.976306 1015.133858 1070.743561
0 ESOPHAGUS 0.430517 2.906840 13.706708 27.577400 24.510384
0 HEART 0.849119 4.545751 15.283582 759.281158 578.930855
0 LUNG_R 0.925969 1.686269 17.872285 1424.428940 1425.152779
../_images/_examples_atlas_segmentation_18_1.png

Multi-atlas Segmentation#

We can often improve the quality of atlas-based segmentation by using multiple atlas cases. PlatiPy has built-in functionality to perform multi-atlas segmentation. The following example uses 4 atlas cases to auto-segment our test case.

[11]:
# make a copy of the default settings
user_settings = MUTLIATLAS_SETTINGS_DEFAULTS

# Define the list of structures we are segmenting
structure_list = [
    "LUNG_L",
    "LUNG_R",
    "HEART",
    "SPINALCORD",
    "ESOPHAGUS",
]

# Define the atlas set
atlas_set = ["101", "102", "103", "104"]

user_settings['atlas_settings'] = {
    'atlas_id_list': atlas_set,
    'atlas_structure_list': structure_list,
    'atlas_path': './data/nifti/lung',
    'atlas_image_format': 'LCTSC-Test-S1-{0}/IMAGES/LCTSC_TEST_S1_{0}_0_CT_0.nii.gz',
    'atlas_label_format': 'LCTSC-Test-S1-{0}/STRUCTURES/LCTSC_TEST_S1_{0}_0_RTSTRUCT_{1}.nii.gz',
    'crop_atlas_to_structures': True,
    'crop_atlas_expansion_mm': 10,
}

# optionally, we can change some of the default registration parameters
user_settings["linear_registration_settings"] = {
    "reg_method": "similarity",
    "shrink_factors": [8, 4, 2],
    "smooth_sigmas": [4, 2, 0],
    "sampling_rate": 1,
    "default_value": -1000,
    "number_of_iterations": 50,
    "metric": "mean_squares",
    "optimiser": "gradient_descent",
    "verbose": False
}

user_settings["deformable_registration_settings"] = {
    "isotropic_resample": True,
    "resolution_staging": [8],
    "iteration_staging": [20],
    # Try commenting out the two lines above and uncommenting the following two lines for a better
    # DIR (but slower runtime)
    # "resolution_staging": [8,4,2],
    # "iteration_staging": [50,50,25],
    "smoothing_sigmas": [4,2,0],
    "ncores": 32,
    "default_value": -1000,
    "verbose": False
}

# Perform the multi-atlas segmentation
output_contours, output_probability = run_segmentation(img_ct_test, user_settings)

Contour Comparison#

Now let’s analyse the performance of the multi-atlas segmentation.

[12]:
fig, df_mas = contour_comparison(
    img = img_ct_test,
    contour_dict_a = contours_test,
    contour_dict_b = output_contours,
    contour_label_a = "MANUAL",
    contour_label_b = "AUTOMATIC",
    title='Multi-Atlas Segmentation',
    subtitle='',
    subsubtitle='',
)

df_mas
[12]:
STRUCTURE DSC MDA_mm HD_mm VOL_MANUAL_cm3 VOL_AUTOMATIC_cm3
0 SPINALCORD 0.697273 1.675756 13.706708 54.210663 42.320251
0 LUNG_L 0.912965 1.515161 13.671875 1015.133858 1120.937347
0 ESOPHAGUS 0.395965 20.715123 88.456873 27.577400 11.844635
0 HEART 0.903796 3.413777 21.593620 759.281158 757.504463
0 LUNG_R 0.917044 1.973885 22.395767 1424.428940 1508.425713
../_images/_examples_atlas_segmentation_22_1.png

Single-atlas vs Multi-atlas#

Finally, we’ll compare how the two approaches performed for each of our structures.

[13]:
df_combined = pd.merge(
    left = df_sas,
    right = df_mas,
    on = "STRUCTURE",
    suffixes = [" (single)", " (multi)"]
)

df_combined[["STRUCTURE","DSC (single)", "DSC (multi)"]]
[13]:
STRUCTURE DSC (single) DSC (multi)
0 SPINALCORD 0.639930 0.697273
1 LUNG_L 0.897254 0.912965
2 ESOPHAGUS 0.430517 0.395965
3 HEART 0.849119 0.903796
4 LUNG_R 0.925969 0.917044
[ ]: