Chapter 19
Classification

19.1 Introduction

Image classification consists in extracting added-value information from images. Such processing methods classify pixels within images into geographical connected zones with similar properties, and identified by a common class label. The classification can be either unsupervised or supervised.

Unsupervised classification does not require any additional information about the properties of the input image to classify it. On the contrary, supervised methods need a preliminary learning to be computed over training datasets having similar properties than the image to classify, in order to build a classification model.

19.2 Machine Learning Framework

19.2.1 Machine learning models

The OTB classification is implemented as a generic Machine Learning framework, supporting several possible machine learning libraries as backends. The base class otb::MachineLearningModel defines this framework. As of now libSVM (the machine learning library historically integrated in OTB), machine learning methods of OpenCV library ([14]) and also Shark machine learning library ([67]) are available. Both supervised and unsupervised classifiers are supported in the framework.

The current list of classifiers available through the same generic interface within the OTB is:

These models have a common interface, with the following major functions:

The PredictBatch(...) function can be multi-threaded when called either from a multi-threaded filter, or from a single location. In the later case, it creates several threads using OpenMP. There is a factory mechanism on top of the model class (see otb::MachineLearningModelFactory ). Given an input file, the static function CreateMachineLearningModel(...) is able to instantiate a model of the right type.

For unsupervised models, the target samples still have to be set. They won’t be used so you can fill a ListSample with zeros.

19.2.2 Training a model

The models are trained from a list of input samples, stored in a itk::Statistics::ListSample . For supervised classifiers, they also need a list of targets associated to each input sample. Whatever the source of samples, it has to be converted into a ListSample before being fed into the model.

Then, model-specific parameters can be set. And finally, the Train() method starts the learning step. Once the model is trained it can be saved to file using the function Save(). The following examples show how to do that.

The source code for this example can be found in the file
Examples/Learning/TrainMachineLearningModelFromSamplesExample.cxx.

This example illustrates the use of the otb::SVMMachineLearningModel class, which inherits from the otb::MachineLearningModel class. This class allows the estimation of a classification model (supervised learning) from samples. In this example, we will train an SVM model with 4 output classes, from 1000 randomly generated training samples, each of them having 7 components. We start by including the appropriate header files.

  // List sample generator
  #include "otbListSampleGenerator.h"
  
  // Random number generator
  
  // SVM model Estimator
  #include "otbSVMMachineLearningModel.h"

The input parameters of the sample generator and of the SVM classifier are initialized.

    int nbSamples = 1000;
    int nbSampleComponents = 7;
    int nbClasses = 4;

Two lists are generated into a itk::Statistics::ListSample which is the structure used to handle both lists of samples and of labels for the machine learning classes derived from otb::MachineLearningModel . The first list is composed of feature vectors representing multi-component samples, and the second one is filled with their corresponding class labels. The list of labels is composed of scalar values.

    // Input related typedefs
    typedef float InputValueType;
    typedef itk::VariableLengthVector<InputValueType> InputSampleType;
    typedef itk::Statistics::ListSample<InputSampleType> InputListSampleType;
  
    // Target related typedefs
    typedef int TargetValueType;
    typedef itk::FixedArray<TargetValueType, 1> TargetSampleType;
    typedef itk::Statistics::ListSample<TargetSampleType> TargetListSampleType;
  
    InputListSampleType::Pointer InputListSample = InputListSampleType::New();
    TargetListSampleType::Pointer TargetListSample = TargetListSampleType::New();
  
    InputListSample->SetMeasurementVectorSize(nbSampleComponents);

In this example, the list of multi-component training samples is randomly filled with a random number generator based on the itk::Statistics::MersenneTwisterRandomVariateGenerator class. Each component’s value is generated from a normal law centered around the corresponding class label of each sample multiplied by 100, with a standard deviation of 10.

    itk::Statistics::MersenneTwisterRandomVariateGenerator::Pointer randGen;
    randGen = itk::Statistics::MersenneTwisterRandomVariateGenerator::GetInstance();
  
    // Filling the two input training lists
    for (int i = 0; i < nbSamples; ++i)
      {
      InputSampleType sample;
      TargetValueType label = (i % nbClasses) + 1;
  
      // Multi-component sample randomly filled from a normal law for each component
      sample.SetSize(nbSampleComponents);
      for (int itComp = 0; itComp < nbSampleComponents; ++itComp)
        {
        sample[itComp] = randGen->GetNormalVariate(100  label, 10);
        }
  
      InputListSample->PushBack(sample);
      TargetListSample->PushBack(label);
      }

Once both sample and label lists are generated, the second step consists in declaring the machine learning classifier. In our case we use an SVM model with the help of the otb::SVMMachineLearningModel class which is derived from the otb::MachineLearningModel class. This pure virtual class is based on the machine learning framework of the OpenCV library ([14]) which handles other classifiers than the SVM.

    typedef otb::SVMMachineLearningModel<InputValueType, TargetValueType> SVMType;
  
    SVMType::Pointer SVMClassifier = SVMType::New();
  
    SVMClassifier->SetInputListSample(InputListSample);
    SVMClassifier->SetTargetListSample(TargetListSample);
  
    SVMClassifier->SetKernelType(CvSVM::LINEAR);

Once the classifier is parametrized with both input lists and default parameters, except for the kernel type in our example of SVM model estimation, the model training is computed with the Train method. Finally, the Save method exports the model to a text file. All the available classifiers based on OpenCV are implemented with these interfaces. Like for the SVM model training, the other classifiers can be parametrized with specific settings.

    SVMClassifier->Train();
    SVMClassifier->Save(outputModelFileName);

The source code for this example can be found in the file
Examples/Learning/TrainMachineLearningModelFromImagesExample.cxx.

This example illustrates the use of the otb::MachineLearningModel class. This class allows the estimation of a classification model (supervised learning) from images. In this example, we will train an SVM with 4 classes. We start by including the appropriate header files.

  // List sample generator
  #include "otbListSampleGenerator.h"
  
  // Extract a ROI of the vectordata
  #include "otbVectorDataIntoImageProjectionFilter.h"
  
  // SVM model Estimator
  #include "otbSVMMachineLearningModel.h"

In this framework, we must transform the input samples store in a vector data into a itk::Statistics::ListSample which is the structure compatible with the machine learning classes. On the one hand, we are using feature vectors for the characterization of the classes, and on the other hand, the class labels are scalar values. We first re-project the input vector data over the input image, using the otb::VectorDataIntoImageProjectionFilter class. To convert the input samples store in a vector data into a itk::Statistics::ListSample , we use the otb::ListSampleGenerator class.

    // VectorData projection filter
    typedef otb::VectorDataIntoImageProjectionFilter<VectorDataType, InputImageType>
                                                          VectorDataReprojectionType;
  
    InputReaderType::Pointer inputReader = InputReaderType::New();
    inputReader->SetFileName(inputImageFileName);
  
    InputImageType::Pointer image = inputReader->GetOutput();
    image->UpdateOutputInformation();
  
    // Read the Vectordata
    VectorDataReaderType::Pointer vectorReader = VectorDataReaderType::New();
    vectorReader->SetFileName(trainingShpFileName);
    vectorReader->Update();
  
    VectorDataType::Pointer vectorData = vectorReader->GetOutput();
    vectorData->Update();
  
    VectorDataReprojectionType::Pointer vdreproj = VectorDataReprojectionType::New();
  
    vdreproj->SetInputImage(image);
    vdreproj->SetInput(vectorData);
    vdreproj->SetUseOutputSpacingAndOriginFromImage(false);
  
    vdreproj->Update();
  
    typedef otb::ListSampleGenerator<InputImageType, VectorDataType>
                                                             ListSampleGeneratorType;
    ListSampleGeneratorType::Pointer sampleGenerator;
    sampleGenerator = ListSampleGeneratorType::New();
  
    sampleGenerator->SetInput(image);
    sampleGenerator->SetInputVectorData(vdreproj->GetOutput());
    sampleGenerator->SetClassKey("Class");
  
    sampleGenerator->Update();

Now, we need to declare the machine learning model which will be used by the classifier. In this example, we train an SVM model. The otb::SVMMachineLearningModel class inherits from the pure virtual class otb::MachineLearningModel which is templated over the type of values used for the measures and the type of pixels used for the labels. Most of the classification and regression algorithms available through this interface in OTB is based on the OpenCV library [14]. Specific methods can be used to set classifier parameters. In the case of SVM, we set here the type of the kernel. Other parameters are let with their default values.

    typedef otb::SVMMachineLearningModel
                                   <InputImageType::InternalPixelType,
                                    ListSampleGeneratorType::ClassLabelType> SVMType;
  
    SVMType::Pointer SVMClassifier = SVMType::New();
  
    SVMClassifier->SetInputListSample(sampleGenerator->GetTrainingListSample());
    SVMClassifier->SetTargetListSample(sampleGenerator->GetTrainingListLabel());
  
    SVMClassifier->SetKernelType(CvSVM::LINEAR);

The machine learning interface is generic and gives access to other classifiers. We now train the SVM model using the Train and save the model to a text file using the Save method.

    SVMClassifier->Train();
    SVMClassifier->Save(outputModelFileName);

You can now use the Predict method which takes a itk::Statistics::ListSample as input and estimates the label of each input sample using the model. Finally, the otb::ImageClassificationModel inherits from the itk::ImageToImageFilter and allows classifying pixels in the input image by predicting their labels using a model.

19.2.3 Prediction of a model

For the prediction step, the usual process is to:

There is an image filter that perform this step on a whole image, supporting streaming and multi-threading: otb::ImageClassificationFilter .

The source code for this example can be found in the file
Examples/Classification/SupervisedImageClassificationExample.cxx.

In OTB, a generic streamed filter called otb::ImageClassificationFilter is available to classify any input multi-channel image according to an input classification model file. This filter is generic because it works with any classification model type (SVM, KNN, Artificial Neural Network,...) generated within the OTB generic Machine Learning framework based on OpenCV ([14]). The input model file is smartly parsed according to its content in order to identify which learning method was used to generate it. Once the classification method and model are known, the input image can be classified. More details are given in subsections ?? and ?? to generate a classification model either from samples or from images. In this example we will illustrate its use. We start by including the appropriate header files.

  #include "otbMachineLearningModelFactory.h"
  #include "otbImageClassificationFilter.h"

We will assume double precision input images and will also define the type for the labeled pixels.

    const unsigned int     Dimension = 2;
    typedef double         PixelType;
    typedef unsigned short LabeledPixelType;

Our classifier is generic enough to be able to process images with any number of bands. We read the input image as a otb::VectorImage . The labeled image will be a scalar image.

    typedef otb::VectorImage<PixelType, Dimension>  ImageType;
    typedef otb::Image<LabeledPixelType, Dimension> LabeledImageType;

We can now define the type for the classifier filter, which is templated over its input and output image types.

    typedef otb::ImageClassificationFilter<ImageType, LabeledImageType>
                                                            ClassificationFilterType;
    typedef ClassificationFilterType::ModelType ModelType;

Moreover, it is necessary to define a otb::MachineLearningModelFactory which is templated over its input and output pixel types. This factory is used to parse the input model file and to define which classification method to use.

    typedef otb::MachineLearningModelFactory<PixelType, LabeledPixelType>
                                                     MachineLearningModelFactoryType;

And finally, we define the reader and the writer. Since the images to classify can be very big, we will use a streamed writer which will trigger the streaming ability of the classifier.

    typedef otb::ImageFileReader<ImageType>        ReaderType;
    typedef otb::ImageFileWriter<LabeledImageType> WriterType;

We instantiate the classifier and the reader objects and we set the existing model obtained in a previous training step.

    ClassificationFilterType::Pointer filter = ClassificationFilterType::New();
  
    ReaderType::Pointer reader = ReaderType::New();
    reader->SetFileName(infname);

The input model file is parsed according to its content and the generated model is then loaded within the otb::ImageClassificationFilter .

    ModelType::Pointer model;
    model = MachineLearningModelFactoryType::CreateMachineLearningModel(
                                          modelfname,
                                          MachineLearningModelFactoryType::ReadMode);
    model->Load(modelfname);
  
    filter->SetModel(model);

We plug the pipeline and trigger its execution by updating the output of the writer.

    filter->SetInput(reader->GetOutput());
  
    WriterType::Pointer writer = WriterType::New();
    writer->SetInput(filter->GetOutput());
    writer->SetFileName(outfname);
    writer->Update();

19.2.4 Integration in applications

The classifiers are integrated in several OTB Applications. There is a base class that provides an easy access to all the classifiers: otb::Wrapper::LearningApplicationBase . As each machine learning model has a specific set of parameters, the base class LearningApplicationBase knows how to expose each type of classifier with its dedicated parameters (a task that is a bit tedious so we want to implement it only once). The DoInit() method creates a choice parameter named classifier which contains the different supported classifiers along with their parameters.

The function Train(...) provide an easy way to train the selected classifier, with the corresponding parameters, and save the model to file.

On the other hand, the function Classify(...) allows to load a model from file and apply it on a list of samples.

19.3 Supervised classification

19.3.1 Support Vector Machines

19.3.1.1 SVM general description

Kernel based learning methods in general and the Support Vector Machines (SVM) in particular, have been introduced in the last years in learning theory for classification and regression tasks, [132]. SVM have been successfully applied to text categorization, [74], and face recognition, [102]. Recently, they have been successfully used for the classification of hyperspectral remote-sensing images, [15].

Simply stated, the approach consists in searching for the separating surface between 2 classes by the determination of the subset of training samples which best describes the boundary between the 2 classes. These samples are called support vectors and completely define the classification system. In the case where the two classes are nonlinearly separable, the method uses a kernel expansion in order to make projections of the feature space onto higher dimensionality spaces where the separation of the classes becomes linear.

19.3.1.2 SVM mathematical formulation

This subsection reminds the basic principles of SVM learning and classification. A good tutorial on SVM can be found in, [17].

We have N samples represented by the couple (yi,xi),i = 1N where yi ∈{-1,+1} is the class label and xi n is the feature vector of dimension n. A classifier is a function

f(x,α ):x↦→ y
where α are the classifier parameters. The SVM finds the optimal separating hyperplane which fulfills the following constraints :

The separating hyperplane has the equation

w⋅x+ b= 0;
with w being its normal vector and x being any point of the hyperplane. The orthogonal distance to the origin is given by -|b|
∥w∥. Vectors located outside the hyperplane have either w x +b > 0 or w x +b < 0.

Therefore, the classifier function can be written as

f(x,w,b)= sgn(w ⋅x+ b).

The SVs are placed on two hyperplanes which are parallel to the optimal separating one. In order to find the optimal hyperplane, one sets w and b :

w⋅x+ b =±1.

Since there must not be any vector inside the margin, the following constraint can be used:

w ⋅x +b ≥ +1ify = +1;
    i          i
w ⋅xi+b ≤ - 1ifyi = - 1;
which can be rewritten as
yi(w ⋅xi+ b)- 1≥ 0 ∀i.

The orthogonal distances of the 2 parallel hyperplanes to the origin are |1-b|
∥w∥ and |-1-b|
 ∥w∥. Therefore the modulus of the margin is equal to 2∥w∥- and it has to be maximised.

