UNPKG

diglettk

Version:

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

559 lines (472 loc) 19.9 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>mprManager.js - DigleTTK</title> <meta name="keywords" content="medical, imaging, dicom, webgl" /> <meta name="keyword" content="medical, imaging, dicom, webgl" /> <script src="scripts/prettify/prettify.js"></script> <script src="scripts/prettify/lang-css.js"></script> <!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <link type="text/css" rel="stylesheet" href="styles/prettify.css"> <link type="text/css" rel="stylesheet" href="styles/jsdoc.css"> <script src="scripts/nav.js" defer></script> <meta name="viewport" content="width=device-width, initial-scale=1.0"> </head> <body> <input type="checkbox" id="nav-trigger" class="nav-trigger" /> <label for="nav-trigger" class="navicon-button x"> <div class="navicon"></div> </label> <label for="nav-trigger" class="overlay"></label> <nav > <input type="text" id="nav-search" placeholder="Search" /> <h2><a href="index.html">Home</a></h2><h2><a href="https://github.com/dvisionlab/DigletTK" target="_blank" class="menu-item" id="repository" >Github repo</a></h2><h3>Classes</h3><ul><li><a href="baseView.html">baseView</a></li><li><a href="MPRManager.html">MPRManager</a><ul class='methods'><li data-type='method' style='display: none;'><a href="MPRManager.html#destroy">destroy</a></li><li data-type='method' style='display: none;'><a href="MPRManager.html#getInitialState">getInitialState</a></li><li data-type='method' style='display: none;'><a href="MPRManager.html#onRotate">onRotate</a></li><li data-type='method' style='display: none;'><a href="MPRManager.html#onThickness">onThickness</a></li><li data-type='method' style='display: none;'><a href="MPRManager.html#resize">resize</a></li><li data-type='method' style='display: none;'><a href="MPRManager.html#setImage">setImage</a></li><li data-type='method' style='display: none;'><a href="MPRManager.html#setTool">setTool</a></li></ul></li><li><a href="VRView.html">VRView</a><ul class='methods'><li data-type='method' style='display: none;'><a href="VRView.html#_initCropWidget">_initCropWidget</a></li><li data-type='method' style='display: none;'><a href="VRView.html#_initPicker">_initPicker</a></li><li data-type='method' style='display: none;'><a href="VRView.html#addLandmarks">addLandmarks</a></li><li data-type='method' style='display: none;'><a href="VRView.html#addSurface">addSurface</a></li><li data-type='method' style='display: none;'><a href="VRView.html#destroy">destroy</a></li><li data-type='method' style='display: none;'><a href="VRView.html#getLutList">getLutList</a></li><li data-type='method' style='display: none;'><a href="VRView.html#resetMeasurementState">resetMeasurementState</a></li><li data-type='method' style='display: none;'><a href="VRView.html#resetView">resetView</a></li><li data-type='method' style='display: none;'><a href="VRView.html#resize">resize</a></li><li data-type='method' style='display: none;'><a href="VRView.html#setImage">setImage</a></li><li data-type='method' style='display: none;'><a href="VRView.html#setSurfaceVisibility">setSurfaceVisibility</a></li><li data-type='method' style='display: none;'><a href="VRView.html#setTool">setTool</a></li><li data-type='method' style='display: none;'><a href="VRView.html#turnPickingOff">turnPickingOff</a></li><li data-type='method' style='display: none;'><a href="VRView.html#turnPickingOn">turnPickingOn</a></li><li data-type='method' style='display: none;'><a href="VRView.html#updateLandmarkPosition">updateLandmarkPosition</a></li><li data-type='method' style='display: none;'><a href="VRView.html#updateSurface">updateSurface</a></li></ul></li></ul><h3>Global</h3><ul><li><a href="global.html#addSphereInPoint">addSphereInPoint</a></li><li><a href="global.html#applyAngleStrategy">applyAngleStrategy</a></li><li><a href="global.html#applyLengthStrategy">applyLengthStrategy</a></li><li><a href="global.html#buildVtkVolume">buildVtkVolume</a></li><li><a href="global.html#createRGBStringFromRGBValues">createRGBStringFromRGBValues</a></li><li><a href="global.html#createSurfaceActor">createSurfaceActor</a></li><li><a href="global.html#createVolumeActor">createVolumeActor</a></li><li><a href="global.html#degrees2radians">degrees2radians</a></li><li><a href="global.html#fitToWindow">fitToWindow</a></li><li><a href="global.html#getAbsoluteRange">getAbsoluteRange</a></li><li><a href="global.html#getCroppingPlanes">getCroppingPlanes</a></li><li><a href="global.html#getRelativeRange">getRelativeRange</a></li><li><a href="global.html#getVideoCardInfo">getVideoCardInfo</a></li><li><a href="global.html#getVOI">getVOI</a></li><li><a href="global.html#getVolumeCenter">getVolumeCenter</a></li><li><a href="global.html#larvitarInitialized">larvitarInitialized</a></li><li><a href="global.html#setActorProperties">setActorProperties</a></li><li><a href="global.html#setCamera">setCamera</a></li><li><a href="global.html#setupCropWidget">setupCropWidget</a></li><li><a href="global.html#setupPickingPlane">setupPickingPlane</a></li><li><a href="global.html#State">State</a></li></ul> </nav> <div id="main"> <h1 class="page-title">mprManager.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>// Use modified MPRSlice interactor import vtkInteractorStyleMPRWindowLevel from "./vtk/vtkInteractorStyleMPRWindowLevel"; import vtkInteractorStyleMPRCrosshairs from "./vtk/vtkInteractorStyleMPRCrosshairs"; import vtkInteractorStyleMPRPanZoom from "./vtk/vtkInteractorStyleMPRPanZoom"; import vtkCoordinate from "@kitware/vtk.js/Rendering/Core/Coordinate"; import vtkMatrixBuilder from "@kitware/vtk.js/Common/Core/MatrixBuilder"; import { getPlaneIntersection, getVolumeCenter, createVolumeActor } from "./utils/utils"; import { MPRView } from "./mprView"; /** * Internal state of a single view * @typedef {Object} State * @property {Number[]} slicePlaneNormal - The slice plane normal as [x,y,z] * @property {Number[]} sliceViewUp - The up vector as [x,y,z] * @property {Number} slicePlaneXRotation - The x axis rotation in deg * @property {Number} slicePlaneYRotation - The y axis rotation in deg * @property {Number} viewRotation - The view rotation in deg * @property {Number} sliceThickness - The MIP slice thickness in px * @property {String} blendMode - The active blending mode ("MIP", "MinIP", "Average") * @property {Object} window - wwwl * @property {Number} window.ww - Window width * @property {Number} window.wl - Window level */ /** A manager for MPR views */ export class MPRManager { /** * Create a manager. * @param {Object} elements - The 3 target HTML elements {key1:{}, key2:{}, key3:{}}. * @param {HTMLElement} elements.element - The target HTML elements. * @param {String} elements.key - The target HTML elements. */ constructor(elements) { this.VERBOSE = false; // TODO setter this.syncWindowLevels = true; // TODO setter this._activeTool = null; // TODO input sanity check this.elements = elements; this.volume = null; this.sliceIntersection = [0, 0, 0]; this.mprViews = {}; this.initMPR(); } /** * wwwl * @type {Array} */ set wwwl([ww, wl]) { const lower = wl - ww / 2.0; const upper = wl + ww / 2.0; this.volume .getProperty() .getRGBTransferFunction(0) .setMappingRange(lower, upper); Object.keys(this.elements).forEach((key, i) => { this.mprViews[key].wwwl = [ww, wl]; }); } /** * Initialize the three MPR views * @private */ initMPR() { Object.keys(this.elements).forEach((key, i) => { try { this.mprViews[key] = new MPRView(key, i, this.elements[key].element); } catch (err) { console.error("Error creating MPRView", key); console.error(err); } }); if (this.VERBOSE) console.log("initialized"); } /** * Get initial State object * @returns {State} The initial internal state */ getInitialState() { // cycle on keys, and reduce extracting only useful properties // NOTE: initialize reduce with cloned object! let viewsState = Object.keys(this.mprViews).reduce((result, key) => { let { slicePlaneNormal, sliceViewUp, slicePlaneXRotation, slicePlaneYRotation, viewRotation, _sliceThickness, _blendMode, window } = result[key]; result[key] = { slicePlaneNormal, sliceViewUp, slicePlaneXRotation, slicePlaneYRotation, viewRotation, sliceThickness: _sliceThickness, blendMode: _blendMode, window }; return result; }, Object.assign({}, this.mprViews)); return { // interactorCenters: { top: [0, 0], left: [0, 0], front: [0, 0] }, interactorCenters: Object.keys(this.elements).reduce( (res, key) => ({ ...res, [key]: [0, 0] }), {} ), sliceIntersection: [...this.sliceIntersection], // clone views: viewsState }; } /** * Set the image to render * @param {State} state - The current manager state * @param {Array} image - The pixel data from DICOM serie */ setImage(state, image) { let actor = createVolumeActor(image); this.volume = actor; this.sliceIntersection = getVolumeCenter(actor.getMapper()); // update external state state.sliceIntersection = [...this.sliceIntersection]; Object.keys(this.elements).forEach(key => { this.mprViews[key].initView( actor, state, // on scroll callback (it's fired but too early) () => { this.onScrolled.call(this, state); }, // on initialized callback (fire when all is set) () => { this.onScrolled.call(this, state); } ); }); if (this._activeTool) { this.setTool(this._activeTool, state); } } /** * Set the active tool * @param {String} toolName - "level" or "crosshair" * @param {State} state - The current manager state */ setTool(toolName, state) { switch (toolName) { case "level": this.setLevelTool(state); break; case "crosshair": this.setCrosshairTool(state); break; case "zoom": this.setZoomTool(state); break; case "pan": this.setPanTool(state); break; } } /** * Set "pan" as active tool * @private * @param {State} state - The current manager state */ setPanTool(state) { Object.entries(state.views).forEach(([key]) => { const istyle = vtkInteractorStyleMPRPanZoom.newInstance({ leftButtonTool: "pan" }); istyle.setOnScroll(() => { this.onScrolled(state); }); // update interactor center istyle.setOnPanChanged(() => { this.updateInteractorCenters(state); }); this.mprViews[key].setInteractor(istyle); }); this._activeTool = "pan"; } /** * Set "zoom" as active tool * @private * @param {State} state - The current manager state */ setZoomTool(state) { Object.entries(state.views).forEach(([key]) => { const istyle = vtkInteractorStyleMPRPanZoom.newInstance({ leftButtonTool: "zoom" }); istyle.setOnScroll(() => { this.onScrolled(state); }); // update interactor center istyle.setOnZoomChanged(() => { this.updateInteractorCenters(state); }); this.mprViews[key].setInteractor(istyle); }); this._activeTool = "zoom"; } /** * Set "level" as active tool * @private * @param {State} state - The current manager state */ setLevelTool(state) { Object.entries(state.views).forEach(([key]) => { const istyle = vtkInteractorStyleMPRWindowLevel.newInstance(); istyle.setOnScroll(() => { this.onScrolled(state); }); istyle.setOnLevelsChanged(levels => { this.updateLevels({ ...levels, srcKey: key }, state); }); this.mprViews[key].setInteractor(istyle); }); this._activeTool = "level"; } /** * Set "crosshair" as active tool * @private * @param {State} state - The current manager state */ setCrosshairTool(state) { let self = this; Object.entries(state.views).forEach(([key]) => { const istyle = vtkInteractorStyleMPRCrosshairs.newInstance(); istyle.setOnScroll(() => { self.onScrolled(state); }); istyle.setOnClickCallback(({ worldPos }) => { self.onCrosshairPointSelected({ worldPos, srcKey: key }, state); }); this.mprViews[key].setInteractor(istyle); }); this._activeTool = "crosshair"; } /** * Update slice positions on user interaction (for crosshair tool) * @private * @param {Object} {} */ onCrosshairPointSelected({ srcKey, worldPos }, externalState) { Object.keys(this.elements).forEach(key => { if (key !== srcKey) { // We are basically doing the same as getSlice but with the world coordinate // that we want to jump to instead of the camera focal point. // I would rather do the camera adjustment directly but I keep // doing it wrong and so this is good enough for now. // ~ swerik const renderWindow = this.mprViews[ key ]._genericRenderWindow.getRenderWindow(); const istyle = renderWindow.getInteractor().getInteractorStyle(); const sliceNormal = istyle.getSliceNormal(); const transform = vtkMatrixBuilder .buildFromDegree() .identity() .rotateFromDirections(sliceNormal, [1, 0, 0]); const mutatedWorldPos = worldPos.slice(); transform.apply(mutatedWorldPos); const slice = mutatedWorldPos[0]; istyle.setSlice(slice); renderWindow.render(); } this.updateInteractorCenters(externalState); }); // update both internal &amp; external state this.sliceIntersection = [...worldPos]; externalState.sliceIntersection = [...worldPos]; } /** * Update wwwl on user interaction (for level tool) * @private * @param {Object} {} * @param {State} state - The current manager state */ updateLevels({ windowCenter, windowWidth, srcKey }, state) { state.views[srcKey].window.center = windowCenter; state.views[srcKey].window.width = windowWidth; if (this.syncWindowLevels) { Object.keys(this.elements) .filter(key => key !== srcKey) .forEach(k => { this.mprViews[k].wwwl = [windowWidth, windowCenter]; }); } } /** * Update slice position when scrolling * @private */ onScrolled(state) { let planes = []; Object.keys(this.elements).forEach(key => { const camera = this.mprViews[key].camera; planes.push({ position: camera.getFocalPoint(), normal: camera.getDirectionOfProjection() // this[viewportIndex].slicePlaneNormal }); }); const newPoint = getPlaneIntersection(...planes); if ( !Number.isNaN(newPoint) &amp;&amp; !newPoint.some(coord => Number.isNaN(coord)) ) { this.sliceIntersection = [...newPoint]; state.sliceIntersection = [...newPoint]; if (this.VERBOSE) console.log("updating slice intersection", newPoint); } this.updateInteractorCenters(state); return newPoint; } /** * Update slice planes on rotation * @param {String} key - One of the initially provided keys (identify a view) * @param {String} axis - 'x' or 'y' axis * @param {Number} angle - The amount of rotation [deg], absolute * @param {State} state - The current manager state */ onRotate(key, axis, angle, state) { // Match the source axis to the associated plane switch (key) { case "top": if (axis === "x") state.views.front.slicePlaneYRotation = angle; else if (axis === "y") state.views.left.slicePlaneYRotation = angle; break; case "left": if (axis === "x") state.views.top.slicePlaneXRotation = angle; else if (axis === "y") state.views.front.slicePlaneXRotation = angle; break; case "front": if (axis === "x") state.views.top.slicePlaneYRotation = angle; else if (axis === "y") state.views.left.slicePlaneXRotation = angle; break; } // dv: this was a watcher in mpr component, update all except myself ? Object.keys(this.elements) .filter(c => c !== key) .forEach(k => { this.mprViews[k].updateSlicePlane(state.views[k]); }); if (this.VERBOSE) console.log("afterOnRotate", state); } /** * Update slice planes on rotation * @param {String} key - One of the initially provided keys (identify a view) * @param {String} axis - 'x' or 'y' axis * @param {Number} thickness - The amount of thickness [px], absolute * @param {State} state - The current manager state */ onThickness(key, axis, thickness, state) { const shouldBeMIP = thickness > 1; let target_view; switch (key) { case "top": if (axis === "x") target_view = "front"; else if (axis === "y") target_view = "left"; break; case "left": if (axis === "x") target_view = "top"; else if (axis === "y") target_view = "front"; break; case "front": if (axis === "x") target_view = "top"; else if (axis === "y") target_view = "left"; break; } // if thickness > 1 switch to MIP if (shouldBeMIP &amp;&amp; this.mprViews[target_view].blendMode === "none") { this.mprViews[target_view].blendMode = "MIP"; state.mprViews[target_view].blendMode = "MIP"; } // update both internal and external state this.mprViews[target_view].sliceThickness = thickness; state.views[target_view].sliceThickness = thickness; } /** * Update interactor centers coordinates on canvas * @private * @param {State} state - The current manager state */ updateInteractorCenters(state) { Object.keys(this.elements).forEach(key => { // compute interactor centers display position const renderer = this.mprViews[key]._genericRenderWindow.getRenderer(); const wPos = vtkCoordinate.newInstance(); wPos.setCoordinateSystemToWorld(); wPos.setValue(...this.sliceIntersection); const displayPosition = wPos.getComputedDisplayValue(renderer); if (this.VERBOSE) console.log("interactor center", key, displayPosition); // set new interactor center on canvas into external state state.interactorCenters[key] = displayPosition; }); } /** * Force views resize * @param {String} key - If provided, resize just its view, otherwise all views */ resize(state, key) { if (key) { this.mprViews[key].onResize(); } else { Object.values(this.mprViews).forEach(view => { view.onResize(); }); } this.updateInteractorCenters(state); } /** * Destroy webgl content and release listeners */ destroy() { Object.keys(this.elements).forEach(k => { this.mprViews[k].destroy(); }); } } </code></pre> </article> </section> </div> <br class="clear"> <footer> Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.6.6</a> using the <a href="https://github.com/clenemt/docdash">docdash</a> theme. </footer> <script>prettyPrint();</script> <script src="scripts/polyfill.js"></script> <script src="scripts/linenumber.js"></script> <script src="scripts/search.js" defer></script> <script src="scripts/collapse.js" defer></script> </body> </html>