Pipeline Inputs and Outputs

Introduction

This example dives deeper into itk::wasm::Pipeline image inputs and outputs and how they are handled. We will create a pipeline to smooth an image with a median filter, run the Wasm from the command line, in Node.js.

Make sure to complete the Hello Pipeline! example before you start your filtering journey.

Write the code

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

mkdir inputs-outputs
cd inputs-outputs

Let’s write some code! Populate inputs-outputs.cxx with the headers we need:

#include "itkPipeline.h"
#include "itkInputImage.h"
#include "itkOutputImage.h"

#include "itkImage.h"
#include "itkMedianImageFilter.h"

The itkImage.h header is ITK’s standard n-dimensional image data structure and the itkMedianImageFilter.h also comes from ITK.

The itkPipeline.h, itkInputImage.h, and itkOutputImage.h headers come from the itk-wasm WebAssemblyInterface ITK module. These will help process arguments, injest input images, and produce output images, respectively.

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("median-filter", "Smooth an image with a median filter", argc, argv);

  return EXIT_SUCCESS;
}

Add options to the pipeline that define our inputs, outputs, and processing parameters.

  itk::wasm::Pipeline pipeline("median-filter", "Smooth an image with a median filter", argc, argv);


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


  // Add a flag to specify the radius of the median filter.
  unsigned int radius = 1;
  pipeline.add_option("-r,--radius", radius, "Kernel radius in pixels");

  // 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");

  // Add an output image argument.
  using OutputImageType = itk::wasm::OutputImage<ImageType>;
  OutputImageType outputImage;
  pipeline.add_option("output-image", outputImage,
    "The output image")->required()->type_name("OUTPUT_IMAGE");

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.

When the program completes, outputImage is written to 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, outputImage is read from WebAssembly memory without file IO.

Parse the command line arguments with the ITK_WASM_PARSE macro:

  pipeline.add_option("output-image", outputImage,
    "The output image")->required()->type_name("OUTPUT_IMAGE");


  ITK_WASM_PARSE(pipeline);

The -h and --help flags are automatically generated from pipeline arguments to print usage information.

inputs-outputs help

Finally, process our data:

  using FilterType = itk::MedianImageFilter< ImageType, ImageType >;
  auto filter = FilterType::New();
  filter->SetInput(inputImage.Get());
  filter->SetRadius(radius);
  filter->Update();

Set the output image before the program completes:

  outputImage.Set(filter->GetOutput());

  return EXIT_SUCCESS;

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

cmake_minimum_required(VERSION 3.16)
project(inputs-outputs)

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

# We always want to build against the WebAssemblyInterface module.
set(itk_components
  WebAssemblyInterface
  ITKSmoothing # provides itkMedianImageFilter.h
  )
# 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
    ${itk_components}
    ITKIOPNG
    # ITKImageIO # Adds support for all available image IO modules
    )
endif()
find_package(ITK REQUIRED
  COMPONENTS ${itk_components}
  )
include(${ITK_USE_FILE})

add_executable(inputs-outputs inputs-outputs.cxx)
target_link_libraries(inputs-outputs PUBLIC ${ITK_LIBRARIES})

Create WebAssembly binary

Build the WASI binary:

npx itk-wasm -b wasi-build -i itkwasm/wasi build

Try running on an example image.

Run WebAssembly binary

npx itk-wasm -b wasi-build run inputs-outputs.wasi.wasm -- -- --radius 2 cthead1.png smoothed.png

The input image:

input image

has been smoothed:

smoothed

Run in Node.js

To run in the Node.js JavaScript environment, first build with the Emscripten toolchain.

npx itk-wasm build

In our Node.js JavaScript script, we will load the file with dedicated image IO WebAssembly modules. These are provided by the itk-image-io package.

npm install -g itk-image-io

Next, let’s create a script to call our pipeline, index.mjs. Start the script with our imports.

import path from 'path'
import { runPipelineNode,
         readImageLocalFile,
         writeImageLocalFile,
         InterfaceTypes } from 'itk-wasm'

To switch from filesystem to WebAssembly memory IO, pass the --memory-io flag. This flag is supported by all itk::wasm::Pipeline’s. Skip the two node ./index.mjs arguments from Node invocation.

const args = ['--memory-io'].concat(process.argv.slice(2))

When using memory IO, interface types, such as images, are specified in the pipeline arguments with integer strings. Inputs and output integer identifiers both start counting from zero.

// Assume we have input and output images as the last arguments
const inputFile = args[args.length-2]
const inputImage = await readImageLocalFile(inputFile)
// '0' is the index of the first input corresponding to the `inputs` array below
args[args.length-2] = '0'

const outputFile = args[args.length-1]
// '0' is the index of the first output corresponding to the `desiredOutputs` below
args[args.length-1] = '0'

Input images can be read with readImageLocalFile. We specify the type and value of the pipeline input interface types. With pipeline outputs, only the type is specified.

const inputs = [
  { type: InterfaceTypes.Image, data: inputImage }
]
const desiredOutputs = [
  { type: InterfaceTypes.Image }
]

Run the pipeline.

// Path to the Emscripten WebAssembly module without extensions
const pipelinePath = path.resolve('emscripten-build', 'inputs-outputs')
const { stdout, stderr, outputs } = await runPipelineNode(pipelinePath, args, desiredOutputs, inputs)

And handle the outputs.

await writeImageLocalFile(outputs[0].data, outputFile)

Invoke the script.

npx node ./index.mjs --radius 2 ./cthead1.png smoothed.png

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