Skip to content

Commit

Permalink
Merge pull request #134 from jadh4v/pet-ct-generalize-overlap
Browse files Browse the repository at this point in the history
Upgrade PT/CT overlay example to generalize detection and handling of PT-CT grid overlap
  • Loading branch information
floryst authored Jun 11, 2024
2 parents b2d750f + e5d6187 commit e5b4e7b
Showing 1 changed file with 149 additions and 40 deletions.
189 changes: 149 additions & 40 deletions usage/src/Volume/PET_CT_Overlay.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import vtkLiteHttpDataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelpe
import vtkResourceLoader from '@kitware/vtk.js/IO/Core/ResourceLoader';
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps.js';
import { BlendMode } from '@kitware/vtk.js/Rendering/Core/VolumeMapper/Constants.js';
import vtkMath from '@kitware/vtk.js/Common/Core/Math';
import { unzipSync } from 'fflate';
import { useContext, useEffect, useState } from 'react';
import './PET_CT_Overlay.css';
Expand All @@ -18,15 +19,15 @@ import {
View,
VolumeRepresentation,
} from 'react-vtk-js';
import vtkImageReslice from '@kitware/vtk.js/Imaging/Core/ImageReslice';
import { InterpolationMode } from '@kitware/vtk.js/Imaging/Core/AbstractImageInterpolator/Constants';

