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.
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¶
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:
has been 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. 🎉