This document contains developer documentation for BioImage Suite Web.
Creating New BisWeb Modules in JS
Introduction
Algorithmic functionality in BioImage Suite Web is packaged in Modules, which are implemented as classes derived from BaseModule (js/modules/basemodule.js).
The module architecture requires that each module implements at least two functions:
-
createDescription— Returns anObjectcontaining a description of the module’s inputs, outputs, parameters, and other metadata. -
directInvokeAlgorithm— The internal execution function of a module that is, in turn, invoked frommodule.execute()defined inBaseModule.execute().
A key aspect of the module architecture is the adaptor framework that enables modules to become command line applications and components of web/desktop applications with automatically generated user interfaces. A variant of the command line adaptor runs the modules for regression testing. The adaptors can be found at:
- Command line –
js/node/commandline.js— see also the driver main script injs/bin/bisweb.js- Regression Testing variant — the main driver script for this is
js/bin/bisweb-test.js
- Regression Testing variant — the main driver script for this is
- Web/Desktop Applications —
js/coreweb/bisweb_custommodule.jswith the UI integration injs/coreweb/bisweb_webparser.jsand viewer integration injs/webcomponents/bisweb_simplealgorithmcontroller.
The Module Architecture
Consider the case of the smoothImage module (js/modules/smoothImage.js).
First set JS strict-mode and import the key libraries. The module biswrap imports the WebAssembly code.
'use strict';
const biswrap = require('libbiswasm_wrapper');
const BaseModule = require('basemodule.js');
We define a new class SmoothImageModule that extends BaseModule
class SmoothImageModule extends BaseModule {
The constructor calls the super class constructor and and then defines the name of the module.
constructor() {
super();
this.name = 'smoothImage';
}
createDescription
The createDescription function returns a dictionary object specifying the module. This has some core elements
name— The name of the element in the GUIdescription— A short description of the module.author— The author of the module.version— The current version of the software.buttonName— The text displayed on the button that will run the module, e.g. thebuttonNameforsmoothImagemight be ‘Smooth’, fornonLinearRegistrationmight be ‘Run Non-Linear Registration’, etc.-
shortname— The tag to append to the input filename when creating default output names. For the example below, if the input isimage.nii.gzthe output will beimage_sm.nii.gz.createDescription() { return { "name": "Smooth", "description": "This algorithm performes image smoothing using a 2D/3D Gaussian kernel", "author": "Zach Saltzman", "version": "1.0", "buttonName": "Smooth", "shortname" : "sm",
Next comes two arrays of input and output objects respectively. Each element has the following members:
type— One ofimage,matrix,vector,transformortransformation. This is used to load the object using theBisWebDataObjectCollection.loadObject(seejs/dataobjects/bisweb_dataobject.js).name— The name of the object.description— A longer version of the name.varname— The name with which the module should reference an input loaded at runtime. Inputs will be passed into a module as a key value dictionary.varnamesets the key for this input. This is also used to create the command line flag,--inputin this case.shortname— The short name of the object. This is used to create the commandline flag, in this case-i.required— If true this input is required to run the module and if it is not set an error will be returned at runtime.-
guiviewerandguiviewertype— Bisweb viewers can display two images,imageandoverlay, the former which loads into the viewer as expected and the latter which loads as an mask over it. These values specify how the module should display its outputs. This could beviewer1orviewer2in dual viewer apps, etc."inputs": [ { 'type': 'image', 'name': 'Input Image', 'description': 'The image to be processed', 'varname': 'input', 'shortname': 'i', 'required': true, 'guiviewertype' : 'image', 'guiviewer' : 'viewer1', } ],
outputs is identical to inputs.
"outputs": [
{
'type': 'image',
'name': 'Output Image',
'description': 'The output image',
'varname': 'output',
'shortname': 'o',
'required': true,
'guiviewertype' : vtype,
'guiviewer' : viewer,
}
],
Next comes the parameters object array. Each parameter has some of the following elements:
name— The name of the parameter.description— A longer version of the name.priority— Elements of higher priority are displayed first in the GUI or the commandline help message. An element with priorityiis considered higher priority than an element with priorityi + 1.advanced— If true, this parameter will be considered more advanced than the typical user will require and will be placed in a different menu by default.gui— The type of GUI element to use — one ofslider,dropdown,checkbox,text.varname— The values of the parameters will be passed into the module as a dictionary similar toinputs.varnameis the key for this parameter in the dictionary. It also sets the flag for the parameter if the module is being run from the command line, e.g. the param below would have its value assigned by entering--sigma=1.0on the command line.default— The default value for the parameter.type— One ofstring,boolean,floatorint.lowandhigh/lowboundandhighbound— In the case offloatorintthis sets the bounds of the allowed valuesstep— Sets the gradation of values betweenlowandhigh.
Dropdown guis may have two more elements
"fields": ["HillClimb", "GradientDescent", "ConjugateGradient"],
"restrictAnswer": ["HillClimb", "GradientDescent", "ConjugateGradient"],
These show the value of the list to select from and the allowed values,which are for the most part the same.
"params": [
{
"name": "Sigma",
"description": "The gaussian kernel standard deviation (either in voxels or mm)",
"priority": 1,
"advanced": false,
"gui": "slider",
"varname": "sigma",
"default": 1.0,
"type": 'float',
"low": 0.0,
"high": 8.0
},
{
"name": "In mm?",
"description": "Determines whether kernel standard deviation (sigma) will be measured in millimeters or voxels",
"priority": 7,
"advanced": false,
"gui": "check",
"varname": "inmm",
"type": 'boolean',
"default": true,
},
{
"name": "Radius Factor",
"description": "This affects the size of the convolution kernel which is computed as sigma*radius+1",
"priority": 2,
"advanced": true,
"gui": "slider",
"type": 'float',
"default": 2.0,
"lowbound": 1.0,
"highbound": 4.0,
"varname": "radiusfactor"
},
{
"name": "Debug",
"description": "Toggles debug logging",
"priority": 1000,
"advanced": true,
"gui": "check",
"varname": "debug",
"type": 'boolean',
"default": false,
}
],
};
}
directInvokeAlgorithm
The last required function is directInvokeAlgorithm, which is called from BaseModule.execute (see next section for details).
The argument vals is a key-value object dictionary e.g.
{
sigma : 2.0,
radiusfactor : 2,
inmm : true,
debug true
}
The function prints a debug message first:
directInvokeAlgorithm(vals) {
console.log('oooo invoking: smoothImage with vals', JSON.stringify(vals));
The function is run through a Promise as the operation is potentially asynchronous.
return new Promise( (resolve, reject) => {
Get the input objects and parameters and check/sanitize them.
let input = this.inputs['input'];
let s = parseFloat(vals.sigma);
Next call biswrap.initialize to initalize the WebAssembly code. This also returns a Promise.
biswrap.initialize().then(() => {
Once the initialize function is complete, invoke the WASM code and store the output in the this.outputs dictionary.
this.outputs['output'] = biswrap.gaussianSmoothImageWASM(input, {
"sigmas": [s, s, s],
"inmm": super.parseBoolean(vals.inmm),
"radiusfactor": parseFloat(vals.radiusfactor)
}, super.parseBoolean(vals.debug));
resolve if the the module returns without error, otherwise catch the error and reject with an error message.
resolve();
}).catch( (e) => {
reject(e.stack);
});
});
}
}
Finally, export the class.
module.exports = SmoothImageModule;
BaseModule.execute
The external interface to the modules is execute. This can take the form:
let smoothModule = new SmoothImageModule();
let inputimage = ...; // some way of getting this
smoothModule.execute( {
'input' : inputimage
}, {
sigma : 2.0
}).then( () => {
let output=smoothModule.getOutputObject('output');
// do something with this
})
Consider execute itself:
/** Runs the module programmatically
* @param {Object} inputs — input objects
* @param {Object} parameters — input parameters
* @returns {Promise}
*/
execute(inputs, params = {}) {
First, parse the params key/value dictionary and add default values for any parameters whose values are not specified.
let fullparams = this.parseValuesAndAddDefaults(params);
Next get the module’s description, which calls createDescription if needed, and check that the required inputs are set:
let des = this.getDescription();
let error = [];
des.inputs.forEach( (param) => {
let name = param['varname'];
this.inputs[name] = inputs[name] || null;
if (this.inputs[name] === null && param.required === true) {
console.log("No/empty " + param.name + " specified.");
error.push("No/empty " + param.name + " specified.");
}
});
if (error.length > 0)
return Promise.reject(error.join("\n"));
Create a new promise and call directInvokeAlgorithm. The constant self holds the value of this (see “this, that, etc.” in AspectsOfJS.md)
const self = this;
let name = this.name;
return new Promise( (resolve, reject) => {
self.directInvokeAlgorithm(fullparams).then( () => {
Store information about the execution environment etc. in the comments metadata field of each output.
self.storeCommentsInOutputs(baseutils.getExecutableArguments(name), params,
baseutils.getSystemInfo(biswrap));
Call resolve to mark the Promise as fuilfilled.
resolve();
Alternatively, trap any errors and call reject.
}).catch( (e) => {
reject(e);
});
});
}
GUI Updates
Some modules have parameters whose ranges depend on the actual inputs. For example thresholdImage has thresholds that should be restricted in range to the intensity range of the input image. This is done using:
updateOnChangedInput(inputs, controllers = null, guiVars = null)
See js/modules/thresholdImage.js for an example.
One module requires updates of the value of the crosshairs of the current image viewer. An example of this is the MorphologyFilterModule. First in the constructor, set the flag mouseobserver to true
class MorphologyFilterModule extends BaseModule {
constructor() {
...
this.mouseobserver=true;
}
Next implement the function setViewerCoordinates to handle viewer crosshairs updates.
setViewerCoordinates(controllers,guivars,coords=null)
Implementing a new Module
This consists of the following steps:
- Implement the module itself and place it in the
js/modulesdirectory. - Register the module in
js/modules/moduleindex.js. This needs to be added to both theexportsstructure and in theexports.modulesNamesArray. In the latter the key should be specified in lower case.
From here the module can be invoked on the command line using
node js/bin/bisweb.js modulename -h
It can also be added to GUI applications. See examples in js/webcomponents/bisweb_modulemanagerelement.js
This page is part of BioImage Suite Web. We gratefully acknowledge support from the NIH Brain Initiative under grant R24 MH114805 (Papademetris X. and Scheinost D. PIs, Dept. of Radiology and Biomedical Imaging, Yale School of Medicine.)