function Slider(props) {
const view = useContext(Contexts.ViewContext);
const onChange = (e) => {
const value = Number(e.currentTarget.value);
props.setValue(value);
if (props.setCTValue) {
props.setCTValue(value * 4);
}
props.setPTValue(value);
setTimeout(view?.renderView, 0);
};
return (
Expand Down Expand Up @@ -96,23 +97,134 @@ function DropDown(props) {
);
}

function haveOverlappingGrids(img1, img2, tolerance = vtkMath.EPSILON) {
if (!img1 || !img2) {
return false;
}
const sameVec3 = (p1, p2, tolerance) => vtkMath.distance2BetweenPoints(p1, p2) < tolerance*tolerance;
if (!sameVec3(img1.getOrigin(), img2.getOrigin())) {
return false;
}
if (!sameVec3(img1.getSpacing(), img2.getSpacing())) {
return false;
}
if (!sameVec3(img1.getDimensions(), img2.getDimensions())) {
return false;
}
const dir1 = img1.getDirection();
const dir2 = img2.getDirection();
const dirDelta = dir1.reduce((accumulator, currentValue, currentIndex) => accumulator + Math.abs(currentValue - dir2[currentIndex]), 0);
if (dirDelta > tolerance) {
return false;
}
return true;
}

function resliceAndSetup(ctImageData, ptImageData) {
loader.hidden = 'hidden';
fileInput.hidden = 'hidden';
exampleInput.hidden = 'hidden';
const overlappingGrids = haveOverlappingGrids(ctImageData, ptImageData, 1e-3);
if (!overlappingGrids) {
// Resample the image with background series grid:
const reslicer = vtkImageReslice.newInstance();
reslicer.setInputData(ptImageData);
reslicer.setOutputDimensionality(3);
reslicer.setOutputExtent(ctImageData.getExtent());
reslicer.setOutputSpacing(ctImageData.getSpacing());
reslicer.setOutputDirection(ctImageData.getDirection());
reslicer.setOutputOrigin(ctImageData.getOrigin());
reslicer.setTransformInputSampling(false);
reslicer.setInterpolationMode(InterpolationMode.LINEAR)
ptImageData = reslicer.getOutputData();
window.setResliced(true);
}
window.ptData = ptImageData;
window.ctData = ctImageData;
window.setMaxKSlice(ctImageData.getDimensions()[2] - 1);
window.setMaxJSlice(ctImageData.getDimensions()[1] - 1);
const range = ptImageData?.getPointData()?.getScalars()?.getRange();
window.setPTColorWindow(range[1] - range[0]);
window.setPTColorLevel((range[1] + range[0]) * 0.5);
window.setStatusText('');
return [ctImageData, ptImageData];
}

/**
* Loads data from local storage. Function expects a zip file with two subfolders: CT, PT
*/
const loadLocalData = async function (event) {
event.preventDefault();
console.log('Loading itk module...');
window.setStatusText('Loading itk module...');
if (!window.itk) {
await vtkResourceLoader.loadScript(
'https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/itk-wasm.js'
);
}
const files = event.target.files;
if (files.length === 1) {
const fileReader = new FileReader();
fileReader.onload = async function onLoad(e) {
const zipFileDataArray = new Uint8Array(fileReader.result);
const decompressedFiles = unzipSync(zipFileDataArray);
const ctDCMFiles = [];
const ptDCMFiles = [];
const PTRe = /PT/;
const CTRe = /CT/;
Object.keys(decompressedFiles).forEach((relativePath) => {
if (relativePath.endsWith('.dcm')) {
if (PTRe.test(relativePath)) {
ptDCMFiles.push(decompressedFiles[relativePath].buffer);
} else if (CTRe.test(relativePath)) {
ctDCMFiles.push(decompressedFiles[relativePath].buffer);
}
}
});

if (ptDCMFiles.length === 0 || ctDCMFiles.length === 0) {
const msg = 'Expected two directories in the zip file: "PT" and "CT"';
console.error(msg);
window.alert(msg);
return;
}

let ctImageData = null;
let ptImageData = null;
if (window.itk) {
const { image: ctitkImage, webWorkerPool: ctWebWorkers } =
await window.itk.readImageDICOMArrayBufferSeries(ctDCMFiles);
ctWebWorkers.terminateWorkers();
ctImageData = vtkITKHelper.convertItkToVtkImage(ctitkImage);
const { image: ptitkImage, webWorkerPool: ptWebWorkers } =
await window.itk.readImageDICOMArrayBufferSeries(ptDCMFiles);
ptWebWorkers.terminateWorkers();
ptImageData = vtkITKHelper.convertItkToVtkImage(ptitkImage);
}
return resliceAndSetup(ctImageData, ptImageData);
};

fileReader.readAsArrayBuffer(files[0]);
}
};

const loadData = async () => {
console.log('Loading itk module...');
loadData.setStatusText('Loading itk module...');
window.setStatusText('Loading itk module...');
if (!window.itk) {
await vtkResourceLoader.loadScript(
'https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/itk-wasm.js'
);
}

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

console.log('Fetching/downloading input file done!');
loadData.setStatusText('Download complete!');
window.setStatusText('Download complete!');

const zipFileDataArray = new Uint8Array(zipFileData);
const decompressedFiles = unzipSync(zipFileDataArray);
Expand Down Expand Up @@ -142,20 +254,13 @@ const loadData = async () => {
ptWebWorkers.terminateWorkers();
ptImageData = vtkITKHelper.convertItkToVtkImage(ptitkImage);
}
loadData.setMaxKSlice(ctImageData.getDimensions()[2] - 1);
loadData.setMaxJSlice(ptImageData.getDimensions()[1] - 1);
const range = ptImageData?.getPointData()?.getScalars()?.getRange();
loadData.setPTColorWindow(range[1] - range[0]);
loadData.setPTColorLevel((range[1] + range[0]) * 0.5);
loadData.setStatusText('');
loader.hidden = 'hidden';
return [ctImageData, ptImageData];
return resliceAndSetup(ctImageData, ptImageData);
};

function Example(props) {
const [statusText, setStatusText] = useState('Loading data, please wait ...');
const [kSlice, setKSlice] = useState(0);
const [jSlice, setJSlice] = useState(0);
const [ptjSlice, setJSlice] = useState(0);
const [ctjSlice, setCTJSlice] = useState(0);
const [colorWindow, setColorWindow] = useState(2048);
const [colorLevel, setColorLevel] = useState(0);
Expand All @@ -165,24 +270,28 @@ function Example(props) {
const [opacity, setOpacity] = useState(0.4);
const [maxKSlice, setMaxKSlice] = useState(310);
const [maxJSlice, setMaxJSlice] = useState(110);
loadData.setMaxKSlice = setMaxKSlice;
loadData.setMaxJSlice = setMaxJSlice;
loadData.setStatusText = setStatusText;
loadData.setPTColorWindow = setPTColorWindow;
loadData.setPTColorLevel = setPTColorLevel;
const [resliced, setResliced] = useState(false);
window.setMaxKSlice = setMaxKSlice;
window.setMaxJSlice = setMaxJSlice;
window.setStatusText = setStatusText;
window.setPTColorWindow = setPTColorWindow;
window.setPTColorLevel = setPTColorLevel;
window.setResliced = setResliced;

useEffect(() => {
loadData().then(([ctData, ptData]) => {
window.ctData = ctData;
window.ptData = ptData;
setKSlice(155);
setJSlice(64);
setCTJSlice(256);
});
}, []);
if (window.ctData && window.ptData) {
const ptDim = window.ptData.getDimensions();
setKSlice(Math.floor(ptDim[2]/2));
setJSlice(Math.floor(ptDim[1]/2));
const ctDim = window.ctData.getDimensions();
setCTJSlice(Math.floor(ctDim[1]/2));
}
}, [window.ctData, window.ptData]);

return (
<MultiViewRoot>
<input id='fileInput' type='file' className='file' accept='.zip' onChange={loadLocalData}/>
<input id='exampleInput' type='button' value='Download Example Data' accept='.zip' onClick={loadData}/>
<ShareDataSetRoot>
<RegisterDataSet id='ctData'>
<Dataset dataset={window.ctData} />
Expand Down Expand Up @@ -215,7 +324,7 @@ function Example(props) {
max={4095}
value={colorLevel}
setValue={setColorLevel}
style={{ top: '60px', left: '205px' }}
style={{ top: '60px', left: '5px' }}
/>
<Slider
label='Color Window'
Expand All @@ -238,7 +347,7 @@ function Example(props) {
options={vtkColorMaps.rgbPresetNames}
value={colorPreset}
setValue={setColorPreset}
style={{ top: '30px', left: '305px' }}
style={{ top: '10px', left: '405px' }}
/>
<div className='loader' id='loader' />
<div
Expand All @@ -250,10 +359,10 @@ function Example(props) {
}}
>
<View
id='0'
id='axial'
camera={{
position: [0, 0, 0],
focalPoint: [0, 0, -1],
focalPoint: [0, 0, 1],
viewUp: [0, -1, 0],
parallelProjection: true,
}}
Expand Down Expand Up @@ -306,10 +415,10 @@ function Example(props) {
}}
>
<View
id='0'
id='coronal'
camera={{
position: [0, 0, 0],
focalPoint: [0, -1, 0],
focalPoint: [0, 1, 0],
viewUp: [0, 0, 1],
parallelProjection: true,
}}
Expand All @@ -318,15 +427,15 @@ function Example(props) {
<Slider
label='Slice'
max={maxJSlice}
value={jSlice}
setValue={setJSlice}
setCTValue={setCTJSlice}
value={ctjSlice}
setValue={setCTJSlice}
setPTValue={setJSlice}
resliced={resliced}
orient='vertical'
style={{ top: '50%', left: '5%' }}
/>
<SliceRepresentation
id='pt'
jSlice={jSlice}
jSlice={ptjSlice}
mapper={{
resolveCoincidentTopology: 'Polygon',
resolveCoincidentTopologyPolygonOffsetParameters: {
Expand Down Expand Up @@ -364,7 +473,7 @@ function Example(props) {
}}
>
<View
id='0'
id='mip3D'
camera={{
position: [0, 0, 0],
focalPoint: [0, 1, 0],
Expand Down

0 comments on commit e5b4e7b

Please sign in to comment.