Skip to content

Commit e5b4e7b

Browse files
authored
Merge pull request #134 from jadh4v/pet-ct-generalize-overlap
Upgrade PT/CT overlay example to generalize detection and handling of PT-CT grid overlap
2 parents b2d750f + e5d6187 commit e5b4e7b

File tree

1 file changed

+149
-40
lines changed

1 file changed

+149
-40
lines changed

usage/src/Volume/PET_CT_Overlay.jsx

+149-40
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import vtkLiteHttpDataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelpe
33
import vtkResourceLoader from '@kitware/vtk.js/IO/Core/ResourceLoader';
44
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps.js';
55
import { BlendMode } from '@kitware/vtk.js/Rendering/Core/VolumeMapper/Constants.js';
6+
import vtkMath from '@kitware/vtk.js/Common/Core/Math';
67
import { unzipSync } from 'fflate';
78
import { useContext, useEffect, useState } from 'react';
89
import './PET_CT_Overlay.css';
@@ -18,15 +19,15 @@ import {
1819
View,
1920
VolumeRepresentation,
2021
} from 'react-vtk-js';
22+
import vtkImageReslice from '@kitware/vtk.js/Imaging/Core/ImageReslice';
23+
import { InterpolationMode } from '@kitware/vtk.js/Imaging/Core/AbstractImageInterpolator/Constants';
2124