Thus, the problem to be solved is:

This problem can be solved by using the Lagrange multipliers with one multiplier per sample. It can be shown that only the support vectors will have a positive Lagrange multiplier.

In the case where the two classes are not exactly linearly separable, one can modify the constraints above by using

w⋅xi+ b≥ +1 - ξi ifyi = +1;
w⋅xi+ b≤ - 1 +ξi ifyi = - 1;
ξi ≥0 ∀i.

If ξi > 1, one considers that the sample is wrong. The function which has then to be minimised is 1
2w2 +C(∑iξi);, where C is a tolerance parameter. The optimisation problem is the same than in the linear case, but one multiplier has to be added for each new constraint ξi 0.

If the decision surface needs to be non-linear, this solution cannot be applied and the kernel approach has to be adopted.

One drawback of the SVM is that, in their basic version, they can only solve two-class problems. Some works exist in the field of multi-class SVM (see [4136], and the comparison made by [59]), but they are not used in our system.

You have to be aware that to achieve better convergence of the algorithm it is strongly advised to normalize feature vector components in the [-1;1] interval.

For problems with N > 2 classes, one can choose either to train N SVM (one class against all the others), or to train N ×(N -1) SVM (one class against each of the others). In the second approach, which is the one that we use, the final decision is taken by choosing the class which is most often selected by the whole set of SVM.

19.3.2 Shark Random Forests

The Random Forests algorithm is also available in OTB machine learning framework. This model builds a set of decision trees. Each tree may not give a reliable prediction, but taking them together, they form a robust classifier. The prediction of this model is the mode of the predictions of individual trees.

There are two implementations: one in OpenCV and the other on in Shark. The Shark implementation has a noteworthy advantage: the training step is parallel. It uses the following parameters:

Except these specific parameter, its usage is exactly the same as the other machine learning models (such as the SVM model).

19.3.3 Generic Kernel SVM (deprecated)

OTB has developed a specific interface for user-defined kernels. However, the following functions use a deprecated OTB interface. The code source for these Generic Kernels has been removed from the official repository. It is now available as a remote module: GKSVM.

A function k(,) is considered to be a kernel when:

g() L2(Rn) so that g(x)2dx be finite, (19.1)
then k(x,y)g(x)g(y)dxdy 0,
which is known as the Mercer condition.

When defined through the OTB, a kernel is a class that inherits from GenericKernelFunctorBase. Several virtual functions have to be overloaded:

Some pre-defined generic kernels have already been implemented in OTB:

19.3.3.1 Learning with User Defined Kernels

The source code for this example can be found in the file
Examples/Learning/SVMGenericKernelImageModelEstimatorExample.cxx.

This example illustrates the modifications to be added to the use of otb::SVMImageModelEstimator in order to add a user defined kernel. This initial program has been explained in section ??.

The first thing to do is to include the header file for the new kernel.

  #include "otbSVMImageModelEstimator.h"
  #include "otbMixturePolyRBFKernelFunctor.h"

Once the otb::SVMImageModelEstimator is instantiated, it is possible to add the new kernel and its parameters.

Then in addition to the initial code:

    EstimatorType::Pointer svmEstimator = EstimatorType::New();
  
    svmEstimator->SetSVMType(C_SVC);
    svmEstimator->SetInputImage(inputReader->GetOutput());
    svmEstimator->SetTrainingImage(trainingReader->GetOutput());

The instantiation of the kernel is to be implemented. The kernel which is used here is a linear combination of a polynomial kernel and an RBF one. It is written as

μk1(x,y)+(1- μ)k2(x,y)
with k1(x,y) = (γ1x⋅y+c0)d and k2(x,y) = exp(        2)
 - γ2∥x- y∥. Then, the specific parameters of this kernel are:

Their instantiations are achieved through the use of the SetValue function.

    otb::MixturePolyRBFKernelFunctor myKernel;
    myKernel.SetValue("Mixture", 0.5);
    myKernel.SetValue("GammaPoly", 1.0);
    myKernel.SetValue("CoefPoly", 0.0);
    myKernel.SetValue("DegreePoly", 1);
    myKernel.SetValue("GammaRBF", 1.5);
    myKernel.Update();

Once the kernel’s parameters are affected and the kernel updated, the connection to otb::SVMImageModelEstimator takes place here.

    svmEstimator->SetKernelFunctor(&myKernel);
    svmEstimator->SetKernelType(GENERIC);

The model estimation procedure is triggered by calling the estimator’s Update method.

    svmEstimator->Update();

The rest of the code remains unchanged...

    svmEstimator->SaveModel(outputModelFileName);

In the file outputModelFileName a specific line will appear when using a generic kernel. It gives the name of the kernel and its parameters name and value.

19.3.3.2 Classification with user defined kernel

The source code for this example can be found in the file
Examples/Learning/SVMGenericKernelImageClassificationExample.cxx.

This example illustrates the modifications to be added to use the otb::SVMClassifier class for performing SVM classification on images with a user-defined kernel. In this example, we will use an SVM model estimated in the previous section to separate between water and non-water pixels by using the RGB values only. The first thing to do is include the header file for the class as well as the header of the appropriated kernel to be used.

  #include "otbSVMClassifier.h"
  #include "otbMixturePolyRBFKernelFunctor.h"

We need to declare the SVM model which is to be used by the classifier. The SVM model is templated over the type of value used for the measures and the type of pixel used for the labels.

    typedef otb::SVMModel<PixelType, LabelPixelType> ModelType;
    ModelType::Pointer model = ModelType::New();

After instantiation, we can load a model saved to a file (see section ?? for an example of model estimation and storage to a file).

When using a user defined kernel, an explicit instantiation has to be performed.

    otb::MixturePolyRBFKernelFunctor myKernel;
    model->SetKernelFunctor(&myKernel);

Then, the rest of the classification program remains unchanged.

    model->LoadModel(modelFilename);

19.4 Unsupervised classification

19.4.1 K-Means Classification

19.4.1.1 Shark version

The KMeans algorithm has been implemented in Shark library, and has been wrapped in the OTB machine learning framework. It is the first unsupervised algorithm in this framework. It can be used in the same way as other machine learning models. Remember that even if unsupervised model don’t use a label information on the samples, the target ListSample still has to be set in MachineLearningModel. A ListSample filled with zeros can be used.

This model uses a hard clustering model with the following parameters:

As with Shark Random Forests, the training step is parallel.

19.4.1.2 Simple version

The source code for this example can be found in the file
Examples/Classification/ScalarImageKmeansClassifier.cxx.

This example shows how to use the KMeans model for classifying the pixel of a scalar image.

The itk::Statistics::ScalarImageKmeansImageFilter is used for taking a scalar image and applying the K-Means algorithm in order to define classes that represents statistical distributions of intensity values in the pixels. The classes are then used in this filter for generating a labeled image where every pixel is assigned to one of the classes.

  #include "otbImage.h"
  #include "otbImageFileReader.h"
  #include "otbImageFileWriter.h"
  #include "itkScalarImageKmeansImageFilter.h"

First we define the pixel type and dimension of the image that we intend to classify. With this image type we can also declare the otb::ImageFileReader needed for reading the input image, create one and set its input filename.

    typedef signed short PixelType;
    const unsigned int Dimension = 2;
  
    typedef otb::Image<PixelType, Dimension> ImageType;
  
    typedef otb::ImageFileReader<ImageType> ReaderType;
    ReaderType::Pointer reader = ReaderType::New();
    reader->SetFileName(inputImageFileName);

With the ImageType we instantiate the type of the itk::ScalarImageKmeansImageFilter that will compute the K-Means model and then classify the image pixels.

    typedef itk::ScalarImageKmeansImageFilter<ImageType> KMeansFilterType;
  
    KMeansFilterType::Pointer kmeansFilter = KMeansFilterType::New();
  
    kmeansFilter->SetInput(reader->GetOutput());
  
    const unsigned int numberOfInitialClasses = atoi(argv[4]);

In general the classification will produce as output an image whose pixel values are integers associated to the labels of the classes. Since typically these integers will be generated in order (0, 1, 2, ...N), the output image will tend to look very dark when displayed with naive viewers. It is therefore convenient to have the option of spreading the label values over the dynamic range of the output image pixel type. When this is done, the dynamic range of the pixels is divided by the number of classes in order to define the increment between labels. For example, an output image of 8 bits will have a dynamic range of [0:255], and when it is used for holding four classes, the non-contiguous labels will be (0, 64, 128, 192). The selection of the mode to use is done with the method SetUseContiguousLabels().

    const unsigned int useNonContiguousLabels = atoi(argv[3]);
  
    kmeansFilter->SetUseNonContiguousLabels(useNonContiguousLabels);

For each one of the classes we must provide a tentative initial value for the mean of the class. Given that this is a scalar image, each one of the means is simply a scalar value. Note however that in a general case of K-Means, the input image would be a vector image and therefore the means will be vectors of the same dimension as the image pixels.

    for (unsigned k = 0; k < numberOfInitialClasses; ++k)
      {
      const double userProvidedInitialMean = atof(argv[k + argoffset]);
      kmeansFilter->AddClassWithInitialMean(userProvidedInitialMean);
      }

The itk::ScalarImageKmeansImageFilter is predefined for producing an 8 bits scalar image as output. This output image contains labels associated to each one of the classes in the K-Means algorithm. In the following lines we use the OutputImageType in order to instantiate the type of a otb::ImageFileWriter . Then create one, and connect it to the output of the classification filter.

    typedef KMeansFilterType::OutputImageType OutputImageType;
  
    typedef otb::ImageFileWriter<OutputImageType> WriterType;
  
    WriterType::Pointer writer = WriterType::New();
  
    writer->SetInput(kmeansFilter->GetOutput());
  
    writer->SetFileName(outputImageFileName);

We are now ready for triggering the execution of the pipeline. This is done by simply invoking the Update() method in the writer. This call will propagate the update request to the reader and then to the classifier.

    try
      {
      writer->Update();
      }
    catch (itk::ExceptionObject& excp)
      {
      std::cerr << "Problem encountered while writing ";
      std::cerr << " image file : " << argv[2] << std::endl;
      std::cerr << excp << std::endl;
      return EXIT_FAILURE;
      }

At this point the classification is done, the labeled image is saved in a file, and we can take a look at the means that were found as a result of the model estimation performed inside the classifier filter.

    KMeansFilterType::ParametersType estimatedMeans =
      kmeansFilter->GetFinalMeans();
  
    const unsigned int numberOfClasses = estimatedMeans.Size();
  
    for (unsigned int i = 0; i < numberOfClasses; ++i)
      {
      std::cout << "cluster[" << i << "] ";
      std::cout << "    estimated mean : " << estimatedMeans[i] << std::endl;
      }


PIC PIC

Figure 19.1: Effect of the KMeans classifier. Left: original image. Right: image of classes.


Figure 19.1 illustrates the effect of this filter with three classes. The means can be estimated by ScalarImageKmeansModelEstimator.cxx.

The source code for this example can be found in the file
Examples/Classification/ScalarImageKmeansModelEstimator.cxx.

This example shows how to compute the KMeans model of an Scalar Image.

The itk::Statistics::KdTreeBasedKmeansEstimator is used for taking a scalar image and applying the K-Means algorithm in order to define classes that represents statistical distributions of intensity values in the pixels. One of the drawbacks of this technique is that the spatial distribution of the pixels is not considered at all. It is common therefore to combine the classification resulting from K-Means with other segmentation techniques that will use the classification as a prior and add spatial information to it in order to produce a better segmentation.

    // Create a List from the scalar image
    typedef itk::Statistics::ImageToListSampleAdaptor<ImageType> AdaptorType;
  
    AdaptorType::Pointer adaptor = AdaptorType::New();
  
    adaptor->SetImage(reader->GetOutput());
  
    // Define the Measurement vector type from the AdaptorType
  
    // Create the K-d tree structure
    typedef itk::Statistics::WeightedCentroidKdTreeGenerator<
        AdaptorType>
    TreeGeneratorType;
  
    TreeGeneratorType::Pointer treeGenerator = TreeGeneratorType::New();
  
    treeGenerator->SetSample(adaptor);
    treeGenerator->SetBucketSize(16);
    treeGenerator->Update();
  
    typedef TreeGeneratorType::KdTreeType                         TreeType;
    typedef itk::Statistics::KdTreeBasedKmeansEstimator<TreeType> EstimatorType;
  
    EstimatorType::Pointer estimator = EstimatorType::New();
  
    const unsigned int numberOfClasses = 4;
  
    EstimatorType::ParametersType initialMeans(numberOfClasses);
    initialMeans[0] = 25.0;
    initialMeans[1] = 125.0;
    initialMeans[2] = 250.0;
  
    estimator->SetParameters(initialMeans);
  
    estimator->SetKdTree(treeGenerator->GetOutput());
    estimator->SetMaximumIteration(200);
    estimator->SetCentroidPositionChangesThreshold(0.0);
    estimator->StartOptimization();
  
    EstimatorType::ParametersType estimatedMeans = estimator->GetParameters();
  
    for (unsigned int i = 0; i < numberOfClasses; ++i)
      {
      std::cout << "cluster[" << i << "] " << std::endl;
      std::cout << "    estimated mean : " << estimatedMeans[i] << std::endl;
      }
19.4.1.3 General approach

The source code for this example can be found in the file
Examples/Classification/KMeansImageClassificationExample.cxx.

The K-Means classification proposed by ITK for images is limited to scalar images and is not streamed. In this example, we show how the use of the otb::KMeansImageClassificationFilter allows for a simple implementation of a K-Means classification application. We will start by including the appropirate header file.

  #include "otbKMeansImageClassificationFilter.h"

We will assume double precision input images and will also define the type for the labeled pixels.

    const unsigned int Dimension = 2;
    typedef double         PixelType;
    typedef unsigned short LabeledPixelType;

Our classifier will be generic enough to be able to process images with any number of bands. We read the images as otb::VectorImage s. The labeled image will be a scalar image.

    typedef otb::VectorImage<PixelType, Dimension>  ImageType;
    typedef otb::Image<LabeledPixelType, Dimension> LabeledImageType;

We can now define the type for the classifier filter, which is templated over its input and output image types.

    typedef otb::KMeansImageClassificationFilter<ImageType, LabeledImageType>
    ClassificationFilterType;
    typedef ClassificationFilterType::KMeansParametersType KMeansParametersType;

And finally, we define the reader and the writer. Since the images to classify can be very big, we will use a streamed writer which will trigger the streaming ability of the classifier.

    typedef otb::ImageFileReader<ImageType>                 ReaderType;
    typedef otb::ImageFileWriter<LabeledImageType> WriterType;

We instantiate the classifier and the reader objects and we set their parameters. Please note the call of the GenerateOutputInformation() method on the reader in order to have available the information about the input image (size, number of bands, etc.) without needing to actually read the image.

    ClassificationFilterType::Pointer filter = ClassificationFilterType::New();
  
    ReaderType::Pointer reader = ReaderType::New();
    reader->SetFileName(infname);
    reader->GenerateOutputInformation();

