ADTF DAT Tool Processor

ADTF stores recorded data to an *.adtfdat file by default when using an ADTFDAT File Recorder. Because there are plenty of reasons to do further (post)processing with this data, ADTF (or rather say the ADTF File Library) offers a simple to use Processor interface to extract streams from an *.adtfdat files as well as a Reader interface to import streams to *.adtfdat files.

This tutorial will implement a Processor capable of extracting images from an *.adtfdat file which contains a previously recorded video stream.


The CMake file

Set up a CMake file that is looking for the required Qt components because the processor uses the QImage class to build the concrete image from a memory block. Another dependency is the library for adtfdat processing itself. Furthermore it gives the project a name (image_processor) and declares a header and source file for the new Processor. We set the target property attribute SUFFIX to .adtffileplugin because this is what the ADTF DAT Tool accepts as additional plugins.

Let's create a new folder and add the following files:

Open the CMakeLists.txt file and add the following content:

      
# request CMake version and define project
cmake_minimum_required(VERSION 3.10.2)
project(image_processor)

# allow to link several 3rd party libraries to be able to build with any desired build type
set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO RelWithDebInfo Release MinSizeRelease "")
set(CMAKE_MAP_IMPORTED_CONFIG_RELEASE RelWithDebInfo Release MinSizeRelease "")
set(CMAKE_MAP_IMPORTED_CONFIG_MINSIZEREL RelWithDebInfo Release MinSizeRelease "")

# get our dependencies
find_package(Qt5 COMPONENTS Gui REQUIRED)
find_package(adtfdat_processing REQUIRED)

# create the processor library
add_library(image_processor MODULE
    tutorial_image_processor.h
    tutorial_image_processor.cpp
)

# link against our dependencies
target_link_libraries(image_processor Qt5::Gui adtfdat_processing)

# define our adtffileplugin to make it availabe for tooling
set_target_properties(image_processor PROPERTIES
    PREFIX ""
    SUFFIX ".adtffileplugin"
    DEFINE_SYMBOL ""
    DEBUG_POSTFIX "d"
)

# create a post build install step
install(TARGETS image_processor
    DESTINATION bin
    CONFIGURATIONS
        Debug
        Release
        RelWithDebInfo
        MinSizeRelease
)

# finally we copy the required Qt libraries next to our binary
set(qt_dir ${Qt5_DIR}/../../../)
set(qt_libs Qt5Core Qt5Gui)
if (WIN32)
    foreach(qt_lib IN LISTS qt_libs)
        install(FILES ${qt_dir}/bin/${CMAKE_SHARED_LIBRARY_PREFIX}${qt_lib}$<$<CONFIG:Debug>:d>.dll DESTINATION bin)
    endforeach()
else()    
    foreach(qt_lib IN LISTS qt_libs)
        install(FILES ${qt_dir}/lib/${CMAKE_SHARED_LIBRARY_PREFIX}${qt_lib}.so.${Qt5Core_VERSION} DESTINATION bin/lib)
        install(FILES ${qt_dir}/lib/${CMAKE_SHARED_LIBRARY_PREFIX}${qt_lib}.so.5 DESTINATION bin/lib)
    endforeach()
endif()
      
    

Open the source folder which contains the CMakeLists.txt file with CMake-GUI. Each time you press configure, you will have to supply additional information:

Now everything should be set up correctly and you can press generate to create your project/solution.

The Header File of the Processor

Each processor implements the adtf::dat::Processor interface which defines the following methods:

Furthermore each processor inherits from adtf::dat::Configurable which adds support for settings.

      
#pragma once

#include <adtfdat_processing/processor.h>
#include <string>
#include <memory>
#include <QCoreApplication>
#include <QImage>

struct ImageFormat
{
    QImage::Format format; // this is the appropriate Qt image format identifier
    int pixel_byte_size; // the size of a pixel in bytes, required for QImage
    bool rbg_flipped; // whether or not the RGB components need to flipped when writing the images.
};

class ImageFileWriter: public adtf::dat::Processor
{
    public:
        ImageFileWriter();

        std::string getProcessorIdentifier() const override;
        bool isCompatible(const adtf_file::Stream& stream) const override;
        void open(const adtf_file::Stream& stream, const std::string& destination_file_name) override;
        void process(const adtf_file::FileItem& item) override;

    private:
        uint64_t _sample_counter = 0;
        std::string _output_basename;

        ImageFormat _format = {};
        int _width = 0;
        int _height = 0;

        std::unique_ptr<QCoreApplication> _qt_app;
};

      
    

The Source File of the Processor

The constructor calls the setConfiguration() method with the default values to initialize the settings, output_basename will define the format rule for the filenames of the extracted image files. To make sure the Qt plugin for image types is loaded we initialize the Qt application inside the the constructor.

To implement the adtf::dat::Processor interface we have to define an unique identifier for the new processor which is returned by the interface method getProcessorIdentifier(). The next method we have to implement is isCompatible() to check if the type of the given stream (from an *.adtfdat file) can be handeled by the processor. In this example we expect the type to be adtf/image which is common for ADTF videos in *.adtfdat files.

Next we implement the open() method where we do some basic initialization. Here we can have a look at the first (meta) sample of the stream and find out what the dimensions of the contained images are. At this point the settings are read out and the destination folder is created where the extracted images are saved to.

The process() method gets each sample of the video stream and we can decide what we want to do with it. The image processor employs a sample counter and extracts each containing image of the video stream to the specified output folder following the rules for the filename defined within output_basename. If required, the RGB value has to be swapped, depending on the given format.

        
#include "tutorial_image_processor.h"

