Chapter 7
Image Adaptors


PIC

Figure 7.1: The difference between using a CastImageFilter and an ImageAdaptor. ImageAdaptors convert pixel values when they are accessed by iterators. Thus, they do not produces an intermediate image. In the example illustrated by this figure, the Image Y is not created by the ImageAdaptor; instead, the image is simulated on the fly each time an iterator from the filter downstream attempts to access the image data.


The purpose of an image adaptor is to make one image appear like another image, possibly of a different pixel type. A typical example is to take an image of pixel type unsigned char and present it as an image of pixel type float. The motivation for using image adaptors in this case is to avoid the extra memory resources required by using a casting filter. When we use the itk::CastImageFilter for the conversion, the filter creates a memory buffer large enough to store the float image. The float image requires four times the memory of the original image and contains no useful additional information. Image adaptors, on the other hand, do not require the extra memory as pixels are converted only when they are read using image iterators (see Chapter 6).

Image adaptors are particularly useful when there is infrequent pixel access, since the actual conversion occurs on the fly during the access operation. In such cases the use of image adaptors may reduce overall computation time as well as reduce memory usage. The use of image adaptors, however, can be disadvantageous in some situations. For example, when the downstream filter is executed multiple times, a CastImageFilter will cache its output after the first execution and will not re-execute when the filter downstream is updated. Conversely, an image adaptor will compute the cast every time.

Another application for image adaptors is to perform lightweight pixel-wise operations replacing the need for a filter. In the toolkit, adaptors are defined for many single valued and single parameter functions such as trigonometric, exponential and logarithmic functions. For example,

The following examples illustrate common applications of image adaptors.

7.1 Image Casting

The source code for this section can be found in the file
ImageAdaptor1.cxx.

This example illustrates how the itk::ImageAdaptor can be used to cast an image from one pixel type to another. In particular, we will adapt an unsigned char image to make it appear as an image of pixel type float.

We begin by including the relevant headers.

  #include "itkImageAdaptor.h"

First, we need to define a pixel accessor class that does the actual conversion. Note that in general, the only valid operations for pixel accessors are those that only require the value of the input pixel. As such, neighborhood type operations are not possible. A pixel accessor must provide methods Set() and Get(), and define the types of InternalPixelType and ExternalPixelType. The InternalPixelType corresponds to the pixel type of the image to be adapted (unsigned char in this example). The ExternalPixelType corresponds to the pixel type we wish to emulate with the ImageAdaptor (float in this case).

  class CastPixelAccessor
  {
  public:
    using InternalType = unsigned char;
    using ExternalType = float;
  
    static void Set(InternalType & output, const ExternalType & input)
      {
      output = static_cast<InternalType>( input );
      }
  
    static ExternalType Get( const InternalType & input )
      {
      return static_cast<ExternalType>( input );
      }
  };

The CastPixelAccessor class simply applies a static_cast to the pixel values. We now use this pixel accessor to define the image adaptor type and create an instance using the standard New() method.

    using InputPixelType = unsigned char;
    constexpr unsigned int Dimension = 2;
    using ImageType = itk::Image< InputPixelType, Dimension >;
  
    using ImageAdaptorType = itk::ImageAdaptor< ImageType, CastPixelAccessor >;
    ImageAdaptorType::Pointer adaptor = ImageAdaptorType::New();

We also create an image reader templated over the input image type and read the input image from file.

    using ReaderType = itk::ImageFileReader< ImageType >;
    ReaderType::Pointer reader = ReaderType::New();

The output of the reader is then connected as the input to the image adaptor.

    adaptor->SetImage( reader->GetOutput() );

In the following code, we visit the image using an iterator instantiated using the adapted image type and compute the sum of the pixel values.

    using IteratorType = itk::ImageRegionIteratorWithIndex< ImageAdaptorType >;
    IteratorType  it( adaptor, adaptor->GetBufferedRegion() );
  
    double sum = 0.0;
    it.GoToBegin();
    while( !it.IsAtEnd() )
      {
      float value = it.Get();
      sum += value;
      ++it;
      }

