UNPKG

diglettk

Version:

A medical imaging toolkit, built on top of vtk.js

711 lines (603 loc) 20.6 kB
import vtkGenericRenderWindow from "@kitware/vtk.js/Rendering/Misc/GenericRenderWindow"; import vtkColorTransferFunction from "@kitware/vtk.js/Rendering/Core/ColorTransferFunction"; import vtkPiecewiseFunction from "@kitware/vtk.js/Common/DataModel/PiecewiseFunction"; import vtkColorMaps from "@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps"; import vtkMouseCameraTrackballRotateManipulator from "@kitware/vtk.js/Interaction/Manipulators/MouseCameraTrackballRotateManipulator"; import vtkMouseCameraTrackballPanManipulator from "@kitware/vtk.js/Interaction/Manipulators/MouseCameraTrackballPanManipulator"; import vtkMouseCameraTrackballZoomManipulator from "@kitware/vtk.js/Interaction/Manipulators/MouseCameraTrackballZoomManipulator"; import vtkMouseRangeManipulator from "@kitware/vtk.js/Interaction/Manipulators/MouseRangeManipulator"; import vtkInteractorStyleManipulator from "@kitware/vtk.js/Interaction/Style/InteractorStyleManipulator"; import vtkPointPicker from "@kitware/vtk.js/Rendering/Core/PointPicker"; import vtkCoordinate from "@kitware/vtk.js/Rendering/Core/Coordinate"; import { createVolumeActor, setupPGwidget, setCamera, setActorProperties, setupCropWidget, setupPickingPlane } from "./utils/utils"; import { applyStrategy } from "./utils/strategies"; import { createPreset } from "./utils/colormaps"; import { getRenderPass } from "./renderPasses"; import { baseView } from "./baseView"; // Add custom presets vtkColorMaps.addPreset(createPreset()); //TODO interactions: /** * setTool("Length/Angle", {mouseButtonMask:1}, measurementState); => Change interactor on left mouse button * setupMouseButtons(config); => Initialize right mouse button behaviour * * Measurement state structure: * measurementState = { * p1: [0, 0], * p2: [0, 0], * p3: [0, 0], * label: `string` * } */ /** A class representing a Volume Rendering scene */ export class VRView extends baseView { /** * Create a volume rendering scene * @param {HTMLElement} element - the target html element to render the scene */ constructor(element) { super(); this.VERBOSE = false; this.element = element; this.renderer = null; this.renderWindow = null; this._genericRenderWindow = null; this.actor = null; this._raysDistance = null; this._blurOnInteraction = null; // piecewise gaussian widget stuff this.PGwidgetElement = null; this.PGwidget = null; this.gaussians = null; this._PGwidgetLoaded = false; // crop widget this._cropWidget = null; // normalized ww wl this.ww = 0.1; this.wl = 0.4; // absolute ww wl this.wwwl = [0, 0]; // LUT options this._rangeLUT = null; this._rescaleLUT = false; // cannot initialize true (must set lut before) // rendering passes this._edgeEnhancement = false; // measurement state this._measurementState = null; this.initVR(); } /** * wwwl * @type {Array} */ set wwwl(value) { if (!this.actor) { return; } let relativeWwwl = getRelativeRange(this.actor, value); this.wl = relativeWwwl.wl; this.ww = relativeWwwl.ww; if (this.PGwidget) { this.updateWidget(); } } get wwwl() { let absoluteWwwl = getAbsoluteRange(this.actor, [this.ww, this.wl]); return [absoluteWwwl.ww, absoluteWwwl.wl]; } /** * raysDistance * @type {Number} */ set resolution(value) { this._raysDistance = 1 / value; this.actor.getMapper().setSampleDistance(this._raysDistance); let maxSamples = value > 1 ? value * 1000 : 1000; this.actor.getMapper().setMaximumSamplesPerRay(maxSamples); this.renderWindow.render(); } get resolution() { return Math.round(1 / this._raysDistance); } /** * Presets * @type {Array} */ get presetsList() { return vtkColorMaps.rgbPresetNames; } /** * PGwidgetElement (set null to hide) * @type {HTMLelement} */ set widgetElement(element) { this.PGwidgetElement = element; let h = element.offsetHeight ? element.offsetHeight - 5 : 100; let w = element.offsetWidth ? element.offsetWidth - 5 : 300; this.PGwidget.setSize(w, h); this.PGwidget.setContainer(this.PGwidgetElement); this.PGwidget.render(); } /** * Flag to set lut rescaling on opacity range * @type {bool} */ set rescaleLUT(bool) { this._rescaleLUT = bool; let range; if (this._rescaleLUT && this.PGwidget) { range = this.PGwidget.getOpacityRange(); } else { range = this.actor .getMapper() .getInputData() .getPointData() .getScalars() .getRange(); } this.ctfun.setMappingRange(...range); this.ctfun.updateRange(); } /** * Set range to apply lut !!! WIP * @type {Array} */ set rangeLUT([min, max]) { this._rangeLUT = [min, max]; this.actor .getProperty() .getRGBTransferFunction(0) .setMappingRange(min, max); } /** * Crop widget on / off * @type {bool} */ set cropWidget(visible) { if (!this._cropWidget) this._initCropWidget(); this._cropWidget.setVisibility(visible); this._widgetManager.renderWidgets(); this.renderWindow.render(); } /** * Set colormap and opacity function * lutName - as in presets list * @type {String} */ set lut(lutName) { // set up color transfer function const lookupTable = vtkColorTransferFunction.newInstance(); lookupTable.applyColorMap(vtkColorMaps.getPresetByName(lutName)); // update lookup table mapping range based on input dataset let range; if (this._rescaleLUT && this._PGwidgetLoaded) { range = this.PGwidget.getOpacityRange(); } else { range = this.actor .getMapper() .getInputData() .getPointData() .getScalars() .getRange(); } // TODO a function to set custom mapping range (unbind from opacity) lookupTable.setMappingRange(...range); lookupTable.updateRange(); this.actor.getProperty().setRGBTransferFunction(0, lookupTable); // setup opacity function (values will be set by PGwidget) const piecewiseFun = vtkPiecewiseFunction.newInstance(); this.actor.getProperty().setScalarOpacity(0, piecewiseFun); this.ctfun = lookupTable; this.ofun = piecewiseFun; this.updateWidget(); } /** * Toggle blurring on interaction (Increase performance) * @type {bool} toggle - if true, blur on interaction */ set blurOnInteraction(toggle) { this._blurOnInteraction = toggle; let interactor = this.renderWindow.getInteractor(); let mapper = this.actor.getMapper(); if (toggle) { interactor.onLeftButtonPress(() => { mapper.setSampleDistance(this._raysDistance * 5); }); interactor.onLeftButtonRelease(() => { mapper.setSampleDistance(this._raysDistance); // update picking plane let camera = this.renderer.getActiveCamera(); if (this._pickingPlane) this._pickingPlane.setNormal(camera.getDirectionOfProjection()); this.renderWindow.render(); }); } else { interactor.onLeftButtonPress(() => { mapper.setSampleDistance(this._raysDistance); }); interactor.onLeftButtonRelease(() => { mapper.setSampleDistance(this._raysDistance); // update picking plane let camera = this.renderer.getActiveCamera(); if (this._pickingPlane) this._pickingPlane.setNormal(camera.getDirectionOfProjection()); this.renderWindow.render(); }); } } /** * Toggle edge enhancement */ set edgeEnhancement([type, value]) { let renderPass = getRenderPass(type, value); let view = this.renderWindow.getViews()[0]; view.setRenderPasses([renderPass]); this.renderWindow.render(); } /** * Initialize rendering scene * @private */ initVR() { const genericRenderWindow = vtkGenericRenderWindow.newInstance(); genericRenderWindow.setContainer(this.element); genericRenderWindow.setBackground([0, 0, 0]); //add custom resize cb genericRenderWindow.onResize(() => { // bypass genericRenderWindow resize method (do not consider devicePixelRatio) // https://kitware.github.io/vtk-js/api/Rendering_Misc_GenericRenderWindow.html let size = [ genericRenderWindow.getContainer().getBoundingClientRect().width, genericRenderWindow.getContainer().getBoundingClientRect().height ]; genericRenderWindow.getRenderWindow().getViews()[0].setSize(size); if (this.VERBOSE) console.log("resize", size); }); // resize callback window.addEventListener("resize", evt => { genericRenderWindow.resize(); }); genericRenderWindow.resize(); this.renderer = genericRenderWindow.getRenderer(); this.renderWindow = genericRenderWindow.getRenderWindow(); this._genericRenderWindow = genericRenderWindow; // initalize piecewise gaussian widget this.PGwidget = setupPGwidget(this.PGwidgetElement); } /** * Set the image to be rendered * @param {ArrayBuffer} image - The image content data as buffer array */ setImage(image) { // clean scene this.renderer.removeAllVolumes(); let actor = createVolumeActor(image); this.actor = actor; this.lut = "Grayscale"; this.resolution = 2; this.renderer.addVolume(actor); // center camera on new volume this.renderer.resetCamera(); setCamera(this.renderer.getActiveCamera(), actor.getCenter()); if (this.PGwidget) { this.updateWidget(); this.setWidgetCallbacks(); } // TODO if crop widget, update to new image (or set to null so that it will be initialized again) // TODO implement a strategy to set rays distance setActorProperties(this.actor); this.setupInteractor(); this.blurOnInteraction = true; this._genericRenderWindow.resize(); this.renderWindow.render(); } /** * Get vtk LUTs list * @returns {Array} - Lut list as array of strings */ getLutList() { return vtkColorMaps.rgbPresetNames; } /** * Setup crop widget */ _initCropWidget() { let cropWidget = setupCropWidget(this.renderer, this.actor.getMapper()); this._widgetManager = cropWidget.widgetManager; this._cropWidget = cropWidget.widget; this.renderWindow.render(); } /** * Update the PGwidget after an image has been loaded * @private */ updateWidget() { const dataArray = this.actor .getMapper() .getInputData() .getPointData() .getScalars(); this.PGwidget.setDataArray(dataArray.getData()); let gaussians = this.PGwidget.getGaussians(); if (gaussians.length > 0) { let gaussian = gaussians[0]; gaussian.position = this.wl; gaussian.width = this.ww; this.PGwidget.setGaussians([gaussian]); } else { // TODO initilize in a smarter way const default_opacity = 1.0; const default_bias = 0.0; // xBias const default_skew = 1.8; // yBias this.PGwidget.addGaussian( this.wl, default_opacity, this.ww, default_bias, default_skew ); // x, y, ampiezza, sbilanciamento, andamento } this.PGwidget.applyOpacity(this.ofun); this.PGwidget.setColorTransferFunction(this.ctfun); this.ctfun.onModified(() => { this.PGwidget.render(); this.renderWindow.render(); }); this._PGwidgetLoaded = true; } /** * Binds callbacks to user interactions on PGwidget * @private */ setWidgetCallbacks() { this.PGwidget.bindMouseListeners(); this.PGwidget.onAnimation(start => { if (start) { this.renderWindow.getInteractor().requestAnimation(this.PGwidget); } else { this.renderWindow.getInteractor().cancelAnimation(this.PGwidget); } }); this.PGwidget.onOpacityChange(widget => { this.PGwidget = widget; this.gaussians = widget.getGaussians().slice(); // store this.PGwidget.applyOpacity(this.ofun); if (!this.renderWindow.getInteractor().isAnimating()) { this.renderWindow.render(); } if (this._rescaleLUT && this.PGwidget) { const range = this.PGwidget.getOpacityRange(); this.ctfun.setMappingRange(...range); this.ctfun.updateRange(); } }); } /** * Init interactor * @private */ setupInteractor() { // TODO setup from user const rotateManipulator = vtkMouseCameraTrackballRotateManipulator.newInstance({ button: 1 }); const panManipulator = vtkMouseCameraTrackballPanManipulator.newInstance({ button: 3, control: true }); const zoomManipulator = vtkMouseCameraTrackballZoomManipulator.newInstance({ button: 3, scrollEnabled: true }); const rangeManipulator = vtkMouseRangeManipulator.newInstance({ button: 1, shift: true }); let self = this; function getWL() { return self.wl; } function getWW() { return self.ww; } function setWL(v) { let wl = self.wl + (v - self.wl) / 25; self.wl = wl; let gaussians = self.PGwidget.getGaussians().slice(); // NOTE: slice() to clone! gaussians[0].position = self.wl; //TODO: foreach self.PGwidget.setGaussians(gaussians); } function setWW(v) { let ww = self.ww + (v - self.ww) / 5; self.ww = ww; let gaussians = self.PGwidget.getGaussians().slice(); // NOTE: slice() to clone! gaussians[0].width = self.ww; //TODO: foreach self.PGwidget.setGaussians(gaussians); } rangeManipulator.setVerticalListener(-1, 1, 0.001, getWL, setWL); rangeManipulator.setHorizontalListener(0.1, 2.1, 0.001, getWW, setWW); const interactorStyle = vtkInteractorStyleManipulator.newInstance(); interactorStyle.addMouseManipulator(rangeManipulator); interactorStyle.addMouseManipulator(rotateManipulator); interactorStyle.addMouseManipulator(panManipulator); interactorStyle.addMouseManipulator(zoomManipulator); interactorStyle.setCenterOfRotation(this.actor.getCenter()); this.renderWindow.getInteractor().setInteractorStyle(interactorStyle); // clear measurements on interactions this.renderWindow .getInteractor() .onMouseWheel(() => this.resetMeasurementState()); this.renderWindow .getInteractor() .onRightButtonPress(() => this.resetMeasurementState()); } /** * Reset measurement state to default * @param {*} measurementState */ resetMeasurementState(state) { if (this._measurementState) { this._measurementState.p1 = new Array(2); this._measurementState.p2 = new Array(2); this._measurementState.p3 = new Array(2); this._measurementState.p1_world = new Array(2); this._measurementState.p2_world = new Array(2); this._measurementState.p3_world = new Array(2); this._measurementState.label = null; } else if (state) { state.p1 = new Array(2); state.p2 = new Array(2); state.p3 = new Array(2); state.p1_world = new Array(2); state.p2_world = new Array(2); state.p3_world = new Array(2); state.label = null; } } /** * Set active tool * ("Length/Angle", {mouseButtonMask:1}, measurementState) * * @param {*} toolName * @param {*} options * @param {*} measurementState */ setTool(toolName, options, measurementState) { if (this._leftButtonCb) { this._leftButtonCb.unsubscribe(); } switch (toolName) { case "Length": this._initPicker(measurementState, toolName); break; case "Angle": this._initPicker(measurementState, toolName); break; case "Rotation": this.resetMeasurementState(measurementState); this.setupInteractor(); break; default: console.warn("No tool found for", toolName); } } /** * initPicker */ _initPicker(state, mode) { // no blur when measure this.blurOnInteraction = false; // de-activate rotation let rotateManipulator = this.renderWindow .getInteractor() .getInteractorStyle() .getMouseManipulators() .filter(i => { return i.getClassName() == "vtkMouseCameraTrackballRotateManipulator"; }) .pop(); this.renderWindow .getInteractor() .getInteractorStyle() .removeMouseManipulator(rotateManipulator); // ---------------------------------------------------------------------------- // Setup picking interaction // ---------------------------------------------------------------------------- // TODO this is slow the first time we pick, maybe we could use cellPicker and decrease resolution const picker = vtkPointPicker.newInstance(); picker.setPickFromList(1); picker.initializePickList(); if (!this._pickingPlane) { // add a 1000x1000 plane let camera = this.renderer.getActiveCamera(); let { plane, planeActor } = setupPickingPlane(camera, this.actor); this.renderer.addActor(planeActor); this._pickingPlane = plane; this._planeActor = planeActor; } // add picking plane to pick list picker.addPickList(this._planeActor); // Pick on mouse left click this._leftButtonCb = this.renderWindow .getInteractor() .onLeftButtonPress(callData => { if (this.renderer !== callData.pokedRenderer) { return; } const pos = callData.position; const point = [pos.x, pos.y, 0.0]; picker.pick(point, this.renderer); if (picker.getActors().length === 0) { const pickedPoint = picker.getPickPosition(); if (this.VERBOSE) console.log(`No point picked, default: ${pickedPoint}`); // addSphereInPoint(pickedPoint, this.renderer); } else { const pickedPoints = picker.getPickedPositions(); const pickedPoint = pickedPoints[0]; // always a single point on a plane if (this.VERBOSE) console.log(`Picked: ${pickedPoint}`); // addSphereInPoint(pickedPoint, this.renderer); // canvas coord const wPos = vtkCoordinate.newInstance(); wPos.setCoordinateSystemToWorld(); wPos.setValue(...pickedPoint); const displayPosition = wPos.getComputedDisplayValue(this.renderer); // apply changes on state based on active tool applyStrategy(state, displayPosition, pickedPoint, mode); if (this.VERBOSE) console.log(state); this._measurementState = state; } this.renderWindow.render(); }); } /** * Reset view */ resetView() { let center = this.actor.getCenter(); let camera = this.renderer.getActiveCamera(); setCamera(camera, center); this.renderWindow.render(); } /** * on resize callback */ resize() { // TODO: debounce for performance reasons? this._genericRenderWindow.resize(); } /** * Destroy webgl content and release listeners */ destroy() { this.element = null; this._genericRenderWindow.delete(); this._genericRenderWindow = null; if (this.actor) { this.actor.getMapper().delete(); this.actor.delete(); this.actor = null; } if (this._planeActor) { this._planeActor.getMapper().delete(); this._planeActor.delete(); this._planeActor = null; } if (this.PGwidgetElement) { this.PGwidgetElement = null; this.PGwidget.getCanvas().remove(); this.PGwidget.delete(); this.PGwidget = null; this.gaussians = null; } if (this._cropWidget) { this._cropWidget.delete(); this._cropWidget = null; } } }