From 4802fd252fc5fad68635c6fb80b655cdfa3893ff Mon Sep 17 00:00:00 2001 From: Erik Ziegler Date: Thu, 17 Oct 2019 20:42:43 +0200 Subject: [PATCH 1/2] fix: Remove broken scaling for volume rendering --- examples/App.js | 9 + examples/VTKFusionExample.js | 5 - examples/VTKVolumeRenderingExample.js | 363 ++++++++++++++++++++++++++ 3 files changed, 372 insertions(+), 5 deletions(-) create mode 100644 examples/VTKVolumeRenderingExample.js diff --git a/examples/App.js b/examples/App.js index 22515abe..c76011ea 100644 --- a/examples/App.js +++ b/examples/App.js @@ -8,6 +8,7 @@ import VTKCornerstonePaintingSyncExample from './VTKCornerstonePaintingSyncExamp import VTKLoadImageDataExample from './VTKLoadImageDataExample.js'; import VTKCrosshairsExample from './VTKCrosshairsExample.js'; import VTKMPRRotateExample from './VTKMPRRotateExample.js'; +import VTKVolumeRenderingExample from './VTKVolumeRenderingExample.js'; function LinkOut({ href, text }) { return ( @@ -47,6 +48,12 @@ function Index() { text: 'Demonstrates how to display two volumes simultaneously with different transfer functions for PET/CT Fusion.', }, + { + title: 'Volume Rendering', + url: '/volume-rendering', + text: + 'Demonstrates how to perform volume rendering for a CT volume.', + }, { title: 'Image Segmentation via Paint Widget', url: '/painting', @@ -137,6 +144,7 @@ function AppRouter() { Example({ children: }); const crosshairs = () => Example({ children: }); const rotateMPR = () => Example({ children: }); + const volumeRendering = () => Example({ children: }); return ( @@ -148,6 +156,7 @@ function AppRouter() { + diff --git a/examples/VTKFusionExample.js b/examples/VTKFusionExample.js index 79752201..b4ef960c 100644 --- a/examples/VTKFusionExample.js +++ b/examples/VTKFusionExample.js @@ -133,11 +133,6 @@ function applyPreset(actor, preset) { const { shiftRange } = getShiftRange(colorTransferArray); let min = shiftRange[0]; const width = shiftRange[1] - shiftRange[0]; - - // TODO: Something about the rescaling is still very wrong - const shift = shiftRange[0]; - min += shift; - const cfun = vtkColorTransferFunction.newInstance(); const normColorTransferValuePoints = []; for (let i = 0; i < colorTransferArray.length; i += 4) { diff --git a/examples/VTKVolumeRenderingExample.js b/examples/VTKVolumeRenderingExample.js new file mode 100644 index 00000000..fbba4a4e --- /dev/null +++ b/examples/VTKVolumeRenderingExample.js @@ -0,0 +1,363 @@ +import React from 'react'; +import { Component } from 'react'; +import { getImageData, loadImageData, View2D, View3D } from '@vtk-viewport'; +import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume'; +import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper'; +import { api } from 'dicomweb-client'; +import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader'; +import './initCornerstone.js'; +import vtkColorTransferFunction from 'vtk.js/Sources/Rendering/Core/ColorTransferFunction'; +import vtkPiecewiseFunction from 'vtk.js/Sources/Common/DataModel/PiecewiseFunction'; +import vtkColorMaps from 'vtk.js/Sources/Rendering/Core/ColorTransferFunction/ColorMaps'; +import presets from './presets.js'; + +window.cornerstoneWADOImageLoader = cornerstoneWADOImageLoader; + +const url = 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs'; +const studyInstanceUID = + '1.3.6.1.4.1.14519.5.2.1.2744.7002.373729467545468642229382466905'; +const ctSeriesInstanceUID = + '1.3.6.1.4.1.14519.5.2.1.2744.7002.182837959725425690842769990419'; +const searchInstanceOptions = { + studyInstanceUID, +}; + +function createActorMapper(imageData) { + const mapper = vtkVolumeMapper.newInstance(); + mapper.setInputData(imageData); + + const actor = vtkVolume.newInstance(); + actor.setMapper(mapper); + + return { + actor, + mapper, + }; +} + +function getShiftRange(colorTransferArray) { + // Credit to paraview-glance + // https://github.com/Kitware/paraview-glance/blob/3fec8eeff31e9c19ad5b6bff8e7159bd745e2ba9/src/components/controls/ColorBy/script.js#L133 + + // shift range is original rgb/opacity range centered around 0 + let min = Infinity; + let max = -Infinity; + for (let i = 0; i < colorTransferArray.length; i += 4) { + min = Math.min(min, colorTransferArray[i]); + max = Math.max(max, colorTransferArray[i]); + } + + const center = (max - min) / 2; + + return { + shiftRange: [-center, center], + min, + max, + }; +} + +function applyPointsToPiecewiseFunction(points, range, pwf) { + const width = range[1] - range[0]; + const rescaled = points.map(([x, y]) => [x * width + range[0], y]); + + pwf.removeAllPoints(); + rescaled.forEach(([x, y]) => pwf.addPoint(x, y)); + + return rescaled; +} + +function applyPointsToRGBFunction(points, range, cfun) { + const width = range[1] - range[0]; + const rescaled = points.map(([x, r, g, b]) => [ + x * width + range[0], + r, + g, + b, + ]); + + cfun.removeAllPoints(); + rescaled.forEach(([x, r, g, b]) => cfun.addRGBPoint(x, r, g, b)); + + return rescaled; +} + +function applyPreset(actor, preset) { + // Create color transfer function + const colorTransferArray = preset.colorTransfer + .split(' ') + .splice(1) + .map(parseFloat); + + const { shiftRange } = getShiftRange(colorTransferArray); + let min = shiftRange[0]; + const width = shiftRange[1] - shiftRange[0]; + const cfun = vtkColorTransferFunction.newInstance(); + const normColorTransferValuePoints = []; + for (let i = 0; i < colorTransferArray.length; i += 4) { + let value = colorTransferArray[i]; + const r = colorTransferArray[i + 1]; + const g = colorTransferArray[i + 2]; + const b = colorTransferArray[i + 3]; + + value = (value - min) / width; + normColorTransferValuePoints.push([value, r, g, b]); + } + + applyPointsToRGBFunction(normColorTransferValuePoints, shiftRange, cfun); + + actor.getProperty().setRGBTransferFunction(0, cfun); + + // Create scalar opacity function + const scalarOpacityArray = preset.scalarOpacity + .split(' ') + .splice(1) + .map(parseFloat); + + const ofun = vtkPiecewiseFunction.newInstance(); + const normPoints = []; + for (let i = 0; i < scalarOpacityArray.length; i += 2) { + let value = scalarOpacityArray[i]; + const opacity = scalarOpacityArray[i + 1]; + + value = (value - min) / width; + + normPoints.push([value, opacity]); + } + + applyPointsToPiecewiseFunction(normPoints, shiftRange, ofun); + + actor.getProperty().setScalarOpacity(0, ofun); + + const [ + gradientMinValue, + gradientMinOpacity, + gradientMaxValue, + gradientMaxOpacity, + ] = preset.gradientOpacity + .split(' ') + .splice(1) + .map(parseFloat); + + actor.getProperty().setUseGradientOpacity(0, true); + actor.getProperty().setGradientOpacityMinimumValue(0, gradientMinValue); + actor.getProperty().setGradientOpacityMinimumOpacity(0, gradientMinOpacity); + actor.getProperty().setGradientOpacityMaximumValue(0, gradientMaxValue); + actor.getProperty().setGradientOpacityMaximumOpacity(0, gradientMaxOpacity); + + if (preset.interpolation === '1') { + actor.getProperty().setInterpolationTypeToFastLinear(); + //actor.getProperty().setInterpolationTypeToLinear() + } + + const ambient = parseFloat(preset.ambient); + //const shade = preset.shade === '1' + const diffuse = parseFloat(preset.diffuse); + const specular = parseFloat(preset.specular); + const specularPower = parseFloat(preset.specularPower); + + //actor.getProperty().setShade(shade) + actor.getProperty().setAmbient(ambient); + actor.getProperty().setDiffuse(diffuse); + actor.getProperty().setSpecular(specular); + actor.getProperty().setSpecularPower(specularPower); +} + +function createCT3dPipeline(imageData, ctTransferFunctionPresetId) { + const { actor, mapper } = createActorMapper(imageData); + const sampleDistance = + 0.7 * + Math.sqrt( + imageData + .getSpacing() + .map(v => v * v) + .reduce((a, b) => a + b, 0) + ); + + const range = imageData + .getPointData() + .getScalars() + .getRange(); + actor + .getProperty() + .getRGBTransferFunction(0) + .setRange(range[0], range[1]); + + mapper.setSampleDistance(sampleDistance); + + const preset = presets.find( + preset => preset.id === ctTransferFunctionPresetId + ); + + applyPreset(actor, preset); + + actor.getProperty().setScalarOpacityUnitDistance(0, 2.5); + + return actor; +} + +function createStudyImageIds(baseUrl, studySearchOptions) { + const SOP_INSTANCE_UID = '00080018'; + const SERIES_INSTANCE_UID = '0020000E'; + + const client = new api.DICOMwebClient({ url }); + + return new Promise((resolve, reject) => { + client.retrieveStudyMetadata(studySearchOptions).then(instances => { + const imageIds = instances.map(metaData => { + const imageId = + `wadors:` + + baseUrl + + '/studies/' + + studyInstanceUID + + '/series/' + + metaData[SERIES_INSTANCE_UID].Value[0] + + '/instances/' + + metaData[SOP_INSTANCE_UID].Value[0] + + '/frames/1'; + + cornerstoneWADOImageLoader.wadors.metaDataManager.add( + imageId, + metaData + ); + + return imageId; + }); + + resolve(imageIds); + }, reject); + }); +} + +function loadDataset(imageIds, displaySetInstanceUid) { + const imageDataObject = getImageData(imageIds, displaySetInstanceUid); + + loadImageData(imageDataObject); + return imageDataObject; +} + +class VTKFusionExample extends Component { + state = { + volumeRenderingVolumes: null, + ctTransferFunctionPresetId: 'vtkMRMLVolumePropertyNode4', + petColorMapId: 'hsv', + }; + + async componentDidMount() { + const imageIdPromise = createStudyImageIds(url, searchInstanceOptions); + + this.components = {}; + + const imageIds = await imageIdPromise; + let ctImageIds = imageIds.filter(imageId => + imageId.includes(ctSeriesInstanceUID) + ); + ctImageIds = ctImageIds.slice(0, ctImageIds.length / 2) + + const ctImageDataObject = loadDataset(ctImageIds, 'ctDisplaySet'); + const promises = [ + ...ctImageDataObject.insertPixelDataPromises, + ]; + + // TODO -> We could stream this ala 2D but its not done yet, so wait. + + Promise.all(promises).then(() => { + const ctImageData = ctImageDataObject.vtkImageData; + + const ctVolVR = createCT3dPipeline( + ctImageData, + this.state.ctTransferFunctionPresetId + ); + + this.setState({ + volumeRenderingVolumes: [ctVolVR], + }); + }); + } + + saveComponentReference = viewportIndex => { + return component => { + this.components[viewportIndex] = component; + }; + }; + + handleChangeCTTransferFunction = event => { + const ctTransferFunctionPresetId = event.target.value; + const preset = presets.find( + preset => preset.id === ctTransferFunctionPresetId + ); + + const actor = this.state.volumeRenderingVolumes[0]; + + applyPreset(actor, preset); + + this.setState({ + ctTransferFunctionPresetId, + }); + }; + + rerenderAll = () => { + // Update all render windows, since the automatic re-render might not + // happen if the viewport is not currently using the painting widget + Object.keys(this.components).forEach(viewportIndex => { + const renderWindow = this.components[ + viewportIndex + ].genericRenderWindow.getRenderWindow(); + + renderWindow.render(); + }); + }; + + render() { + if (!this.state.volumeRenderingVolumes) { + return

