Skip to content

Commit 1ccc505

Browse files
committed
Example of PET-CT fusion overlay rendering
1 parent c4e287e commit 1ccc505

3 files changed

Lines changed: 331 additions & 0 deletions

File tree

usage/src/App.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const demos = new Map([
3232
'Volume/ImageSeriesRendering',
3333
lazy(() => import('./Volume/ImageSeriesRendering')),
3434
],
35+
['Volume/PET_CT_Overlay', lazy(() => import('./Volume/PET_CT_Overlay'))],
3536
['Tests/PropertyUpdate', lazy(() => import('./Tests/PropertyUpdate'))],
3637
['Tests/CameraTest', lazy(() => import('./Tests/CameraTest'))],
3738
['Tests/ShareGeometry', lazy(() => import('./Tests/ShareGeometry'))],
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
label {
2+
color: lightGray;
3+
}
4+
5+
input[type=range] {
6+
-webkit-appearance: none;
7+
}
8+
9+
input[type=range]::-webkit-slider-runnable-track {
10+
width: 300px;
11+
height: 5px;
12+
background: #ddd;
13+
border: none;
14+
border-radius: 5px;
15+
}
16+
17+
18+
input[type=range]::-webkit-slider-thumb {
19+
-webkit-appearance: none;
20+
border: none;
21+
height: 15px;
22+
width: 6px;
23+
border-radius: 50%;
24+
background: goldenrod;
25+
margin-top: -4px;
26+
}
27+
28+
29+
input[type=range]:focus {
30+
outline: none;
31+
}
32+
33+
input[type=range]:focus::-webkit-slider-runnable-track {
34+
background: #ccc;
35+
}
36+
37+
38+
input[type=range][orient='vertical'] {
39+
position: absolute;
40+
zIndex: 1000;
41+
margin: 0;
42+
padding: 0;
43+
width: 2500%;
44+
height: 0.5em;
45+
transform: translate(-50%, -50%) rotate(-90deg);
46+
background: transparent;
47+
font: 1em/1 arial, sans-serif;
48+
}
49+
50+
.loader {
51+
border: 16px solid #f3f3f3;
52+
border-radius: 50%;
53+
border-top: 16px solid blue;
54+
border-right: 16px solid green;
55+
border-bottom: 16px solid red;
56+
width: 120px;
57+
height: 120px;
58+
position: absolute;
59+
top: 50%;
60+
left: 50%;
61+
transform: translate(-50%, -50%);
62+
-webkit-animation: spin 2s linear infinite;
63+
animation: spin 2s linear infinite;
64+
}
65+
@-webkit-keyframes spin {
66+
0% { -webkit-transform: rotate(0deg); }
67+
100% { -webkit-transform: rotate(360deg); }
68+
}
69+
70+
@keyframes spin {
71+
0% { transform: rotate(0deg); }
72+
100% { transform: rotate(360deg); }
73+
}
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import vtkITKHelper from '@kitware/vtk.js/Common/DataModel/ITKHelper';
2+
import vtkLiteHttpDataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper/LiteHttpDataAccessHelper';
3+
import vtkResourceLoader from '@kitware/vtk.js/IO/Core/ResourceLoader';
4+
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps.js';
5+
import { unzipSync } from 'fflate';
6+
import { useContext, useEffect, useState } from 'react';
7+
import './PET_CT_Overlay.css';
8+
9+
import {
10+
Contexts,
11+
Dataset,
12+
RegisterDataSet,
13+
ShareDataSetRoot,
14+
SliceRepresentation,
15+
UseDataSet,
16+
View,
17+
} from 'react-vtk-js';
18+
19+
function Slider(props) {
20+
const view = useContext(Contexts.ViewContext);
21+
const onChange = (e) => {
22+
const value = Number(e.currentTarget.value);
23+
props.setValue(value);
24+
setTimeout(view.renderView, 0);
25+
};
26+
return (
27+
<label
28+
style={{
29+
position: 'absolute',
30+
zIndex: 100,
31+
left: props.style.width + 10,
32+
...props.style,
33+
}}
34+
>
35+
{props.label}
36+
<input
37+
type='range'
38+
orient={props.orient}
39+
min={props.min}
40+
max={props.max}
41+
step={props.step}
42+
value={props.value}
43+
onChange={onChange}
44+
style={{
45+
zIndex: 100,
46+
...props.style,
47+
}}
48+
/>
49+
</label>
50+
);
51+
}
52+
53+
function DropDown(props) {
54+
const view = useContext(Contexts.ViewContext);
55+
function onChange(e) {
56+
const value = e.currentTarget.value;
57+
props.setValue(value);
58+
setTimeout(view.renderView, 0);
59+
}
60+
return (
61+
<form>
62+
<label
63+
htmlFor={props.label}
64+
style={{
65+
position: 'relative',
66+
zIndex: 100,
67+
left: '-50px',
68+
...props.style,
69+
}}
70+
>
71+
{props.label}
72+
</label>
73+
<select
74+
id={props.label}
75+
value={props.value}
76+
onChange={onChange}
77+
style={{
78+
position: 'relative',
79+
zIndex: 100,
80+
left: '50px',
81+
top: '5px',
82+
...props.style,
83+
}}
84+
>
85+
{props.options.map((opt) => (
86+
<option key={opt}>{opt}</option>
87+
))}
88+
</select>
89+
</form>
90+
);
91+
}
92+
93+
const loadData = async () => {
94+
console.log('Loading itk module...');
95+
loadData.setStatusText('Loading itk module...');
96+
if (!window.itk) {
97+
await vtkResourceLoader.loadScript(
98+
'https://cdn.jsdelivr.net/npm/itk-wasm@1.0.0-b.8/dist/umd/itk-wasm.js'
99+
);
100+
}
101+
102+
console.log('Fetching/downloading the input file, please wait...');
103+
loadData.setStatusText('Loading data, please wait...');
104+
const zipFileData = await vtkLiteHttpDataAccessHelper.fetchBinary(
105+
'https://data.kitware.com/api/v1/folder/661ad10a5165b19d36c87220/download'
106+
);
107+
108+
console.log('Fetching/downloading input file done!');
109+
loadData.setStatusText('Download complete!');
110+
111+
const zipFileDataArray = new Uint8Array(zipFileData);
112+
const decompressedFiles = unzipSync(zipFileDataArray);
113+
const ctDCMFiles = [];
114+
const ptDCMFiles = [];
115+
const PTRe = /PET AC/;
116+
const CTRe = /CT IMAGES/;
117+
Object.keys(decompressedFiles).forEach((relativePath) => {
118+
if (relativePath.endsWith('.dcm')) {
119+
if (PTRe.test(relativePath)) {
120+
ptDCMFiles.push(decompressedFiles[relativePath].buffer);
121+
} else if (CTRe.test(relativePath)) {
122+
ctDCMFiles.push(decompressedFiles[relativePath].buffer);
123+
}
124+
}
125+
});
126+
127+
let ctImageData = null;
128+
let ptImageData = null;
129+
if (window.itk) {
130+
const { image: ctitkImage, webWorkerPool: ctWebWorkers } =
131+
await window.itk.readImageDICOMArrayBufferSeries(ctDCMFiles);
132+
ctWebWorkers.terminateWorkers();
133+
ctImageData = vtkITKHelper.convertItkToVtkImage(ctitkImage);
134+
const { image: ptitkImage, webWorkerPool: ptWebWorkers } =
135+
await window.itk.readImageDICOMArrayBufferSeries(ptDCMFiles);
136+
ptWebWorkers.terminateWorkers();
137+
ptImageData = vtkITKHelper.convertItkToVtkImage(ptitkImage);
138+
}
139+
loadData.setMaxSlicingValue(ctImageData.getDimensions()[2] - 1);
140+
loadData.setStatusText('');
141+
loader.hidden = 'hidden';
142+
return [ctImageData, ptImageData];
143+
};
144+
145+
function Example(props) {
146+
const [statusText, setStatusText] = useState('Loading data, please wait ...');
147+
const [kSlice, setKSlice] = useState(0);
148+
const [colorWindow, setColorWindow] = useState(2048);
149+
const [colorLevel, setColorLevel] = useState(0);
150+
const [colorPreset, setColorPreset] = useState('jet');
151+
const [opacity, setOpacity] = useState(0.4);
152+
const [maxKSlice, setMaxKSlice] = useState(310);
153+
loadData.setMaxSlicingValue = setMaxKSlice;
154+
loadData.setStatusText = setStatusText;
155+
156+
useEffect(() => {
157+
loadData().then(([ctData, ptData]) => {
158+
window.ctData = ctData;
159+
window.ptData = ptData;
160+
setKSlice(155);
161+
});
162+
}, []);
163+
164+
return (
165+
<div style={{ width: '100%', height: '100%' }}>
166+
<ShareDataSetRoot>
167+
<RegisterDataSet id='ctData'>
168+
<Dataset dataset={window.ctData} />
169+
</RegisterDataSet>
170+
<RegisterDataSet id='ptData'>
171+
<Dataset dataset={window.ptData} />
172+
</RegisterDataSet>
173+
<View
174+
id='0'
175+
camera={{
176+
position: [0, 0, 0],
177+
focalPoint: [0, 0, -1],
178+
viewUp: [0, -1, 0],
179+
parallelProjection: true,
180+
}}
181+
background={[0, 0, 0]}
182+
>
183+
<label
184+
style={{
185+
position: 'absolute',
186+
zIndex: 100,
187+
left: '45%',
188+
top: '65%',
189+
fontSize: '25px',
190+
}}
191+
>
192+
{statusText}
193+
</label>
194+
<Slider
195+
label='Slice'
196+
max={maxKSlice}
197+
value={kSlice}
198+
setValue={setKSlice}
199+
orient='vertical'
200+
style={{ top: '50%', left: '1%' }}
201+
/>
202+
<Slider
203+
label='Color Level'
204+
max={4095}
205+
value={colorLevel}
206+
setValue={setColorLevel}
207+
style={{ top: '60px', left: '205px' }}
208+
/>
209+
<Slider
210+
label='Color Window'
211+
max={4095}
212+
value={colorWindow}
213+
setValue={setColorWindow}
214+
style={{ top: '60px', left: '455px' }}
215+
/>
216+
<Slider
217+
label='PET Opacity'
218+
min={0.0}
219+
step={0.1}
220+
max={1.0}
221+
value={opacity}
222+
setValue={setOpacity}
223+
style={{ top: '30px', left: '5px' }}
224+
/>
225+
<DropDown
226+
label='Color Preset'
227+
options={vtkColorMaps.rgbPresetNames}
228+
value={colorPreset}
229+
setValue={setColorPreset}
230+
style={{ top: '30px', left: '305px' }}
231+
/>
232+
<div className='loader' id='loader' />
233+
<SliceRepresentation
234+
kSlice={kSlice}
235+
property={{
236+
opacity,
237+
}}
238+
colorMapPreset={colorPreset}
239+
>
240+
<UseDataSet id='ptData' />
241+
</SliceRepresentation>
242+
<SliceRepresentation
243+
kSlice={kSlice}
244+
property={{
245+
colorWindow,
246+
colorLevel,
247+
}}
248+
>
249+
<UseDataSet id='ctData' />
250+
</SliceRepresentation>
251+
</View>
252+
</ShareDataSetRoot>
253+
</div>
254+
);
255+
}
256+
257+
export default Example;

0 commit comments

Comments
 (0)