The classifier needs as input the centroids of the classes. We declare the parameter vector, and we read the centroids from the arguments of the program.

    const unsigned int sampleSize =
      ClassificationFilterType::MaxSampleDimension;
    const unsigned int   parameterSize = nbClasses  sampleSize;
    KMeansParametersType parameters;
  
    parameters.SetSize(parameterSize);
    parameters.Fill(0);
  
    for (unsigned int i = 0; i < nbClasses; ++i)
      {
      for (unsigned int j = 0; j <
           reader->GetOutput()->GetNumberOfComponentsPerPixel(); ++j)
        {
        parameters[i  sampleSize + j] =
          atof(argv[4 + i 
                    reader->GetOutput()->GetNumberOfComponentsPerPixel()
                    + j]);
        }
      }
  
    std::cout << "Parameters: " << parameters << std::endl;

We set the parameters for the classifier, we plug the pipeline and trigger its execution by updating the output of the writer.

    filter->SetCentroids(parameters);
    filter->SetInput(reader->GetOutput());
  
    WriterType::Pointer writer = WriterType::New();
    writer->SetInput(filter->GetOutput());
    writer->SetFileName(outfname);
    writer->Update();

19.4.1.4 k-d Tree Based k-Means Clustering

The source code for this example can be found in the file
Examples/Classification/KdTreeBasedKMeansClustering.cxx.

K-means clustering is a popular clustering algorithm because it is simple and usually converges to a reasonable solution. The k-means algorithm works as follows:

  1. Obtains the initial k means input from the user.
  2. Assigns each measurement vector in a sample container to its closest mean among the k number of means (i.e., update the membership of each measurement vectors to the nearest of the k clusters).
  3. Calculates each cluster’s mean from the newly assigned measurement vectors (updates the centroid (mean) of k clusters).
  4. Repeats step 2 and step 3 until it meets the termination criteria.

The most common termination criteria is that if there is no measurement vector that changes its cluster membership from the previous iteration, then the algorithm stops.

The itk::Statistics::KdTreeBasedKmeansEstimator is a variation of this logic. The k-means clustering algorithm is computationally very expensive because it has to recalculate the mean at each iteration. To update the mean values, we have to calculate the distance between k means and each and every measurement vector. To reduce the computational burden, the KdTreeBasedKmeansEstimator uses a special data structure: the k-d tree ( itk::Statistics::KdTree ) with additional information. The additional information includes the number and the vector sum of measurement vectors under each node under the tree architecture.

With such additional information and the k-d tree data structure, we can reduce the computational cost of the distance calculation and means. Instead of calculating each measurement vectors and k means, we can simply compare each node of the k-d tree and the k means. This idea of utilizing a k-d tree can be found in multiple articles [5] [104] [78]. Our implementation of this scheme follows the article by the Kanungo et al [78].

We use the itk::Statistics::ListSample as the input sample, the itk::Vector as the measurement vector. The following code snippet includes their header files.

  #include "itkVector.h"
  #include "itkListSample.h"

Since this k-means algorithm requires a itk::Statistics::KdTree object as an input, we include the KdTree class header file. As mentioned above, we need a k-d tree with the vector sum and the number of measurement vectors. Therefore we use the itk::Statistics::WeightedCentroidKdTreeGenerator instead of the itk::Statistics::KdTreeGenerator that generate a k-d tree without such additional information.

  #include "itkKdTree.h"
  #include "itkWeightedCentroidKdTreeGenerator.h"

The KdTreeBasedKmeansEstimator class is the implementation of the k-means algorithm. It does not create k clusters. Instead, it returns the mean estimates for the k clusters.

  #include "itkKdTreeBasedKmeansEstimator.h"

To generate the clusters, we must create k instances of itk::Statistics::EuclideanDistanceMetric function as the membership functions for each cluster and plug that—along with a sample—into an itk::Statistics::SampleClassifierFilter object to get a itk::Statistics::MembershipSample that stores pairs of measurement vectors and their associated class labels (k labels).

  #include "itkMinimumDecisionRule.h"
  #include "itkSampleClassifierFilter.h"

We will fill the sample with random variables from two normal distribution using the itk::Statistics::NormalVariateGenerator .

  #include "itkNormalVariateGenerator.h"

Since the NormalVariateGenerator class only supports 1-D, we define our measurement vector type as one component vector. We then, create a ListSample object for data inputs. Each measurement vector is of length 1. We set this using the SetMeasurementVectorSize() method.

    typedef itk::Vector<double,
        1>
    MeasurementVectorType;
    typedef itk::Statistics::ListSample<MeasurementVectorType> SampleType;
    SampleType::Pointer sample = SampleType::New();
    sample->SetMeasurementVectorSize(1);

The following code snippet creates a NormalVariateGenerator object. Since the random variable generator returns values according to the standard normal distribution (The mean is zero, and the standard deviation is one), before pushing random values into the sample, we change the mean and standard deviation. We want two normal (Gaussian) distribution data. We have two for loops. Each for loop uses different mean and standard deviation. Before we fill the sample with the second distribution data, we call Initialize(random seed) method, to recreate the pool of random variables in the normalGenerator.

To see the probability density plots from the two distribution, refer to the Figure 19.2.


PIC

Figure 19.2: Two normal distributions’ probability density plot (The means are 100 and 200, and the standard deviation is 30 )


    typedef itk::Statistics::NormalVariateGenerator NormalGeneratorType;
    NormalGeneratorType::Pointer normalGenerator = NormalGeneratorType::New();
  
    normalGenerator->Initialize(101);
  
    MeasurementVectorType mv;
    double                mean = 100;
    double                standardDeviation = 30;
    for (unsigned int i = 0; i < 100; ++i)
      {
      mv[0] = (normalGenerator->GetVariate()  standardDeviation) + mean;
      sample->PushBack(mv);
      }
  
    normalGenerator->Initialize(3024);
    mean = 200;
    standardDeviation = 30;
    for (unsigned int i = 0; i < 100; ++i)
      {
      mv[0] = (normalGenerator->GetVariate()  standardDeviation) + mean;
      sample->PushBack(mv);
      }

We create a k-d tree.

    typedef itk::Statistics::WeightedCentroidKdTreeGenerator<SampleType>
    TreeGeneratorType;
    TreeGeneratorType::Pointer treeGenerator = TreeGeneratorType::New();
  
    treeGenerator->SetSample(sample);
    treeGenerator->SetBucketSize(16);
    treeGenerator->Update();

Once we have the k-d tree, it is a simple procedure to produce k mean estimates.

We create the KdTreeBasedKmeansEstimator. Then, we provide the initial mean values using the SetParameters(). Since we are dealing with two normal distribution in a 1-D space, the size of the mean value array is two. The first element is the first mean value, and the second is the second mean value. If we used two normal distributions in a 2-D space, the size of array would be four, and the first two elements would be the two components of the first normal distribution’s mean vector. We plug-in the k-d tree using the SetKdTree().

The remaining two methods specify the termination condition. The estimation process stops when the number of iterations reaches the maximum iteration value set by the SetMaximumIteration(), or the distances between the newly calculated mean (centroid) values and previous ones are within the threshold set by the SetCentroidPositionChangesThreshold(). The final step is to call the StartOptimization() method.

The for loop will print out the mean estimates from the estimation process.

    typedef TreeGeneratorType::KdTreeType                         TreeType;
    typedef itk::Statistics::KdTreeBasedKmeansEstimator<TreeType> EstimatorType;
    EstimatorType::Pointer estimator = EstimatorType::New();
  
    EstimatorType::ParametersType initialMeans(2);
    initialMeans[0] = 0.0;
    initialMeans[1] = 0.0;
  
    estimator->SetParameters(initialMeans);
    estimator->SetKdTree(treeGenerator->GetOutput());
    estimator->SetMaximumIteration(200);
    estimator->SetCentroidPositionChangesThreshold(0.0);
    estimator->StartOptimization();
  
    EstimatorType::ParametersType estimatedMeans = estimator->GetParameters();
  
    for (unsigned int i = 0; i < 2; ++i)
      {
      std::cout << "cluster[" << i << "] " << std::endl;
      std::cout << "    estimated mean : " << estimatedMeans[i] << std::endl;
      }

If we are only interested in finding the mean estimates, we might stop. However, to illustrate how a classifier can be formed using the statistical classification framework. We go a little bit further in this example.

Since the k-means algorithm is an minimum distance classifier using the estimated k means and the measurement vectors. We use the EuclideanDistanceMetric class as membership functions. Our choice for the decision rule is the itk::Statistics::MinimumDecisionRule that returns the index of the membership functions that have the smallest value for a measurement vector.

After creating a SampleClassifierFilter object and a MinimumDecisionRule object, we plug-in the decisionRule and the sample to the classifier. Then, we must specify the number of classes that will be considered using the SetNumberOfClasses() method.

The remainder of the following code snippet shows how to use user-specified class labels. The classification result will be stored in a MembershipSample object, and for each measurement vector, its class label will be one of the two class labels, 100 and 200 (unsigned int).

    typedef itk::Statistics::DistanceToCentroidMembershipFunction<
      MeasurementVectorType >   MembershipFunctionType;
    typedef itk::Statistics::EuclideanDistanceMetric< MeasurementVectorType >  DistanceMetricType;
  
    typedef itk::Statistics::MinimumDecisionRule DecisionRuleType;
    DecisionRuleType::Pointer decisionRule = DecisionRuleType::New();
  
    typedef itk::Statistics::SampleClassifierFilter<SampleType> ClassifierType;
    ClassifierType::Pointer classifier = ClassifierType::New();
  
    classifier->SetDecisionRule(decisionRule);
    classifier->SetInput(sample);
    classifier->SetNumberOfClasses(2);
  
  
    typedef ClassifierType::ClassLabelVectorObjectType               ClassLabelVectorObjectType;
    ClassLabelVectorObjectType::Pointer classLabels  = ClassLabelVectorObjectType::New();
    classLabels->Get().push_back(100);
    classLabels->Get().push_back(200);
  
    classifier->SetClassLabels(classLabels);

The classifier is almost ready to do the classification process except that it needs two membership functions that represents two clusters respectively.

In this example, the two clusters are modeled by two Euclidean distance functions. The distance function (model) has only one parameter, its mean (centroid) set by the SetOrigin() method. To plug-in two distance functions, we call the AddMembershipFunction() method. Then invocation of the Update() method will perform the classification.

    typedef ClassifierType::MembershipFunctionVectorObjectType       MembershipFunctionVectorObjectType;
    MembershipFunctionVectorObjectType::Pointer membershipFunctions =
                                          MembershipFunctionVectorObjectType::New();
  
  //  std::vector<MembershipFunctionType::Pointer> membershipFunctions;
  
    DistanceMetricType::OriginType origin(
      sample->GetMeasurementVectorSize());
    int index = 0;
    for (unsigned int i = 0; i < 2; ++i)
      {
      MembershipFunctionType::Pointer membershipFunction1 = MembershipFunctionType::New();
      for (unsigned int j = 0; j < sample->GetMeasurementVectorSize(); ++j)
        {
        origin[j] = estimatedMeans[index++];
        }
        DistanceMetricType::Pointer distanceMetric = DistanceMetricType::New();
        distanceMetric->SetOrigin(origin);
        membershipFunction1->SetDistanceMetric( distanceMetric );
  //    classifier->AddMembershipFunction(membershipFunctions[i].GetPointer());
        membershipFunctions->Get().push_back(membershipFunction1.GetPointer() );
      }
  
    classifier->SetMembershipFunctions(membershipFunctions);
    classifier->Update();

The following code snippet prints out the measurement vectors and their class labels in the sample.

    const ClassifierType::MembershipSampleType membershipSample =
      classifier->GetOutput();
    ClassifierType::MembershipSampleType::ConstIterator iter = membershipSample->Begin();
  
    while (iter != membershipSample->End())
      {
      std::cout << "measurement vector = " << iter.GetMeasurementVector()
                << "class label = " << iter.GetClassLabel()
                << std::endl;
      ++iter;
      }

19.4.2 Kohonen’s Self Organizing Map

The Self Organizing Map, SOM, introduced by Kohonen is a non-supervised neural learning algorithm. The map is composed of neighboring cells which are in competition by means of mutual interactions and they adapt in order to match characteristic patterns of the examples given during the learning. The SOM is usually on a plane (2D).

The algorithm implements a nonlinear projection from a high dimensional feature space to a lower dimension space, usually 2D. It is able to find the correspondence between a set of structured data and a network of much lower dimension while keeping the topological relationships existing in the feature space. Thanks to this topological organization, the final map presents clusters and their relationships.

Kohonen’s SOM is usually represented as an array of cells where each cell is, i, associated to a feature (or weight) vector mi = [mi1,mi2,⋅⋅⋅,min]T n (figure 19.3).


PIC

Figure 19.3: Kohonen’s Self Organizing Map


A cell (or neuron) in the map is a good detector for a given input vector x = [x1,x2,⋅⋅⋅,xn]T n if the latter is close to the former. This distance between vectors can be represented by the scalar product xT mi, but for most of the cases other distances can be used, as for instance the Euclidean one. The cell having the weight vector closest to the input vector is called the winner.

The goal of the learning step is to get a map which is representative of an input example set. It is an iterative procedure which consists in passing each input example to the map, testing the response of each neuron and modifying the map to get it closer to the examples.

Algorithm 1 SOM learning:

  1. t = 0.
  2. Initialize the weight vectors of the map (randomly, for instance).
  3. While t < number of iterations, do:
    1. k = 0.
    2. While k < number of examples, do:
      1. Find the vector mi(t) which minimizes the distance d(xk,mi(t))
      2. For a neighborhood Nc(t) around the winner cell, apply the transformation:
        mi(t+ 1)= mi(t)+ β(t)[xk(t)- mi(t)]
        (19.2)

      3. k = k+1
    3. t = t +1.

In 19.2, β(t) is a decreasing function with the geometrical distance to the winner cell. For instance:

          - ∥ri-2rc∥2-
β(t)= β0(t)e   σ(t) ,
(19.3)

with β0(t) and σ(t) decreasing functions with time and r the cell coordinates in the output – map – space.

Therefore the algorithm consists in getting the map closer to the learning set. The use of a neighborhood around the winner cell allows the organization of the map into areas which specialize in the recognition of different patterns. This neighborhood also ensures that cells which are topologically close are also close in terms of the distance defined in the feature space.

19.4.2.1 Building a color table

The source code for this example can be found in the file
Examples/Learning/SOMExample.cxx.

This example illustrates the use of the otb::SOM class for building Kohonen’s Self Organizing Maps.

We will use the SOM in order to build a color table from an input image. Our input image is coded with 3×8 bits and we would like to code it with only 16 levels. We will use the SOM in order to learn which are the 16 most representative RGB values of the input image and we will assume that this is the optimal color table for the image.

The first thing to do is include the header file for the class. We will also need the header files for the map itself and the activation map builder whose utility will be explained at the end of the example.

  #include "otbSOMMap.h"
  #include "otbSOM.h"
  #include "otbSOMActivationBuilder.h"

Since the otb::SOM class uses a distance, we will need to include the header file for the one we want to use

  

