The credit of this post goes to Guillaume Cerutti from Virtual Plants project-team.
A modular platform dealing with a given scientific domain is made of some key ingredients (see dtk-introduction post for more details):
- a set of abstract classes defining the interfaces for the data, the algorithms and the views dedicated to the scientific field * a collection of plugins implementing these interfaces using home-made code or third-party libraries
Moreover, users of such a platform wish to prototype workflows in a very flexible manner, hence, the visual programming framework turns out to be very useful.
dtk provides tools to design such a platform with all of these ingredients. Let us see how these is done in practice.
<!–more–
Defining a dtk Concept for an Algorithm : Watershed example
Our goals are firstly to add a concept watershed to the dtkImaging thematic layer, then to define a plugin that will be visible for any dtk-based application, and eventually to use the third-party VT library to define an implementation of this concept.
![]() |
:—: |
Create a new abstraction in the dtk-imaging layer
Define the abstraction for the imaging filter
Let us create a header file that contains the code defining the interface of an abstract watershed algorithm: dtkAbstractWatershedFilter.h
. This abstract class defines the input and output methods of the filter. It inherits from QRunnable
so that it is directly compatible with the distributed framework of dtk which provides both multithreading and parallelism tools.
```cpp #pragma once
#include
class dtkImage;
class dtkAbstractWatershedFilter : public QRunnable {
public: virtual void setImage(dtkImage *image) = 0; virtual void setSeed(dtkImage *image) = 0;
public: virtual dtkImage *filteredImage(void) const = 0; public: virtual void run(void) = 0; };
The class methods (setters for the input and getters for the outputs) are made pure virtual: the classes implementing this abstraction have to to implement these functions. For sake of clarity, we recall the pure virtual `run` method of `QRunnable` class to make sure it will be implemented.
One can notice the forward declaration of `dtkImage` class. As a pointer to this class is only used (no method of the class is required in the header), this tells the compiler that it just needs to reserve the size of a pointer in memory.
#### Add export header and macro for symbols' visibility
In order to make the symbols of the class visible outside the library (especially in the plugin libraries), one has to include the export header generated automatically by CMake and add the export macro to the definition of the class.
```cpp #pragma once
#include
#include
class DTKIMAGING_EXPORT dtkAbstractWatershedFilter : public QRunnable
This step is mandatory for windows.
Add plugin system
The dtk plugin system is made of:
- a plugin class defining an interface that is implemented into concrete plugins and which are containers for the classes implementing the abstraction * a plugin manager in charge of loading the concrete plugins * a plugin factory in charge of instanciating concrete plugins using literal identifiers (usually the name of the plugin)
dtkCore provides macros that provides these three classes for any abstraction of the layer. Furthermore, as we intend to use the watershed algorithm through visual programming, there is an additional macro which enables to register the class to the QMetaType
system and to use it through QVariant
. In practice, these four macros are added at the bottom of the header.
```cpp };
//
DTK_DECLARE_OBJECT(dtkAbstractWatershedFilter *) DTK_DECLARE_PLUGIN(dtkAbstractWatershedFilter, DTKIMAGING_EXPORT) DTK_DECLARE_PLUGIN_FACTORY(dtkAbstractWatershedFilter, DTKIMAGING_EXPORT) DTK_DECLARE_PLUGIN_MANAGER(dtkAbstractWatershedFilter, DTKIMAGING_EXPORT)
#### Register the abstraction to the layer
In practice when using dtkImaging layer, one wants to be able to load all the plugins of all the abstractions in one single instruction. To do so, one has to add the `DTK_DECLARE_CONCEPT` macro in which, one has to give the name of the abstraction, the name of the export macro, the namespace that will enable to class the plugin factory of the abstraction as follows `dtkImaging::watershed::pluginFactory()-create("dummyWatershedFilter");`. Here is the macro:
```cpp namespace dtkImaging { DTK_DECLARE_CONCEPT(dtkAbstractWatershedFilter, DTKIMAGING_EXPORT, watershed); }
This macro has its counterpart DTK_DEFINE_CONCEPT
which must be inserted into a cpp file. So, the file dtkAbstractWatershedFilter.cpp
is created and contains the following code that ensure the registration to the manager of the layer:
#include "dtkImaging.h" #include "dtkAbstractWatershedFilter.h"
namespace dtkImaging { DTK_DEFINE_CONCEPT(dtkAbstractWatershedFilter, watershed, dtkImaging); }
Eventually, for aesthetic considerations, a file without extension dtkAbstractWatershedFilter
that contains #include "dtkAbstractWatershedFilter.h"
is added so that client code can include the abstraction as follows:
```cpp #include
#### Edit the compilation files to include the new abstraction
Once the files defining the abstraction have been written, one has to edit the `CMakeLists.txt` file of the layer and add the header file to the `${PROJECT_NAME}_HEADERS` section, and the source file to the `${PROJECT_NAME}_SOURCES` section:
```c set(${PROJECT_NAME}_HEADERS dtkAbstractAddFilter.h ... dtkAbstractWatershedFilter dtkAbstractWatershedFilter.h ...)
```c set(${PROJECT_NAME}_SOURCES dtkAbstractAddFilter.cpp … dtkAbstractWatershedFilter.cpp …)
One can then compile to include the changes. There is no need to re-run cmake configuration as the changes in the `CMakeLists.txt` will be detected.
cd build make -j4
## Define the plugin implementation
The goal is to implement the abstraction defined in the previous section and the plugin defined by the macros. The plugin will use the `dtkVtImageConverter` to make the bridge between the image data structure used in the VT library (native) and the `dtkImage` class.
#### Prepare the file arborescence and compilation files
* Create the file arborescence in the `dtk-plugins-imaging` project, under `src/VTPlugins`
cd src/VTPlugins mkdir dtkVtImageWatershedFilter cd dtkVtImageWatershedFilter
* The directory will contain 2 source files (+ headers) containing respectively the implementation of the abstraction and the definition of the plugin
dtkVtWatershedFilter.h dtkVtWatershedFilter.cpp dtkVtWatershedFilterPlugin.h dtkVtWatershedFilterPlugin.cpp dtkVtWatershedFilterPlugin.json
* Create the `CMakeLists.txt` file (you can copy it from another plugin if you're lazy...) that specifies the dtk layers and other libraries it has to know about.
```c project(dtkVtWatershedFilterPlugin)
## ################################################################### ## Build rules ## ###################################################################
add_definitions(-DQT_PLUGIN)
add_library(${PROJECT_NAME} SHARED dtkVtWatershedFilter.h dtkVtWatershedFilter.cpp dtkVtWatershedFilterPlugin.h dtkVtWatershedFilterPlugin.cpp)
## ################################################################### ## Link rules ## ###################################################################
target_link_libraries(${PROJECT_NAME} Qt5::Core)
target_link_libraries(${PROJECT_NAME} dtkCore) target_link_libraries(${PROJECT_NAME} dtkLog) target_link_libraries(${PROJECT_NAME} dtkImaging)
target_link_libraries(${PROJECT_NAME} dtkVtImageConverter) target_link_libraries(${PROJECT_NAME} vt) target_link_libraries(${PROJECT_NAME} exec) target_link_libraries(${PROJECT_NAME} io) target_link_libraries(${PROJECT_NAME} basic)
## ################################################################# ## Install rules ## #################################################################
install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION plugins/${DTK_CURRENT_LAYER} LIBRARY DESTINATION plugins/${DTK_CURRENT_LAYER} ARCHIVE DESTINATION plugins/${DTK_CURRENT_LAYER})
###################################################################### ### CMakeLists.txt ends here
Define the implementation of the abstraction using the VT library
- Create the implementation header file
dtkVtWatershedFilter.h
with creator and destructor, and a D-Pointer to store the members of the class (to avoid listing the private data in the headers and hide the details of the implementation of the class), and a forward declaration ofdtkImage
.
```cpp #pragma once
#include
class dtkImage;
class dtkVtWatershedFilter final : public dtkAbstractWatershedFilter { public: dtkVtWatershedFilter(void); ~dtkVtWatershedFilter(void);
public: void setImage(dtkImage *image); void setSeed(dtkImage *image);
public: dtkImage *filteredImage(void) const;
public: void run(void);
private: class dtkVtWatershedFilterPrivate *d; };
* Add a creation function of the plugin implementation to be able to link it from outside the library
```cpp inline dtkAbstractWatershedFilter *dtkVtWatershedFilterCreator(void) { return new dtkVtWatershedFilter(); }
// // dtkVtWatershedFilter.h ends here
- Create the implementation source file
dtkVtWatershedFilter.cpp
with the corresponding dtk-plugins-imaging imports and the right VT library imports…
```cpp #include “dtkVtWatershedFilter.h” #include “dtkVtImageConverter.h”
#include
#include #include
* Create the private class for the D-Pointer, defining the private data used by the filter in its members, and implement a creator and a destructor. The private class also has a `run` method that will be called to perform the wrapped algorithm.
```cpp class dtkVtWatershedFilterPrivate { public: dtkImage *image_in; dtkImage *seed; dtkImage *image_out; public: template < typename ImgT, int dim void exec(void); };
dtkVtWatershedFilter::dtkVtWathershedFilter(void): dtkAbstractWatershedFilter(), d(new dtkVtWatershedFilterPrivate) { d-image_in = nullptr; d-seed = nullptr; d-image_out = new dtkImage(); }
dtkVtWatershedFilter::~dtkVtWatershedFilter(void) { delete d; }
- Implement the setters and getters of the public class
```cpp void dtkVtWatershedFilter::setImage(dtkImage *image) { d-image_in = image; }
void dtkVtWatershedFilter::setImage(dtkImage *seed) { d-seed = seed; }
dtkImage *dtkVtWatershedFilter::filteredImage(void) const { return d-image_out; }
* Finally, define the run function implementing the actual functionality of the filter. Raise a warning if one of the inputs is missing, and let the `Executor` call the `exec` function of the private class that will in turn call the functions from the VT library by its `exec` function.
```cpp void dtkVtWatershedFilter::run(void) { if (!d-image_in || !d-seed) { dtkWarn() << Q_FUNC_INFO << "no image input"; return; }
dtkFilterExecutor::run(d, d-image_in); }
- The only thing left to do is to implement the
exec
function of theExecutor
using the VT library functions that actually perform the job.
```cpp template < typename ImgT, int dim inline void dtkVtWatershedFilterPrivate::exec(void) { vt_image image; vt_image seed; char str[100]; sprintf( str, “-labelchoice %s”,”first”);
if (dtkVtImageConverter::convertToNative(this-image_in, &image) != EXIT_FAILURE) { if (dtkVtImageConverter::convertToNative(this-seed, &seed) != EXIT_FAILURE) { if ( API_watershed( &image, &seed, nullptr, str, (char*)nullptr ) != 1 ) { free( image.array ); image.array = nullptr; free( seed.array ); seed.array = nullptr; dtkError() « Q_FUNC_INFO « “Computation failed. Aborting.”; return; }
dtkVtImageConverter::convertFromNative(&image, this-image_out);
free( seed.array ); seed.array = nullptr; } free( image.array ); image.array = nullptr;
} else { dtkError() « Q_FUNC_INFO « “Conversion to VT format failed. Aborting.”; } }
#### Define the plugin container
The plugins will be discovered by the manager that will initialize them using their `initialize` function that has to be implemented. The plugin does not have to be included in the compilation (nor linked) to be loaded dynamically at runtime.
* Create the plugin header file `dtkVtWatershedFilterPlugin.h` to define the specific plugin class that inherits from the abstract plugin class created by the macros in the abstraction declaration.
```cpp #pragma once
#include #include
class dtkVtWatershedFilterPlugin: public dtkAbstractWatershedFilterPlugin { Q_OBJECT Q_INTERFACES(dtkAbstractWatershedFilterPlugin) Q_PLUGIN_METADATA(IID "fr.inria.dtkVtWatershedFilterPlugin" FILE "dtkVtWatershedFilterPlugin.json")
public: dtkVtWatershedFilterPlugin(void) {} ~dtkVtWatershedFilterPlugin(void) {}
public: void initialize(void); void uninitialize(void); };
- Add the plugin metadata-file
dtkVtWatershedFilterPlugin.json
containing information on the plugin requirements (library dependencies, data formats, cross-plugin dependencies,…)
```json { “name” : “dtkVtWatershedFilterPlugin”, “concept” : “dtkAbstractWatershedFilter”, “version” : “0.0.1”, “dependencies” : [] }
* Create the plugin source file `dtkVtWatershedFilterPlugin.cpp`, including the header of the implementation to be able to access the creator function.
```cpp #include "dtkVtWatershedFilter.h" #include "dtkVtWatershedFilterPlugin.h"
#include #include
- Implement the initialize function to register the creator function to the plugin factory using the name of the implementation.
```cpp void dtkVtWatershedFilterPlugin::initialize(void) { dtkImaging::filters::watershed::pluginFactory().record(“dtkVtWatershedFilter”, dtkVtWatershedFilterCreator); }
void dtkVtWatershedFilterPlugin::uninitialize(void) {
}
This allows to create a new instance of the concept by the command :
```cpp dtkImaging::filters::watershed::pluginFactory().create("dtkVtWatershedFilter")
- Add the macro to define the plugin, that wraps the Qt macro
Q_PLUGIN_METADATA
to allow the plugin to be seen by the Qt plugin system.
```cpp DTK_DEFINE_PLUGIN(dtkVtWatershedFilter)
#### Add the plugin to the system
* Open the `CMakeLists.txt` file of the parent directory (here `VTPlugins`) to add the new plugin directory to the `Inputs` section.
```c ## ################################################################# ## Inputs ## #################################################################
add_subdirectory(dtkVtImageConverter) ... add_subdirectory(dtkVtImageWatershedFilter) ...
- Complile the plugins projects, using simply the
make
command.
Declare a node wrapper for the concept
The idea is to wrap our concept to make it visible in the composer. Note that the it is the abstraction that will be wrapped, not the implementation itself (so that a specific implementation) can be used at runtime. Therefore the node files will be created in the dtk-imaging
project, under src/composer
.
cd src/composer
Create node header and source files
- Create the header file
dtkWatershedFilterNode.h
that contains the node class, inheriting an object node classdtkComposerNodeObject
templated by the abstraction. Once again the private data is stored as a D-Pointer (pointing on a private class defined in the source file).
```cpp #pragma once
#include #include
class dtkAbstractWatershedFilter; class dtkWatershedFilterNodePrivate;
class DTKIMAGING_EXPORT dtkWatershedFilterNode : public dtkComposerNodeObject { public: dtkWatershedFilterNode(void); ~dtkWatershedFilterNode(void);
public: void run(void);
private: dtkWatershedFilterNodePrivate *d; };
* Create the source file `dtkWatershedFilterNode.cpp` including the abstraction and the necessary data structures for the members of the private class.
```cpp #include "dtkWatershedFilterNode.h"
#include "dtkAbstractWatershedFilter.h" #include "dtkImage.h" #include #include "dtkImaging.h"
#include
- The first part is the definition of the private class defining the members of the node. The private class actually defines a mapping beteen inputs/outputs of the concept abstraction and receivers/emitters for
dtkComposer
.
```cpp class dtkWatershedFilterNodePrivate { public: dtkComposerTransmitterReceiver image_in; dtkComposerTransmitterReceiver seed; dtkComposerTransmitterReceiver control;
dtkComposerTransmitterEmitter image_out; };
* Then the creator of the public node class consists in appending the receivers and emitters corresponding to the node, and to define the plugin factory that will provide the implementation of the wrapped abstraction.
```cpp dtkWatershedFilterNode::dtkWatershedFilterNode(void) : dtkComposerNodeObject(), d(new dtkWatershedFilterNodePrivate()) { this-setFactory(dtkImaging::filters::watershed::pluginFactory());
this-appendReceiver(&d-image_in); this-appendReceiver(&d-seed); this-appendReceiver(&d-control);
this-appendEmitter (&d-image_out); }
dtkWatershedFilterNode::~dtkWatershedFilterNode(void) { delete d; }
- Finally, complete the
run
method of the node that consists in passing the inputs coming from the composer to the implementation coming from the factory and enabling the following nodes to access the outputs.
```cpp void dtkWatershedFilterNode::run(void) {
* Connecters (receivers and emitters) come with a way of testing if the are correctly connected to another node in the composer, and in the case that the mandatory connections are missing the `run` can not be called.
```cpp if ((d-image_in.isEmpty() || d-seed.isEmpty()) || d-control.isEmpty()) { dtkError() << Q_FUNC_INFO << "The input is not set. Aborting."; return;
- Otherwise, a plugin can be instantiated, using the plugin factory and the plugin name that has been selected in the GUI.
```cpp } else {
dtkAbstractWatershedFilter *filter = this-object(); if (!filter) { dtkError() « Q_FUNC_INFO « “No Watershed filter found. Aborting.”; return; }
* The inputs can be set using the `data` methods of the receivers, and the outputs of the filter are passed to the emitters through the `setData`. This is the only thing to implement, the specific mapping between inputs/outputs and receivers/emitters.
cpp filter-setImage(d-image_in.data()); filter-setSeed(d-seed.data()); filter-setControlParameter(d-control.data());
filter-run();
d-image_out.setData(filter-filteredImage()); } }
// // dtkWatershedFilterNode.cpp ends here
#### Add the node decoration file
* Create the `dtkWatershedFilterNode.json` file that defines the appearance of the node in the composer. For instance it provides a *kind* defining the color of the node, *tags* for querying nodes, a textual description that will appear in the composer, and names for the inputs and outputs, in the order that their respective receivers and emitters were appended in the node.
```json { "title" : "Watershed Filter", "kind" : "process", "type" : "dtkWatershedFilter", "tags" : ["filter","watershed","imaging"], "description" : "
### Filter dtkWatershedFilter
Apply the watershed filter provided by any library (e.g. VT, ITK)
", "inputs" : ["image","seed","control"], "outputs": ["image"] }
- Add the decoration filename to the
dtkComposer.qrc
file that will store path to the files directly in the .so, making it more portable.
```xml dtkAddFilterNode.json … dtkWatershedFilterNode.json …
#### Add the node to the system
* Once again, open the `CMakeLists.txt` file to add the two files we just created in the appropriate section.
```c ... set(${PROJECT_NAME}_COMPOSER_HEADERS_TMP dtkAddFilterNode.h ... dtkWatershedFilterNode.h ...
set(${PROJECT_NAME}_COMPOSER_SOURCES_TMP dtkAddFilterNode.cpp ... dtkWatershedFilterNode.cpp ...
- For the nodes to appear in the composer, make sure that the lib directory of the layer is added to the
~/.config/inria/dtk-composer.ini
of your system. Otherwise you have to do it by hand…
```c [extension] plugins=…/dtk-imaging/build/lib
```