PiecewiseGaussianWidget

Live example

## Piecewise Gaussian Widget

The widget can be used with either a mouse or a touch device. The interaction is as follow:

With a mouse:

  • Double click:
    • create a gaussian at that given position and height with no bias.
  • Click:
    • On a gaussian, that will active it so if you click on the (-) button, that will be the one removed.
    • On a button: perform the action of the button (add or remove a selected gaussian).
  • Right click
    • delete the gaussian underneath.
  • Drag:
    • Bottom of a gaussian will adjust the width. From left to right will increase the width.
    • Middle of the gaussian will adjust the biais (x and y).
    • Top of the gaussian will adjust height.
    • Anywhere else will adjust its position.

With touch:

  • Double Tap:
    • One finger: create a gaussian at that given position and height with no bias.
    • Two fingers: delete the gaussian underneath.
  • Single Tap:
    • On a gaussian, that will active it so if you click on the (-) button, that will be the one removed.
    • On a button: perform the action of the button (add or remove a selected gaussian).
  • Drag:
    • Bottom of a gaussian will adjust the width. From left to right will increase the width.
    • Middle of the gaussian will adjust the biais (x and y).
    • Top of the gaussian will adjust height.
    • Anywhere else will adjust its position.

Just for that example

You can stop the looping through the color presets by clicking on the preset name.
Moreover, clicking on the left side of that name will pick the previous preset while clicking on the right side of that name will move to the next one.

That logic was implemented in that example by the following set of code lines.

function changePreset(delta = 1) {
presetIndex = (presetIndex + delta + presets.length) % presets.length;
lookupTable.applyColorMap(presets[presetIndex]);
lookupTable.setMappingRange(...globalDataRange);
lookupTable.updateRange();
labelContainer.innerHTML = presets[presetIndex].Name;
}

let intervalID = null;
function stopInterval() {
if (intervalID !== null) {
clearInterval(intervalID);
intervalID = null;
}
}

labelContainer.addEventListener('click', (event) => {
if (event.pageX < 200) {
stopInterval();
changePreset(-1);
} else {
stopInterval();
changePreset(1);
}
});

Source

import '@kitware/vtk.js/favicon';

// Load the rendering pieces we want to use (for both WebGL and WebGPU)
import '@kitware/vtk.js/Rendering/Profiles/Volume';

import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction';
import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow';
import vtkHttpDataSetReader from '@kitware/vtk.js/IO/Core/HttpDataSetReader';
import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction';
import vtkPiecewiseGaussianWidget from '@kitware/vtk.js/Interaction/Widgets/PiecewiseGaussianWidget';
import vtkVolume from '@kitware/vtk.js/Rendering/Core/Volume';
import vtkVolumeMapper from '@kitware/vtk.js/Rendering/Core/VolumeMapper';

// Force the loading of HttpDataAccessHelper to support gzip decompression
import '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper';

import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps';

// ----------------------------------------------------------------------------
// Standard rendering code setup
// ----------------------------------------------------------------------------

const rootContainer = document.querySelector(
'.vtk-js-example-piecewise-gaussian-widget'
);
const containerStyle = rootContainer ? { height: '100%' } : null;
const urlToLoad = rootContainer
? rootContainer.dataset.url ||
'https://kitware.github.io/vtk-js/data/volume/LIDC2.vti'
: `${__BASE_PATH__}/data/volume/LIDC2.vti`;

const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({
background: [0, 0, 0],
rootContainer,
containerStyle,
});
const renderer = fullScreenRenderer.getRenderer();
const renderWindow = fullScreenRenderer.getRenderWindow();

renderWindow.getInteractor().setDesiredUpdateRate(15.0);

// ----------------------------------------------------------------------------
// Example code
// ----------------------------------------------------------------------------

const body = rootContainer || document.querySelector('body');

// Create Widget container
const widgetContainer = document.createElement('div');
widgetContainer.style.position = 'absolute';
widgetContainer.style.top = 'calc(10px + 1em)';
widgetContainer.style.left = '5px';
widgetContainer.style.background = 'rgba(255, 255, 255, 0.3)';
body.appendChild(widgetContainer);

// Create Label for preset
const labelContainer = document.createElement('div');
labelContainer.style.position = 'absolute';
labelContainer.style.top = '5px';
labelContainer.style.left = '5px';
labelContainer.style.width = '100%';
labelContainer.style.color = 'white';
labelContainer.style.textAlign = 'center';
labelContainer.style.userSelect = 'none';
labelContainer.style.cursor = 'pointer';
body.appendChild(labelContainer);