The Self Organizing Map itself is actually an N-dimensional image where each pixel contains a neuron. In our case, we decide to build a 2-dimensional SOM, where the neurons store RGB values with floating point precision.

    const unsigned int Dimension = 2;
    typedef double                                 PixelType;
    typedef otb::VectorImage<PixelType, Dimension> ImageType;
    typedef ImageType::PixelType                   VectorType;

The distance that we want to apply between the RGB values is the Euclidean one. Of course we could choose to use other type of distance, as for instance, a distance defined in any other color space.

    typedef itk::Statistics::EuclideanDistanceMetric<VectorType> DistanceType;

We can now define the type for the map. The otb::SOMMap class is templated over the neuron type – PixelType here –, the distance type and the number of dimensions. Note that the number of dimensions of the map could be different from the one of the images to be processed.

    typedef otb::SOMMap<VectorType, DistanceType, Dimension> MapType;

We are going to perform the learning directly on the pixels of the input image. Therefore, the image type is defined using the same pixel type as we used for the map. We also define the type for the imge file reader.

    typedef otb::ImageFileReader<ImageType> ReaderType;

Since the otb::SOM class works on lists of samples, it will need to access the input image through an adaptor. Its type is defined as follows:

    typedef itk::Statistics::ListSample<VectorType> SampleListType;

We can now define the type for the SOM, which is templated over the input sample list and the type of the map to be produced and the two functors that hold the training behavior.

    typedef otb::Functor::CzihoSOMLearningBehaviorFunctor
    LearningBehaviorFunctorType;
    typedef otb::Functor::CzihoSOMNeighborhoodBehaviorFunctor
    NeighborhoodBehaviorFunctorType;
    typedef otb::SOM<SampleListType, MapType,
        LearningBehaviorFunctorType, NeighborhoodBehaviorFunctorType>
    SOMType;

As an alternative to standard SOMType, one can decide to use an otb::PeriodicSOM , which behaves like otb::SOM but is to be considered to as a torus instead of a simple map. Hence, the neighborhood behavior of the winning neuron does not depend on its location on the map... otb::PeriodicSOM is defined in otbPeriodicSOM.h.

We can now start building the pipeline. The first step is to instantiate the reader and pass its output to the adaptor.

    ReaderType::Pointer reader = ReaderType::New();
    reader->SetFileName(inputFileName);
    reader->Update();
  
    SampleListType::Pointer sampleList = SampleListType::New();
    sampleList->SetMeasurementVectorSize(reader->GetOutput()->GetVectorLength());
  
    itk::ImageRegionIterator<ImageType> imgIter (reader->GetOutput(),
                                                 reader->GetOutput()->
                                                 GetBufferedRegion());
    imgIter.GoToBegin();
  
    itk::ImageRegionIterator<ImageType> imgIterEnd (reader->GetOutput(),
                                                    reader->GetOutput()->
                                                    GetBufferedRegion());
    imgIterEnd.GoToEnd();
  
    do
      {
      sampleList->PushBack(imgIter.Get());
      ++imgIter;
      }
    while (imgIter != imgIterEnd);

We can now instantiate the SOM algorithm and set the sample list as input.

    SOMType::Pointer som = SOMType::New();
    som->SetListSample(sampleList);

We use a SOMType::SizeType array in order to set the sizes of the map.

    SOMType::SizeType size;
    size[0] = sizeX;
    size[1] = sizeY;
    som->SetMapSize(size);

The initial size of the neighborhood of each neuron is set in the same way.

    SOMType::SizeType radius;
    radius[0] = neighInitX;
    radius[1] = neighInitY;
    som->SetNeighborhoodSizeInit(radius);

The other parameters are the number of iterations, the initial and the final values for the learning rate – β – and the maximum initial value for the neurons (the map will be randomly initialized).

    som->SetNumberOfIterations(nbIterations);
    som->SetBetaInit(betaInit);
    som->SetBetaEnd(betaEnd);
    som->SetMaxWeight(static_cast<PixelType>(initValue));

Now comes the initialization of the functors.

    LearningBehaviorFunctorType learningFunctor;
    learningFunctor.SetIterationThreshold(radius, nbIterations);
    som->SetBetaFunctor(learningFunctor);
  
    NeighborhoodBehaviorFunctorType neighborFunctor;
    som->SetNeighborhoodSizeFunctor(neighborFunctor);
    som->Update();

Finally, we set up the las part of the pipeline where the plug the output of the SOM into the writer. The learning procedure is triggered by calling the Update() method on the writer. Since the map is itself an image, we can write it to disk with an otb::ImageFileWriter .

Figure 19.4 shows the result of the SOM learning. Since we have performed a learning on RGB pixel values, the produced SOM can be interpreted as an optimal color table for the input image. It can be observed that the obtained colors are topologically organised, so similar colors are also close in the map. This topological organisation can be exploited to further reduce the number of coding levels of the pixels without performing a new learning: we can subsample the map to get a new color table. Also, a bilinear interpolation between the neurons can be used to increase the number of coding levels.


PIC PIC PIC

Figure 19.4: Result of the SOM learning. Left: RGB image. Center: SOM. Right: Activation map


We can now compute the activation map for the input image. The activation map tells us how many times a given neuron is activated for the set of examples given to the map. The activation map is stored as a scalar image and an integer pixel type is usually enough.

    typedef unsigned char OutputPixelType;
  
    typedef otb::Image<OutputPixelType, Dimension> OutputImageType;
    typedef otb::ImageFileWriter<OutputImageType>  ActivationWriterType;

In a similar way to the otb::SOM class the otb::SOMActivationBuilder is templated over the sample list given as input, the SOM map type and the activation map to be built as output.

    typedef otb::SOMActivationBuilder<SampleListType, MapType,
        OutputImageType> SOMActivationBuilderType;

We instantiate the activation map builder and set as input the SOM map build before and the image (using the adaptor).

    SOMActivationBuilderType::Pointer somAct
      = SOMActivationBuilderType::New();
    somAct->SetInput(som->GetOutput());
    somAct->SetListSample(sampleList);
    somAct->Update();