Loading...

; + } + + const ctTransferFunctionPresetOptions = presets.map(preset => { + return ( + + ); + }); + + return ( +
+
+

Image Fusion

+

+ This example demonstrates volume rendering of a CT Volume. +

+

+ Images are retrieved via DICOMWeb from a publicly available server + and constructed into vtkImageData volumes before they + are provided to the component. When each slice arrives, its pixel + data is dumped into the proper location in the volume array. +

+
+
+
+ + +
+
+
+
+ +
+
+ ); + } +} + +export default VTKFusionExample; From bad8e69a9190301d86ea79c6ac5fa0b83313e2ca Mon Sep 17 00:00:00 2001 From: "James A. Petts" Date: Fri, 18 Oct 2019 15:25:01 +0100 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=F0=9F=90=9B=20Fix=20volume=20render?= =?UTF-8?q?ing=20example.=20Thanks=20@swederik?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/VTKFusionExample.js | 2 ++ examples/VTKVolumeRenderingExample.js | 35 +++++++++++++-------------- src/lib/getImageData.js | 1 - 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/examples/VTKFusionExample.js b/examples/VTKFusionExample.js index b4ef960c..9b10430e 100644 --- a/examples/VTKFusionExample.js +++ b/examples/VTKFusionExample.js @@ -385,6 +385,8 @@ class VTKFusionExample extends Component { applyPreset(actor, preset); + this.rerenderAll(); + this.setState({ ctTransferFunctionPresetId, }); diff --git a/examples/VTKVolumeRenderingExample.js b/examples/VTKVolumeRenderingExample.js index fbba4a4e..79d25f9b 100644 --- a/examples/VTKVolumeRenderingExample.js +++ b/examples/VTKVolumeRenderingExample.js @@ -1,6 +1,6 @@ import React from 'react'; import { Component } from 'react'; -import { getImageData, loadImageData, View2D, View3D } from '@vtk-viewport'; +import { getImageData, loadImageData, View3D } from '@vtk-viewport'; import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume'; import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper'; import { api } from 'dicomweb-client'; @@ -164,8 +164,9 @@ function applyPreset(actor, preset) { function createCT3dPipeline(imageData, ctTransferFunctionPresetId) { const { actor, mapper } = createActorMapper(imageData); + const sampleDistance = - 0.7 * + 1.2 * Math.sqrt( imageData .getSpacing() @@ -245,20 +246,20 @@ class VTKFusionExample extends Component { async componentDidMount() { const imageIdPromise = createStudyImageIds(url, searchInstanceOptions); - this.components = {}; + this.apis = []; const imageIds = await imageIdPromise; let ctImageIds = imageIds.filter(imageId => imageId.includes(ctSeriesInstanceUID) ); - ctImageIds = ctImageIds.slice(0, ctImageIds.length / 2) + ctImageIds = ctImageIds.slice(0, ctImageIds.length / 2); const ctImageDataObject = loadDataset(ctImageIds, 'ctDisplaySet'); - const promises = [ - ...ctImageDataObject.insertPixelDataPromises, - ]; + const promises = [...ctImageDataObject.insertPixelDataPromises]; // TODO -> We could stream this ala 2D but its not done yet, so wait. + // TODO -> @Erik ^ We also don't have the metadata ahead of time in + // These examples anyway right now. Promise.all(promises).then(() => { const ctImageData = ctImageDataObject.vtkImageData; @@ -274,10 +275,8 @@ class VTKFusionExample extends Component { }); } - saveComponentReference = viewportIndex => { - return component => { - this.components[viewportIndex] = component; - }; + saveApiReference = api => { + this.apis = [api]; }; handleChangeCTTransferFunction = event => { @@ -290,6 +289,8 @@ class VTKFusionExample extends Component { applyPreset(actor, preset); + this.rerenderAll(); + this.setState({ ctTransferFunctionPresetId, }); @@ -298,8 +299,8 @@ class VTKFusionExample extends Component { rerenderAll = () => { // Update all render windows, since the automatic re-render might not // happen if the viewport is not currently using the painting widget - Object.keys(this.components).forEach(viewportIndex => { - const renderWindow = this.components[ + Object.keys(this.apis).forEach(viewportIndex => { + const renderWindow = this.apis[ viewportIndex ].genericRenderWindow.getRenderWindow(); @@ -323,10 +324,8 @@ class VTKFusionExample extends Component { return (
-

Image Fusion

-

- This example demonstrates volume rendering of a CT Volume. -

+

Volume Rendering

+

This example demonstrates volume rendering of a CT Volume.

Images are retrieved via DICOMWeb from a publicly available server and constructed into vtkImageData volumes before they @@ -352,7 +351,7 @@ class VTKFusionExample extends Component {

diff --git a/src/lib/getImageData.js b/src/lib/getImageData.js index a6a5ed9c..71350de6 100644 --- a/src/lib/getImageData.js +++ b/src/lib/getImageData.js @@ -89,7 +89,6 @@ export default function getImageData(imageIds, displaySetInstanceUid) { const imageDataObject = { imageIds, metaData0, - imageMetaData0, dimensions: [xVoxels, yVoxels, zVoxels], spacing: [xSpacing, ySpacing, zSpacing], origin: zAxis.origin,