Skip to content

Commit e4733ac

Browse files
authored
perf: ⚡️ Use Float32Array and avoid duplicated slice insertion (#53)
* perf: ⚡️ Use Float32Array and avoid duplicated slice insertion Use a Float32Array and fill it using the cornerstone Int16 data to avoid recalculation. Also prevent multiple loadImageData calls on the same imageDataObject from overloading each other.
1 parent a768121 commit e4733ac

File tree

6 files changed

+224
-68
lines changed

6 files changed

+224
-68
lines changed

examples/App.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import VTKBasicExample from './VTKBasicExample.js';
55
import VTKFusionExample from './VTKFusionExample.js';
66
import VTKMPRPaintingExample from './VTKMPRPaintingExample.js';
77
import VTKCornerstonePaintingSyncExample from './VTKCornerstonePaintingSyncExample.js';
8+
import VTKLoadImageDataExample from './VTKLoadImageDataExample.js';
89
import VTKCrosshairsExample from './VTKCrosshairsExample.js';
910
import VTKMPRRotateExample from './VTKMPRRotateExample.js';
1011

@@ -69,6 +70,12 @@ function Index() {
6970
url: '/rotate',
7071
text: 'Demonstrates how to set up the MPR Rotate interactor style',
7172
},
73+
{
74+
title: 'LoadImageData Example',
75+
url: '/cornerstone-load-image-data',
76+
text:
77+
'Generating vtkjs imagedata from cornerstone images and displaying them in a VTK viewport.',
78+
},
7279
];
7380

