Measuring Aorta Diameter from centerline using ITK

Hello!

I’ve been trying to implement a python method for automatically measuring the aortic diameter from CT segmentations. I was previously trying to do this all in numpy/scipy, but I think I need the efficiency and thought put into the SimpleITK package to improve the output. I’m trying to use SimpleITK’s ResampleImageFilter for the transformation. In my case, the aorta is saved in a binary 3d numpy array, and I have calculated the centerline, it’s tangent vectors, and the vectors orthogonal to the tangents to define the plane. Currently my script looks something like this:

import numpy as np
import SimpleITK as sitk
# Smoothed centerline is the generated centerline from the 3d array, smoothed with Savitzky-Golay filtering using the kimimaro package
centerline_z = smoothed_centerline[:, 0] # Is this the proper way to realign the centerline to itk's orientation?
centerline_y = smoothed_centerline[:, 1]
centerline_x = smoothed_centerline[:, 2]
sitk_centerline = np.column_stack((centerline_x, centerline_y, centerline_z))
sitk_tangents = compute_tangent_vectors(sitk_centerline) # using scipy.interpolate.CubicSpline here
sitk_normals, sitk_binormals = compute_orthogonal_vectors(sitk_tangents)
# tangents, normals, and binormals are all normalized vectors
# Transform the aorta segmentation to sitk image
aorta_itk = sitk.GetImageFromArray(aorta)
# This is my attempt at the function to extract the orthogonal slice using the centerline
def extract_orthogonal_slice_sitk(sitk_image, center, tangent, normal, binormal, slice_resolution):
    # Calculate orientation
    rotation_matrix =  np.vstack([normal, binormal, tangent])
    
    # Create affine transform with rotation and translation
    transform = sitk.AffineTransform(3)
    transform.SetMatrix(rotation_matrix.ravel())
    transform.SetTranslation(np.array(center) - np.array(sitk_image.GetSize()) / 2)
    
    # Define resampler
    resampler = sitk.ResampleImageFilter()
    resampler.SetReferenceImage(sitk_image)
    resampler.SetSize((slice_resolution, slice_resolution, 1))
    resampler.SetTransform(transform)
    resampler.SetInterpolator(sitk.sitkNearestNeighbor)
    
    # Execute resampling
    resampled_image = resampler.Execute(sitk_image)
    
    # Convert back to numpy array
    slice_2d = sitk.GetArrayFromImage(resampled_image).squeeze()
    
    return slice_2d

idx = 50
slice_2d = extract_oblique_slice_sitk(aorta_itk, sitk_centerline[idx], sitk_tangents[idx], sitk_normals[idx], sitk_binormals[idx], 100) # this returns an empty array

Any help would be greatly appreciated as I’ve been working on this for some time now without much success. Thanks!

Hello @idinsmore1,

You might find the LabelShapeStatisticsImageFilter useful (see the GetEquivalentEllipsoidDiameter method amongst others). For usage example see this Jupyter notebook.

1 Like

Hi @zivy

Thank you for your response! I’m not sure if this is exactly what I’m after. I implemented the GetEquivalentEllipsoidDiameter to my aorta_itk image, and I’m confused by the output. Is the first index in the returned tuple the measured diameter?

shape_stats = sitk.LabelShapeStatisticsImageFilter()
shape_stats.ComputeOrientedBoundingBoxOn()
shape_stats.Execute(aorta_itk)
shape_stats.GetEquivalentEllipsoidDiameter(1)
(33.9884161954578, 105.55717526475205, 288.84235603536797)

If so, I know this is not correct as the spacing is in (1mm, 1mm, 1mm) (I resampled as a part of preprocessing), and I had a professional verify that in this image the diameter should be 59mm. What would be best is if I could reconstruct the entire aorta straightened, to verify that my transformations worked correctly. Thanks again for your response!

Perhaps it is easiest to use the danielssondistancemap function to create a distancemap for the inside of the aorta. Then the diameter is twice the value of the distancemap at the centerline points.

https://itk.org/Doxygen/html/classitk_1_1DanielssonDistanceMapImageFilter.html

1 Like

@Stephen_Aylward is there an example of how could I implement this? I’m very new to ITK and not confident of the syntax and operations yet.

I use python ITK instead of simple ITK, so…

pip install itk

import itk

ct_img = itk.imread("ct.nii")   # Read in your original CT image so that you know spacing/info

# assuming your aorta array has 1 on the inside and zeros on the outside, we need to flip so
# that it is 0 inside and 1 outside.
aorta = 1 - aorta

# Convert aorta array to itk image and copy info (spacing) from original ct.
# spacing is important so that distances are in physical units, not as voxels.
aorta_img = itk.GetImageFromArray(aorta)
aorta_img.CopyInformation(ct_img)

ddFilter = itk.DanielssonDistanceMapImageFilter(aorta_img)
ddFilter.SetInputIsBinary(True)
ddFilter.SetSquaredDistance(False)
ddFilter.SetUseImageSpacing(True)  #important for real-world data
ddFilter.Update()
dist_map_img = ddFilter.GetDistanceMap()

dist_map = itk.GetArrayFromImage(dMap_img)
radius = [ dist_map[centerline_z[i], centerline_y[i], centerline_x[i]] for i in range(len(centerline_x)) ]
diameter = 2 * radius

This will give you the radius of the maximally inscribed sphere at that centerline point. This is NOT the same as fitting an ellipse to the cross section at sharp curves, at sections with rapidly changing radii, at non-circular cross-sections, and at the ends. So, the “correctness” of this approach is application dependent.

Also, you may need to check the ordering of indexing into dMap/aorta. Not certain if it is aorta[z,y,x] or aorta[x,y,z].

And…I haven’t tested this, so there might be bugs, but it should be close.

1 Like

Looks like you may need

ddFilter = itk.DanielssonDistanceMapImageFilter[type(aorta_img), type(aorta_img)].New(aorta_img)

Experiment (aka ask copilot) if that doesn’t work :slight_smile:

@Stephen_Aylward @zivy thank you both this was super helpful! I will keep experimenting with ITK and let you know if I find a solution!