Create Elegant C++ Spatial Processing Pipelines in WebAssembly

thewtex

Matt McCormick

Posted on February 10, 2023

Create Elegant C++ Spatial Processing Pipelines in WebAssembly
By: Matt McCormick ORCID, Mary Elise Dedicke ORCID, Henry Schreiner ORCID

WebAssembly's origins date back to Alon Zakai's incredible effort to build C++ to JavaScript. In 2015, we demonstrated the power of this technology to make scientific computational sustainable and accessible. Try it -- reproducibility is still possible all these years later, with no installation (or maintenance!) required.

An interactive, accessible and sustainable open science publication on anisotropic diffusion where C++ is built into JavaScript for browser execution.

Since that time, Emscripten's capabilities have advanced and been standardized with WebAssembly (Wasm) in the Web Platform. Moreover, Wasm's scope has expanded dramatically with the advent of the WebAssembly System Interface, WASI, and The Component Model.

However, there was a significant gap in capabilites for research software developers who aimed to create data and computationally intense scientific processing pipelines for applications like spatial analysis and visualization. Namely,

  • An elegant, simple way to write processing pipelines in C++.
  • Handling of non-trivial spatial data structures (Wasm natively only supports integers and floats 😮).
  • Easy-to-use, reproducible tools to build Wasm modules.
  • Safe and efficient memory handling.
  • Parallelism, whether multi-module, multi-threading, or SIMD.
  • Provide bindings for the command line and functional interfaces for languages like JavaScript, Typescript, Python, Rust, C#, R, and Java.
  • Debugging support.

In this post, adapted from itk-wasm's documentation, we provide a C++ Wasm processing pipeline tutorial that demonstrates how we can write elegant processing pipelines in C++ via itk-wasm's CLI11 command line parser, which provides a rich feature set with a simple and intuitive interface. At the end of this tutorial, you will have built and executed C++ code to Wasm for standalone execution on the command line and in the browser.

itk-wasm combines the Insight Toolkit (ITK) and WebAssembly to enable high-performance spatial analysis in a web browser, Node.js, and reproducible execution across programming languages and hardware architectures.

CLI11 provides all the features you expect in a powerful command line parser, with a beautiful, minimal syntax and no dependencies beyond C++11. itk-wasm enhances CLI11 with a itk::wasm::Pipeline wrapper to support efficient execution in multiple Wasm contexts, scientific data structures, and lovely colorized help output 🥰.

Let's get started! 🚀

0. Preliminaries

Before starting this tutorial, check out our Hello Wasm World tutorial.

1. Write the code

First, let's create a new directory to house our project.

mkdir hello-pipeline
cd hello-pipeline
Enter fullscreen mode Exit fullscreen mode

Let's write some code! Populate hello-pipeline.cxx first with the headers we need:

#include "itkPipeline.h"
#include "itkInputImage.h"
#include "itkImage.h"
Enter fullscreen mode Exit fullscreen mode

The itkPipeline.h and itkInputImage.h headers come from the itk-wasm WebAssemblyInterface ITK module.

The itkImage.h header is ITK's standard n-dimensional image data structure.

Next, create a standard main C command line interface function and an itk::wasm::Pipeline:

int main(int argc, char * argv[]) {
  // Create the pipeline for parsing arguments. Provide a description.
  itk::wasm::Pipeline pipeline("hello-pipeline", "A hello world itk::wasm::Pipeline", argc, argv);

  return EXIT_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode

The itk::wasm::Pipeline extends the CLI11 modern C++ command line parser. In addition to all of CLI11's functionality, itk::wasm::Pipeline's adds:

  • Support for execution in Wasm modules along with command line execution
  • Support for spatial data structures such as Image, Mesh, PolyData, and Transform
  • Support for multiple dimensions and pixel types
  • Colored help output

Add a standard CLI11 flag to the pipeline:

  itk::wasm::Pipeline pipeline("hello-pipeline", "A hello world itk::wasm::Pipeline", argc, argv);


  bool quiet = false;
  pipeline.add_flag("-q,--quiet", quiet, "Do not print image information");
Enter fullscreen mode Exit fullscreen mode

Add an input image argument to the pipeline:

  pipeline.add_flag("-q,--quiet", quiet, "Do not print image information");


  constexpr unsigned int Dimension = 2;
  using PixelType = unsigned char;
  using ImageType = itk::Image<PixelType, Dimension>;

  // Add a input image argument.
  using InputImageType = itk::wasm::InputImage<ImageType>;
  InputImageType inputImage;
  pipeline.add_option("input-image", inputImage,
    "The input image")->required()->type_name("INPUT_IMAGE");
Enter fullscreen mode Exit fullscreen mode

The inputImage variable is populated from the filesystem if built as a native executable or a WASI binary run from the command line. When running in the browser or in a wrapped language, inputImage is read from WebAssembly memory without file IO.

Parse the command line arguments with the ITK_WASM_PARSE macro:

  pipeline.add_option("InputImage", inputImage,
    "The input image")->required()->type_name("INPUT_IMAGE");


  ITK_WASM_PARSE(pipeline);
Enter fullscreen mode Exit fullscreen mode

If -q or --quiet is set, the quiet variable will be set to true. Missing or invalid arguments will print an error and exit. The -h and --help flags are automatically generated from pipeline arguments to print usage information.

Finally, run the pipeline:

  std::cout << "Hello pipeline world!\n" << std::endl;

  if (!quiet)
  {
    // Obtain the itk::Image * from the itk::wasm::InputImage with `.Get()`.
    std::cout << "Input image: " << *inputImage.Get() << std::endl;
  }

  return EXIT_SUCCESS;
Enter fullscreen mode Exit fullscreen mode

Next, provide a CMake build configuration in CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)
project(hello-pipeline)

