Writing a kernel

Author:Bradley Chambers
Contact:brad.chambers@gmail.com
Date:01/21/2015

PDAL’s command-line application can be extended through the development of kernel functions. In this tutorial, we will give a brief example.

The header

First, we provide a full listing of the kernel header.

// MyKernel.hpp

#pragma once

#include <pdal/Kernel.hpp>
#include <pdal/plugin.hpp>

#include <string>

namespace pdal
{

  class PDAL_DLL MyKernel : public Kernel
  {
  public:
    static void * create();
    static int32_t destroy(void *);
    std::string getName() const;
    int execute(); // override

  private:
    MyKernel();
    void validateSwitches();
    void addSwitches();

    std::string m_input_file;
    std::string m_output_file;
  };

} // namespace pdal

As with other plugins, the MyKernel class needs to have the following three methods declared for the plugin interface to be satisfied:

    static void * create();
    static int32_t destroy(void *);
    std::string getName() const;

The source

Again, we start with a full listing of the kernel source.

// MyKernel.cpp

#include "MyKernel.hpp"

#include <boost/program_options.hpp>

#include <pdal/Filter.hpp>
#include <pdal/Kernel.hpp>
#include <pdal/KernelFactory.hpp>
#include <pdal/KernelSupport.hpp>
#include <pdal/Options.hpp>
#include <pdal/pdal_macros.hpp>
#include <pdal/StageFactory.hpp>
#include <pdal/PointTable.hpp>

#include <memory>
#include <string>

namespace po = boost::program_options;

namespace pdal {

  static PluginInfo const s_info {
    "kernels.mykernel",
    "MyKernel",
    "http://link/to/documentation"
  };

  CREATE_SHARED_PLUGIN(1, 0, MyKernel, Kernel, s_info);

  std::string MyKernel::getName() const { return s_info.name; }

  MyKernel::MyKernel() : Kernel()
  {}

  void MyKernel::validateSwitches()
  {
    if (m_input_file == "")
      throw pdal::app_usage_error("--input/-i required");
    if (m_output_file == "")
      throw pdal::app_usage_error("--output/-o required");
  }

  void MyKernel::addSwitches()
  {
    po::options_description* options = new po::options_description("file options");
    options->add_options()
    ("input,i", po::value<std::string>(&m_input_file)->default_value(""), "input file name")
    ("output,o", po::value<std::string>(&m_output_file)->default_value(""), "output file name")
    ;

    addSwitchSet(options);
    addPositionalSwitch("input", 1);
    addPositionalSwitch("output", 1);
  }

  int MyKernel::execute()
  {
    PointTable table;
    StageFactory f;

    Stage * reader = f.createStage("readers.las");
    Options readerOptions;
    readerOptions.add("filename", m_input_file);
    reader->setOptions(readerOptions);

    Stage * filter = f.createStage("filters.decimation");
    Options filterOptions;
    filterOptions.add("step", 10);
    filter->setOptions(filterOptions);
    filter->setInput(*reader);

    Stage * writer = f.createStage("writers.text");
    Options writerOptions;
    writerOptions.add("filename", m_output_file);
    writer->setOptions(writerOptions);
    writer->setInput(*filter);
    writer->prepare(table);
    writer->execute(table);

    return 0;
  }

} // namespace pdal

In your kernel implementation, you will use a macro defined in pdal_macros. This macro registers the plugin with the Kernel factory. It is only required by plugins.

  static PluginInfo const s_info {
    "kernels.mykernel",
    "MyKernel",
    "http://link/to/documentation"
  };

  CREATE_SHARED_PLUGIN(1, 0, MyKernel, Kernel, s_info);

A static plugin macro can also be used to integrate the kernel with the main code. This will not be described here. Using this as a shared plugin will be described later.

To build up a processing pipeline in this example, we need to create two objects: the PointTable and the StageFactory. The latter is used to create the various stages that will be used within the kernel.

    StageFactory f;

The Reader is created from the StageFactory, and is specified by the stage name, in this case an LAS reader. For brevity, we provide the reader a single option, the filename of the file to be read.

    Stage * reader = f.createStage("readers.las");
    Options readerOptions;
    readerOptions.add("filename", m_input_file);
    reader->setOptions(readerOptions);

The Filter is also created from the StageFactory. Here, we create a decimation filter that will pass every tenth point to subsequent stages. We also specify the input to this stage, which is the reader.

    Stage * filter = f.createStage("filters.decimation");
    Options filterOptions;
    filterOptions.add("step", 10);
    filter->setOptions(filterOptions);
    filter->setInput(*reader);

Finally, the Writer is created from the StageFactory. This text writer, takes as input the previous stage (the decimation filter) and the output filename as its sole option.

    Stage * writer = f.createStage("writers.text");
    Options writerOptions;
    writerOptions.add("filename", m_output_file);
    writer->setOptions(writerOptions);
    writer->setInput(*filter);

The final two steps are to prepare and execute the pipeline. This is achieved by calling prepare and execute on the final stage.

    writer->prepare(table);
    writer->execute(table);

When compiled, a dynamic library file will be created; in this case, libpdal_plugin_kernel_mykernel.dylib

Put this file in whatever directory PDAL_DRIVER_PATH is pointing to. Then, if you run pdal --help, you should see mykernel listed in the possible commands.

To run this kernel, you would use pdal mykernel -i <input las file> -o <output text file>.