Skip to content

Commit

Permalink
Merge pull request #29 from rordenlab/fix/help-text-for-mesh-options
Browse files Browse the repository at this point in the history
Enable mesh suboptions in javascript API
  • Loading branch information
neurolabusc authored Aug 23, 2024
2 parents 6cdc495 + 2035ce7 commit a905ecd
Show file tree
Hide file tree
Showing 8 changed files with 333 additions and 136 deletions.
33 changes: 32 additions & 1 deletion js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

The `@niivue/niimath` JavaScript library offers an object oriented API for working with the `niimath` CLI. Since `niimath` is a CLI tool, the API implemented in `@niivue/niimath` is just a wrapper around the CLI options and arguments.

### example
### Example: volumes

For example, the [difference of gaussian](https://www.biorxiv.org/content/biorxiv/early/2022/09/17/2022.09.14.507937.full.pdf) command `niimath input.nii -dog 2 3.2 output.nii` can be executed using the following `@niivue/niimath` JavaScript code:

Expand All @@ -25,6 +25,37 @@ await niimath.init();
const outFile = await niimath.image(selectedFile).dog(2, 3.2).run();
```

### Example: meshes

The `@niivue/niimath` library also supports the `-mesh` options available in the `niimath` CLI. However, the JavaScript API is slightly different from the volume processing due to the use of the `-mesh` suboptions.

```javascript
import { Niimath } from '@niivue/niimath';
const niimath = new Niimath();
await niimath.init();
const outName = 'out.mz3'; // outname must be a mesh format!
const outMesh = await niimath.image(selectedFile)
.mesh({
i: 'm', // 'd'ark, 'm'edium, 'b'right or numeric (e.g. 128) isosurface
b: 1, // fill bubbles
})
.run(outName);
/*
Here's the help from the niimath CLI program
The mesh option has multiple sub-options:
-mesh : meshify requires 'd'ark, 'm'edium, 'b'right or numeric isosurface ('niimath bet -mesh -i d mesh.gii')
-i <isovalue> : 'd'ark, 'm'edium, 'b'right or numeric isosurface
-a <atlasFile> : roi based atlas to mesh
-b <fillBubbles> : fill bubbles
-l <onlyLargest> : only largest
-o <originalMC> : original marching cubes
-q <quality> : quality
-s <postSmooth> : post smooth
-r <reduceFraction> : reduce fraction
-v <verbose> : verbose
*/
```

## Installation

To install `@niivue/niimath` in your project, run the following command:
Expand Down
22 changes: 20 additions & 2 deletions js/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,33 @@ <h1>Niimath WASM Demo</h1>
console.log('Initializing niimath wasm...');
await niimath.init();
console.log('niimath wasm initialized.');

const t0 = performance.now();
const outFile = await niimath.image(selectedFile).sobel().thr(20).fmean().otsu(5).run()

// test the -mesh command and mesh output
const outName = 'out.mz3';
const outFile = await niimath.image(selectedFile)
.mesh({
i: 'm', // 'm' for medium
b: 1, // fill bubbles
v: 0 // not verbose
})
.run(outName)
const t1 = performance.now();
console.log("niimath wasm took " + (t1 - t0) + " milliseconds.")

// test with a volume output
// const outName = 'out.nii';
// const outFile = await niimath.image(selectedFile)
// .sobel()
// .run(outName)
// const t1 = performance.now();
// console.log("niimath wasm took " + (t1 - t0) + " milliseconds.")

// Create a download link for the processed file
const url = URL.createObjectURL(outFile);
downloadLink.href = url;
downloadLink.download = 'processed_image.nii.gz';
downloadLink.download = outName;
downloadLink.style.display = 'block';
downloadLink.textContent = 'Download Processed Image';

Expand Down
4 changes: 2 additions & 2 deletions js/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 10 additions & 3 deletions js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@niivue/niimath",
"version": "0.1.0",
"version": "0.1.1",
"main": "dist/index.js",
"module": "dist/index.js",
"exports": {
Expand All @@ -18,7 +18,14 @@
"demo": "npm run build && npx http-server .",
"pub": "npm run build && npm publish --access public"
},
"keywords": ["niivue", "niimath", "nifti", "medical", "imaging", "brain"],
"keywords": [
"niivue",
"niimath",
"nifti",
"medical",
"imaging",
"brain"
],
"author": "NiiVue developers",
"license": "BSD-2-Clause",
"description": "A javascript library to easily use the WASM build of Chris Rorden's niimath command line program written in C",
Expand All @@ -28,4 +35,4 @@
"devDependencies": {
"esbuild": "^0.23.1"
}
}
}
29 changes: 27 additions & 2 deletions js/scripts/parseNiimathHelp.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ function parseHelpText(helpText) {
const methodDefinitions = {};

let currentKernel = false;
let currentMesh = false;

lines.forEach(line => {
// Handle kernel operations
Expand All @@ -20,12 +21,19 @@ function parseHelpText(helpText) {
const match = line.match(/^\s*(-[\w]+)\s*(.*?)\s*:\s*(.*)/);

if (match) {
// each -mesh suboption has at least 4 spaces before the dash (DO NOT USE TABS)
// so we can use this to determine if we are in a mesh suboption after seeing the -mesh string.
// Store the first 4 characters of the line to determine if we are in a mesh suboption
const leadingChars = line.substring(0, 4);
const nSpaces = ' ';
const command = match[1].trim();
// console log below is for debugging
// console.log(leadingChars, command, leadingChars === nSpaces);
const argsString = match[2].trim();
const args = argsString.split(/\s+/).filter(arg => arg.startsWith('<') && arg.endsWith('>'));
const key = command.replace(/^-+/, ''); // Remove leading dashes

const helpText = line.trim();
const helpText = match[3].trim();

if (currentKernel) {
// Special handling for kernel operations
Expand All @@ -41,12 +49,29 @@ function parseHelpText(helpText) {
};
}
}
} else if (command === '-mesh') {
// Special handling for the mesh option
currentMesh = true;
methodDefinitions.mesh = {
args: args.map(arg => arg.replace(/[<>]/g, '')),
help: helpText,
subOperations: {}
};
// check if this is a valid mesh suboption (which is indented in the help text)
} else if (currentMesh && leadingChars === nSpaces && command.startsWith('-')) {
// Handling sub-options of the mesh command
const subKey = command.replace(/^-+/, ''); // Remove leading dashes
methodDefinitions.mesh.subOperations[subKey] = {
args: args.map(arg => arg.replace(/[<>]/g, '')),
help: helpText
};
} else {
// General case for non-kernel operations
// General case for non-kernel and non-mesh operations
methodDefinitions[key] = {
args: args.map(arg => arg.replace(/[<>]/g, '')),
help: helpText
};
currentMesh = false; // Stop handling mesh sub-options if another main option is encountered
}
}

Expand Down
38 changes: 32 additions & 6 deletions js/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ export class Niimath {
// This gets reassigned in the run() method,
// but we need to handle the ready message before that.
// Maybe there is a less hacky way to do this?
this.worker.onmessage = (event) => {
this.worker.onmessage = (event) => {
if (event.data && event.data.type === 'ready') {
resolve(true); // Resolve the promise when the worker is ready
}
}

// Handle worker init errors.
this.worker.onerror = (error) => {
reject(new Error(`Worker failed to load: ${error.message}`));
Expand All @@ -26,13 +26,13 @@ export class Niimath {
}

image(file) {
return new ImageProcessor({worker: this.worker, file, operators: this.operators});
return new ImageProcessor({ worker: this.worker, file, operators: this.operators });
}
}

class ImageProcessor {

constructor({worker, file, operators}) {
constructor({ worker, file, operators }) {
this.worker = worker;
this.file = file;
this.operators = operators;
Expand All @@ -50,7 +50,7 @@ class ImageProcessor {
const definition = this.operators[methodName];

if (methodName === 'kernel') {
// special case for kernels because they have different types with varying arguments
// Special case for kernels because they have different types with varying arguments
Object.keys(definition.subOperations).forEach((subOpName) => {
const subOpDefinition = definition.subOperations[subOpName];
this[`kernel${subOpName.charAt(0).toUpperCase() + subOpName.slice(1)}`] = (...args) => {
Expand All @@ -60,8 +60,34 @@ class ImageProcessor {
return this._addCommand('-kernel', subOpName, ...args);
};
});
} else if (methodName === 'mesh') {
// Special case for mesh because it has sub-options that can be passed as an object
this.mesh = (options = {}) => {
const subCommands = [];

Object.keys(options).forEach((subOptionKey) => {
if (definition.subOperations[subOptionKey]) {
const subOpDefinition = definition.subOperations[subOptionKey];
const subOptionValue = options[subOptionKey];

if (subOpDefinition.args.length > 0 && subOptionValue === undefined) {
throw new Error(`Sub-option -${subOptionKey} requires a value.`);
}

subCommands.push(`-${subOptionKey}`);

if (subOpDefinition.args.length > 0) {
subCommands.push(subOptionValue);
}
} else {
throw new Error(`Invalid sub-option -${subOptionKey} for mesh.`);
}
});

return this._addCommand('-mesh', ...subCommands);
};
} else {
// all other non-kernel operations
// General case for non-kernel and non-mesh operations
this[methodName] = (...args) => {
if (args.length < definition.args.length || (!definition.optional && args.length > definition.args.length)) {
throw new Error(`Expected ${definition.args.length} arguments for ${methodName}, but got ${args.length}`);
Expand Down
Loading

0 comments on commit a905ecd

Please sign in to comment.