Although in this example, we are just performing a simple summation, the key concept is that access to pixels is performed as if the pixel is of type float. Additionally, it should be noted that the adaptor is used as if it was an actual image and not as a filter. ImageAdaptors conform to the same API as the itk::Image class.

7.2 Adapting RGB Images

The source code for this section can be found in the file
ImageAdaptor2.cxx.

This example illustrates how to use the itk::ImageAdaptor to access the individual components of an RGB image. In this case, we create an ImageAdaptor that will accept a RGB image as input and presents it as a scalar image. The pixel data will be taken directly from the red channel of the original image.

As with the previous example, the bulk of the effort in creating the image adaptor is associated with the definition of the pixel accessor class. In this case, the accessor converts a RGB vector to a scalar containing the red channel component. Note that in the following, we do not need to define the Set() method since we only expect the adaptor to be used for reading data from the image.

  class RedChannelPixelAccessor
  {
  public:
    using InternalType = itk::RGBPixel<float>;
    using ExternalType = float;
  
    static ExternalType Get( const InternalType & input )
      {
      return static_cast<ExternalType>( input.GetRed() );
      }
  };

The Get() method simply calls the GetRed() method defined in the itk::RGBPixel class.

Now we use the internal pixel type of the pixel accessor to define the input image type, and then proceed to instantiate the ImageAdaptor type.

    using InputPixelType = RedChannelPixelAccessor::InternalType;
    constexpr unsigned int Dimension = 2;
    using ImageType = itk::Image< InputPixelType, Dimension >;
  
    using ImageAdaptorType = itk::ImageAdaptor<  ImageType,
                                RedChannelPixelAccessor >;
  
    ImageAdaptorType::Pointer adaptor = ImageAdaptorType::New();

We create an image reader and connect the output to the adaptor as before.

    using ReaderType = itk::ImageFileReader< ImageType >;
    ReaderType::Pointer reader = ReaderType::New();
    adaptor->SetImage( reader->GetOutput() );

We create an itk::RescaleIntensityImageFilter and an itk::ImageFileWriter to rescale the dynamic range of the pixel values and send the extracted channel to an image file. Note that the image type used for the rescaling filter is the ImageAdaptorType itself. That is, the adaptor type is used in the same context as an image type.

    using OutputImageType = itk::Image< unsigned char, Dimension >;
    using RescalerType = itk::RescaleIntensityImageFilter<
                                              ImageAdaptorType,
                                              OutputImageType >;
  
    RescalerType::Pointer rescaler = RescalerType::New();
    using WriterType = itk::ImageFileWriter< OutputImageType >;
    WriterType::Pointer writer = WriterType::New();

Now we connect the adaptor as the input to the rescaler and set the parameters for the intensity rescaling.

    rescaler->SetOutputMinimum(  0  );
    rescaler->SetOutputMaximum( 255 );
  
    rescaler->SetInput( adaptor );
    writer->SetInput( rescaler->GetOutput() );

Finally, we invoke the Update() method on the writer and take precautions to catch any exception that may be thrown during the execution of the pipeline.

    try
      {
      writer->Update();
      }
    catch( itk::ExceptionObject & excp )
      {
      std::cerr << "Exception caught " << excp << std::endl;
      return EXIT_FAILURE;
      }

ImageAdaptors for the green and blue channels can easily be implemented by modifying the pixel accessor of the red channel and then using the new pixel accessor for instantiating the type of an image adaptor. The following define a green channel pixel accessor.

    class GreenChannelPixelAccessor
    {
    public:
      using InternalType = itk::RGBPixel<float>;
      using ExternalType = float;
  
      static ExternalType Get( const InternalType & input )
        {
        return static_cast<ExternalType>( input.GetGreen() );
        }
      };