# Use C++17 or newer with itk-wasm
set(CMAKE_CXX_STANDARD 17)

# We always want to build against the WebAssemblyInterface module.
set(itk_components
  WebAssemblyInterface
  )
# WASI or native binaries
if (NOT EMSCRIPTEN)
  # WebAssemblyInterface supports the .iwi, .iwi.cbor itk-wasm format.
  # We can list other ITK IO modules to build against to support other
  # formats when building native executable or WASI WebAssembly.
  # However, this will bloat the size of the WASI WebAssembly binary, so
  # add them judiciously.
  set(itk_components
    WebAssemblyInterface
    ITKIOPNG
    # ITKImageIO # Adds support for all available image IO modules
    )
endif()
find_package(ITK REQUIRED
  COMPONENTS ${itk_components}
  )
include(${ITK_USE_FILE})

add_executable(hello-pipeline hello-pipeline.cxx)
target_link_libraries(hello-pipeline PUBLIC ${ITK_LIBRARIES})
Enter fullscreen mode Exit fullscreen mode

2. Create and Run WebAssembly binary

Build the WASI binary:

npx itk-wasm@1.0.0-b.70 -i itkwasm/wasi build
Enter fullscreen mode Exit fullscreen mode

And check the generated help output:

npx itk-wasm@1.0.0-b.70 run hello-pipeline.wasi.wasm -- -- --help
Enter fullscreen mode Exit fullscreen mode

Hello pipeline help

The two --'s are to separate arguments for the Wasm module from arguments to the itk-wasm CLI and the WebAssembly interpreter.

Try running on an example image.

> npx itk-wasm@1.0.0-b.65 run hello-pipeline.wasi.wasm -- -- cthead1.png

Hello pipeline world!

Input image: Image (0x2b910)
  RTTI typeinfo:   itk::Image<unsigned char, 2u>
  Reference Count: 1
  Modified Time: 54
  Debug: Off
  Object Name:
  Observers:
    none
  Source: (none)
  Source output name: (none)
  Release Data: Off
  Data Released: False
  Global Release Data: Off
  PipelineMTime: 22
  UpdateMTime: 53
  RealTimeStamp: 0 seconds
  LargestPossibleRegion:
    Dimension: 2
    Index: [0, 0]
    Size: [256, 256]
  BufferedRegion:
    Dimension: 2
    Index: [0, 0]
    Size: [256, 256]
  RequestedRegion:
    Dimension: 2
    Index: [0, 0]
    Size: [256, 256]
  Spacing: [1, 1]
  Origin: [0, 0]
  Direction:
1 0
0 1

  IndexToPointMatrix:
1 0
0 1

  PointToIndexMatrix:
1 0
0 1

  Inverse Direction:
1 0
0 1

  PixelContainer:
    ImportImageContainer (0x2ba60)
      RTTI typeinfo:   itk::ImportImageContainer<unsigned long, unsigned char>
      Reference Count: 1
      Modified Time: 50
      Debug: Off
      Object Name:
      Observers:
        none
      Pointer: 0x2c070
      Container manages memory: true
      Size: 65536
      Capacity: 65536
Enter fullscreen mode Exit fullscreen mode

And with the --quiet flag:

> npx itk-wasm@1.0.0-b.65 run hello-pipeline.wasi.wasm -- -- --quiet cthead1.png

Hello pipeline world!
Enter fullscreen mode Exit fullscreen mode

Congratulations! You just executed a C++ pipeline capable of processsing a scientific image in WebAssembly. 🎉

What's next

We created a processing pipeline that works with real data -- exciting! When creating these pipelines, however, we will sometimes need a more detailed understanding of their execution. In our next post, we will address this need by describing how to debug itk-wasm WASI modules.

💖 💪 🙅 🚩
thewtex
Matt McCormick

Posted on February 10, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related