Chapter 30
How to write an application

This chapter presents the different steps to write your own application. It also contains a description of the framework surrounding the applications.

30.1 Application design

The first logical step is to define the role of your application:

Then you should have a good vision of your application pipeline. Depending on the different filters used, the application can be streamed and threaded. The threading capabilities can be different between the filters so there is no overall threading parameter (by default, each filter has its own threading settings).

It is a different story for streaming. Since the image writers are handled within the framework and outside the reach of the developer, the default behaviour is to use streaming. If one of the filters doesn’t support streaming, it will enlarge the requested output region to the largest possible region and the entire image will be processed at once. As a result, the developer doesn’t have to handle streaming nor threading. However, there is a way to choose the number of streaming divisions (see section 30.2.4).

30.2 Architecture of the class

Every application derive from the class otb::Wrapper::Application . An application can’t be templated. It must contain the standard class typedefs and a call to the OTB_APPLICATION_EXPORT macro.

You need also to define standard macros itk::NewMacro and itk::TypeMacro .

It is also mandatory to implement three methods in a new application:

30.2.1 DoInit()

This method is called once, when the application is instantiated. It should contain the following actions:

30.2.2 DoUpdateParameters()

This method is called after every modification of a parameter value. With the command line launcher, it is called each time a parameter is loaded. With the Qt launcher, it is called each time a parameter field is modified. It can be used to maintain consistency and relationship between parameters (e.g. in ExtractROI: when changing the input image, maybe the ROI size has to be updated).

30.2.3 DoExecute()

This method contains the real action of the application. This is where the pipeline must be set up. The application framework provides different methods to get a value or an object associated to a parameter:

where key refers to parameter key, defined using AddParameter() method in DoInit() method.

Similar methods exist for binding a data object to an output parameter:

If possible, no filter update should be called inside this function. The update will be automatically called afterwards : for every image or vector data output, a writer is created and updated.

30.2.4 Parameters selection

In the new application framework, every input, output or parameter derive from otb::Wrapper::Parameter . The application engine supplies the following types of parameters:

30.2.5 Parameters description

Each created parameter has a unique key and several boolean flags to represent its state. These flags can be used to set a parameter optional or test if the user has modified the parameter value. The parameters are created in the DoInit() method, then the framework will set their value (either by parsing the command line or reading the graphical user interface). The DoExecute() method is called when all mandatory parameters have been given a value, which can be obtained with ”Get” methods defined in otb::Wrapper::Application . Parameters are set mandatory (or not) using MandatoryOn(key) method (MandatoryOff(key)).

Some functions are specific to numeric parameters, such as SetMinimumParameterIntValue(key,value) or SetMaximumParameterFloatValue(key,value). By default, numeric parameters are treated as inputs. If your application outputs a number, you can use a numeric parameter and change its role by calling SetParameterRole(key,Role_Output).

The input types InputImage, InputImageList, ComplexInputImage, InputVectorData and InputVectorDataList store the name of the files to load, but they also encapsulate the readers needed to produce the input data.

The output types OutputImage, ComplexOutputImage and OutputVectorData store the name of the files to write, but they also encapsulate the corresponding writers.

30.3 Composite application

The application framework has been extended to allow the implementation of composite applications : applications that use other applications. The concept is simple : you have two applications A and B that you want to chain in order to build a third application C. Rather than writing C by copying the code of A and B, you would like to re-use applications A and B. This plain example will be re-used in this section for explanations.

A dedicated class otb::Wrapper::CompositeApplication has been added to create such applications. If you derive this class to implement application C, you will be able to create a composite application.

30.3.1 Creating internal applications

Like with standard applications, you have to write a DoInit() function. In this function, you should first clean any internal application with the function ClearApplications() (the DoInit() function is called twice in some cases). Then you can instantiate the internal applications that you want to use (for instance A and B). The function AddApplication() will do that, based on :

Using the function GetInternalApplication(), you can get a pointer to the internal application corresponding to a given identifier.

In the example given in introduction, we assume that :

30.3.2 Connecting parameters

Once you have internal applications, you may want to setup their parameters. There are typically 3 cases.

You may want to expose a parameter of an internal application as a parameter of your composite application. Let say you want to expose parameter io.in from application a into your composite application C with the key input. You can call the function :

ShareParameter("input","a.io.in")

As a result, the parameters input in application C and io.in in application a will point to the same object. Under the two parameter keys, there is a unique value. These two parameters can be considered as synchronized.

This leads to the second case : you may want to synchronize two parameters from internal applications. Let say you want to synchronize parameter field from application a with parameter fname from application b. You can call the function :