7481
const exampleComponents = examples.map(e => {
@@ -125,6 +132,7 @@ function AppRouter() {
125132
const basic = () => Example({ children: <VTKBasicExample /> });
126133
const fusion = () => Example({ children: <VTKFusionExample /> });
127134
const painting = () => Example({ children: <VTKMPRPaintingExample /> });
135+
const loadImage = () => Example({ children: <VTKLoadImageDataExample /> });
128136
const synced = () =>
129137
Example({ children: <VTKCornerstonePaintingSyncExample /> });
130138
const crosshairs = () => Example({ children: <VTKCrosshairsExample /> });
@@ -140,6 +148,7 @@ function AppRouter() {
140148
<Route exact path="/cornerstone-sync-painting" render={synced} />
141149
<Route exact path="/crosshairs" render={crosshairs} />
142150
<Route exact path="/rotate" render={rotateMPR} />
151+
<Route exact path="/cornerstone-load-image-data" render={loadImage} />
143152
<Route exact component={Index} />
144153
</Switch>
145154
</Router>

examples/VTKCornerstonePaintingSyncExample.js

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -93,16 +93,6 @@ const imageIds = [
9393
`dicomweb://${ROOT_URL}/PTCTStudy/1.3.6.1.4.1.25403.52237031786.3872.20100510032221.5.dcm`,
9494
];
9595

96-
// Pre-retrieve the images for demo purposes
97-
// Note: In a real application you wouldn't need to do this
98-
// since you would probably have the image metadata ahead of time.
99-
// In this case, we preload the images so the WADO Image Loader can
100-
// read and store all of their metadata and subsequently the 'getImageData'
101-
// can run properly (it requires metadata).
102-
const promises = imageIds.map(imageId => {
103-
return cornerstone.loadAndCacheImage(imageId);
104-
});
105-
10696
class VTKCornerstonePaintingSyncExample extends Component {
10797
state = {
10898
volumes: null,
@@ -116,6 +106,16 @@ class VTKCornerstonePaintingSyncExample extends Component {
116106
this.components = {};
117107
this.cornerstoneElements = {};
118108

109+
// Pre-retrieve the images for demo purposes
110+
// Note: In a real application you wouldn't need to do this
111+
// since you would probably have the image metadata ahead of time.
112+
// In this case, we preload the images so the WADO Image Loader can
113+
// read and store all of their metadata and subsequently the 'getImageData'
114+
// can run properly (it requires metadata).
115+
const promises = imageIds.map(imageId => {
116+
return cornerstone.loadAndCacheImage(imageId);
117+
});
118+
119119
Promise.all(promises).then(
120120
() => {
121121
const displaySetInstanceUid = '12345';
@@ -257,10 +257,11 @@ class VTKCornerstonePaintingSyncExample extends Component {
257257
accessed in 2D.
258258
</p>
259259
<p>
260-
Both components are displaying the same labelmap UInt8Array. For
260+
Both components are displaying the same labelmap UInt16Array. For
261261
VTK, it has been encapsulated in a vtkDataArray and then a
262-
vtkImageData Object. For Cornerstone Tools, it is accessed by
263-
reference and index for each of the 2D slices.
262+
vtkImageData Object. For Cornerstone Tools, the Uint16Array is
263+
accessed through helpers based on the actively displayed image stack
264+
and the index of the currently displayed image
264265
</p>
265266
<p>
266267
<strong>Note:</strong> The PaintWidget (circle on hover) is not
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import React from 'react';
2+
import { Component } from 'react';
3+
4+
import { View2D, getImageData, loadImageData } from '@vtk-viewport';
5+
import cornerstone from 'cornerstone-core';
6+
import cornerstoneTools from 'cornerstone-tools';
7+
import './initCornerstone.js';
8+
import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper';
9+
import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume';
10+
11+
window.cornerstoneTools = cornerstoneTools;
12+
13+
function createActorMapper(imageData) {
14+
const mapper = vtkVolumeMapper.newInstance();
15+
mapper.setInputData(imageData);
16+
17+
const actor = vtkVolume.newInstance();
18+
actor.setMapper(mapper);
19+
20+
return {
21+
actor,
22+
mapper,
23+
};
24+
}
25+
26+
const ROOT_URL =
27+
window.location.hostname === 'localhost'
28+
? window.location.host
29+
: window.location.hostname;
30+
31+
const imageIds = [
32+
`dicomweb://${ROOT_URL}/PTCTStudy/1.3.6.1.4.1.25403.52237031786.3872.20100510032221.1.dcm`,
33+
`dicomweb://${ROOT_URL}/PTCTStudy/1.3.6.1.4.1.25403.52237031786.3872.20100510032221.2.dcm`,
34+
`dicomweb://${ROOT_URL}/PTCTStudy/1.3.6.1.4.1.25403.52237031786.3872.20100510032221.3.dcm`,
35+
`dicomweb://${ROOT_URL}/PTCTStudy/1.3.6.1.4.1.25403.52237031786.3872.20100510032221.4.dcm`,
36+
`dicomweb://${ROOT_URL}/PTCTStudy/1.3.6.1.4.1.25403.52237031786.3872.20100510032221.5.dcm`,
37+
];
38+
39+
class VTKLoadImageDataExample extends Component {
40+
state = {
41+
volumes: null,
42+
vtkImageData: null,
43+
cornerstoneViewportData: null,
44+
focusedWidgetId: null,
45+
isSetup: false,
46+
};
47+
48+
componentDidMount() {
49+
this.components = {};
50+
this.cornerstoneElements = {};
51+
52+
// Pre-retrieve the images for demo purposes
53+
// Note: In a real application you wouldn't need to do this
54+
// since you would probably have the image metadata ahead of time.
55+
// In this case, we preload the images so the WADO Image Loader can
56+
// read and store all of their metadata and subsequently the 'getImageData'
57+
// can run properly (it requires metadata).
58+
const promises = imageIds.map(imageId => {
59+
return cornerstone.loadAndCacheImage(imageId);
60+
});
61+
62+
Promise.all(promises).then(
63+
() => {
64+
const displaySetInstanceUid = '12345';
65+
const cornerstoneViewportData = {
66+
stack: {
67+
imageIds,
68+
currentImageIdIndex: 0,
69+
},
70+
displaySetInstanceUid,
71+
};
72+
73+
const imageDataObject = getImageData(imageIds, displaySetInstanceUid);
74+
75+
loadImageData(imageDataObject).then(() => {
76+
const { actor } = createActorMapper(imageDataObject.vtkImageData);
77+
78+
this.setState({
79+
vtkImageData: imageDataObject.vtkImageData,
80+
volumes: [actor],
81+
cornerstoneViewportData,
82+
});
83+
});
84+
},
85+
error => {
86+
throw new Error(error);
87+
}
88+
);
89+
}
90+
91+
saveCornerstoneElements = viewportIndex => {
92+
return event => {
93+
this.cornerstoneElements[viewportIndex] = event.detail.element;
94+
};
95+
};
96+
97+
setWidget = event => {
98+
const widgetId = event.target.value;
99+
100+
if (widgetId === 'rotate') {
101+
this.setState({
102+
focusedWidgetId: null,
103+
});
104+
}
105+
};
106+
107+
render() {
108+
return (
109+
<div className="row">
110+
<div className="col-xs-12">
111+
<h1>Loading a cornerstone displayset into vtkjs</h1>
112+
<p>
113+
The example demonstrates loading cornerstone images already
114+
available in the application into a vtkjs viewport.
115+
</p>
116+
<hr />
117+
</div>
118+
<div className="col-xs-12">
119+
<div className="col-xs-12">
120+
<label>
121+
<input
122+
type="radio"
123+
value="rotate"
124+
name="widget"
125+
onChange={this.setWidget}
126+
checked={this.state.focusedWidgetId === null}
127+
/>{' '}
128+
Rotate
129+
</label>
130+
</div>
131+
<div className="col-xs-12 col-sm-6">
132+
{this.state.volumes && <View2D volumes={this.state.volumes} />}
133+
</div>
134+
</div>
135+
</div>
136+
);
137+
}
138+
}
139+
140+
export default VTKLoadImageDataExample;

examples/VTKMPRRotateExample.js

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -434,34 +434,6 @@ class VTKMPRRotateExample extends Component {
434434
for (let index = 0; index < volumeData.length; index++) {
435435
columns.push(
436436
<div key={index.toString()} className="col-xs-12 col-sm-6">
437-
{/* <div>
438-
<input
439-
className="rotate"
440-
type="range"
441-
min={min}
442-
max={max}
443-
step="1"
444-
value={this.state.rotation[index].x}
445-
onChange={event => {
446-
this.handleChangeX(index, event);
447-
}}
448-
/>
449-
<span>{this.state.rotation[index].x}</span>
450-
</div>
451-
<div>
452-
<input
453-
className="rotate"
454-
type="range"
455-
min={min}
456-
max={max}
457-
step="1"
458-
value={this.state.rotation[index].y}
459-
onChange={event => {
460-
this.handleChangeY(index, event);
461-
}}
462-
/>
463-
<span>{this.state.rotation[index].y}</span>
464-
</div> */}
465437
<View2D
466438
volumes={this.state.volumes}
467439
onCreated={this.storeApi(index)}

src/lib/getImageData.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export default function getImageData(imageIds, displaySetInstanceUid) {
5555
}
5656

5757
case 16:
58-
pixelArray = new Int16Array(xVoxels * yVoxels * zVoxels);
58+
pixelArray = new Float32Array(xVoxels * yVoxels * zVoxels);
5959

6060
break;
6161
}

src/lib/loadImageData.js

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,72 @@ import cornerstone from 'cornerstone-core';
22
import getSliceIndex from './data/getSliceIndex.js';
33
import insertSlice from './data/insertSlice.js';
44

5-
function loadImageDataProgressively(imageIds, imageData, metaDataMap, zAxis) {
6-
const loadImagePromises = imageIds.map(cornerstone.loadAndCacheImage);
5+
const resolveStack = [];
76

8-
const insertPixelData = image => {
9-
const { imagePositionPatient } = metaDataMap.get(image.imageId);
10-
const sliceIndex = getSliceIndex(zAxis, imagePositionPatient);
7+
// TODO: If we attempt to load multiple imageDataObjects at once this will break.
8+
export default function loadImageDataProgressively(imageDataObject) {
9+
if (imageDataObject.loaded) {
10+
// Returning instantly resolved promise as good to go.
11+
return new Promise(resolve => {
12+
resolve();
13+
});
14+
} else if (imageDataObject.isLoading) {
15+
// Returning promise to be resolved by other process as loading.
16+
return new Promise(resolve => {
17+
resolveStack.push(resolve);
18+
});
19+
}
1120

12-
insertSlice(imageData, sliceIndex, image);
13-
};
21+
return new Promise((resolve, reject) => {
22+
const { imageIds, vtkImageData, metaDataMap, zAxis } = imageDataObject;
23+
const loadImagePromises = imageIds.map(cornerstone.loadAndCacheImage);
1424

15-
loadImagePromises.forEach(promise => {
16-
promise.then(insertPixelData).catch(error => {
17-
console.error(error);
18-
//throw new Error(error);
19-
});
20-
});
25+
imageDataObject.isLoading = true;
26+
27+
let numberOfSlices = imageIds.length;
28+
29+
let slicesInserted = 0;
30+
31+
//let resolved = false;
32+
33+
const insertPixelData = image => {
34+
const { imagePositionPatient } = metaDataMap.get(image.imageId);
35+
const sliceIndex = getSliceIndex(zAxis, imagePositionPatient);
2136

22-
// TODO: Investigate progressive loading. Right now the UI gets super slow because
23-
// we are rendering and decoding simultaneously. We might want to use fewer web workers
24-
// for the decoding tasks.
37+
insertSlice(vtkImageData, sliceIndex, image);
2538

26-
//return loadImagePromises[0];
27-
return Promise.all(loadImagePromises);
39+
slicesInserted++;
40+
41+
//if (!resolved) {
42+
if (slicesInserted === numberOfSlices) {
43+
imageDataObject.isLoading = false;
44+
imageDataObject.loaded = true;
45+
console.log('LOADED');
46+
while (resolveStack.length) {
47+
resolveStack.pop()();
48+
}
49+
50+
//resolved = true;
51+
52+
resolve();
53+
}
54+
};
55+
56+
loadImagePromises.forEach(promise => {
57+
promise.then(insertPixelData).catch(error => {
58+
console.error(error);
59+
reject(error);
60+
});
61+
});
62+
63+
// TODO: Investigate progressive loading. Right now the UI gets super slow because
64+
// we are rendering and decoding simultaneously. We might want to use fewer web workers
65+
// for the decoding tasks.
66+
});
2867
}
2968

69+
/*
3070
export default function loadImageData(imageDataObject) {
31-
return loadImageDataProgressively(
32-
imageDataObject.imageIds,
33-
imageDataObject.vtkImageData,
34-
imageDataObject.metaDataMap,
35-
imageDataObject.zAxis
36-
).then(() => {
37-
imageDataObject.loaded = true;
38-
});
71+
return loadImageDataProgressively(imageDataObject).then();
3972
}
73+
*/

0 commit comments

Comments
 (0)