import '@kitware/vtk.js/favicon';
import '@kitware/vtk.js/Rendering/Profiles/All';
import '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper';
import { ProjectionMode } from '@kitware/vtk.js/Rendering/Core/ImageCPRMapper/Constants'; import { radiansFromDegrees } from '@kitware/vtk.js/Common/Core/Math'; import { updateState } from '@kitware/vtk.js/Widgets/Widgets3D/ResliceCursorWidget/helpers'; import { vec3, mat3, mat4 } from 'gl-matrix'; import { ViewTypes } from '@kitware/vtk.js/Widgets/Core/WidgetManager/Constants'; import vtkCPRManipulator from '@kitware/vtk.js/Widgets/Manipulators/CPRManipulator'; import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray'; import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; import vtkHttpDataSetReader from '@kitware/vtk.js/IO/Core/HttpDataSetReader'; import vtkImageCPRMapper from '@kitware/vtk.js/Rendering/Core/ImageCPRMapper'; import vtkImageMapper from '@kitware/vtk.js/Rendering/Core/ImageMapper'; import vtkImageReslice from '@kitware/vtk.js/Imaging/Core/ImageReslice'; import vtkImageSlice from '@kitware/vtk.js/Rendering/Core/ImageSlice'; import vtkInteractorStyleImage from '@kitware/vtk.js/Interaction/Style/InteractorStyleImage'; import vtkPlaneManipulator from '@kitware/vtk.js/Widgets/Manipulators/PlaneManipulator'; import vtkPolyData from '@kitware/vtk.js/Common/DataModel/PolyData'; import vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer'; import vtkResliceCursorWidget from '@kitware/vtk.js/Widgets/Widgets3D/ResliceCursorWidget'; import vtkWidgetManager from '@kitware/vtk.js/Widgets/Core/WidgetManager'; import widgetBehavior from '@kitware/vtk.js/Widgets/Widgets3D/ResliceCursorWidget/cprBehavior';
import controlPanel from './controller.html'; import aortaJSON from './aorta_centerline.json'; import spineJSON from './spine_centerline.json';
const volumePath = `${__BASE_PATH__}/data/volume/LIDC2.vti`; const centerlineJsons = { Aorta: aortaJSON, Spine: spineJSON }; const centerlineKeys = Object.keys(centerlineJsons);
const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance(); const stretchRenderer = fullScreenRenderer.getRenderer(); const renderWindow = fullScreenRenderer.getRenderWindow();
fullScreenRenderer.addController(controlPanel); const angleEl = document.getElementById('angle'); const animateEl = document.getElementById('animate'); const centerlineEl = document.getElementById('centerline'); const modeEl = document.getElementById('mode'); const projectionModeEl = document.getElementById('projectionMode'); const projectionThicknessEl = document.getElementById('projectionThickness'); const projectionSamplesEl = document.getElementById('projectionSamples');
const interactor = renderWindow.getInteractor(); interactor.setInteractorStyle(vtkInteractorStyleImage.newInstance()); interactor.setDesiredUpdateRate(15.0);
const stretchPlane = 'Y'; const crossPlane = 'Z'; const widget = vtkResliceCursorWidget.newInstance({ planes: [stretchPlane, crossPlane], behavior: widgetBehavior, }); const widgetManager = vtkWidgetManager.newInstance(); widgetManager.setRenderer(stretchRenderer); const stretchViewType = ViewTypes.XZ_PLANE; const crossViewType = ViewTypes.XY_PLANE; const stretchViewWidgetInstance = widgetManager.addWidget( widget, stretchViewType ); const widgetState = widget.getWidgetState();
widgetState .getStatesWithLabel('sphere') .forEach((handle) => handle.setScale1(20)); widgetState.getCenterHandle().setVisible(false); widgetState .getStatesWithLabel(`rotationIn${stretchPlane}`) .forEach((handle) => handle.setVisible(false));
const crossRenderer = vtkRenderer.newInstance(); crossRenderer.setViewport(0.7, 0, 1, 0.3); renderWindow.addRenderer(crossRenderer); renderWindow.setNumberOfLayers(2); crossRenderer.setLayer(1); const crossWidgetManager = vtkWidgetManager.newInstance(); crossWidgetManager.setRenderer(crossRenderer); const crossViewWidgetInstance = crossWidgetManager.addWidget( widget, crossViewType );
const reslice = vtkImageReslice.newInstance(); reslice.setTransformInputSampling(false); reslice.setAutoCropOutput(true); reslice.setOutputDimensionality(2); const resliceMapper = vtkImageMapper.newInstance(); resliceMapper.setBackgroundColor(0, 0, 0, 0); resliceMapper.setInputConnection(reslice.getOutputPort()); const resliceActor = vtkImageSlice.newInstance(); resliceActor.setMapper(resliceMapper);
const reader = vtkHttpDataSetReader.newInstance({ fetchGzip: true });
const centerline = vtkPolyData.newInstance();
const actor = vtkImageSlice.newInstance(); const mapper = vtkImageCPRMapper.newInstance(); mapper.setBackgroundColor(0, 0, 0, 0); actor.setMapper(mapper);
mapper.setInputConnection(reader.getOutputPort(), 0); mapper.setInputData(centerline, 1); mapper.setWidth(400);
const cprManipulator = vtkCPRManipulator.newInstance({ cprActor: actor, }); const planeManipulator = vtkPlaneManipulator.newInstance();
function updateDistanceAndDirection() { const widgetPlanes = widgetState.getPlanes(); const worldBitangent = widgetPlanes[stretchViewType].normal; const worldNormal = widgetPlanes[stretchViewType].viewUp; widgetPlanes[crossViewType].normal = worldNormal; widgetPlanes[crossViewType].viewUp = worldBitangent; const worldTangent = vec3.cross([], worldBitangent, worldNormal); vec3.normalize(worldTangent, worldTangent); const worldWidgetCenter = widgetState.getCenter(); const distance = cprManipulator.getCurrentDistance();
const { orientation } = mapper.getCenterlinePositionAndOrientation(distance); const modelDirections = mat3.fromQuat([], orientation); const inverseModelDirections = mat3.invert([], modelDirections); const worldDirections = mat3.fromValues( ...worldTangent, ...worldBitangent, ...worldNormal ); const baseDirections = mat3.mul([], inverseModelDirections, worldDirections); mapper.setDirectionMatrix(baseDirections);
widget.updateReslicePlane(reslice, crossViewType); resliceActor.setUserMatrix(reslice.getResliceAxes()); widget.updateCameraPoints(crossRenderer, crossViewType, false, false); const crossCamera = crossRenderer.getActiveCamera(); crossCamera.setViewUp( modelDirections[3], modelDirections[4], modelDirections[5] );
planeManipulator.setUserOrigin(worldWidgetCenter); planeManipulator.setUserNormal(worldNormal);
const signedRadAngle = Math.atan2(baseDirections[1], baseDirections[0]); const signedDegAngle = (signedRadAngle * 180) / Math.PI; const degAngle = signedDegAngle > 0 ? signedDegAngle : 360 + signedDegAngle; angleEl.value = degAngle; updateState( widgetState, widget.getScaleInPixels(), widget.getRotationHandlePosition() );
const width = mapper.getWidth(); const height = mapper.getHeight();
const worldActorTranslation = vec3.scaleAndAdd( [], worldWidgetCenter, worldTangent, -0.5 * width ); vec3.scaleAndAdd( worldActorTranslation, worldActorTranslation, worldNormal, distance - height ); const worldActorTransform = mat4.fromValues( ...worldTangent, 0, ...worldNormal, 0, ...vec3.scale([], worldBitangent, -1), 0, ...worldActorTranslation, 1 ); actor.setUserMatrix(worldActorTransform);
const stretchCamera = stretchRenderer.getActiveCamera(); const cameraDistance = (0.5 * height) / Math.tan(radiansFromDegrees(0.5 * stretchCamera.getViewAngle())); stretchCamera.setParallelScale(0.5 * height); stretchCamera.setParallelProjection(true); const cameraFocalPoint = vec3.scaleAndAdd( [], worldWidgetCenter, worldNormal, distance - 0.5 * height ); const cameraPosition = vec3.scaleAndAdd( [], cameraFocalPoint, worldBitangent, -cameraDistance ); stretchCamera.setPosition(...cameraPosition); stretchCamera.setFocalPoint(...cameraFocalPoint); stretchCamera.setViewUp(...worldNormal); stretchRenderer.resetCameraClippingRange(); interactor.render();
renderWindow.render(); }
let currentCenterlineKey = centerlineKeys[0]; let currentImage = null; function setCenterlineKey(centerlineKey) { currentCenterlineKey = centerlineKey; const centerlineJson = centerlineJsons[centerlineKey]; if (!currentImage) { return; } const centerlinePoints = Float32Array.from(centerlineJson.position); const nPoints = centerlinePoints.length / 3; centerline.getPoints().setData(centerlinePoints, 3);
const centerlineLines = new Uint16Array(1 + nPoints); centerlineLines[0] = nPoints; for (let i = 0; i < nPoints; ++i) { centerlineLines[i + 1] = i; } centerline.getLines().setData(centerlineLines);
centerline.getPointData().setTensors( vtkDataArray.newInstance({ name: 'Orientation', numberOfComponents: 16, values: Float32Array.from(centerlineJson.orientation), }) ); centerline.modified();
const midPointDistance = mapper.getHeight() / 2; const { worldCoords } = cprManipulator.distanceEvent(midPointDistance); widgetState.setCenter(worldCoords); updateDistanceAndDirection();
widgetState[`getAxis${crossPlane}in${stretchPlane}`]().setManipulator( cprManipulator ); widgetState[`getAxis${stretchPlane}in${crossPlane}`]().setManipulator( planeManipulator ); widget.setManipulator(cprManipulator);
renderWindow.render(); }
for (let i = 0; i < centerlineKeys.length; ++i) { const name = centerlineKeys[i]; const optionEl = document.createElement('option'); optionEl.innerText = name; optionEl.value = name; centerlineEl.appendChild(optionEl); } centerlineEl.addEventListener('input', () => setCenterlineKey(centerlineEl.value) );
reader.setUrl(volumePath).then(() => { reader.loadData().then(() => { const image = reader.getOutputData(); widget.setImage(image); const imageDimensions = image.getDimensions(); const imageSpacing = image.getSpacing(); const diagonal = vec3.mul([], imageDimensions, imageSpacing); mapper.setWidth(2 * vec3.len(diagonal));
actor.setUserMatrix(widget.getResliceAxes(stretchViewType)); stretchRenderer.addVolume(actor); widget.updateCameraPoints(stretchRenderer, stretchViewType, true, true);
reslice.setInputData(image); crossRenderer.addActor(resliceActor); widget.updateReslicePlane(reslice, crossViewType); resliceActor.setUserMatrix(reslice.getResliceAxes()); widget.updateCameraPoints(crossRenderer, crossViewType, true, true);
currentImage = image; setCenterlineKey(currentCenterlineKey);
global.imageData = image; }); });
function setAngleFromSlider(radAngle) { const origin = [0, 0, 0]; const normalDir = [0, 0, 1]; const bitangentDir = [0, 1, 0]; vec3.rotateZ(bitangentDir, bitangentDir, origin, radAngle);
const distance = cprManipulator.getCurrentDistance(); const { orientation } = mapper.getCenterlinePositionAndOrientation(distance); const modelDirections = mat3.fromQuat([], orientation);
const worldBitangent = vec3.transformMat3([], bitangentDir, modelDirections); const worldNormal = vec3.transformMat3([], normalDir, modelDirections); const widgetPlanes = widgetState.getPlanes(); widgetPlanes[stretchViewType].normal = worldBitangent; widgetPlanes[stretchViewType].viewUp = worldNormal; widgetPlanes[crossViewType].normal = worldNormal; widgetPlanes[crossViewType].viewUp = worldBitangent; widgetState.setPlanes(widgetPlanes);
updateDistanceAndDirection(); }
angleEl.addEventListener('input', () => setAngleFromSlider(radiansFromDegrees(Number.parseFloat(angleEl.value, 10))) );
let animationId; animateEl.addEventListener('change', () => { if (animateEl.checked) { animationId = setInterval(() => { const currentAngle = radiansFromDegrees( Number.parseFloat(angleEl.value, 10) ); setAngleFromSlider(currentAngle + 0.1); }, 60); } else { clearInterval(animationId); } });
function useStraightenedMode() { mapper.useStraightenedMode(); updateDistanceAndDirection(); }
function useStretchedMode() { mapper.useStretchedMode(); updateDistanceAndDirection(); }
let cprMode; function setUseStretched(value) { cprMode = value; switch (cprMode) { case 'stretched': useStretchedMode(); break; default: useStraightenedMode(); break; } }
const stretchEl = document.createElement('option'); stretchEl.innerText = 'Stretched Mode'; stretchEl.value = 'stretched'; modeEl.appendChild(stretchEl); const straightEl = document.createElement('option'); straightEl.innerText = 'Straightened Mode'; straightEl.value = 'straightened'; modeEl.appendChild(straightEl); modeEl.addEventListener('input', () => setUseStretched(modeEl.value)); modeEl.value = 'straightened';
Object.keys(ProjectionMode).forEach((projectionMode) => { const optionEl = document.createElement('option'); optionEl.innerText = projectionMode.charAt(0) + projectionMode.substring(1).toLowerCase(); optionEl.value = projectionMode; projectionModeEl.appendChild(optionEl); });
projectionModeEl.addEventListener('input', (ev) => { mapper.setProjectionMode(ProjectionMode[projectionModeEl.value]); renderWindow.render(); });
projectionThicknessEl.addEventListener('input', (ev) => { const thicknessRatio = Number.parseFloat(projectionThicknessEl.value, 10); const image = mapper.getInputData(); if (image) { const spacing = image.getSpacing(); const dimensions = image.getDimensions(); const diagonal = vec3.len(vec3.mul([], spacing, dimensions)); const thickness = diagonal * thicknessRatio; mapper.setProjectionSlabThickness(thickness); } renderWindow.render(); }); mapper.setProjectionSlabThickness(0.1); projectionThicknessEl.value = mapper.getProjectionSlabThickness();
projectionSamplesEl.addEventListener('input', (ev) => { const samples = Number.parseInt(projectionSamplesEl.value, 10); mapper.setProjectionSlabNumberOfSamples(samples); renderWindow.render(); }); projectionSamplesEl.value = mapper.getProjectionSlabNumberOfSamples();
stretchViewWidgetInstance.onInteractionEvent(updateDistanceAndDirection); crossViewWidgetInstance.onInteractionEvent(updateDistanceAndDirection);
global.source = reader; global.mapper = mapper; global.actor = actor; global.renderer = stretchRenderer; global.renderWindow = renderWindow; global.centerline = centerline; global.centerlines = centerlineJsons;
|