let presetIndex = 1;
const globalDataRange = [0, 255];
const lookupTable = vtkColorTransferFunction.newInstance();

function changePreset(delta = 1) {
presetIndex =
(presetIndex + delta + vtkColorMaps.rgbPresetNames.length) %
vtkColorMaps.rgbPresetNames.length;
lookupTable.applyColorMap(
vtkColorMaps.getPresetByName(vtkColorMaps.rgbPresetNames[presetIndex])
);
lookupTable.setMappingRange(...globalDataRange);
lookupTable.updateRange();
labelContainer.innerHTML = vtkColorMaps.rgbPresetNames[presetIndex];
}

let intervalID = null;
function stopInterval() {
if (intervalID !== null) {
clearInterval(intervalID);
intervalID = null;
}
}

labelContainer.addEventListener('click', (event) => {
if (event.pageX < 200) {
stopInterval();
changePreset(-1);
} else {
stopInterval();
changePreset(1);
}
});

// ----------------------------------------------------------------------------
// Example code
// ----------------------------------------------------------------------------

const widget = vtkPiecewiseGaussianWidget.newInstance({
numberOfBins: 256,
size: [400, 150],
});
widget.updateStyle({
backgroundColor: 'rgba(255, 255, 255, 0.6)',
histogramColor: 'rgba(100, 100, 100, 0.5)',
strokeColor: 'rgb(0, 0, 0)',
activeColor: 'rgb(255, 255, 255)',
handleColor: 'rgb(50, 150, 50)',
buttonDisableFillColor: 'rgba(255, 255, 255, 0.5)',
buttonDisableStrokeColor: 'rgba(0, 0, 0, 0.5)',
buttonStrokeColor: 'rgba(0, 0, 0, 1)',
buttonFillColor: 'rgba(255, 255, 255, 1)',
strokeWidth: 2,
activeStrokeWidth: 3,
buttonStrokeWidth: 1.5,
handleWidth: 3,
iconSize: 20, // Can be 0 if you want to remove buttons (dblClick for (+) / rightClick for (-))
padding: 10,
});

fullScreenRenderer.setResizeCallback(({ width, height }) => {
widget.setSize(Math.min(450, width - 10), 150);
});

const piecewiseFunction = vtkPiecewiseFunction.newInstance();

const actor = vtkVolume.newInstance();
const mapper = vtkVolumeMapper.newInstance({ sampleDistance: 1.1 });
const reader = vtkHttpDataSetReader.newInstance({ fetchGzip: true });

reader.setUrl(urlToLoad).then(() => {
reader.loadData().then(() => {
const imageData = reader.getOutputData();
const dataArray = imageData.getPointData().getScalars();
const dataRange = dataArray.getRange();
globalDataRange[0] = dataRange[0];
globalDataRange[1] = dataRange[1];

// Update Lookup table
changePreset();

// Automatic switch to next preset every 5s
if (!rootContainer) {
intervalID = setInterval(changePreset, 5000);
}

widget.setDataArray(dataArray.getData());
widget.applyOpacity(piecewiseFunction);

widget.setColorTransferFunction(lookupTable);
lookupTable.onModified(() => {
widget.render();
renderWindow.render();
});

renderer.addVolume(actor);
renderer.resetCamera();
renderer.getActiveCamera().elevation(70);
renderWindow.render();
});
});

actor.setMapper(mapper);
mapper.setInputConnection(reader.getOutputPort());

actor.getProperty().setRGBTransferFunction(0, lookupTable);
actor.getProperty().setScalarOpacity(0, piecewiseFunction);
actor.getProperty().setInterpolationTypeToFastLinear();

// ----------------------------------------------------------------------------
// Default setting Piecewise function widget
// ----------------------------------------------------------------------------

widget.addGaussian(0.425, 0.5, 0.2, 0.3, 0.2);
widget.addGaussian(0.75, 1, 0.3, 0, 0);

widget.setContainer(widgetContainer);
widget.bindMouseListeners();

widget.onAnimation((start) => {
if (start) {
renderWindow.getInteractor().requestAnimation(widget);
} else {
renderWindow.getInteractor().cancelAnimation(widget);
}
});

widget.onOpacityChange(() => {
widget.applyOpacity(piecewiseFunction);
if (!renderWindow.getInteractor().isAnimating()) {
renderWindow.render();
}
});

// ----------------------------------------------------------------------------
// Expose variable to global namespace
// ----------------------------------------------------------------------------

global.widget = widget;