C++ InPlace Filter Execution

Overview

With the C++ interface of SimpleITK, some filters and image operators can run “inplace” to reduce memory usage and improve performance. When a filter is run in-place the memory allocated to the image buffer of the input image is reused for the output. Since the output buffer is not separately allocated, the peak memory usage can be reduced by 50%. The performance improvement of running an algorithm in-place comes from not only removing the cost of memory allocating but also reduced memory accessed during algorithm execution. Speed up of 1.5x are common while speedup of 10x have been observed.

The SimpleITK filters automatically run in-place with rvalue references. When a temporary result is passed as an input argument to a filter ( procedure or a filter’s Execute method ), the temporary’s buffer will be re-used for the output. Move semantics and rvalue references are an advanced C++ topic which require additional studying to fully understand the details. Procedures and Execute methods with rvalue references (&& before argument name) as the first argument support the in-place execution. The following is an example of two procedural methods which support execution in-place.

Image UnaryMinus(Image &&image1);
Image SmoothingRecursiveGaussian(Image &&image1, std::vector<double> sigma, bool normalizeAcrossScale);

Additionally, most C++ overloaded Image numeric operators such as +, -, *, %, etc. support in-place execution.

The SimpleITK Image class supports move semantics with the constructor and assignment operator. Since the Image class also uses a lazy copy-on-write implement for the constructors and assignment there is not a significant direct performance benefit using these operations. However, the move semantics can be useful to reduce the number of Image objects referencing the same image buffer to prevent unnecessary copies. Lastly, if a rvalue Image object is passed to a filter and that Image object’s buffer is not unique, then the image buffer will be copied before the filter is executed in-place. Generally, copying the image then executing the filter in-place is faster than executing the not in-place implementation.

The following example demonstrates implicit and explicit use of C++ rvalues to execute filter in-place for improve performance.

Code

#include "SimpleITK.h"
#include "sitkImageOperators.h"

#include <cstdlib>
#include <iostream>

namespace sitk = itk::simple;


// Rescale the intensity range for image to -1 to 1.
sitk::Image MinusOneToOne(const sitk::Image &image)
{
  sitk::MinimumMaximumImageFilter mm;
  mm.Execute(image);

  const double shift = 0.5 * (mm.GetMinimum() + mm.GetMaximum());
  const double scale = (mm.GetMaximum() - mm.GetMinimum())*2.0;

  // The CastImageFilter does not operate in-place, so the image argument to
  // this function is declared as a constant reference.
  //
  // The Image operators use in-place execution with the rvalue reference
  // returned from the Cast procedure.
  return  (sitk::Cast(image, sitk::sitkFloat32) - shift) / scale;
}


// Compute the difference between two Gaussian convoluted images.
sitk::Image DifferenceOfGaussian(sitk::Image image, double sigma1, double sigma2)
{
  sitk::SmoothingRecursiveGaussianImageFilter g1;
  g1.SetSigma(sigma1);

  sitk::SmoothingRecursiveGaussianImageFilter g2;
  g2.SetSigma(sigma2);

  // This temporary is create to ensure the method is executed before image is
  // moved and made invalid.
  sitk::Image &&temp = g1.Execute(image);

  // Both Subtract and g2 are executed inplace.
  return sitk::Subtract(temp, g2.Execute(std::move(image)));
}

int main( int argc, char *argv[])
{

  //
  // Check command line parameters
  //
  if( argc < 5 )
  {
    std::cerr << "Missing Parameters " << std::endl;
    std::cerr << "Usage: " << argv[0];
    std::cerr << " inputImage outputImage lowerSigma upperSigma" << std::endl;
    return 1;
  }


  const double sigma1 = atof(argv[3]);
  const double sigma2 = atof(argv[4]);

  //
  // Read the image
  //
  sitk::ImageFileReader reader;
  reader.SetFileName( std::string( argv[1] ) );


  // The below explicitly declares the image variable as a rvalue temporary.
  // Having a rvalue typed variable enables explicit declaration of temporary
  // temporary images used between filters.Alternatively this could be written
  // in one line at the sacrifice of readability and explicitness:
  // image = DifferenceOfGaussian(MinusOneToOne(reader.Execute()), sigma1, sigma2);
  //

  sitk::Image &&image = reader.Execute();
  image = MinusOneToOne(image);
  image = DifferenceOfGaussian(image, sigma1, sigma2);


  //
  // Set up writer
  //
  sitk::WriteImage( image, std::string( argv[2] ) );

  return 0;
}