The final step is to write the activation map to a file.

    if (actMapFileName != ITK_NULLPTR)
      {
      ActivationWriterType::Pointer actWriter = ActivationWriterType::New();
      actWriter->SetFileName(actMapFileName);

The righthand side of figure 19.4 shows the activation map obtained.

19.4.2.2 SOM Classification

The source code for this example can be found in the file
Examples/Learning/SOMClassifierExample.cxx.

This example illustrates the use of the otb::SOMClassifier class for performing a classification using an existing Kohonen’s Self Organizing. Actually, the SOM classification consists only in the attribution of the winner neuron index to a given feature vector.

We will use the SOM created in section 19.4.2.1 and we will assume that each neuron represents a class in the image.

The first thing to do is include the header file for the class.

  #include "otbSOMClassifier.h"

As for the SOM learning step, we must define the types for the otb::SOMMap, and therefore, also for the distance to be used. We will also define the type for the SOM reader, which is actually an otb::ImageFileReader which the appropriate image type.

    typedef itk::Statistics::EuclideanDistanceMetric<PixelType>   DistanceType;
    typedef otb::SOMMap<PixelType, DistanceType, Dimension> SOMMapType;
    typedef otb::ImageFileReader<SOMMapType>                SOMReaderType;

The classification will be performed by the otb::SOMClassifier , which, as most of the classifiers, works on itk::Statistics::ListSample s. In order to be able to perform an image classification, we will need to use the itk::Statistics::ImageToListAdaptor which is templated over the type of image to be adapted. The SOMClassifier is templated over the sample type, the SOMMap type and the pixel type for the labels.

    typedef itk::Statistics::ListSample<PixelType> SampleType;
    typedef otb::SOMClassifier<SampleType, SOMMapType, LabelPixelType>
    ClassifierType;

The result of the classification will be stored on an image and saved to a file. Therefore, we define the types needed for this step.

    typedef otb::Image<LabelPixelType, Dimension> OutputImageType;
    typedef otb::ImageFileWriter<OutputImageType> WriterType;

We can now start reading the input image and the SOM given as inputs to the program. We instantiate the readers as usual.

    ReaderType::Pointer reader = ReaderType::New();
    reader->SetFileName(imageFilename);
    reader->Update();
  
    SOMReaderType::Pointer somreader = SOMReaderType::New();
    somreader->SetFileName(mapFilename);
    somreader->Update();

The conversion of the input data from image to list sample is easily done using the adaptor.

    SampleType::Pointer sample = SampleType::New();
  
    itk::ImageRegionIterator<InputImageType> it(reader->GetOutput(),
                                                reader->GetOutput()->
                                                GetLargestPossibleRegion());
  
    sample->SetMeasurementVectorSize(reader->GetOutput()->GetNumberOfComponentsPerPixel());
    it.GoToBegin();
  
    while (!it.IsAtEnd())
      {
      sample->PushBack(it.Get());
      ++it;
      }

The classifier can now be instantiated. The input data is set by using the SetSample() method and the SOM si set using the SetMap() method. The classification is triggered by using the Update() method.

    ClassifierType::Pointer classifier = ClassifierType::New();
    classifier->SetSample(sample.GetPointer());
    classifier->SetMap(somreader->GetOutput());
    classifier->Update();

Once the classification has been performed, the sample list obtained at the output of the classifier must be converted into an image. We create the image as follows :

    OutputImageType::Pointer outputImage = OutputImageType::New();
    outputImage->SetRegions(reader->GetOutput()->GetLargestPossibleRegion());
    outputImage->Allocate();

We can now get a pointer to the classification result.

    ClassifierType::OutputType membershipSample = classifier->GetOutput();

And we can declare the iterators pointing to the front and the back of the sample list.

    ClassifierType::OutputType::ConstIterator m_iter =  membershipSample->Begin();
    ClassifierType::OutputType::ConstIterator m_last =  membershipSample->End();

We also declare an itk::ImageRegionIterator in order to fill the output image with the class labels.

    typedef itk::ImageRegionIterator<OutputImageType> OutputIteratorType;
  
    OutputIteratorType outIt(outputImage, outputImage->GetLargestPossibleRegion());

We iterate through the sample list and the output image and assign the label values to the image pixels.

    outIt.GoToBegin();
  
    while (m_iter != m_last && !outIt.IsAtEnd())
      {
      outIt.Set(m_iter.GetClassLabel());
      ++m_iter;
      ++outIt;
      }

Finally, we write the classified image to a file.

    WriterType::Pointer writer = WriterType::New();
    writer->SetFileName(outputFilename);
    writer->SetInput(outputImage);
    writer->Update();

Figure 19.5 shows the result of the SOM classification.


PIC PIC PIC

Figure 19.5: Result of the SOM learning. Left: RGB image. Center: SOM. Right: Classified Image


19.4.2.3 Multi-band, streamed classification

The source code for this example can be found in the file
Examples/Classification/SOMImageClassificationExample.cxx.

In previous examples, we have used the otb::SOMClassifier , which uses the ITK classification framework. This good for compatibility with the ITK framework, but introduces the limitations of not being able to use streaming and being able to know at compilation time the number of bands of the image to be classified. In OTB we have avoided this limitation by developing the otb::SOMImageClassificationFilter . In this example we will illustrate its use. We start by including the appropriate header file.

  #include "otbSOMImageClassificationFilter.h"

We will assume double precision input images and will also define the type for the labeled pixels.

    const unsigned int Dimension = 2;
    typedef double         PixelType;
    typedef unsigned short LabeledPixelType;

Our classifier will be generic enough to be able to process images with any number of bands. We read the images as otb::VectorImage s. The labeled image will be a scalar image.

    typedef otb::VectorImage<PixelType, Dimension>  ImageType;
    typedef otb::Image<LabeledPixelType, Dimension> LabeledImageType;

We can now define the type for the classifier filter, which is templated over its input and output image types and the SOM type.

    typedef otb::SOMMap<ImageType::PixelType> SOMMapType;
    typedef otb::SOMImageClassificationFilter<ImageType,
        LabeledImageType,
        SOMMapType>
    ClassificationFilterType;

And finally, we define the readers (for the input image and theSOM) and the writer. Since the images, to classify can be very big, we will use a streamed writer which will trigger the streaming ability of the classifier.

    typedef otb::ImageFileReader<ImageType>                 ReaderType;
    typedef otb::ImageFileReader<SOMMapType>                SOMReaderType;
    typedef otb::ImageFileWriter<LabeledImageType> WriterType;

We instantiate the classifier and the reader objects and we set the existing SOM obtained in a previous training step.

    ClassificationFilterType::Pointer filter = ClassificationFilterType::New();
  
    ReaderType::Pointer reader = ReaderType::New();
    reader->SetFileName(infname);
  
    SOMReaderType::Pointer somreader = SOMReaderType::New();
    somreader->SetFileName(somfname);
    somreader->Update();
  
    filter->SetMap(somreader->GetOutput());

We plug the pipeline and trigger its execution by updating the output of the writer.

    filter->SetInput(reader->GetOutput());
  
    WriterType::Pointer writer = WriterType::New();
    writer->SetInput(filter->GetOutput());
    writer->SetFileName(outfname);
    writer->Update();

19.4.3 Bayesian Plug-In Classifier

The source code for this example can be found in the file
Examples/Classification/BayesianPluginClassifier.cxx.

In this example, we present a system that places measurement vectors into two Gaussian classes. The Figure 19.6 shows all the components of the classifier system and the data flow. This system differs from the previous k-means clustering algorithms in several ways. The biggest difference is that this classifier uses the itk::Statistics::GaussianMembershipFunction as membership functions instead of the itk::Statistics::EuclideanDistanceMetric . Since the membership function is different, it requires a different set of parameters, mean vectors and covariance matrices. We choose the itk::Statistics::MeanSampleFilter (sample mean) and the itk::Statistics::CovarianceSampleFilter (sample covariance) for the estimation algorithms of the two parameters. If we want a more robust estimation algorithm, we can replace these estimation algorithms with additional alternatives without changing other components in the classifier system.

It is a bad idea to use the same sample for both testing and training (parameter estimation) of the parameters. However, for simplicity, in this example, we use a sample for test and training.


PIC

Figure 19.6: Bayesian plug-in classifier for two Gaussian classes.


We use the itk::Statistics::ListSample as the sample (test and training). The itk::Vector is our measurement vector class. To store measurement vectors into two separate sample containers, we use the itk::Statistics::Subsample objects.

  #include "itkVector.h"
  #include "itkListSample.h"
  #include "itkSubsample.h"

The following two files provides us with the parameter estimation algorithms.

  #include "itkMeanSampleFilter.h"
  #include "itkCovarianceSampleFilter.h"

The following files define the components required by ITK statistical classification framework: the decision rule, the membership function, and the classifier.

  #include "itkMaximumRatioDecisionRule.h"
  #include "itkGaussianMembershipFunction.h"
  #include "itkSampleClassifierFilter.h"

We will fill the sample with random variables from two normal distributions using the itk::Statistics::NormalVariateGenerator .

  #include "itkNormalVariateGenerator.h"

Since the NormalVariateGenerator class only supports 1-D, we define our measurement vector type as a one component vector. We then, create a ListSample object for data inputs.

We also create two Subsample objects that will store the measurement vectors in sample into two separate sample containers. Each Subsample object stores only the measurement vectors belonging to a single class. This class sample will be used by the parameter estimation algorithms.

    typedef itk::Vector<double,
        1>
    MeasurementVectorType;
    typedef itk::Statistics::ListSample<MeasurementVectorType> SampleType;
    SampleType::Pointer sample = SampleType::New();
    sample->SetMeasurementVectorSize(1);   // length of measurement vectors
    // in the sample.
  
    typedef itk::Statistics::Subsample<SampleType> ClassSampleType;
    std::vector<ClassSampleType::Pointer> classSamples;
    for (unsigned int i = 0; i < 2; ++i)
      {
      classSamples.push_back(ClassSampleType::New());
      classSamples[i]->SetSample(sample);
      }

The following code snippet creates a NormalVariateGenerator object. Since the random variable generator returns values according to the standard normal distribution (the mean is zero, and the standard deviation is one), before pushing random values into the sample, we change the mean and standard deviation. We need two normally (Gaussian) distributed datasets. We have two for loops, within which each uses a different mean and standard deviation. Before we fill the sample with the second distribution data, we call Initialize(random seed) method, to recreate the pool of random variables in the normalGenerator. In the second for loop, we fill the two class samples with measurement vectors using the AddInstance() method.

To see the probability density plots from the two distributions, refer to Figure 19.2.

    typedef itk::Statistics::NormalVariateGenerator NormalGeneratorType;
    NormalGeneratorType::Pointer normalGenerator = NormalGeneratorType::New();
  
    normalGenerator->Initialize(101);
  
    MeasurementVectorType          mv;
    double                         mean = 100;
    double                         standardDeviation = 30;
    SampleType::InstanceIdentifier id = 0UL;
    for (unsigned int i = 0; i < 100; ++i)
      {
      mv.Fill((normalGenerator->GetVariate()  standardDeviation) + mean);
      sample->PushBack(mv);
      classSamples[0]->AddInstance(id);
      ++id;
      }
  
    normalGenerator->Initialize(3024);
    mean = 200;
    standardDeviation = 30;
    for (unsigned int i = 0; i < 100; ++i)
      {
      mv.Fill((normalGenerator->GetVariate()  standardDeviation) + mean);
      sample->PushBack(mv);
      classSamples[1]->AddInstance(id);
      ++id;
      }

In the following code snippet, notice that the template argument for the MeanSampleFilter and CovarianceFilter is ClassSampleType (i.e., type of Subsample) instead of SampleType (i.e. type of ListSample). This is because the parameter estimation algorithms are applied to the class sample.

    typedef itk::Statistics::MeanSampleFilter<ClassSampleType> MeanEstimatorType;
    typedef itk::Statistics::CovarianceSampleFilter<ClassSampleType>
    CovarianceEstimatorType;
  
    std::vector<MeanEstimatorType::Pointer>       meanEstimators;
    std::vector<CovarianceEstimatorType::Pointer> covarianceEstimators;
  
    for (unsigned int i = 0; i < 2; ++i)
      {
      meanEstimators.push_back(MeanEstimatorType::New());
      meanEstimators[i]->SetInput(classSamples[i]);
      meanEstimators[i]->Update();
  
      covarianceEstimators.push_back(CovarianceEstimatorType::New());
      covarianceEstimators[i]->SetInput(classSamples[i]);
      //covarianceEstimators[i]->SetMean(meanEstimators[i]->GetOutput());
      covarianceEstimators[i]->Update();
      }

We print out the estimated parameters.

    for (unsigned int i = 0; i < 2; ++i)
      {
      std::cout << "class[" << i << "] " << std::endl;
      std::cout << "    estimated mean : "
                << meanEstimators[i]->GetMean()
                << "    covariance matrix : "
                << covarianceEstimators[i]->GetCovarianceMatrixOutput()->Get() << std::endl;
      }

After creating a SampleClassifierFilter object and a MaximumRatioDecisionRule object, we plug in the decisionRule and the sample to the classifier. We then specify the number of classes that will be considered using the SetNumberOfClasses() method.

The MaximumRatioDecisionRule requires a vector of a priori probability values. Such a priori probability will be the P(ωi) of the following variation of the Bayes decision rule:

            -→
Decideωi if p(-→x |ωi)> P-(ωj) forallj⁄= i
          p(x |ωj)  P(ωi)
(19.4)

The remainder of the code snippet demonstrates how user-specified class labels are used. The classification result will be stored in a MembershipSample object, and for each measurement vector, its class label will be one of the two class labels, 100 and 200 (unsigned int).

    typedef itk::Statistics::GaussianMembershipFunction
    <MeasurementVectorType> MembershipFunctionType;
    typedef itk::Statistics::MaximumRatioDecisionRule DecisionRuleType;
    DecisionRuleType::Pointer decisionRule = DecisionRuleType::New();
  
    DecisionRuleType::PriorProbabilityVectorType aPrioris;
    aPrioris.push_back(classSamples[0]->GetTotalFrequency()
                       / sample->GetTotalFrequency());
    aPrioris.push_back(classSamples[1]->GetTotalFrequency()
                       / sample->GetTotalFrequency());
    decisionRule->SetPriorProbabilities(aPrioris);
  
    typedef itk::Statistics::SampleClassifierFilter<SampleType> ClassifierType;
    ClassifierType::Pointer classifier = ClassifierType::New();
  
    classifier->SetDecisionRule(dynamic_cast< itk::Statistics::DecisionRule >( decisionRule.GetPointer()));
    classifier->SetInput(sample);
    classifier->SetNumberOfClasses(2);
  
    std::vector<long unsigned int> classLabels;
    classLabels.resize(2);
    classLabels[0] = 100;
    classLabels[1] = 200;
  
    typedef itk::SimpleDataObjectDecorator<std::vector<long unsigned int> >   ClassLabelDecoratedType;
    ClassLabelDecoratedType::Pointer classLabelsDecorated = ClassLabelDecoratedType::New();
    classLabelsDecorated->Set(classLabels);
  
    classifier->SetClassLabels(classLabelsDecorated);

The classifier is almost ready to perform the classification except that it needs two membership functions that represent the two clusters.

In this example, we can imagine that the two clusters are modeled by two Euclidean distance functions. The distance function (model) has only one parameter, the mean (centroid) set by the SetOrigin() method. To plug-in two distance functions, we call the AddMembershipFunction() method. Finally, the invocation of the Update() method will perform the classification.

    typedef ClassifierType::MembershipFunctionType                              MembershipFunctionBaseType;
    typedef ClassifierType::MembershipFunctionVectorObjectType::ComponentType   ComponentMembershipType;
  
    // Vector Containing the membership function used
    ComponentMembershipType     membershipFunctions;
  
    for (unsigned int i = 0; i < 2; i++)
      {
      MembershipFunctionType::Pointer curMemshpFunction =  MembershipFunctionType::New();
      curMemshpFunction->SetMean(meanEstimators[i]->GetMean());
      curMemshpFunction->SetCovariance(covarianceEstimators[i]->GetCovarianceMatrix());
  
      // cast the GaussianMembershipFunction in a
      // itk::MembershipFunctionBase
      membershipFunctions.push_back(dynamic_cast<const MembershipFunctionBaseType  >(curMemshpFunction.GetPointer()));
      }
  
    ClassifierType::MembershipFunctionVectorObjectPointer membershipVectorObject = ClassifierType::MembershipFunctionVectorObjectType::New();
    membershipVectorObject->Set(membershipFunctions);
  
    classifier->SetMembershipFunctions(membershipVectorObject);
    classifier->Update();

The following code snippet prints out pairs of a measurement vector and its class label in the sample.

    const ClassifierType::MembershipSampleType membershipSample = classifier->GetOutput();
    ClassifierType::MembershipSampleType::ConstIterator iter = membershipSample->Begin();
  
    while (iter != membershipSample->End())
      {
      std::cout << "measurement vector = " << iter.GetMeasurementVector()
                << "class label = " << iter.GetClassLabel() << std::endl;
      ++iter;
      }

19.4.4 Expectation Maximization Mixture Model Estimation

The source code for this example can be found in the file
Examples/Classification/ExpectationMaximizationMixtureModelEstimator.cxx.

In this example, we present ITK’s implementation of the expectation maximization (EM) process to generate parameter estimates for a two Gaussian component mixture model.

The Bayesian plug-in classifier example (see Section 19.4.3) used two Gaussian probability density functions (PDF) to model two Gaussian distribution classes (two models for two class). However, in some cases, we want to model a distribution as a mixture of several different distributions. Therefore, the probability density function (p(x)) of a mixture model can be stated as follows :

      c
p(x)= ∑ αifi(x)
      i=0
(19.5)

where i is the index of the component, c is the number of components, αi is the proportion of the component, and fi is the probability density function of the component.

Now the task is to find the parameters(the component PDF’s parameters and the proportion values) to maximize the likelihood of the parameters. If we know which component a measurement vector belongs to, the solutions to this problem is easy to solve. However, we don’t know the membership of each measurement vector. Therefore, we use the expectation of membership instead of the exact membership. The EM process splits into two steps:

  1. E step: calculate the expected membership values for each measurement vector to each classes.
  2. M step: find the next parameter sets that maximize the likelihood with the expected membership values and the current set of parameters.

The E step is basically a step that calculates the a posteriori probability for each measurement vector.

The M step is dependent on the type of each PDF. Most of distributions belonging to exponential family such as Poisson, Binomial, Exponential, and Normal distributions have analytical solutions for updating the parameter set. The itk::Statistics::ExpectationMaximizationMixtureModelEstimator class assumes that such type of components.

In the following example we use the itk::Statistics::ListSample as the sample (test and training). The itk::Vector is our measurement vector class. To store measurement vectors into two separate sample container, we use the itk::Statistics::Subsample objects.

  #include "itkVector.h"
  #include "itkListSample.h"

The following two files provide us the parameter estimation algorithms.

  #include "itkGaussianMixtureModelComponent.h"
  #include "itkExpectationMaximizationMixtureModelEstimator.h"

We will fill the sample with random variables from two normal distribution using the itk::Statistics::NormalVariateGenerator .

  #include "itkNormalVariateGenerator.h"

Since the NormalVariateGenerator class only supports 1-D, we define our measurement vector type as a one component vector. We then, create a ListSample object for data inputs.

We also create two Subsample objects that will store the measurement vectors in the sample into two separate sample containers. Each Subsample object stores only the measurement vectors belonging to a single class. This class sample will be used by the parameter estimation algorithms.

    unsigned int numberOfClasses = 2;
    typedef itk::Vector<double,
        1>
    MeasurementVectorType;
    typedef itk::Statistics::ListSample<MeasurementVectorType> SampleType;
    SampleType::Pointer sample = SampleType::New();
    sample->SetMeasurementVectorSize(1);   // length of measurement vectors
    // in the sample.

The following code snippet creates a NormalVariateGenerator object. Since the random variable generator returns values according to the standard normal distribution (the mean is zero, and the standard deviation is one) before pushing random values into the sample, we change the mean and standard deviation. We want two normal (Gaussian) distribution data. We have two for loops. Each for loop uses different mean and standard deviation. Before we fill the sample with the second distribution data, we call Initialize() method to recreate the pool of random variables in the normalGenerator. In the second for loop, we fill the two class samples with measurement vectors using the AddInstance() method.

To see the probability density plots from the two distribution, refer to Figure 19.2.

    typedef itk::Statistics::NormalVariateGenerator NormalGeneratorType;
    NormalGeneratorType::Pointer normalGenerator = NormalGeneratorType::New();
  
    normalGenerator->Initialize(101);
  
    MeasurementVectorType mv;
    double                mean = 100;
    double                standardDeviation = 30;
    for (unsigned int i = 0; i < 100; ++i)
      {
      mv[0] = (normalGenerator->GetVariate()  standardDeviation) + mean;
      sample->PushBack(mv);
      }
  
    normalGenerator->Initialize(3024);
    mean = 200;
    standardDeviation = 30;
    for (unsigned int i = 0; i < 100; ++i)
      {
      mv[0] = (normalGenerator->GetVariate()  standardDeviation) + mean;
      sample->PushBack(mv);
      }

In the following code snippet notice that the template argument for the MeanSampleFilter and CovarianceSampleFilter is ClassSampleType (i.e., type of Subsample) instead of SampleType (i.e., type of ListSample). This is because the parameter estimation algorithms are applied to the class sample.

    typedef itk::Array<double> ParametersType;
    ParametersType params(2);
  
    std::vector<ParametersType> initialParameters(numberOfClasses);
    params[0] = 110.0;
    params[1] = 800.0;
    initialParameters[0] = params;
  
    params[0] = 210.0;
    params[1] = 850.0;
    initialParameters[1] = params;
  
    typedef itk::Statistics::GaussianMixtureModelComponent<SampleType>
    ComponentType;
  
    std::vector<ComponentType::Pointer> components;
    for (unsigned int i = 0; i < numberOfClasses; ++i)
      {
      components.push_back(ComponentType::New());
      (components[i])->SetSample(sample);
      (components[i])->SetParameters(initialParameters[i]);
      }

We run the estimator.

    typedef itk::Statistics::ExpectationMaximizationMixtureModelEstimator<
        SampleType> EstimatorType;
    EstimatorType::Pointer estimator = EstimatorType::New();
  
    estimator->SetSample(sample);
    estimator->SetMaximumIteration(200);
  
    itk::Array<double> initialProportions(numberOfClasses);
    initialProportions[0] = 0.5;
    initialProportions[1] = 0.5;
  
    estimator->SetInitialProportions(initialProportions);
  
    for (unsigned int i = 0; i < numberOfClasses; ++i)
      {
      estimator->AddComponent((ComponentType::Superclass)
                              (components[i]).GetPointer());
      }
  
    estimator->Update();

We then print out the estimated parameters.

    for (unsigned int i = 0; i < numberOfClasses; ++i)
      {
      std::cout << "Cluster[" << i << "]" << std::endl;
      std::cout << "    Parameters:" << std::endl;
      std::cout << "         " << (components[i])->GetFullParameters()
                << std::endl;
      std::cout << "    Proportion: ";
      std::cout << "         " << (estimator->GetProportions())[i] << std::endl;
      }

19.4.5 Statistical Segmentations

19.4.5.1 Stochastic Expectation Maximization

The Stochastic Expectation Maximization (SEM) approach is a stochastic version of the EM mixture estimation seen on section 19.4.4. It has been introduced by [22] to prevent convergence of the EM approach from local minima. It avoids the analytical maximization issued by integrating a stochastic sampling procedure in the estimation process. It induces an almost sure (a.s.) convergence to the algorithm.

From the initial two step formulation of the EM mixture estimation, the SEM may be decomposed into 3 steps:

  1. E-step, calculates the expected membership values for each measurement vector to each classes.
  2. S-step, performs a stochastic sampling of the membership vector to each classes, according to the membership values computed in the E-step.
  3. M-step, updates the parameters of the membership probabilities (parameters to be defined through the class itk::Statistics::ModelComponentBase and its inherited classes).

The implementation of the SEM has been turned to a contextual SEM in the sense where the evaluation of the membership parameters is conditioned to membership values of the spatial neighborhood of each pixels.

The source code for this example can be found in the file
Examples/Learning/SEMModelEstimatorExample.cxx.

In this example, we present OTB’s implementation of SEM, through the class otb::SEMClassifier . This class performs a stochastic version of the EM algorithm, but instead of inheriting from itk::ExpectationMaximizationMixtureModelEstimator , we chose to inherit from itk::Statistics::ListSample , in the same way as otb::SVMClassifier .

The program begins with otb::VectorImage and outputs otb::Image . Then appropriate header files have to be included:

  #include "otbImage.h"
  #include "otbVectorImage.h"
  #include "otbImageFileReader.h"
  #include "otbImageFileWriter.h"

otb::SEMClassifier performs estimation of mixture to fit the initial histogram. Actually, mixture of Gaussian pdf can be performed. Those generic pdf are treated in otb::Statistics::ModelComponentBase . The Gaussian model is taken in charge with the class otb::Statistics::GaussianModelComponent .

  #include "otbSEMClassifier.h"

Input/Output images type are define in a classical way. In fact, a itk::VariableLengthVector is to be considered for the templated MeasurementVectorType, which will be used in the ListSample interface.

      typedef double PixelType;
  
      typedef otb::VectorImage<PixelType, 2>  ImageType;
      typedef otb::ImageFileReader<ImageType> ReaderType;
  
      typedef otb::Image<unsigned char, 2>          OutputImageType;
      typedef otb::ImageFileWriter<OutputImageType> WriterType;

Once the input image is opened, the classifier may be initialised by SmartPointer.

      typedef otb::SEMClassifier<ImageType, OutputImageType> ClassifType;
      ClassifType::Pointer classifier = ClassifType::New();

Then, it follows, classical initializations of the pipeline.

      classifier->SetNumberOfClasses(numberOfClasses);
      classifier->SetMaximumIteration(numberOfIteration);
      classifier->SetNeighborhood(neighborhood);
      classifier->SetTerminationThreshold(terminationThreshold);
      classifier->SetSample(reader->GetOutput());

When an initial segmentation is available, the classifier may use it as image (of type OutputImageType) or as a itk::SampleClassifier result (of type itk::Statistics::MembershipSample ).

      if (fileNameImgInit != ITK_NULLPTR)
        {
        typedef otb::ImageFileReader<OutputImageType> ImgInitReaderType;
        ImgInitReaderType::Pointer segReader = ImgInitReaderType::New();
        segReader->SetFileName(fileNameImgInit);
        segReader->Update();
        classifier->SetClassLabels(segReader->GetOutput());
        }

By default, otb::SEMClassifier performs initialization of ModelComponentBase by as many instantiation of otb::Statistics::GaussianModelComponent as the number of classes to estimate in the mixture. Nevertheless, the user may add specific distribution into the mixture estimation. It is permitted by the use of AddComponent for the given class number and the specific distribution.

      typedef ClassifType::ClassSampleType ClassSampleType;
      typedef otb::Statistics::GaussianModelComponent<ClassSampleType>
      GaussianType;
  
      for (int i = 0; i < numberOfClasses; ++i)
        {
        GaussianType::Pointer model = GaussianType::New();
        classifier->AddComponent(i, model);
        }

Once the pipeline is instantiated. The segmentation by itself may be launched by using the Update function.

      try
        {
        classifier->Update();
        }

The segmentation may outputs a result of type itk::Statistics::MembershipSample as it is the case for the otb::SVMClassifier . But when using GetOutputImage the output is directly an Image.

Only for visualization purposes, we choose to rescale the image of classes before saving it to a file. We will use the itk::RescaleIntensityImageFilter for this purpose.

      typedef itk::RescaleIntensityImageFilter<OutputImageType,
          OutputImageType> RescalerType;
      RescalerType::Pointer rescaler = RescalerType::New();
  
      rescaler->SetOutputMinimum(itk::NumericTraits<unsigned char>::min());
      rescaler->SetOutputMaximum(itk::NumericTraits<unsigned char>::max());
  
      rescaler->SetInput(classifier->GetOutputImage());
  
      WriterType::Pointer writer = WriterType::New();
      writer->SetFileName(fileNameOut);
      writer->SetInput(rescaler->GetOutput());
      writer->Update();

Figure 19.7 shows the result of the SEM segmentation with 4 different classes and a contextual neighborhood of 3 pixels.


PIC

Figure 19.7: SEM Classification results.


As soon as the segmentation is performed by an iterative stochastic process, it is worth verifying the output status: does the segmentation ends when it has converged or just at the limit of the iteration numbers.

      std::cerr << "Program terminated with a ";
      if (classifier->GetTerminationCode() ==
          ClassifType::CONVERGED) std::cerr << "converged ";
      else std::cerr << "not-converged ";
      std::cerr << "code...\n";

The text output gives for each class the parameters of the pdf (e.g. mean of each component of the class and there covariance matrix, in the case of a Gaussian mixture model).

      classifier->Print(std::cerr);

19.4.6 Classification using Markov Random Fields

Markov Random Fields are probabilistic models that use the statistical dependency between pixels in a neighborhood to infeer the value of a give pixel.

19.4.6.1 ITK framework

The itk::Statistics::MRFImageFilter uses the maximum a posteriori (MAP) estimates for modeling the MRF. The object traverses the data set and uses the model generated by the Mahalanobis distance classifier to get the the distance between each pixel in the data set to a set of known classes, updates the distances by evaluating the influence of its neighboring pixels (based on a MRF model) and finally, classifies each pixel to the class which has the minimum distance to that pixel (taking the neighborhood influence under consideration). The energy function minimization is done using the iterated conditional modes (ICM) algorithm [12].

The source code for this example can be found in the file
Examples/Classification/ScalarImageMarkovRandomField1.cxx.

This example shows how to use the Markov Random Field approach for classifying the pixel of a scalar image.

The itk::Statistics::MRFImageFilter is used for refining an initial classification by introducing the spatial coherence of the labels. The user should provide two images as input. The first image is the one to be classified while the second image is an image of labels representing an initial classification.

The following headers are related to reading input images, writing the output image, and making the necessary conversions between scalar and vector images.

  #include "otbImage.h"
  #include "itkFixedArray.h"
  #include "otbImageFileReader.h"
  #include "otbImageFileWriter.h"
  #include "itkComposeImageFilter.h"

The following headers are related to the statistical classification classes.

  #include "itkMRFImageFilter.h"
  #include "itkDistanceToCentroidMembershipFunction.h"
  #include "itkMinimumDecisionRule.h"

First we define the pixel type and dimension of the image that we intend to classify. With this image type we can also declare the otb::ImageFileReader needed for reading the input image, create one and set its input filename.

    typedef unsigned char PixelType;
    const unsigned int Dimension = 2;
  
    typedef otb::Image<PixelType, Dimension> ImageType;
  
    typedef otb::ImageFileReader<ImageType> ReaderType;
    ReaderType::Pointer reader = ReaderType::New();
    reader->SetFileName(inputImageFileName);

As a second step we define the pixel type and dimension of the image of labels that provides the initial classification of the pixels from the first image. This initial labeled image can be the output of a K-Means method like the one illustrated in section 19.4.1.

    typedef unsigned char LabelPixelType;
  
    typedef otb::Image<LabelPixelType, Dimension> LabelImageType;
  
    typedef otb::ImageFileReader<LabelImageType> LabelReaderType;
    LabelReaderType::Pointer labelReader = LabelReaderType::New();
    labelReader->SetFileName(inputLabelImageFileName);

Since the Markov Random Field algorithm is defined in general for images whose pixels have multiple components, that is, images of vector type, we must adapt our scalar image in order to satisfy the interface expected by the MRFImageFilter. We do this by using the itk::ScalarToArrayCastImageFilter . With this filter we will present our scalar image as a vector image whose vector pixels contain a single component.

    typedef itk::FixedArray<LabelPixelType, 1> ArrayPixelType;
  
    typedef otb::Image<ArrayPixelType, Dimension> ArrayImageType;
  
    typedef itk::ComposeImageFilter<
        ImageType, ArrayImageType> ScalarToArrayFilterType;
  
    ScalarToArrayFilterType::Pointer
      scalarToArrayFilter = ScalarToArrayFilterType::New();
    scalarToArrayFilter->SetInput(0, reader->GetOutput());

With the input image type ImageType and labeled image type LabelImageType we instantiate the type of the itk::MRFImageFilter that will apply the Markov Random Field algorithm in order to refine the pixel classification.

    typedef itk::MRFImageFilter<ArrayImageType, LabelImageType> MRFFilterType;
  
    MRFFilterType::Pointer mrfFilter = MRFFilterType::New();
  
    mrfFilter->SetInput(scalarToArrayFilter->GetOutput());

We set now some of the parameters for the MRF filter. In particular, the number of classes to be used during the classification, the maximum number of iterations to be run in this filter and the error tolerance that will be used as a criterion for convergence.

    mrfFilter->SetNumberOfClasses(numberOfClasses);
    mrfFilter->SetMaximumNumberOfIterations(numberOfIterations);
    mrfFilter->SetErrorTolerance(1e-7);

The smoothing factor represents the tradeoff between fidelity to the observed image and the smoothness of the segmented image. Typical smoothing factors have values between 1 5. This factor will multiply the weights that define the influence of neighbors on the classification of a given pixel. The higher the value, the more uniform will be the regions resulting from the classification refinement.

    mrfFilter->SetSmoothingFactor(smoothingFactor);

Given that the MRF filter needs to continually relabel the pixels, it needs access to a set of membership functions that will measure to what degree every pixel belongs to a particular class. The classification is performed by the itk::ImageClassifierBase class, that is instantiated using the type of the input vector image and the type of the labeled image.

    typedef itk::ImageClassifierBase<
        ArrayImageType,
        LabelImageType>   SupervisedClassifierType;
  
    SupervisedClassifierType::Pointer classifier =
      SupervisedClassifierType::New();

The classifier needs a decision rule to be set by the user. Note that we must use GetPointer() in the call of the SetDecisionRule() method because we are passing a SmartPointer, and smart pointer cannot perform polymorphism, we must then extract the raw pointer that is associated to the smart pointer. This extraction is done with the GetPointer() method.

    typedef itk::Statistics::MinimumDecisionRule DecisionRuleType;
  
    DecisionRuleType::Pointer classifierDecisionRule = DecisionRuleType::New();
  
    classifier->SetDecisionRule(classifierDecisionRule.GetPointer());

We now instantiate the membership functions. In this case we use the itk::Statistics::DistanceToCentroidMembershipFunction class templated over the pixel type of the vector image, which in our example happens to be a vector of dimension 1.

    typedef itk::Statistics::DistanceToCentroidMembershipFunction<
        ArrayPixelType>
    MembershipFunctionType;
  
    typedef MembershipFunctionType::Pointer MembershipFunctionPointer;
  
    double             meanDistance = 0;
    MembershipFunctionType::CentroidType centroid(reader->GetOutput()->GetNumberOfComponentsPerPixel());
    for (unsigned int i = 0; i < numberOfClasses; ++i)
      {
      MembershipFunctionPointer membershipFunction =
        MembershipFunctionType::New();
  
      membershipFunction->SetMeasurementVectorSize(reader->GetOutput()->GetNumberOfComponentsPerPixel());
      centroid[0] = atof(argv[i + numberOfArgumentsBeforeMeans]);
  
      membershipFunction->SetCentroid(centroid);
  
      classifier->AddMembershipFunction(membershipFunction);
      meanDistance += static_cast<double> (centroid[0]);
      }
    meanDistance /= numberOfClasses;

and we set the neighborhood radius that will define the size of the clique to be used in the computation of the neighbors’ influence in the classification of any given pixel. Note that despite the fact that we call this a radius, it is actually the half size of an hypercube. That is, the actual region of influence will not be circular but rather an N-Dimensional box. For example, a neighborhood radius of 2 in a 3D image will result in a clique of size 5x5x5 pixels, and a radius of 1 will result in a clique of size 3x3x3 pixels.

    mrfFilter->SetNeighborhoodRadius(1);

We should now set the weights used for the neighbors. This is done by passing an array of values that contains the linear sequence of weights for the neighbors. For example, in a neighborhood of size 3x3x3, we should provide a linear array of 9 weight values. The values are packaged in a std::vector and are supposed to be double. The following lines illustrate a typical set of values for a 3x3x3 neighborhood. The array is arranged and then passed to the filter by using the method SetMRFNeighborhoodWeight().

    std::vector<double> weights;
    weights.push_back(1.5);
    weights.push_back(2.0);
    weights.push_back(1.5);
    weights.push_back(2.0);
    weights.push_back(0.0); // This is the central pixel
    weights.push_back(2.0);
    weights.push_back(1.5);
    weights.push_back(2.0);
    weights.push_back(1.5);

We now scale weights so that the smoothing function and the image fidelity functions have comparable value. This is necessary since the label image and the input image can have different dynamic ranges. The fidelity function is usually computed using a distance function, such as the itk::DistanceToCentroidMembershipFunction or one of the other membership functions. They tend to have values in the order of the means specified.

    double totalWeight = 0;
    for (std::vector<double>::const_iterator wcIt = weights.begin();
         wcIt != weights.end(); ++wcIt)
      {
      totalWeight += wcIt;
      }
    for (std::vector<double>::iterator wIt = weights.begin();
         wIt != weights.end(); wIt++)
      {
      wIt = static_cast<double> ((wIt)  meanDistance / (2  totalWeight));
      }
  
    mrfFilter->SetMRFNeighborhoodWeight(weights);

Finally, the classifier class is connected to the Markov Random Fields filter.

    mrfFilter->SetClassifier(classifier);

The output image produced by the itk::MRFImageFilter has the same pixel type as the labeled input image. In the following lines we use the OutputImageType in order to instantiate the type of a otb::ImageFileWriter . Then create one, and connect it to the output of the classification filter after passing it through an intensity rescaler to rescale it to an 8 bit dynamic range

    typedef MRFFilterType::OutputImageType OutputImageType;

    typedef otb::ImageFileWriter<OutputImageType> WriterType;
  
    WriterType::Pointer writer = WriterType::New();
  
    writer->SetInput(intensityRescaler->GetOutput());
  
    writer->SetFileName(outputImageFileName);

We are now ready for triggering the execution of the pipeline. This is done by simply invoking the Update() method in the writer. This call will propagate the update request to the reader and then to the MRF filter.

    try
      {
      writer->Update();
      }
    catch (itk::ExceptionObject& excp)
      {
      std::cerr << "Problem encountered while writing ";
      std::cerr << " image file : " << argv[2] << std::endl;
      std::cerr << excp << std::endl;
      return EXIT_FAILURE;
      }


PIC

Figure 19.8: Effect of the MRF filter.


Figure 19.8 illustrates the effect of this filter with four classes. In this example the filter was run with a smoothing factor of 3. The labeled image was produced by ScalarImageKmeansClassifier.cxx and the means were estimated by ScalarImageKmeansModelEstimator.cxx described in section 19.4.1. The obtained result can be compared with the one of figure 19.1 to see the interest of using the MRF approach in order to ensure the regularization of the classified image.

19.4.6.2 OTB framework

The ITK approach was considered not to be flexible enough for some remote sensing applications. Therefore, we decided to implement our own framework.


PIC

Figure 19.9: OTB Markov Framework.


The source code for this example can be found in the file
Examples/Markov/MarkovClassification1Example.cxx.

This example illustrates the details of the otb::MarkovRandomFieldFilter . This filter is an application of the Markov Random Fields for classification, segmentation or restoration.

This example applies the otb::MarkovRandomFieldFilter to classify an image into four classes defined by their mean and variance. The optimization is done using an Metropolis algorithm with a random sampler. The regularization energy is defined by a Potts model and the fidelity by a Gaussian model.

The first step toward the use of this filter is the inclusion of the proper header files.

  #include "otbMRFEnergyPotts.h"
  #include "otbMRFEnergyGaussianClassification.h"
  #include "otbMRFOptimizerMetropolis.h"
  #include "otbMRFSamplerRandom.h"

Then we must decide what pixel type to use for the image. We choose to make all computations with double precision. The labelled image is of type unsigned char which allows up to 256 different classes.

    const unsigned int Dimension = 2;
  
    typedef double                                   InternalPixelType;
    typedef unsigned char                            LabelledPixelType;
    typedef otb::Image<InternalPixelType, Dimension> InputImageType;
    typedef otb::Image<LabelledPixelType, Dimension> LabelledImageType;

We define a reader for the image to be classified, an initialization for the classification (which could be random) and a writer for the final classification.

    typedef otb::ImageFileReader<InputImageType>    ReaderType;
    typedef otb::ImageFileWriter<LabelledImageType> WriterType;
  
    ReaderType::Pointer reader = ReaderType::New();
    WriterType::Pointer writer = WriterType::New();
  
    const char  inputFilename  = argv[1];
    const char  outputFilename = argv[2];
  
    reader->SetFileName(inputFilename);
    writer->SetFileName(outputFilename);

Finally, we define the different classes necessary for the Markov classification. A otb::MarkovRandomFieldFilter is instantiated, this is the main class which connect the other to do the Markov classification.

    typedef otb::MarkovRandomFieldFilter
    <InputImageType, LabelledImageType> MarkovRandomFieldFilterType;

An otb::MRFSamplerRandomMAP , which derives from the otb::MRFSampler , is instantiated. The sampler is in charge of proposing a modification for a given site. The otb::MRFSamplerRandomMAP , randomly pick one possible value according to the MAP probability.

    typedef otb::MRFSamplerRandom<InputImageType, LabelledImageType> SamplerType;

An otb::MRFOptimizerMetropoli , which derives from the otb::MRFOptimizer , is instantiated. The optimizer is in charge of accepting or rejecting the value proposed by the sampler. The otb::MRFSamplerRandomMAP , accept the proposal according to the variation of energy it causes and a temperature parameter.

    typedef otb::MRFOptimizerMetropolis OptimizerType;

Two energy, deriving from the otb::MRFEnergy class need to be instantiated. One energy is required for the regularization, taking into account the relashionship between neighborhing pixels in the classified image. Here it is done with the otb::MRFEnergyPotts which implement a Potts model.

The second energy is for the fidelity to the original data. Here it is done with an otb::MRFEnergyGaussianClassification class, which defines a gaussian model for the data.

    typedef otb::MRFEnergyPotts
    <LabelledImageType, LabelledImageType>  EnergyRegularizationType;
    typedef otb::MRFEnergyGaussianClassification
    <InputImageType, LabelledImageType>  EnergyFidelityType;

The different filters composing our pipeline are created by invoking their New() methods, assigning the results to smart pointers.

    MarkovRandomFieldFilterType::Pointer markovFilter =
      MarkovRandomFieldFilterType::New();
    EnergyRegularizationType::Pointer energyRegularization =
      EnergyRegularizationType::New();
    EnergyFidelityType::Pointer energyFidelity = EnergyFidelityType::New();
    OptimizerType::Pointer      optimizer = OptimizerType::New();
    SamplerType::Pointer        sampler = SamplerType::New();

Parameter for the otb::MRFEnergyGaussianClassification class, meand and standard deviation are created.

    unsigned int nClass = 4;
    energyFidelity->SetNumberOfParameters(2  nClass);
    EnergyFidelityType::ParametersType parameters;
    parameters.SetSize(energyFidelity->GetNumberOfParameters());
    parameters[0] = 10.0; //Class 0 mean
    parameters[1] = 10.0; //Class 0 stdev
    parameters[2] = 80.0; //Class 1 mean
    parameters[3] = 10.0; //Class 1 stdev
    parameters[4] = 150.0; //Class 2 mean
    parameters[5] = 10.0; //Class 2 stdev
    parameters[6] = 220.0; //Class 3 mean
    parameters[7] = 10.0; //Class 3 stde
    energyFidelity->SetParameters(parameters);

Parameters are given to the different class an the sampler, optimizer and energies are connected with the Markov filter.

    OptimizerType::ParametersType param(1);
    param.Fill(atof(argv[5]));
    optimizer->SetParameters(param);
    markovFilter->SetNumberOfClasses(nClass);
    markovFilter->SetMaximumNumberOfIterations(atoi(argv[4]));
    markovFilter->SetErrorTolerance(0.0);
    markovFilter->SetLambda(atof(argv[3]));
    markovFilter->SetNeighborhoodRadius(1);
  
    markovFilter->SetEnergyRegularization(energyRegularization);
    markovFilter->SetEnergyFidelity(energyFidelity);
    markovFilter->SetOptimizer(optimizer);
    markovFilter->SetSampler(sampler);

The pipeline is connected. An itk::RescaleIntensityImageFilter rescale the classified image before saving it.

    markovFilter->SetInput(reader->GetOutput());
  
    typedef itk::RescaleIntensityImageFilter
    <LabelledImageType, LabelledImageType> RescaleType;
    RescaleType::Pointer rescaleFilter = RescaleType::New();
    rescaleFilter->SetOutputMinimum(0);
    rescaleFilter->SetOutputMaximum(255);
  
    rescaleFilter->SetInput(markovFilter->GetOutput());
  
    writer->SetInput(rescaleFilter->GetOutput());

Finally, the pipeline execution is trigerred.

    writer->Update();

Figure 19.10 shows the output of the Markov Random Field classification after 20 iterations with a random sampler and a Metropolis optimizer.


PIC PIC

Figure 19.10: Result of applying the otb::MarkovRandomFieldFilter to an extract from a PAN Quickbird image for classification. The result is obtained after 20 iterations with a random sampler and a Metropolis optimizer. From left to right : original image, classification.


The source code for this example can be found in the file
Examples/Markov/MarkovClassification2Example.cxx.

Using a similar structure as the previous program and the same energy function, we are now going to slightly alter the program to use a different sampler and optimizer. The proposed sample is proposed randomly according to the MAP probability and the optimizer is the ICM which accept the proposed sample if it enable a reduction of the energy.

First, we need to include header specific to these class:

  #include "otbMRFSamplerRandomMAP.h"
  #include "otbMRFOptimizerICM.h"

And to declare these new type:

    typedef otb::MRFSamplerRandomMAP<InputImageType,
        LabelledImageType> SamplerType;
  //   typedef otb::MRFSamplerRandom< InputImageType, LabelledImageType> SamplerType;
    typedef otb::MRFOptimizerICM OptimizerType;

As the otb::MRFOptimizerICM does not have any parameters, the call to optimizer->SetParameters() must be removed

Apart from these, no further modification is required.

Figure 19.11 shows the output of the Markov Random Field classification after 5 iterations with a MAP random sampler and an ICM optimizer.


PIC PIC

Figure 19.11: Result of applying the otb::MarkovRandomFieldFilter to an extract from a PAN Quickbird image for classification. The result is obtained after 5 iterations with a MAP random sampler and an ICM optimizer. From left to right : original image, classification.


The source code for this example can be found in the file
Examples/Markov/MarkovClassification3Example.cxx.

This example illustrates the details of the MarkovRandomFieldFilter by using the Fisher distribution to model the likelihood energy. This filter is an application of the Markov Random Fields for classification.

This example applies the MarkovRandomFieldFilter to classify an image into four classes defined by their Fisher distribution parameters L, M and mu. The optimization is done using a Metropolis algorithm with a random sampler. The regularization energy is defined by a Potts model and the fidelity or likelihood energy is modelled by a Fisher distribution. The parameter of the Fisher distribution was determined for each class in a supervised step. ( See the File OtbParameterEstimatioOfFisherDistribution ) This example is a contribution from Jan Wegner.

Then we must decide what pixel type to use for the image. We choose to make all computations with double precision. The labeled image is of type unsigned char which allows up to 256 different classes.

    const unsigned int Dimension = 2;
    typedef double        InternalPixelType;
    typedef unsigned char LabelledPixelType;
  
    typedef otb::Image<InternalPixelType, Dimension>    InputImageType;
    typedef otb::Image<LabelledPixelType, Dimension>    LabelledImageType;

We define a reader for the image to be classified, an initialization for the classification (which could be random) and a writer for the final classification.

    typedef otb::ImageFileReader< InputImageType >              ReaderType;
    typedef otb::ImageFileWriter< LabelledImageType >  WriterType;
  
    ReaderType::Pointer reader = ReaderType::New();
    WriterType::Pointer writer = WriterType::New();

Finally, we define the different classes necessary for the Markov classification. A MarkovRandomFieldFilter is instantiated, this is the main class which connect the other to do the Markov classification.

    typedef otb::MarkovRandomFieldFilter
        <InputImageType, LabelledImageType> MarkovRandomFieldFilterType;

An MRFSamplerRandomMAP, which derives from the MRFSampler, is instantiated. The sampler is in charge of proposing a modification for a given site. The MRFSamplerRandomMAP, randomly pick one possible value according to the MAP probability.

    typedef otb::MRFSamplerRandom< InputImageType, LabelledImageType> SamplerType;

An MRFOptimizerMetropolis, which derives from the MRFOptimizer, is instantiated. The optimizer is in charge of accepting or rejecting the value proposed by the sampler. The MRFSamplerRandomMAP, accept the proposal according to the variation of energy it causes and a temperature parameter.

    typedef otb::MRFOptimizerMetropolis OptimizerType;

Two energy, deriving from the MRFEnergy class need to be instantiated. One energy is required for the regularization, taking into account the relationship between neighboring pixels in the classified image. Here it is done with the MRFEnergyPotts, which implements a Potts model.

The second energy is used for the fidelity to the original data. Here it is done with a MRFEnergyFisherClassification class, which defines a Fisher distribution to model the data.

    typedef otb::MRFEnergyPotts
        <LabelledImageType, LabelledImageType>  EnergyRegularizationType;
    typedef otb::MRFEnergyFisherClassification
        <InputImageType, LabelledImageType>     EnergyFidelityType;

The different filters composing our pipeline are created by invoking their New() methods, assigning the results to smart pointers.

    MarkovRandomFieldFilterType::Pointer markovFilter = MarkovRandomFieldFilterType::New();
    EnergyRegularizationType::Pointer energyRegularization = EnergyRegularizationType::New();
    EnergyFidelityType::Pointer energyFidelity = EnergyFidelityType::New();
    OptimizerType::Pointer optimizer = OptimizerType::New();
    SamplerType::Pointer sampler = SamplerType::New();

Parameter for the MRFEnergyFisherClassification class are created. The shape parameters M, L and the weighting parameter mu are computed in a supervised step

    unsigned int nClass =4;
    energyFidelity->SetNumberOfParameters(3nClass);
    EnergyFidelityType::ParametersType parameters;
    parameters.SetSize(energyFidelity->GetNumberOfParameters());
    //Class 0
    parameters[0] =         12.353042;    //Class 0 mu
    parameters[1] =         2.156422;       //Class 0 L
    parameters[2] =         4.920403;       //Class 0 M
    //Class 1
    parameters[3] =         72.068291;    //Class 1 mu
    parameters[4] =         11.000000;   //Class 1 L
    parameters[5] =         50.950001;    //Class 1 M
    //Class 2
    parameters[6] =         146.665985;   //Class 2 mu
    parameters[7] =         11.000000;   //Class 2 L
    parameters[8] =         50.900002;    //Class 2 M
    //Class 3
    parameters[9]  =      200.010132;     //Class 3 mu
    parameters[10] =      11.000000;      //Class 3 L
    parameters[11] =      50.950001;      //Class 3 M
  
    energyFidelity->SetParameters(parameters);

Parameters are given to the different classes and the sampler, optimizer and energies are connected with the Markov filter.

    OptimizerType::ParametersType param(1);
    param.Fill(atof(argv[6]));
    optimizer->SetParameters(param);
    markovFilter->SetNumberOfClasses(nClass);
    markovFilter->SetMaximumNumberOfIterations(atoi(argv[5]));
    markovFilter->SetErrorTolerance(0.0);
    markovFilter->SetLambda(atof(argv[4]));
    markovFilter->SetNeighborhoodRadius(1);
  
    markovFilter->SetEnergyRegularization(energyRegularization);
    markovFilter->SetEnergyFidelity(energyFidelity);
    markovFilter->SetOptimizer(optimizer);
    markovFilter->SetSampler(sampler);

The pipeline is connected. An itkRescaleIntensityImageFilter rescales the classified image before saving it.

    markovFilter->SetInput(reader->GetOutput());
  
    typedef itk::RescaleIntensityImageFilter
        < LabelledImageType, LabelledImageType > RescaleType;
    RescaleType::Pointer rescaleFilter = RescaleType::New();
    rescaleFilter->SetOutputMinimum(0);
    rescaleFilter->SetOutputMaximum(255);
  
    rescaleFilter->SetInput( markovFilter->GetOutput() );
  
    writer->SetInput( rescaleFilter->GetOutput() );
    writer->Update();

We can now create an image file writer and save the image.

    typedef otb::ImageFileWriter<RGBImageType> WriterRescaledType;
  
    WriterRescaledType::Pointer writerRescaled = WriterRescaledType::New();
  
    writerRescaled->SetFileName( outputRescaledImageFileName );
    writerRescaled->SetInput( colormapper->GetOutput() );
  
    writerRescaled->Update();

Figure 19.12 shows the output of the Markov Random Field classification into four classes using the Fisher-distribution as likelihood term.


PIC PIC

Figure 19.12: Result of applying the otb::MarkovRandomFieldFilter to an extract from a PAN Quickbird image for classification into four classes using the Fisher-distribution as likehood term. From left to right : original image, classification.


The source code for this example can be found in the file
Examples/Markov/MarkovRegularizationExample.cxx.

This example illustrates the use of the otb::MarkovRandomFieldFilter . to regularize a classification obtained previously by another classifier. Here we will apply the regularization to the output of an SVM classifier presented in ??.

The reference image and the starting image are both going to be the original classification. Both regularization and fidelity energy are defined by Potts model.

The convergence of the Markov Random Field is done with a random sampler and a Metropolis model as in example 1. As you should get use to the general program structure to use the MRF framework, we are not going to repeat the entire example. However, remember you can find the full source code for this example in your OTB source directory.

To find the number of classes available in the original image we use the itk::LabelStatisticsImageFilter and more particularly the method GetNumberOfLabels().

    typedef itk::LabelStatisticsImageFilter
    <LabelledImageType, LabelledImageType> LabelledStatType;
    LabelledStatType::Pointer labelledStat = LabelledStatType::New();
    labelledStat->SetInput(reader->GetOutput());
    labelledStat->SetLabelInput(reader->GetOutput());
    labelledStat->Update();
  
    unsigned int nClass = labelledStat->GetNumberOfLabels();

Figure 19.13 shows the output of the Markov Random Field regularization on the classification output of another method.


PIC PIC

Figure 19.13: Result of applying the otb::MarkovRandomFieldFilter to regularized the result of another classification. From left to right : original classification, regularized classification


19.5 Fusion of Classification maps

19.5.1 General approach of image fusion

In order to obtain a relevant image classification it is sometimes necessary to fuse several classification maps coming from different classification methods (SVM, KNN, Random Forest, Artificial Neural Networks,...). The fusion of classification maps combines them in a more robust and precise one. Two methods are available in the OTB: the majority voting and the Demspter Shafer framework.

19.5.2 Majority voting

19.5.2.1 General description

For each input pixel, the Majority Voting method consists in choosing the more frequent class label among all classification maps to fuse. In case of not unique more frequent class labels, the undecided value is set for such pixels in the fused output image.

19.5.2.2 An example of majority voting fusion

The source code for this example can be found in the file
Examples/Classification/MajorityVotingFusionOfClassificationMapsExample.cxx.

The Majority Voting fusion filter itk::LabelVotingImageFilter used is based on ITK. For each pixel, it chooses the more frequent class label among the input classification maps. In case of not unique more frequent class labels, the output pixel is set to the undecidedLabel value. We start by including the appropriate header file.

  #include "itkLabelVotingImageFilter.h"

We will assume unsigned short type input labeled images.

    const unsigned int     Dimension = 2;
    typedef unsigned short LabelPixelType;

The input labeled images to be fused are expected to be scalar images.

    typedef otb::Image<LabelPixelType, Dimension> LabelImageType;

The Majority Voting fusion filter itk::LabelVotingImageFilter based on ITK is templated over the input and output labeled image type.

    // Majority Voting
    typedef itk::LabelVotingImageFilter<LabelImageType, LabelImageType>
                                                               LabelVotingFilterType;

Both reader and writer are defined. Since the images to classify can be very big, we will use a streamed writer which will trigger the streaming ability of the fusion filter.

    typedef otb::ImageFileReader<LabelImageType> ReaderType;
    typedef otb::ImageFileWriter<LabelImageType> WriterType;

The input classification maps to be fused are pushed into the itk::LabelVotingImageFilter . Moreover, the label value for the undecided pixels (in case of not unique majority voting) is set too.

    ReaderType::Pointer reader;
    LabelVotingFilterType::Pointer labelVotingFilter = LabelVotingFilterType::New();
    for (unsigned int itCM = 0; itCM < nbClassificationMaps; ++itCM)
      {
      std::string fileNameClassifiedImage = argv[itCM + 1];
  
      reader = ReaderType::New();
      reader->SetFileName(fileNameClassifiedImage);
      reader->Update();
  
      labelVotingFilter->SetInput(itCM, reader->GetOutput());
      }
  
    labelVotingFilter->SetLabelForUndecidedPixels(undecidedLabel);

Once it is plugged the pipeline triggers its execution by updating the output of the writer.

    WriterType::Pointer writer = WriterType::New();
    writer->SetInput(labelVotingFilter->GetOutput());
    writer->SetFileName(outfname);
    writer->Update();

19.5.3 Dempster Shafer

19.5.3.1 General description

A more adaptive fusion method using the Dempster Shafer theory (http://en.wikipedia.org/wiki/Dempster-Shafer_theory) is available within the OTB. This method is adaptive as it is based on the so-called belief function of each class label for each classification map. Thus, each classified pixel is associated to a degree of confidence according to the classifier used. In the Dempster Shafer framework, the expert’s point of view (i.e. with a high belief function) is considered as the truth. In order to estimate the belief function of each class label, we use the Dempster Shafer combination of masses of belief for each class label and for each classification map. In this framework, the output fused label of each pixel is the one with the maximal belief function.

Like for the majority voting method, the Dempster Shafer fusion handles not unique class labels with the maximal belief function. In this case, the output fused pixels are set to the undecided value.

The confidence levels of all the class labels are estimated from a comparison of the classification maps to fuse with a ground truth, which results in a confusion matrix. For each classification maps, these confusion matrices are then used to estimate the mass of belief of each class label.

19.5.3.2 Mathematical formulation of the combination algorithm

A description of the mathematical formulation of the Dempster Shafer combination algorithm is available in the following OTB Wiki page: http://wiki.orfeo-toolbox.org/index.php/Information_fusion_framework.

19.5.3.3 An example of Dempster Shafer fusion

The source code for this example can be found in the file
Examples/Classification/DempsterShaferFusionOfClassificationMapsExample.cxx.

The fusion filter otb::DSFusionOfClassifiersImageFilter is based on the Dempster Shafer (DS) fusion framework. For each pixel, it chooses the class label Ai for which the belief function bel(Ai) is maximal after the DS combination of all the available masses of belief of all the class labels. The masses of belief (MOBs) of all the labels present in each classification map are read from input *.CSV confusion matrix files. Moreover, the pixels into the input classification maps to be fused which are equal to the nodataLabel value are ignored by the fusion process. In case of not unique class labels with the maximal belief function, the output pixels are set to the undecidedLabel value. We start by including the appropriate header files.

  #include "otbImageListToVectorImageFilter.h"
  #include "otbConfusionMatrixToMassOfBelief.h"
  #include "otbDSFusionOfClassifiersImageFilter.h"
  
  #include <fstream>

We will assume unsigned short type input labeled images. We define a type for confusion matrices as itk::VariableSizeMatrix which will be used to estimate the masses of belief of all the class labels for each input classification map. For this purpose, the otb::ConfusionMatrixToMassOfBelief will be used to convert each input confusion matrix into masses of belief for each class label.

    typedef unsigned short LabelPixelType;
    typedef unsigned long ConfusionMatrixEltType;
    typedef itk::VariableSizeMatrix<ConfusionMatrixEltType> ConfusionMatrixType;
    typedef otb::ConfusionMatrixToMassOfBelief
             <ConfusionMatrixType, LabelPixelType> ConfusionMatrixToMassOfBeliefType;
    typedef ConfusionMatrixToMassOfBeliefType::MapOfClassesType MapOfClassesType;

The input labeled images to be fused are expected to be scalar images.

    const unsigned int     Dimension = 2;
    typedef otb::Image<LabelPixelType, Dimension> LabelImageType;
    typedef otb::VectorImage<LabelPixelType, Dimension> VectorImageType;

We declare an otb::ImageListToVectorImageFilter which will stack all the input classification maps to be fused as a single VectorImage for which each band is a classification map. This VectorImage will then be the input of the Dempster Shafer fusion filter otb::DSFusionOfClassifiersImageFilter .

    typedef otb::ImageList<LabelImageType> LabelImageListType;
    typedef otb::ImageListToVectorImageFilter
              <LabelImageListType, VectorImageType> ImageListToVectorImageFilterType;

The Dempster Shafer fusion filter otb::DSFusionOfClassifiersImageFilter is declared.

    // Dempster Shafer
    typedef otb::DSFusionOfClassifiersImageFilter
          <VectorImageType, LabelImageType> DSFusionOfClassifiersImageFilterType;

Both reader and writer are defined. Since the images to classify can be very big, we will use a streamed writer which will trigger the streaming ability of the fusion filter.

    typedef otb::ImageFileReader<LabelImageType> ReaderType;
    typedef otb::ImageFileWriter<LabelImageType> WriterType;

The image list of input classification maps is filled. Moreover, the input confusion matrix files are converted into masses of belief.

    ReaderType::Pointer reader;
    LabelImageListType::Pointer imageList = LabelImageListType::New();
    ConfusionMatrixToMassOfBeliefType::Pointer confusionMatrixToMassOfBeliefFilter;
    confusionMatrixToMassOfBeliefFilter = ConfusionMatrixToMassOfBeliefType::New();
  
    MassOfBeliefDefinitionMethod massOfBeliefDef;
  
    // Several parameters are available to estimate the masses of belief
    // from the confusion matrices: PRECISION, RECALL, ACCURACY and KAPPA
    massOfBeliefDef = ConfusionMatrixToMassOfBeliefType::PRECISION;
  
    VectorOfMapOfMassesOfBeliefType vectorOfMapOfMassesOfBelief;
    for (unsigned int itCM = 0; itCM < nbClassificationMaps; ++itCM)
      {
      std::string fileNameClassifiedImage = argv[itCM + 1];
      std::string fileNameConfMat = argv[itCM + 1 + nbClassificationMaps];
  
      reader = ReaderType::New();
      reader->SetFileName(fileNameClassifiedImage);
      reader->Update();
  
      imageList->PushBack(reader->GetOutput());
  
      MapOfClassesType mapOfClassesClk;
      ConfusionMatrixType confusionMatrixClk;
  
      // The data (class labels and confusion matrix values) are read and
      // extracted from the ⋆.CSV file with an ad-hoc file parser
      CSVConfusionMatrixFileReader(
          fileNameConfMat, mapOfClassesClk, confusionMatrixClk);
  
      // The parameters of the ConfusionMatrixToMassOfBelief filter are set
      confusionMatrixToMassOfBeliefFilter->SetMapOfClasses(mapOfClassesClk);
      confusionMatrixToMassOfBeliefFilter->SetConfusionMatrix(confusionMatrixClk);
      confusionMatrixToMassOfBeliefFilter->SetDefinitionMethod(massOfBeliefDef);
      confusionMatrixToMassOfBeliefFilter->Update();
  
      // Vector containing ALL the K (= nbClassificationMaps) std::map<Label, MOB>
      // of Masses of Belief
      vectorOfMapOfMassesOfBelief.push_back(
          confusionMatrixToMassOfBeliefFilter->GetMapMassOfBelief());
      }

The image list of input classification maps is converted into a VectorImage to be used as input of the otb::DSFusionOfClassifiersImageFilter .

    // Image List To VectorImage
    ImageListToVectorImageFilterType::Pointer imageListToVectorImageFilter;
    imageListToVectorImageFilter = ImageListToVectorImageFilterType::New();
    imageListToVectorImageFilter->SetInput(imageList);
  
    DSFusionOfClassifiersImageFilterType::Pointer dsFusionFilter;
    dsFusionFilter = DSFusionOfClassifiersImageFilterType::New();
  
    // The parameters of the DSFusionOfClassifiersImageFilter are set
    dsFusionFilter->SetInput(imageListToVectorImageFilter->GetOutput());
    dsFusionFilter->SetInputMapsOfMassesOfBelief(&vectorOfMapOfMassesOfBelief);
    dsFusionFilter->SetLabelForNoDataPixels(nodataLabel);
    dsFusionFilter->SetLabelForUndecidedPixels(undecidedLabel);

Once it is plugged the pipeline triggers its execution by updating the output of the writer.

    WriterType::Pointer writer = WriterType::New();
    writer->SetInput(dsFusionFilter->GetOutput());
    writer->SetFileName(outfname);
    writer->Update();

19.6 Classification map regularization

The source code for this example can be found in the file
Examples/Classification/ClassificationMapRegularizationExample.cxx.

After having generated a classification map, it is possible to regularize such a labeled image in order to obtain more homogeneous areas, which facilitates its interpretation. For this purpose, the otb::NeighborhoodMajorityVotingImageFilter was implemented. Like a morphological filter, this filter uses majority voting in a ball shaped neighborhood in order to set each pixel of the classification map to the most representative label value in its neighborhood.

In this example we will illustrate its use. We start by including the appropriate header file.

  #include "otbNeighborhoodMajorityVotingImageFilter.h"

Since the input image is a classification map, we will assume a single band input image for which each pixel value is a label coded on 8 bits as an integer between 0 and 255.

    typedef unsigned char IOLabelPixelType; // 8 bits
    const unsigned int Dimension = 2;

Thus, both input and output images are single band labeled images, which are composed of the same type of pixels in this example (unsigned char).

    typedef otb::Image<IOLabelPixelType, Dimension> IOLabelImageType;

We can now define the type for the neighborhood majority voting filter, which is templated over its input and output images types as well as its structuring element type. Choosing only the input image type in the template of this filter induces that, both input and output images types are the same and that the structuring element is a ball ( itk::BinaryBallStructuringElement ).

    // Neighborhood majority voting filter type
    typedef otb::NeighborhoodMajorityVotingImageFilter<IOLabelImageType>
     NeighborhoodMajorityVotingFilterType;

Since the otb::NeighborhoodMajorityVotingImageFilter is a neighborhood based image filter, it is necessary to set the structuring element which will be used for the majority voting process. By default, the structuring element is a ball ( itk::BinaryBallStructuringElement ) with a radius defined by two sizes (respectively along X and Y). Thus, it is possible to handle anisotropic structuring elements such as ovals.

    // Binary ball Structuring Element type
    typedef NeighborhoodMajorityVotingFilterType::KernelType StructuringType;
    typedef StructuringType::RadiusType RadiusType;

Finally, we define the reader and the writer.

    typedef otb::ImageFileReader<IOLabelImageType> ReaderType;
    typedef otb::ImageFileWriter<IOLabelImageType> WriterType;

We instantiate the otb::NeighborhoodMajorityVotingImageFilter and the reader objects.

    // Neighborhood majority voting filter
    NeighborhoodMajorityVotingFilterType::Pointer NeighMajVotingFilter;
    NeighMajVotingFilter = NeighborhoodMajorityVotingFilterType::New();
  
    ReaderType::Pointer reader = ReaderType::New();
    reader->SetFileName(inputFileName);

The ball shaped structuring element seBall is instantiated and its two radii along X and Y are initialized.

    StructuringType seBall;
    RadiusType rad;
  
    rad[0] = radiusX;
    rad[1] = radiusY;
  
    seBall.SetRadius(rad);
    seBall.CreateStructuringElement();

Then, this ball shaped neighborhood is used as the kernel structuring element for the otb::NeighborhoodMajorityVotingImageFilter .

    NeighMajVotingFilter->SetKernel(seBall);

Not classified input pixels are assumed to have the noDataValue label and will keep this label in the output image.

    NeighMajVotingFilter->SetLabelForNoDataPixels(noDataValue);

Furthermore, since the majority voting regularization may lead to different majority labels in the neighborhood, in this case, it would be important to define the filter’s behaviour. For this purpose, a Boolean parameter is used in the filter to choose whether pixels with more than one majority class are set to undecidedValue (true), or to their Original labels (false = default value) in the output image.

    NeighMajVotingFilter->SetLabelForUndecidedPixels(undecidedValue);
  
    if (KeepOriginalLabelBoolStr.compare("true") == 0)
    {
      NeighMajVotingFilter->SetKeepOriginalLabelBool(true);
    }
    else
    {
      NeighMajVotingFilter->SetKeepOriginalLabelBool(false);
    }

We plug the pipeline and trigger its execution by updating the output of the writer.

    NeighMajVotingFilter->SetInput(reader->GetOutput());
  
    WriterType::Pointer writer = WriterType::New();
    writer->SetFileName(outputFileName);
    writer->SetInput(NeighMajVotingFilter->GetOutput());
    writer->Update();