Connect("a.field","b.fname")

Note that the functions ShareParameter() and Connect() :

In this synchronization, the two parameters should have the same type, or have a similar interface, such as input and output filenames that are both accessed using GetParameterString() and SetParameterString().

This type of connection is a transition to the third case : you may want to connect the output of an internal application to the input of an other internal application. Here the difficulty is that the two parameters to connect probably have different types. Let say you want to connect parameter a.out to parameter b.in. The ”Connect()” function may work in favorable cases (see previous paragraph), but for images, you have two options :

At the moment, the in-memory connexion of vector data parameters is not supported.

30.3.3 Orchestration

In the DoUpdateParameters() of your composite application, you can call the same function on an internal application with the function UpdateInternalParameters(). This is needed only if your internal applications have a specific behaviour during parameter update.

In the DoExecute() of your composite application, you have to call ExecuteInternal() in order to launch each internal application. The order should be compatible with image parameter connexions. If you want to do ”in-memory” connexions, you can do it between two calls to ExecuteInternal(), for instance :

ExecuteInternal("a"); 
GetInternalApplication("b")->SetParameterInputImage("in", 
  GetInternalApplication("a")->GetParameterOutputImage("out")); 
ExecuteInternal("b");

The application BundleToPerfectSensor is a simple example of composite applications. For a more complex example, you can check the application TrainImagesClassifier.

30.4 Compile your application

In order to compile your application you must call the macro OTB_CREATE_APPLICATION in the CMakelists.txt file. This macro generates the lib otbapp_XXX.so, in (OTB_BINARY_DIR/lib/otb/applications), where XXX refers to the class name.

30.5 Execute your application

There are different ways to launch applicatons :

CommandLine :
The command line option is invoked using otbApplicationLauncherCommandLine executable followed by the classname, the application dir and the application parameters.
QT :
Application can be encapsuled in Qt framework using otbApplicationLauncherQt executable followed by the classname and the application dir.
Python :
A Python wrapper is also available.

30.6 Testing your application

It is possible to write application tests. They are quite similar to filters tests. The macro OTB_TEST_APPLICATION makes it easy to define a new test.

30.7 Application Example

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

This example illustrates the creation of an application. A new application is a class, which derives from otb::Wrapper::Application class. We start by including the needed header files.

#include "otbWrapperApplication.h" 
#include "otbWrapperApplicationFactory.h"

Application class is defined in Wrapper namespace.

namespace Wrapper 
{

ExampleApplication class is derived from Application class.

class ApplicationExample : public Application

Class declaration is followed by ITK public types for the class, the superclass and smart pointers.

  typedef ApplicationExample Self; 
  typedef Application Superclass; 
  typedef itk::SmartPointer<Self> Pointer; 
  typedef itk::SmartPointer<const Self> ConstPointer;

Following macros are necessary to respect ITK object factory mechanisms. Please report to 28.5 for additional information.

  itkNewMacro(Self) 
; 
 
  itkTypeMacro(ExampleApplication, otb::Application) 
;

otb::Application relies on three main private methods: DoInit(), DoUpdate(), and DoExecute(). Section 30.2 gives a description a these methods. DoInit() method contains class information and description, parameter set up, and example values. Application name and description are set using following methods :

SetName()
Name of the application.
SetDescription()
Set the short description of the class.
SetDocName()
Set long name of the application (that can be displayed …).
SetDocLongDescription()
This methods is used to describe the class.
SetDocLimitations()
Set known limitations (threading, invalid pixel type …) or bugs.
SetDocAuthors()
Set the application Authors. Author List. Format : ”John Doe, Winnie the Pooh” …
SetDocSeeAlso()
If the application is related to one another, it can be mentioned.
    SetName("Example"); 
    SetDescription("This application opens an image and save it. " 
      "Pay attention, it includes Latex snippets in order to generate " 
      "software guide documentation"); 
 
    SetDocName("Example"); 
    SetDocLongDescription("The purpose of this application is " 
      "to present parameters types," 
      " and Application class framework. " 
      "It is used to generate Software guide documentation" 
      " for Application chapter example."); 
    SetDocLimitations("None"); 
    SetDocAuthors("OTB-Team"); 
    SetDocSeeAlso(" ");

AddDocTag() method categorize the application using relevant tags. Code/ApplicationEngine/otbWrapperTags.h contains some predefined tags defined in Tags namespace.

    AddDocTag(Tags::Analysis); 
    AddDocTag("Test");

Application parameters declaration is done using AddParameter() method. AddParameter() requires Parameter type, its name and description. otb::Wrapper::Application class contains methods to set parameters characteristics.