A blue channel pixel accessor is similarly defined.

    class BlueChannelPixelAccessor
      {
    public:
      using InternalType = itk::RGBPixel<float>;
      using ExternalType = float;
  
      static ExternalType Get( const InternalType & input )
        {
        return static_cast<ExternalType>( input.GetBlue() );
        }
      };


PIC PIC PIC PIC

Figure 7.2: Using ImageAdaptor to extract the components of an RGB image. The image on the left is a subregion of the Visible Woman cryogenic data set. The red, green and blue components are shown from left to right as scalar images extracted with an ImageAdaptor.


Figure 7.2 shows the result of extracting the red, green and blue components from a region of the Visible Woman cryogenic data set.

7.3 Adapting Vector Images

The source code for this section can be found in the file
ImageAdaptor3.cxx.

This example illustrates the use of itk::ImageAdaptor to obtain access to the components of a vector image. Specifically, it shows how to manage pixel accessors containing internal parameters. In this example we create an image of vectors by using a gradient filter. Then, we use an image adaptor to extract one of the components of the vector image. The vector type used by the gradient filter is the itk::CovariantVector class.

We start by including the relevant headers.

  #include "itkGradientRecursiveGaussianImageFilter.h"

A pixel accessors class may have internal parameters that affect the operations performed on input pixel data. Image adaptors support parameters in their internal pixel accessor by using the assignment operator. Any pixel accessor which has internal parameters must therefore implement the assignment operator. The following defines a pixel accessor for extracting components from a vector pixel. The m_Index member variable is used to select the vector component to be returned.

  class VectorPixelAccessor
  {
  public:
    using InternalType = itk::CovariantVector<float,2>;
    using ExternalType = float;
  
    VectorPixelAccessor()  {}
  
    VectorPixelAccessor & operator=( const VectorPixelAccessor & vpa ) = default;
    ExternalType Get( const InternalType & input ) const
      {
      return static_cast<ExternalType>( input[ m_Index ] );
      }
    void SetIndex( unsigned int index )
      {
      m_Index = index;
      }
  
  private:
    unsigned int m_Index{0};
  };

The Get() method simply returns the i-th component of the vector as indicated by the index. The assignment operator transfers the value of the index member variable from one instance of the pixel accessor to another.

In order to test the pixel accessor, we generate an image of vectors using the itk::GradientRecursiveGaussianImageFilter. This filter produces an output image of itk::CovariantVector pixel type. Covariant vectors are the natural representation for gradients since they are the equivalent of normals to iso-values manifolds.

    using InputPixelType = unsigned char;
    constexpr unsigned int Dimension = 2;
    using InputImageType = itk::Image< InputPixelType, Dimension >;
    using VectorPixelType = itk::CovariantVector< float, Dimension >;
    using VectorImageType = itk::Image< VectorPixelType, Dimension >;
    using GradientFilterType =
      itk::GradientRecursiveGaussianImageFilter< InputImageType,
        VectorImageType>;
  
    GradientFilterType::Pointer gradient = GradientFilterType::New();

We instantiate the ImageAdaptor using the vector image type as the first template parameter and the pixel accessor as the second template parameter.

    using ImageAdaptorType = itk::ImageAdaptor< VectorImageType,
                                itk::VectorPixelAccessor >;
  
    ImageAdaptorType::Pointer adaptor = ImageAdaptorType::New();

The index of the component to be extracted is specified from the command line. In the following, we create the accessor, set the index and connect the accessor to the image adaptor using the SetPixelAccessor() method.

    itk::VectorPixelAccessor  accessor;
    accessor.SetIndex( std::stoi( argv[3] ) );
    adaptor->SetPixelAccessor( accessor );

We create a reader to load the image specified from the command line and pass its output as the input to the gradient filter.

    using ReaderType = itk::ImageFileReader< InputImageType >;
    ReaderType::Pointer reader = ReaderType::New();
    gradient->SetInput( reader->GetOutput() );
  
    reader->SetFileName( argv[1] );
    gradient->Update();

