Chapter 27
Image Adaptors


PIC

Figure 27.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 26).

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.

27.1 Image Casting

The source code for this example can be found in the file
Examples/DataRepresentation/Image/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 "otbImage.h" 
#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: 
  typedef unsigned char InternalType; 
  typedef float         ExternalType; 
 
  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.

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

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

  typedef otb::ImageFileReader<ImageType> ReaderType; 
  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.

  typedef itk::ImageRegionIteratorWithIndex<ImageAdaptorType> IteratorType; 
  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 otb::Image class.

27.2 Adapting RGB Images

The source code for this example can be found in the file
Examples/DataRepresentation/Image/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: 
  typedef itk::RGBPixel<float> InternalType; 
  typedef               float  ExternalType; 
 
  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.

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

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

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

We create an itk::RescaleIntensityImageFilter and an otb::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.

  typedef otb::Image<unsigned char, Dimension> OutputImageType; 
  typedef itk::RescaleIntensityImageFilter<ImageAdaptorType, 
      OutputImageType 
      >   RescalerType; 
 
  RescalerType::Pointer rescaler = RescalerType::New(); 
  typedef otb::ImageFileWriter<OutputImageType> WriterType; 
  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 1; 
    }

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: 
    typedef itk::RGBPixel<float> InternalType; 
    typedef               float  ExternalType; 
 
    static ExternalType Get(const InternalType& input) 
    { 
      return static_cast<ExternalType>(input.GetGreen()); 
    } 
  };

A blue channel pixel accessor is similarly defined.

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

27.3 Adapting Vector Images

The source code for this example can be found in the file
Examples/DataRepresentation/Image/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: 
  typedef itk::CovariantVector<float, 2> InternalType; 
  typedef                      float     ExternalType; 
 
  void operator =(const VectorPixelAccessor& vpa) 
  { 
    m_Index = vpa.m_Index; 
  } 
  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; 
};

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.

  typedef unsigned char InputPixelType; 
  const unsigned int Dimension = 2; 
  typedef otb::Image<InputPixelType,  Dimension> InputImageType; 
  typedef itk::CovariantVector<float, Dimension> VectorPixelType; 
  typedef otb::Image<VectorPixelType, Dimension> VectorImageType; 
  typedef itk::GradientRecursiveGaussianImageFilter<InputImageType, 
      VectorImageType> 
  GradientFilterType; 
 
  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.

  typedef itk::ImageAdaptor<VectorImageType, 
      VectorPixelAccessor> ImageAdaptorType; 
 
  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.

  VectorPixelAccessor accessor; 
  accessor.SetIndex(atoi(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.

  typedef otb::ImageFileReader<InputImageType> ReaderType; 
  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());

27.4 Adaptors for Simple Computation

The source code for this example can be found in the file
Examples/DataRepresentation/Image/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: 
  typedef unsigned char InternalType; 
  typedef unsigned char ExternalType; 
 
  ExternalType Get(const InternalType& input) const 
  { 
    return (input > m_Threshold) ? 1 : 0; 
  } 
  void SetThreshold(const InternalType threshold) 
  { 
    m_Threshold = threshold; 
  } 
 
  void operator =(const ThresholdingPixelAccessor& vpa) 
  { 
    m_Threshold = vpa.m_Threshold; 
  } 
private: 
  InternalType m_Threshold; 
};

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.

  typedef ThresholdingPixelAccessor::InternalType PixelType; 
  const unsigned int Dimension = 2; 
  typedef otb::Image<PixelType,  Dimension> ImageType;

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

  typedef itk::ImageAdaptor<ImageType, 
      ThresholdingPixelAccessor> ImageAdaptorType; 
 
  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.

  ThresholdingPixelAccessor accessor; 
  accessor.SetThreshold(atoi(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.

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


PIC PIC PIC

Figure 27.2: 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 100, while the image on the right corresponds to a threshold of 200.


As before, we rescale the emulated scalar image before writing it out to file. Figure 27.2 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.

27.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.