    AddParameter(ParameterType_InputImage, "in", "Input Image"); 
    AddParameter(ParameterType_OutputImage, "out", "Output Image"); 
    AddParameter(ParameterType_Empty, "boolean", "Boolean"); 
    MandatoryOff("boolean"); 
    AddParameter(ParameterType_Int, "int", "Integer"); 
    MandatoryOff("int"); 
    SetDefaultParameterInt("int", 1); 
    SetMinimumParameterIntValue("int", 0); 
    SetMaximumParameterIntValue("int", 10); 
    AddParameter(ParameterType_Float, "float", "Float"); 
    MandatoryOff("float"); 
    SetDefaultParameterFloat("float", 0.2); 
    SetMinimumParameterFloatValue("float", -1.0); 
    SetMaximumParameterFloatValue("float", 15.0); 
    AddParameter(ParameterType_String, "string", "String"); 
    MandatoryOff("string"); 
    AddParameter(ParameterType_InputFilename, "filename", "File name"); 
    MandatoryOff("filename"); 
    AddParameter(ParameterType_Directory, "directory", "Directory name"); 
    MandatoryOff("directory"); 
 
    AddParameter(ParameterType_Choice, "choice", "Choice"); 
    AddChoice("choice.choice1", "Choice 1"); 
    AddChoice("choice.choice2", "Choice 2"); 
    AddChoice("choice.choice3", "Choice 3"); 
    AddParameter(ParameterType_Float, "choice.choice1.floatchoice1" 
                 , "Float of choice1"); 
    SetDefaultParameterFloat("choice.choice1.floatchoice1", 0.125); 
    AddParameter(ParameterType_Float, "choice.choice3.floatchoice3" 
                 , "Float of choice3"); 
    SetDefaultParameterFloat("choice.choice3.floatchoice3", 5.0); 
 
    AddParameter(ParameterType_Group, "ingroup", "Input Group"); 
    MandatoryOff("ingroup"); 
    AddParameter(ParameterType_Int, "ingroup.integer", "Integer of Group"); 
    MandatoryOff("ingroup.integer"); 
    AddParameter(ParameterType_Group, "ingroup.images", "Input Images Group"); 
    AddParameter(ParameterType_InputImage, "ingroup.images.inputimage" 
                 , "Input Image"); 
    MandatoryOff("ingroup.images.inputimage"); 
    AddParameter(ParameterType_Group, "outgroup", "Output Group"); 
    MandatoryOff("outgroup"); 
    AddParameter(ParameterType_OutputImage, "outgroup.outputimage" 
                 , "Output Image"); 
    MandatoryOff("outgroup.outputimage"); 
    AddParameter(ParameterType_InputImageList, "il", "Input image list"); 
    MandatoryOff("il"); 
 
    AddParameter(ParameterType_ListView, "cl", "Output Image channels"); 
    AddChoice("cl.choice1", "cl.choice1"); 
    AddChoice("cl.choice2", "cl.choice2"); 
    MandatoryOff("cl"); 
 
    AddParameter(ParameterType_RAM, "ram", "Available RAM"); 
    SetDefaultParameterInt("ram", 256); 
    MandatoryOff("ram"); 
 
    AddParameter(ParameterType_ComplexInputImage, "cin", "Input Complex Image"); 
    AddParameter(ParameterType_ComplexOutputImage, "cout", "Output Complex Image"); 
    MandatoryOff("cin"); 
    MandatoryOff("cout");

An example commandline is automatically generated. Method SetDocExampleParameterValue() is used to set parameters. Dataset should be located in OTB-Data/Examples directory.

    SetDocExampleParameterValue("boolean", "true"); 
    SetDocExampleParameterValue("in", "QB_Suburb.png"); 
    SetDocExampleParameterValue("out", "Application_Example.png");

DoUpdateParameters() is called as soon as a parameter value change. Section 30.2.2 gives a complete description of this method.

  void DoUpdateParameters() ITK_OVERRIDE 
  { 
  }

DoExecute() contains the application core. Section 30.2.3 gives a complete description of this method.

  void DoExecute() ITK_OVERRIDE 
  { 
    FloatVectorImageType::Pointer inImage = GetParameterImage("in"); 
 
    int paramInt = GetParameterInt("int"); 
    otbAppLogDEBUG( << paramInt <<std::endl ); 
    int paramFloat = GetParameterFloat("float"); 
    otbAppLogINFO( << paramFloat ); 
 
    SetParameterOutputImage("out", inImage); 
  }

Finally OTB_APPLICATION_EXPORT is called.

OTB_APPLICATION_EXPORT(otb::Wrapper::ApplicationExample)