Introduction to SimpleITKv4 Registration

SimpleITK conventions:
  • Dimensionality and pixel type of registered images is required to be the same (2D/2D or 3D/3D).
  • Supported pixel types are sitkFloat32 and sitkFloat64 (use the SimpleITK Cast() function if your image's pixel type is something else).

Registration Components



There are many options for creating an instance of the registration framework, all of which are configured in SimpleITK via methods of the ImageRegistrationMethod class. This class encapsulates many of the components available in ITK for constructing a registration instance.

Currently, the available choices from the following groups of ITK components are:

Optimizers

The SimpleITK registration framework supports several optimizer types via the SetOptimizerAsX() methods, these include:

Similarity metrics

The SimpleITK registration framework supports several metric types via the SetMetricAsX() methods, these include:

Interpolators

The SimpleITK registration framework supports several interpolators via the SetInterpolator() method, which receives one of the following enumerations:

  • sitkNearestNeighbor
  • sitkLinear
  • sitkBSpline
  • sitkGaussian
  • sitkHammingWindowedSinc
  • sitkCosineWindowedSinc
  • sitkWelchWindowedSinc
  • sitkLanczosWindowedSinc
  • sitkBlackmanWindowedSinc

Data - Retrospective Image Registration Evaluation

We will be using part of the training data from the Retrospective Image Registration Evaluation (RIRE) project.


In [ ]:
library(SimpleITK)
library(ggplot2)
# Utility method that either downloads data from the Girder repository or
# if already downloaded returns the file name for reading from disk (cached data).
source("downloaddata.R")

# Always write output to a separate directory, we don't want to pollute the source directory. 
OUTPUT_DIR = "Output"

# alpha blending value we will use for displaying an overlay of the registered images.
alpha <- 0.7

Utility functions

A number of utility callback functions for plotting the similarity metric during registration.


In [ ]:
# Callback invoked when the StartEvent happens, sets up our new data.
# Functions ending in _jn are for use with Jupyter notebooks, as the display
# behaviour is a bit different.
# Note that we could use a special plotting environment instead of the global
# environment, but we may want to use the metrics elsewhere and they are easier to
# access if they are global
start_plot <- function()
{   #global empty vectors (via assignment operator) 
    metric_values <<- c()
    multires_iterations <<- c()
}

# Callback invoked when the EndEvent happens, do cleanup of global data.
end_plot <- function()
{    
    rm(metric_values, pos = ".GlobalEnv")
    rm(multires_iterations, pos = ".GlobalEnv")
}
# In notebooks we only get one plot per cell, so use the end to plot it
end_plot_jn <- function()
{    
    Multi <- rep(NA, length(metric_values))
    Multi[multires_iterations] <- "M"
    DDF <- data.frame(IterationNumber=1:length(metric_values), 
                      MetricValue=metric_values,
                      MultiresIteration=Multi)
    DDFM <- subset(DDF, !is.na(MultiresIteration))
    pl <- ggplot(DDF, aes(x=IterationNumber, y=MetricValue)) + 
          geom_line() + 
          geom_point(data=DDFM, aes(colour=MultiresIteration)) +
          theme(legend.position="none")    
    print(pl)
    rm(metric_values, pos = ".GlobalEnv")
    rm(multires_iterations, pos = ".GlobalEnv")
}
# Callback invoked when the IterationEvent happens, update our data and display new figure.
# Note that this won't appear as an animation in R studio, but you can use the arrows to cycle
# through plots
plot_values <- function(registration_method)
{
    metric_values <<- c(metric_values, registration_method$GetMetricValue())
    Multi <- rep(NA, length(metric_values))
    Multi[multires_iterations] <- "M"
    DDF <- data.frame(IterationNumber=1:length(metric_values),
                      MetricValue=metric_values,
                      MultiresIteration=Multi)
    DDFM <- subset(DDF, !is.na(MultiresIteration))

    pl <- ggplot(DDF, aes(x=IterationNumber, y=MetricValue)) +
          geom_line() +
          theme(legend.position="none")

    if(nrow(DDFM) > 1) {
        pl <- pl + geom_point(data=DDFM, aes(colour=MultiresIteration))
    }
    print(pl)
    dev.flush()
    Sys.sleep(0)
}
# Use this one inside a notebook
plot_values_jn <- function(registration_method)
{    
    ## No point attempting to plot every one in a notebook
    metric_values <<- c(metric_values, registration_method$GetMetricValue())
}
                                 
# Callback invoked when the sitkMultiResolutionIterationEvent happens, update the index into the 
# metric_values list. 
update_multires_iterations <- function()
{
    multires_iterations <<- c(multires_iterations, length(metric_values)+1)
}

Read images

We first read the images, casting the pixel type to that required for registration (Float32 or Float64) and look at them.


In [ ]:
fixed_image <- ReadImage(fetch_data("training_001_ct.mha"), "sitkFloat32")
moving_image <- ReadImage(fetch_data("training_001_mr_T1.mha"), "sitkFloat32")

Show(fixed_image,"fixed_image")
Show(moving_image, "moving_image")

Initial Alignment

Use the CenteredTransformInitializer to align the centers of the two volumes and set the center of rotation to the center of the fixed image.


In [ ]:
initial_transform <- CenteredTransformInitializer(fixed_image, 
                                                  moving_image, 
                                                  Euler3DTransform(), 
                                                  "GEOMETRY")

moving_resampled <- Resample(moving_image, fixed_image, initial_transform)
Show((1-alpha)*fixed_image + alpha*moving_resampled, "initial alignment")

Registration

The specific registration task at hand estimates a 3D rigid transformation between images of different modalities. There are multiple components from each group (optimizers, similarity metrics, interpolators) that are appropriate for the task. Note that each component selection requires setting some parameter values. We have made the following choices:

  • Similarity metric, mutual information (Mattes MI):
    • Number of histogram bins, 50.
    • Sampling strategy, random.
    • Sampling percentage, 1%.
  • Interpolator, sitkLinear.
  • Optimizer, gradient descent:
    • Learning rate, step size along traversal direction in parameter space, 1.0 .
    • Number of iterations, maximal number of iterations, 100.
    • Convergence minimum value, value used for convergence checking in conjunction with the energy profile of the similarity metric that is estimated in the given window size, 1e-6.
    • Convergence window size, number of values of the similarity metric which are used to estimate the energy profile of the similarity metric, 10.

Perform registration using the settings given above, and take advantage of the built in multi-resolution framework, use a three tier pyramid.

In this example we plot the similarity metric's value during registration. Note that the change of scales in the multi-resolution framework is readily visible.


In [ ]:
registration_method <- ImageRegistrationMethod()

# Similarity metric settings.
registration_method$SetMetricAsMattesMutualInformation(numberOfHistogramBins=50)
registration_method$SetMetricSamplingStrategy("RANDOM")
registration_method$SetMetricSamplingPercentage(0.01)

registration_method$SetInterpolator("sitkLinear")

# Optimizer settings.
registration_method$SetOptimizerAsGradientDescent(learningRate=1.0, numberOfIterations=100, convergenceMinimumValue=1e-6, convergenceWindowSize=10)
registration_method$SetOptimizerScalesFromPhysicalShift()

# Setup for the multi-resolution framework.            
registration_method$SetShrinkFactorsPerLevel(shrinkFactors = c(4,2,1))
registration_method$SetSmoothingSigmasPerLevel(smoothingSigmas=c(2,1,0))
registration_method$SmoothingSigmasAreSpecifiedInPhysicalUnitsOn()

# Don't optimize in-place, we would possibly like to run this cell multiple times.
registration_method$SetInitialTransform(initial_transform, inPlace=FALSE)

# Connect all of the observers so that we can perform plotting during registration. 
# (assignment of the return values to dev_null so they aren't printed)
dev_null <- registration_method$AddCommand("sitkStartEvent", start_plot)
dev_null <- registration_method$AddCommand("sitkEndEvent", end_plot_jn)
dev_null <- registration_method$AddCommand("sitkMultiResolutionIterationEvent", update_multires_iterations) 
dev_null <- registration_method$AddCommand("sitkIterationEvent", function() plot_values_jn(registration_method))

final_transform <- registration_method$Execute(fixed_image, moving_image)

Post registration analysis

Query the registration method to see the metric value and the reason the optimization terminated.

The metric value allows us to compare multiple registration runs as there is a probabilistic aspect to our registration, we are using random sampling to estimate the similarity metric.

Always remember to query why the optimizer terminated. This will help you understand whether termination is too early, either due to thresholds being too tight, early termination due to small number of iterations - numberOfIterations, or too loose, early termination due to large value for minimal change in similarity measure - convergenceMinimumValue)


In [ ]:
cat(paste0("Final metric value: ",registration_method$GetMetricValue(),"\n"))
cat(paste0("Optimizer\'s stopping condition, ",registration_method$GetOptimizerStopConditionDescription()))

Now visually inspect the results.


In [ ]:
moving_resampled <- Resample(moving_image, fixed_image, final_transform)
Show((1-alpha)*fixed_image + alpha*moving_resampled, "final alignment")

If we are satisfied with the results, save them to file.


In [ ]:
WriteImage(moving_resampled, file.path(OUTPUT_DIR, "RIRE_training_001_mr_T1_resampled.mha"))
WriteTransform(final_transform, file.path(OUTPUT_DIR, "RIRE_training_001_CT_2_mr_T1.tfm"))