Registration of CT Images with Large Organ Deformations

Hello everyone,

I have two CT images that I want to register together. One is from external beam radiotherapy (EBRT), and the other is from brachytherapy. Before registration, I harmonized both images by making their Origin, dimensions, Spacing, and etc. identical. Additionally, I modified the Hounsfield units of the bladder and rectum in both images. Since the applicator is not present in the EBRT image, I set its Hounsfield unit to zero in the brachytherapy image.

After that, I ran the following code for registration. In writing parts of this code, I also drew from examples in the link: Examples — SimpleITK 2.4.0 documentation.
However, despite the long execution time, I didn’t achieve good results. here is my result image. As you can see in the image, the registered image is not satisfactory.
Can you guide me on how to register two CT images with significant changes in organs like the bladder and rectum, so that I can achieve good accuracy without the process taking too long?

Thanks a lot

Here is my code:

import SimpleITK as sitk
import os
import time
import gc
sitk.ProcessObject.SetGlobalDefaultNumberOfThreads(0)
def read_dicom_series(directory):
print(f"Loading DICOM series from: {directory}“)
series_IDs = sitk.ImageSeriesReader.GetGDCMSeriesIDs(directory)
if not series_IDs:
raise ValueError(f"No DICOM series found in directory: {directory}”)
series_files = sitk.ImageSeriesReader.GetGDCMSeriesFileNames(directory, series_IDs[0])
reader = sitk.ImageSeriesReader()
reader.SetFileNames(series_files)
image = reader.Execute()
return sitk.Cast(image, sitk.sitkFloat32)
#Configuration
fixed_ct_dir = r’D:\fr1\CT_Unified’
moving_ct_dir = r’D:\extr\CT_Unified’
output_dir = r’D:\registration_output’
#Main Script
os.makedirs(output_dir, exist_ok=True)
dvf_output_path = os.path.join(output_dir, ‘final_dvf_extr_to_fr1.mha’)
resampled_moving_ct_path = os.path.join(output_dir, ‘resampled_moving_ct.nii.gz’)
total_start_time = time.time()
fixed_image = read_dicom_series(fixed_ct_dir)
moving_image = read_dicom_series(moving_ct_dir)
print(f"Images loaded successfully. Fixed image size: {fixed_image.GetSize()}“)
sampling_percentage = 0.05
#Stages 1 & 2: Rigid and Affine
print(”\n— Starting Stage 1: Rigid Registration —“)
R_rigid = sitk.ImageRegistrationMethod()
R_rigid.SetMetricAsMattesMutualInformation(numberOfHistogramBins=50)
R_rigid.SetMetricSamplingStrategy(R_rigid.RANDOM)
R_rigid.SetMetricSamplingPercentage(sampling_percentage)
R_rigid.SetInterpolator(sitk.sitkLinear)
R_rigid.SetOptimizerAsGradientDescent(learningRate=1.0, numberOfIterations=200, convergenceMinimumValue=1e-6, convergenceWindowSize=20)
R_rigid.SetOptimizerScalesFromPhysicalShift()
R_rigid.SetShrinkFactorsPerLevel(shrinkFactors=[8, 4, 2])
R_rigid.SetSmoothingSigmasPerLevel(smoothingSigmas=[4, 2, 1])
R_rigid.SmoothingSigmasAreSpecifiedInPhysicalUnitsOn()
initial_rigid_transform = sitk.CenteredTransformInitializer(fixed_image, moving_image, sitk.Euler3DTransform(), sitk.CenteredTransformInitializerFilter.GEOMETRY)
R_rigid.SetInitialTransform(initial_rigid_transform)
rigid_transform = R_rigid.Execute(fixed_image, moving_image)
print(f"Rigid registration finished. Final Metric: {R_rigid.GetMetricValue():.4f}”)
print(“\n— Starting Stage 2: Affine Registration —”)
R_affine = sitk.ImageRegistrationMethod()
R_affine.SetMetricAsMattesMutualInformation(numberOfHistogramBins=50)
R_affine.SetMetricSamplingStrategy(R_affine.RANDOM)
R_affine.SetMetricSamplingPercentage(sampling_percentage)
R_affine.SetInterpolator(sitk.sitkLinear)
R_affine.SetOptimizerAsGradientDescent(learningRate=1.0, numberOfIterations=200, convergenceMinimumValue=1e-6, convergenceWindowSize=20)
R_affine.SetOptimizerScalesFromPhysicalShift()
R_affine.SetShrinkFactorsPerLevel(shrinkFactors=[4, 2])
R_affine.SetSmoothingSigmasPerLevel(smoothingSigmas=[2, 1])
R_affine.SmoothingSigmasAreSpecifiedInPhysicalUnitsOn()
initial_affine_transform = sitk.AffineTransform(3)
initial_affine_transform.SetMatrix(rigid_transform.GetMatrix())
initial_affine_transform.SetTranslation(rigid_transform.GetTranslation())
initial_affine_transform.SetCenter(rigid_transform.GetCenter())
R_affine.SetInitialTransform(initial_affine_transform)
affine_transform = R_affine.Execute(fixed_image, moving_image)
print(f"Affine registration finished. Final Metric: {R_affine.GetMetricValue():.4f}“)
#Stage 3: Multi-level B-Spline Registration
print(”\n— Starting Stage 3: Multi-Level B-Spline Registration —“)
moving_prealigned = sitk.Resample(moving_image, fixed_image, affine_transform, sitk.sitkLinear, 0.0, moving_image.GetPixelID())
#B-Spline Coarse Stage
print(”— Sub-stage: Coarse B-Spline —“)
R_bspline_coarse = sitk.ImageRegistrationMethod()
R_bspline_coarse.SetMetricAsMattesMutualInformation(numberOfHistogramBins=50)
R_bspline_coarse.SetMetricSamplingStrategy(R_bspline_coarse.RANDOM)
R_bspline_coarse.SetMetricSamplingPercentage(sampling_percentage)
R_bspline_coarse.SetInterpolator(sitk.sitkLinear)
R_bspline_coarse.SetOptimizerAsLBFGSB(gradientConvergenceTolerance=1e-5, numberOfIterations=100, maximumNumberOfCorrections=5)
transform_domain_mesh_size_coarse = [10] * fixed_image.GetDimension()
R_bspline_coarse.SetInitialTransform(sitk.BSplineTransformInitializer(image1=fixed_image, transformDomainMeshSize=transform_domain_mesh_size_coarse))
R_bspline_coarse.SetShrinkFactorsPerLevel([4, 2])
R_bspline_coarse.SetSmoothingSigmasPerLevel([2, 1])
bspline_transform_coarse = R_bspline_coarse.Execute(fixed_image, moving_prealigned)
print(f"B-Spline (Coarse) finished. Metric: {R_bspline_coarse.GetMetricValue():.4f}”)
#B-Spline Fine Stage
print(“— Sub-stage: Fine B-Spline —”)
R_bspline_fine = sitk.ImageRegistrationMethod()
R_bspline_fine.SetMetricAsANTSNeighborhoodCorrelation(radius=5)
R_bspline_fine.SetMetricSamplingStrategy(R_bspline_fine.RANDOM)
R_bspline_fine.SetMetricSamplingPercentage(sampling_percentage)
R_bspline_fine.SetInterpolator(sitk.sitkLinear)
R_bspline_fine.SetOptimizerAsLBFGSB(gradientConvergenceTolerance=1e-5, numberOfIterations=100, maximumNumberOfCorrections=5)
transform_domain_mesh_size_fine = [12] * fixed_image.GetDimension()
R_bspline_fine.SetInitialTransform(bspline_transform_coarse, inPlace=False)
R_bspline_fine.SetShrinkFactorsPerLevel([2, 1])
R_bspline_fine.SetSmoothingSigmasPerLevel([1, 0])
bspline_transform_fine = R_bspline_fine.Execute(fixed_image, moving_prealigned)
print(f"B-Spline (Fine) finished. Final Metric: {R_bspline_fine.GetMetricValue():.4f}“)
#Final Stage: Combine and Save
print(”\n— Final Stage: Combining transforms and generating DVF —“)
final_transform = sitk.CompositeTransform(affine_transform)
final_transform.AddTransform(bspline_transform_fine)
displacement_filter = sitk.TransformToDisplacementFieldFilter()
displacement_filter.SetReferenceImage(fixed_image)
final_dvf = displacement_filter.Execute(final_transform)
final_dvf = sitk.Cast(final_dvf, sitk.sitkVectorFloat64)
print(“Clearing intermediate objects from memory…”)
del R_rigid, R_affine, R_bspline_coarse, R_bspline_fine, rigid_transform, affine_transform, bspline_transform_coarse, bspline_transform_fine, moving_prealigned, final_transform, displacement_filter
gc.collect()
print(“Writing final DVF…”)
sitk.WriteImage(final_dvf, dvf_output_path)
print(f"Final DVF saved to: {dvf_output_path}”)
print(“\n— Generating resampled CT —”)
dvf_transform = sitk.DisplacementFieldTransform(final_dvf)
resampled_moving = sitk.Resample(moving_image, fixed_image, dvf_transform, sitk.sitkLinear, 0.0)
sitk.WriteImage(resampled_moving, resampled_moving_ct_path)
print(f"Final resampled moving CT (for validation) saved to: {resampled_moving_ct_path}“)
print(f”\nRegistration completed successfully in {time.time() - total_start_time:.2f} seconds.")

Hello @blue_sky,

The changes made to the images prior to registration, origin/spacing/etc., are not necessary. You should leave the original scans as is.

This “harmonization” step is introducing unnecessary resampling which may have a detrimental effect on registration. The modifications of the intensities for bladder and rectum are also likely not helpful. Finally, setting the applicator pixels to zero is likely not what you want. A HU value of 0 corresponds to water, which is likely the same value as urine in the bladder. If you want to change the intensity of the applicator a value of -1000, air, is probably more appropriate, but I would leave it as is.

Before trying to do this programmatically, please try using Slicer (see registration documentation). The interface for the advanced registration is friendlier than the programmatic SimpleITK interface and the default task specific values for the optimization settings mean you do not need expertise in registration to set them in an appropriate way for your setting (CT/CT non-rigid registration).

2 Likes