#include <adtf_file/stream_type.h>
#include <adtf_file/sample.h>
#include <QString>

// add our factory to the global adtf_file object list.
static adtf_file::PluginInitializer initializer([] {
    adtf_file::getObjects().push_back(
      std::make_shared<adtf::dat::ProcessorFactoryImplementation<ImageFileWriter>>());
});

// we use this little helper for mapping the received image format
// to valid QImage format
ImageFormat get_qimage_format(const std::string& format_name)
{
    static std::map<std::string, ImageFormat> format_map
    {
        { "R(8)G(8)B(8)(8)", {QImage::Format::Format_RGB32, 4, false}},
        { "B(8)G(8)R(8)(8)", {QImage::Format::Format_RGB32, 4, true}},
        { "R(5)G(5)B(5)(1)", {QImage::Format::Format_RGB555, 2, false}},
        { "R(8)G(8)B(8)", {QImage::Format::Format_RGB888, 3, false}},
        { "B(8)G(8)R(8)", {QImage::Format::Format_RGB888, 3, true}},
        { "A(8)R(8)G(8)B(8)", {QImage::Format::Format_ARGB32, 4, false}},
        { "R(8)G(8)B(8)A(8)", {QImage::Format::Format_RGBA8888, 4, false}},
        { "B(8)G(8)R(8)A(8)", {QImage::Format::Format_ARGB32, 4, true}},
        { "GREY(8)", {QImage::Format::Format_Grayscale8, 1, false}},
        { "GREY(16)", {QImage::Format::Format_RGB16, 2, false}},
    };

    auto supported_format = format_map.find(format_name);
    if (supported_format == format_map.end())
    {
        throw std::runtime_error("Unsupported image format: " + format_name);
    }

    return supported_format->second;
}

ImageFileWriter::ImageFileWriter()
{
    // create a qt application if there is none
    if (!QCoreApplication::instance())
    {
        int dummy_argc = 0;
        char** dummy_argv = new char*[0];
        _qt_app = std::make_unique<QCoreApplication>(dummy_argc, dummy_argv);
    }

    // set all configuration options
    setConfiguration(
    {
        { "output_basename",{ "images_%04d.png", "cString" } },
    });
}

std::string ImageFileWriter::getProcessorIdentifier() const
{
    return "image_file_writer";
}

bool ImageFileWriter::isCompatible(const adtf_file::Stream& stream) const
{
    // this processor can only handle valid adtf image streams
    auto tmp = std::dynamic_pointer_cast<const adtf_file::PropertyStreamType>(stream.initial_type);
    if (!tmp)
    {
        return false;
    }

    return tmp->getMetaType() == "adtf/image";
}

void ImageFileWriter::open(const adtf_file::Stream& stream, const std::string& destination_file_name)
{
    // some checks regarding given output directory
    if (destination_file_name.empty())
    {
        throw std::runtime_error("no output directory specified");
    }
    if (!a_util::filesystem::exists(destination_file_name))
    {
        a_util::filesystem::createDirectory(destination_file_name);
    }

    _output_basename = destination_file_name + "/" + adtf::dat::getPropertyValue<std::string>(getConfiguration(), "output_basename");

    // cast for stream type
    auto property_type = std::dynamic_pointer_cast<const adtf_file::PropertyStreamType>(stream.initial_type);
    if (!property_type)
    {
        throw std::runtime_error("The stream type of '" + stream.name + "' is not a property stream type");
    }

    // get image information and dimensions from the stream type
    _format = get_qimage_format(property_type->getProperty("format_name").second);
    _width = std::stoi(property_type->getProperty("pixel_width").second);
    _height = std::stoi(property_type->getProperty("pixel_height").second);
}

void ImageFileWriter::process(const adtf_file::FileItem& item)
{
    auto sample = std::dynamic_pointer_cast<const adtf_file::WriteSample>(item.stream_item);
    if (sample)
    {
        auto buffer = sample->beginBufferRead();

        // extract the image from the sample
        QImage image(static_cast<const uchar*>(buffer.first),
                     _width,
                     _height,
                     _format.pixel_byte_size * _width,
                     _format.format);

        if (_format.rbg_flipped)
        {
            image = image.rgbSwapped();
        }

        auto image_filename = QString::asprintf(_output_basename.c_str(), _sample_counter);
        if (!image.save(image_filename))
        {
            throw std::runtime_error("Could not save image to " + image_filename.toStdString());
        }

        sample->endBufferRead();

        ++_sample_counter;
    }
}

        
      

Creating the binary

Next we build the following Visual Studio projects:

After the INSTALL step, there should be the binary and Qt depended libraries (as set up in CMake) in our install folder, dependend on the chosen build type:

Using the processor

Now we are ready to run the ADTF DAT Tool and use our image processor plugin. The command line version of the tool expects the following parameters when extracting images from a video stream of an *.adtfdat file:

Switch to the install folder of our binary and open a shell there. An example usage of the release version of the ADTF DAT Tool in combination with our created release version of the image processor and custom properties could look like this:
      
<ADTF_DIR>/pkg/adtf_file/bin/adtf_dattool.exe --plugin image_processor.adtffileplugin --export <ADTF_DIR>/src/examples/datfiles/example_file.adtfdat --stream VIDEO --output extracted_images
    

To change the output filenames extend the call with argument with the output_basename property, e.g. --property output_basename=my_fancy_output_name_%d.png

Where to go next?

Now you should have a detailed look and feeling for ADTF 3. It's time to create your own ADTF Components and use the Framework and Tools. This is currently the last page so go back to home overview.