2225
function Slider(props) {
2326
const view = useContext(Contexts.ViewContext);
2427
const onChange = (e) => {
2528
const value = Number(e.currentTarget.value);
2629
props.setValue(value);
27-
if (props.setCTValue) {
28-
props.setCTValue(value * 4);
29-
}
30+
props.setPTValue(value);
3031
setTimeout(view?.renderView, 0);
3132
};
3233
return (
@@ -96,23 +97,134 @@ function DropDown(props) {
9697
);
9798
}
9899

100+
function haveOverlappingGrids(img1, img2, tolerance = vtkMath.EPSILON) {
101+
if (!img1 || !img2) {
102+
return false;
103+
}
104+
const sameVec3 = (p1, p2, tolerance) => vtkMath.distance2BetweenPoints(p1, p2) < tolerance*tolerance;
105+
if (!sameVec3(img1.getOrigin(), img2.getOrigin())) {
106+
return false;
107+
}
108+
if (!sameVec3(img1.getSpacing(), img2.getSpacing())) {
109+
return false;
110+
}
111+
if (!sameVec3(img1.getDimensions(), img2.getDimensions())) {
112+
return false;
113+
}
114+
const dir1 = img1.getDirection();
115+
const dir2 = img2.getDirection();
116+
const dirDelta = dir1.reduce((accumulator, currentValue, currentIndex) => accumulator + Math.abs(currentValue - dir2[currentIndex]), 0);
117+
if (dirDelta > tolerance) {
118+
return false;
119+
}
120+
return true;
121+
}
122+
123+
function resliceAndSetup(ctImageData, ptImageData) {
124+
loader.hidden = 'hidden';
125+
fileInput.hidden = 'hidden';
126+
exampleInput.hidden = 'hidden';
127+
const overlappingGrids = haveOverlappingGrids(ctImageData, ptImageData, 1e-3);
128+
if (!overlappingGrids) {
129+
// Resample the image with background series grid:
130+
const reslicer = vtkImageReslice.newInstance();
131+
reslicer.setInputData(ptImageData);
132+
reslicer.setOutputDimensionality(3);
133+
reslicer.setOutputExtent(ctImageData.getExtent());
134+
reslicer.setOutputSpacing(ctImageData.getSpacing());
135+
reslicer.setOutputDirection(ctImageData.getDirection());
136+
reslicer.setOutputOrigin(ctImageData.getOrigin());
137+
reslicer.setTransformInputSampling(false);
138+
reslicer.setInterpolationMode(InterpolationMode.LINEAR)
139+
ptImageData = reslicer.getOutputData();
140+
window.setResliced(true);
141+
}
142+
window.ptData = ptImageData;
143+
window.ctData = ctImageData;
144+
window.setMaxKSlice(ctImageData.getDimensions()[2] - 1);
145+
window.setMaxJSlice(ctImageData.getDimensions()[1] - 1);
146+
const range = ptImageData?.getPointData()?.getScalars()?.getRange();
147+
window.setPTColorWindow(range[1] - range[0]);
148+
window.setPTColorLevel((range[1] + range[0]) * 0.5);
149+
window.setStatusText('');
150+
return [ctImageData, ptImageData];
151+
}
152+
153+
/**
154+
* Loads data from local storage. Function expects a zip file with two subfolders: CT, PT
155+
*/
156+
const loadLocalData = async function (event) {
157+
event.preventDefault();
158+
console.log('Loading itk module...');
159+
window.setStatusText('Loading itk module...');
160+
if (!window.itk) {
161+
await vtkResourceLoader.loadScript(
162+
'https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/itk-wasm.js'
163+
);
164+
}
165+
const files = event.target.files;
166+
if (files.length === 1) {
167+
const fileReader = new FileReader();
168+
fileReader.onload = async function onLoad(e) {
169+
const zipFileDataArray = new Uint8Array(fileReader.result);
170+
const decompressedFiles = unzipSync(zipFileDataArray);
171+
const ctDCMFiles = [];
172+
const ptDCMFiles = [];
173+
const PTRe = /PT/;
174+
const CTRe = /CT/;
175+
Object.keys(decompressedFiles).forEach((relativePath) => {
176+
if (relativePath.endsWith('.dcm')) {
177+
if (PTRe.test(relativePath)) {
178+
ptDCMFiles.push(decompressedFiles[relativePath].buffer);
179+
} else if (CTRe.test(relativePath)) {
180+
ctDCMFiles.push(decompressedFiles[relativePath].buffer);
181+
}
182+
}
183+
});
184+
185+
if (ptDCMFiles.length === 0 || ctDCMFiles.length === 0) {
186+
const msg = 'Expected two directories in the zip file: "PT" and "CT"';
187+
console.error(msg);
188+
window.alert(msg);
189+
return;
190+
}
191+
192+
let ctImageData = null;
193+
let ptImageData = null;
194+
if (window.itk) {
195+
const { image: ctitkImage, webWorkerPool: ctWebWorkers } =
196+
await window.itk.readImageDICOMArrayBufferSeries(ctDCMFiles);
197+
ctWebWorkers.terminateWorkers();
198+
ctImageData = vtkITKHelper.convertItkToVtkImage(ctitkImage);
199+
const { image: ptitkImage, webWorkerPool: ptWebWorkers } =
200+
await window.itk.readImageDICOMArrayBufferSeries(ptDCMFiles);
201+
ptWebWorkers.terminateWorkers();
202+
ptImageData = vtkITKHelper.convertItkToVtkImage(ptitkImage);
203+
}
204+
return resliceAndSetup(ctImageData, ptImageData);
205+
};
206+
207+
fileReader.readAsArrayBuffer(files[0]);
208+
}
209+
};
210+
99211
const loadData = async () => {
100212
console.log('Loading itk module...');
101-
loadData.setStatusText('Loading itk module...');
213+
window.setStatusText('Loading itk module...');
102214
if (!window.itk) {
103215
await vtkResourceLoader.loadScript(
104216
'https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/itk-wasm.js'
105217
);
106218
}
107219

108220
console.log('Fetching/downloading the input file, please wait...');
109-
loadData.setStatusText('Loading data, please wait...');
221+
window.setStatusText('Loading data, please wait...');
110222
const zipFileData = await vtkLiteHttpDataAccessHelper.fetchBinary(
111223
'https://data.kitware.com/api/v1/folder/661ad10a5165b19d36c87220/download'
112224
);
113225

114226
console.log('Fetching/downloading input file done!');
115-
loadData.setStatusText('Download complete!');
227+
window.setStatusText('Download complete!');
116228

117229
const zipFileDataArray = new Uint8Array(zipFileData);
118230
const decompressedFiles = unzipSync(zipFileDataArray);
@@ -142,20 +254,13 @@ const loadData = async () => {
142254
ptWebWorkers.terminateWorkers();
143255
ptImageData = vtkITKHelper.convertItkToVtkImage(ptitkImage);
144256
}
145-
loadData.setMaxKSlice(ctImageData.getDimensions()[2] - 1);
146-
loadData.setMaxJSlice(ptImageData.getDimensions()[1] - 1);
147-
const range = ptImageData?.getPointData()?.getScalars()?.getRange();
148-
loadData.setPTColorWindow(range[1] - range[0]);
149-
loadData.setPTColorLevel((range[1] + range[0]) * 0.5);
150-
loadData.setStatusText('');
151-
loader.hidden = 'hidden';
152-
return [ctImageData, ptImageData];
257+
return resliceAndSetup(ctImageData, ptImageData);
153258
};
154259

155260
function Example(props) {
156261
const [statusText, setStatusText] = useState('Loading data, please wait ...');
157262
const [kSlice, setKSlice] = useState(0);
158-
const [jSlice, setJSlice] = useState(0);
263+
const [ptjSlice, setJSlice] = useState(0);
159264
const [ctjSlice, setCTJSlice] = useState(0);
160265
const [colorWindow, setColorWindow] = useState(2048);
161266
const [colorLevel, setColorLevel] = useState(0);
@@ -165,24 +270,28 @@ function Example(props) {
165270
const [opacity, setOpacity] = useState(0.4);
166271
const [maxKSlice, setMaxKSlice] = useState(310);
167272
const [maxJSlice, setMaxJSlice] = useState(110);
168-
loadData.setMaxKSlice = setMaxKSlice;
169-
loadData.setMaxJSlice = setMaxJSlice;
170-
loadData.setStatusText = setStatusText;
171-
loadData.setPTColorWindow = setPTColorWindow;
172-
loadData.setPTColorLevel = setPTColorLevel;
273+
const [resliced, setResliced] = useState(false);
274+
window.setMaxKSlice = setMaxKSlice;
275+
window.setMaxJSlice = setMaxJSlice;
276+
window.setStatusText = setStatusText;
277+
window.setPTColorWindow = setPTColorWindow;
278+
window.setPTColorLevel = setPTColorLevel;
279+
window.setResliced = setResliced;
173280

174281
useEffect(() => {
175-
loadData().then(([ctData, ptData]) => {
176-
window.ctData = ctData;
177-
window.ptData = ptData;
178-
setKSlice(155);
179-
setJSlice(64);
180-
setCTJSlice(256);
181-
});
182-
}, []);
282+
if (window.ctData && window.ptData) {
283+
const ptDim = window.ptData.getDimensions();
284+
setKSlice(Math.floor(ptDim[2]/2));
285+
setJSlice(Math.floor(ptDim[1]/2));
286+
const ctDim = window.ctData.getDimensions();
287+
setCTJSlice(Math.floor(ctDim[1]/2));
288+
}
289+
}, [window.ctData, window.ptData]);
183290

184291
return (
185292
<MultiViewRoot>
293+
<input id='fileInput' type='file' className='file' accept='.zip' onChange={loadLocalData}/>
294+
<input id='exampleInput' type='button' value='Download Example Data' accept='.zip' onClick={loadData}/>
186295
<ShareDataSetRoot>
187296
<RegisterDataSet id='ctData'>
188297
<Dataset dataset={window.ctData} />
@@ -215,7 +324,7 @@ function Example(props) {
215324
max={4095}
216325
value={colorLevel}
217326
setValue={setColorLevel}
218-
style={{ top: '60px', left: '205px' }}
327+
style={{ top: '60px', left: '5px' }}
219328
/>
220329
<Slider
221330
label='Color Window'
@@ -238,7 +347,7 @@ function Example(props) {
238347
options={vtkColorMaps.rgbPresetNames}
239348
value={colorPreset}
240349
setValue={setColorPreset}
241-
style={{ top: '30px', left: '305px' }}
350+
style={{ top: '10px', left: '405px' }}
242351
/>
243352
<div className='loader' id='loader' />
244353
<div
@@ -250,10 +359,10 @@ function Example(props) {
250359
}}
251360
>
252361
<View
253-
id='0'
362+
id='axial'
254363
camera={{
255364
position: [0, 0, 0],
256-
focalPoint: [0, 0, -1],
365+
focalPoint: [0, 0, 1],
257366
viewUp: [0, -1, 0],
258367
parallelProjection: true,
259368
}}
@@ -306,10 +415,10 @@ function Example(props) {
306415
}}
307416
>
308417
<View
309-
id='0'
418+
id='coronal'
310419
camera={{
311420
position: [0, 0, 0],
312-
focalPoint: [0, -1, 0],
421+
focalPoint: [0, 1, 0],
313422
viewUp: [0, 0, 1],
314423
parallelProjection: true,
315424
}}
@@ -318,15 +427,15 @@ function Example(props) {
318427
<Slider
319428
label='Slice'
320429
max={maxJSlice}
321-
value={jSlice}
322-
setValue={setJSlice}
323-
setCTValue={setCTJSlice}
430+
value={ctjSlice}
431+
setValue={setCTJSlice}
432+
setPTValue={setJSlice}
433+
resliced={resliced}
324434
orient='vertical'
325435
style={{ top: '50%', left: '5%' }}
326436
/>
327437
<SliceRepresentation
328-
id='pt'
329-
jSlice={jSlice}
438+
jSlice={ptjSlice}
330439
mapper={{
331440
resolveCoincidentTopology: 'Polygon',
332441
resolveCoincidentTopologyPolygonOffsetParameters: {
@@ -364,7 +473,7 @@ function Example(props) {
364473
}}
365474
>
366475
<View
367-
id='0'
476+
id='mip3D'
368477
camera={{
369478
position: [0, 0, 0],
370479
focalPoint: [0, 1, 0],

0 commit comments

Comments
 (0)