import '@kitware/vtk.js/favicon';
import '@kitware/vtk.js/Rendering/Profiles/All';
import '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper';
import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; import vtkWidgetManager from '@kitware/vtk.js/Widgets/Core/WidgetManager'; import vtkRectangleWidget from '@kitware/vtk.js/Widgets/Widgets3D/RectangleWidget'; import vtkEllipseWidget from '@kitware/vtk.js/Widgets/Widgets3D/EllipseWidget'; import vtkInteractorStyleImage from '@kitware/vtk.js/Interaction/Style/InteractorStyleImage'; import vtkHttpDataSetReader from '@kitware/vtk.js/IO/Core/HttpDataSetReader'; import vtkImageMapper from '@kitware/vtk.js/Rendering/Core/ImageMapper'; import vtkImageSlice from '@kitware/vtk.js/Rendering/Core/ImageSlice'; import vtkImplicitBoolean from '@kitware/vtk.js/Common/DataModel/ImplicitBoolean'; import vtkMatrixBuilder from '@kitware/vtk.js/Common/Core/MatrixBuilder'; import vtkMath from '@kitware/vtk.js/Common/Core/Math'; import vtkPlane from '@kitware/vtk.js/Common/DataModel/Plane'; import vtkSphere from '@kitware/vtk.js/Common/DataModel/Sphere'; import vtkTransform from '@kitware/vtk.js/Common/Transform/Transform'; import { VTK_SMALL_NUMBER } from 'vtk.js/Sources/Common/Core/Math/Constants';
import vtkInteractorObserver from '@kitware/vtk.js/Rendering/Core/InteractorObserver'; import { bindSVGRepresentation, multiLineTextCalculator, VerticalTextAlignment, } from 'vtk.js/Examples/Widgets/Utilities/SVGHelpers';
import { BehaviorCategory, ShapeBehavior, TextPosition, } from '@kitware/vtk.js/Widgets/Widgets3D/ShapeWidget/Constants';
import { ViewTypes } from '@kitware/vtk.js/Widgets/Core/WidgetManager/Constants';
import { vec3 } from 'gl-matrix';
import controlPanel from './controlPanel.html';
const { computeWorldToDisplay } = vtkInteractorObserver; const { Operation } = vtkImplicitBoolean;
const slicingModeNormal = { [vtkImageMapper.SlicingMode.X]: [1, 0, 0], [vtkImageMapper.SlicingMode.Y]: [0, 1, 0], [vtkImageMapper.SlicingMode.Z]: [0, 0, 0], [vtkImageMapper.SlicingMode.I]: [1, 0, 0], [vtkImageMapper.SlicingMode.J]: [0, 1, 0], [vtkImageMapper.SlicingMode.K]: [0, 0, 1], };
const scene = {};
scene.fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ rootContainer: document.body, background: [0.1, 0.1, 0.1], });
scene.renderer = scene.fullScreenRenderer.getRenderer(); scene.renderWindow = scene.fullScreenRenderer.getRenderWindow(); scene.openGLRenderWindow = scene.fullScreenRenderer.getApiSpecificRenderWindow(); scene.camera = scene.renderer.getActiveCamera();
scene.camera.setParallelProjection(true); scene.iStyle = vtkInteractorStyleImage.newInstance(); scene.iStyle.setInteractionMode('IMAGE_SLICING'); scene.renderWindow.getInteractor().setInteractorStyle(scene.iStyle); scene.fullScreenRenderer.addController(controlPanel);
function setCamera(sliceMode, renderer, data) { const ijk = [0, 0, 0]; const position = [0, 0, 0]; const focalPoint = [0, 0, 0]; const viewUp = sliceMode === 1 ? [0, 0, 1] : [0, 1, 0]; data.indexToWorld(ijk, focalPoint); ijk[sliceMode] = 1; data.indexToWorld(ijk, position); renderer.getActiveCamera().set({ focalPoint, position, viewUp }); renderer.resetCamera(); }
scene.widgetManager = vtkWidgetManager.newInstance(); scene.widgetManager.setRenderer(scene.renderer);
const widgets = {}; widgets.rectangleWidget = vtkRectangleWidget.newInstance({ resetAfterPointPlacement: true, }); widgets.ellipseWidget = vtkEllipseWidget.newInstance({ modifierBehavior: { None: { [BehaviorCategory.PLACEMENT]: ShapeBehavior[BehaviorCategory.PLACEMENT].CLICK_AND_DRAG, [BehaviorCategory.POINTS]: ShapeBehavior[BehaviorCategory.POINTS].CORNER_TO_CORNER, [BehaviorCategory.RATIO]: ShapeBehavior[BehaviorCategory.RATIO].FREE, }, }, }); widgets.circleWidget = vtkEllipseWidget.newInstance({ modifierBehavior: { None: { [BehaviorCategory.PLACEMENT]: ShapeBehavior[BehaviorCategory.PLACEMENT].CLICK_AND_DRAG, [BehaviorCategory.POINTS]: ShapeBehavior[BehaviorCategory.POINTS].RADIUS, [BehaviorCategory.RATIO]: ShapeBehavior[BehaviorCategory.RATIO].FREE, }, }, });
widgets.circleWidget.getWidgetState().getPoint1Handle().setScale1(20); widgets.circleWidget .getWidgetState() .setTextPosition([ TextPosition.MAX, TextPosition.CENTER, TextPosition.CENTER, ]);
scene.rectangleHandle = scene.widgetManager.addWidget( widgets.rectangleWidget, ViewTypes.SLICE ); scene.rectangleHandle.setHandleVisibility(false); widgets.rectangleWidget .getWidgetState() .setTextPosition([ TextPosition.CENTER, TextPosition.CENTER, TextPosition.CENTER, ]);
scene.ellipseHandle = scene.widgetManager.addWidget( widgets.ellipseWidget, ViewTypes.SLICE );
scene.circleHandle = scene.widgetManager.addWidget( widgets.circleWidget, ViewTypes.SLICE ); scene.circleHandle.setGlyphResolution(64);
scene.widgetManager.grabFocus(widgets.ellipseWidget); let activeWidget = 'ellipseWidget';
function ready(scope, picking = false) { scope.renderer.resetCamera(); scope.fullScreenRenderer.resize(); if (picking) { scope.widgetManager.enablePicking(); } else { scope.widgetManager.disablePicking(); } }
function readyAll() { ready(scene, true); }
function updateControlPanel(im, ds) { const slicingMode = im.getSlicingMode(); const extent = ds.getExtent(); document.querySelector('.slice').setAttribute('min', extent[slicingMode * 2]); document .querySelector('.slice') .setAttribute('max', extent[slicingMode * 2 + 1]); }
function updateWidgetVisibility(widget, slicePos, i, widgetIndex) { const widgetVisibility = !scene.widgetManager.getWidgets()[widgetIndex].getPoint1() || widget.getWidgetState().getPoint1Handle().getOrigin()[i] === slicePos[i]; return widget.setVisibility(widgetVisibility); }
function updateWidgetsVisibility(slicePos, slicingMode) { updateWidgetVisibility(widgets.rectangleWidget, slicePos, slicingMode, 0); updateWidgetVisibility(widgets.ellipseWidget, slicePos, slicingMode, 1); updateWidgetVisibility(widgets.circleWidget, slicePos, slicingMode, 2); }
const image = { imageMapper: vtkImageMapper.newInstance(), actor: vtkImageSlice.newInstance(), };
image.actor.setMapper(image.imageMapper);
const reader = vtkHttpDataSetReader.newInstance({ fetchGzip: true }); reader .setUrl(`${__BASE_PATH__}/data/volume/LIDC2.vti`, { loadData: true }) .then(() => { const data = reader.getOutputData(); image.data = data;
image.imageMapper.setInputData(data);
scene.renderer.addViewProp(image.actor); const sliceMode = vtkImageMapper.SlicingMode.K; image.imageMapper.setSlicingMode(sliceMode); image.imageMapper.setSlice(0);
setCamera(sliceMode, scene.renderer, image.data);
updateControlPanel(image.imageMapper, data);
scene.rectangleHandle.getRepresentations()[1].setDrawBorder(true); scene.rectangleHandle.getRepresentations()[1].setDrawFace(false); scene.rectangleHandle.getRepresentations()[1].setOpacity(1); scene.circleHandle.getRepresentations()[1].setDrawBorder(true); scene.circleHandle.getRepresentations()[1].setDrawFace(false); scene.circleHandle.getRepresentations()[1].setOpacity(1); scene.ellipseHandle.getRepresentations()[1].setDrawBorder(true); scene.ellipseHandle.getRepresentations()[1].setDrawFace(false); scene.ellipseHandle.getRepresentations()[1].setOpacity(1);
const isPointInEllipse = vtkImplicitBoolean.newInstance(); isPointInEllipse.setOperation(Operation.INTERSECTION); const positivePlane = vtkPlane.newInstance(); isPointInEllipse.addFunction(positivePlane); const negativePlane = vtkPlane.newInstance(); isPointInEllipse.addFunction(negativePlane); const currentEllipse = vtkSphere.newInstance(); const transform = vtkTransform.newInstance(); currentEllipse.setTransform(transform); isPointInEllipse.addFunction(currentEllipse);
scene.ellipseHandle.onInteractionEvent(() => { const worldBounds = scene.ellipseHandle.getBounds(); const planeNormal = slicingModeNormal[image.imageMapper.getSlicingMode()]; const planeOrigin = [0, 0, 0]; planeOrigin[image.imageMapper.getSlicingMode() % 3] = image.imageMapper.getSlice(); image.data.indexToWorld(planeOrigin, planeOrigin); positivePlane.setOrigin(planeOrigin); negativePlane.setOrigin(planeOrigin); positivePlane.setNormal(planeNormal); negativePlane.setNormal(vtkMath.multiplyScalar([...planeNormal], -1)); const rotationMatrix = vtkMatrixBuilder .buildFromRadian() .translate(...planeOrigin) .rotateFromDirections(planeNormal, [0, 0, 1]) .translate(...vtkMath.multiplyScalar([...planeOrigin], -1)); const corner1 = widgets.ellipseWidget .getWidgetState() .getPoint1Handle() .getOrigin(); const corner2 = widgets.ellipseWidget .getWidgetState() .getPoint2Handle() .getOrigin(); rotationMatrix.apply(corner1); rotationMatrix.apply(corner2); transform.setMatrix(rotationMatrix.getMatrix()); currentEllipse.setRadius([ Math.max(Math.abs(corner2[0] - corner1[0]) / 2, 1 + VTK_SMALL_NUMBER), Math.max(Math.abs(corner2[1] - corner1[1]) / 2, 1), 10, ]); currentEllipse.setCenter([ (corner1[0] + corner2[0]) / 2, (corner1[1] + corner2[1]) / 2, (corner1[2] + corner2[2]) / 2, ]);
const w = []; const { average, minimum, maximum } = image.data.computeHistogram( worldBounds, (coord, _) => isPointInEllipse.functionValue(image.data.indexToWorld(coord, w)) <= 0 );
const text = `average: ${average.toFixed( 0 )} \nmin: ${minimum} \nmax: ${maximum} `;
widgets.ellipseWidget.getWidgetState().getText().setText(text); });
scene.circleHandle.onInteractionEvent(() => { const worldBounds = scene.circleHandle.getBounds();
const text = `radius: ${( vec3.distance( [worldBounds[0], worldBounds[2], worldBounds[4]], [worldBounds[1], worldBounds[3], worldBounds[5]] ) / 2 ).toFixed(2)}`; widgets.circleWidget.getWidgetState().getText().setText(text); });
scene.rectangleHandle.onInteractionEvent(() => { const worldBounds = scene.rectangleHandle.getBounds();
const dx = Math.abs(worldBounds[0] - worldBounds[1]); const dy = Math.abs(worldBounds[2] - worldBounds[3]); const dz = Math.abs(worldBounds[4] - worldBounds[5]);
const perimeter = 2 * (dx + dy + dz); const area = dx * dy + dy * dz + dz * dx;
const text = `perimeter: ${perimeter.toFixed(1)}mm\narea: ${area.toFixed( 1 )}mm²`; widgets.rectangleWidget.getWidgetState().getText().setText(text); });
const update = () => { const slicingMode = image.imageMapper.getSlicingMode() % 3;
if (slicingMode > -1) { const ijk = [0, 0, 0]; const slicePos = [0, 0, 0];
ijk[slicingMode] = image.imageMapper.getSlice(); data.indexToWorld(ijk, slicePos);
widgets.rectangleWidget.getManipulator().setUserOrigin(slicePos); widgets.ellipseWidget.getManipulator().setUserOrigin(slicePos); widgets.circleWidget.getManipulator().setUserOrigin(slicePos);
updateWidgetsVisibility(slicePos, slicingMode);
scene.renderWindow.render();
document .querySelector('.slice') .setAttribute('max', data.getDimensions()[slicingMode] - 1); } }; image.imageMapper.onModified(update); update();
readyAll(); });
window.addEventListener('resize', readyAll); readyAll();
function resetWidgets() { scene.rectangleHandle.reset(); scene.ellipseHandle.reset(); scene.circleHandle.reset(); const slicingMode = image.imageMapper.getSlicingMode() % 3; updateWidgetsVisibility(null, slicingMode); scene.widgetManager.grabFocus(widgets[activeWidget]); }
document.querySelector('.slice').addEventListener('input', (ev) => { image.imageMapper.setSlice(Number(ev.target.value)); });
document.querySelector('.axis').addEventListener('input', (ev) => { const sliceMode = 'IJKXYZ'.indexOf(ev.target.value) % 3; image.imageMapper.setSlicingMode(sliceMode);
setCamera(sliceMode, scene.renderer, image.data); resetWidgets(); scene.renderWindow.render(); });
document.querySelector('.widget').addEventListener('input', (ev) => { if (activeWidget === 'ellipseWidget') { widgets.ellipseWidget.setHandleVisibility(false); } scene.widgetManager.grabFocus(widgets[ev.target.value]); activeWidget = ev.target.value; if (activeWidget === 'ellipseWidget') { widgets.ellipseWidget.setHandleVisibility(true); scene.ellipseHandle.updateRepresentationForRender(); } });
document.querySelector('.place').addEventListener('click', () => { if (activeWidget !== 'rectangleWidget') { const widget = widgets[activeWidget]; const widgetIndex = activeWidget === 'ellipseWidget' ? 1 : 2; const handle = activeWidget === 'ellipseWidget' ? scene.ellipseHandle : scene.circleHandle; const coord1 = [0, 0, 0]; const coord2 = [100, 100, 100]; const slicePos = image.imageMapper.getSlice(); const axis = image.imageMapper.getSlicingMode() % 3; coord1[axis] = slicePos; coord2[axis] = slicePos; handle.grabFocus(); handle.placePoint1(coord1); handle.placePoint2(coord2); handle.setCorners(coord1, coord2); handle.invokeInteractionEvent(); handle.loseFocus(); updateWidgetVisibility(widget, coord1, axis, widgetIndex); scene.renderWindow.render(); } });
document.querySelector('.reset').addEventListener('click', () => { resetWidgets(); scene.renderWindow.render(); });
function setupSVG(widget, options) { bindSVGRepresentation(scene.renderer, widget.getWidgetState(), { mapState(widgetState, { size }) { const textState = widgetState.getText(); const text = textState.getText(); const origin = textState.getOrigin(); if (origin && textState.getVisible()) { const coords = computeWorldToDisplay(scene.renderer, ...origin); const position = [coords[0], size[1] - coords[1]]; return { text, position, }; } return null; }, render(data, h) { if (data) { const lines = data.text.split('\n'); const fontSize = 32; const dys = multiLineTextCalculator( lines.length, fontSize, VerticalTextAlignment.MIDDLE ); return lines.map((line, index) => h( 'text', { key: index, attrs: { x: data.position[0], y: data.position[1], dx: 12, dy: dys[index], fill: 'white', 'font-size': fontSize, ...options?.textProps, }, }, line ) ); } return []; }, }); }
setupSVG(widgets.rectangleWidget, { textProps: { 'text-anchor': 'middle', }, }); setupSVG(widgets.ellipseWidget, { textProps: { 'text-anchor': 'middle', }, }); setupSVG(widgets.circleWidget);
global.scene = scene; global.widgets = widgets;
|