We now connect the output of the gradient filter as input to the image adaptor. The adaptor emulates a scalar image whose pixel values are taken from the selected component of the vector image.

    adaptor->SetImage( gradient->GetOutput() );


PIC PIC PIC

Figure 7.3: Using ImageAdaptor to access components of a vector image. The input image on the left was passed through a gradient image filter and the two components of the resulting vector image were extracted using an image adaptor.


As in the previous example, we rescale the scalar image before writing the image out to file. Figure 7.3 shows the result of applying the example code for extracting both components of a two dimensional gradient.

7.4 Adaptors for Simple Computation

The source code for this section can be found in the file
ImageAdaptor4.cxx.

Image adaptors can also be used to perform simple pixel-wise computations on image data. The following example illustrates how to use the itk::ImageAdaptor for image thresholding.

A pixel accessor for image thresholding requires that the accessor maintain the threshold value. Therefore, it must also implement the assignment operator to set this internal parameter.

  class ThresholdingPixelAccessor
  {
  public:
    using InternalType = unsigned char;
    using ExternalType = unsigned char;
  
    ThresholdingPixelAccessor()  {};
  
    ExternalType Get( const InternalType & input ) const
      {
      return (input > m_Threshold) ? 1 : 0;
      }
    void SetThreshold( const InternalType threshold )
      {
      m_Threshold = threshold;
      }
  
    ThresholdingPixelAccessor &
      operator=( const ThresholdingPixelAccessor & vpa ) = default;
  
  private:
    InternalType m_Threshold{0};
  };
  }

The Get() method returns one if the input pixel is above the threshold and zero otherwise. The assignment operator transfers the value of the threshold member variable from one instance of the pixel accessor to another.

To create an image adaptor, we first instantiate an image type whose pixel type is the same as the internal pixel type of the pixel accessor.

    using PixelType = itk::ThresholdingPixelAccessor::InternalType;
    constexpr unsigned int Dimension = 2;
    using ImageType = itk::Image< PixelType,  Dimension >;

We instantiate the ImageAdaptor using the image type as the first template parameter and the pixel accessor as the second template parameter.

    using ImageAdaptorType = itk::ImageAdaptor< ImageType,
                               itk::ThresholdingPixelAccessor >;
  
    ImageAdaptorType::Pointer adaptor = ImageAdaptorType::New();

The threshold value is set from the command line. A threshold pixel accessor is created and connected to the image adaptor in the same manner as in the previous example.

    itk::ThresholdingPixelAccessor  accessor;
    accessor.SetThreshold( std::stoi( argv[3] ) );
    adaptor->SetPixelAccessor( accessor );

We create a reader to load the input image and connect the output of the reader as the input to the adaptor.

    using ReaderType = itk::ImageFileReader< ImageType >;
    ReaderType::Pointer reader = ReaderType::New();
    reader->SetFileName( argv[1] );
    reader->Update();
  
    adaptor->SetImage( reader->GetOutput() );


PIC PIC PIC

Figure 7.4: Using ImageAdaptor to perform a simple image computation. An ImageAdaptor is used to perform binary thresholding on the input image on the left. The center image was created using a threshold of 180, while the image on the right corresponds to a threshold of 220.


As before, we rescale the emulated scalar image before writing it out to file. Figure 7.4 illustrates the result of applying the thresholding adaptor to a typical gray scale image using two different threshold values. Note that the same effect could have been achieved by using the itk::BinaryThresholdImageFilter but at the price of holding an extra copy of the image in memory.

7.5 Adaptors and Writers

Image adaptors will not behave correctly when connected directly to a writer. The reason is that writers tend to get direct access to the image buffer from their input, since image adaptors do not have a real buffer their behavior in this circumstances is incorrect. You should avoid instantiating the ImageFileWriter or the ImageSeriesWriter over an image adaptor type.