-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
);
}
diff --git a/usage/src/Volume/ImageSeriesRendering.jsx b/usage/src/Volume/ImageSeriesRendering.jsx
index 3bc0dfe..e72d44b 100644
--- a/usage/src/Volume/ImageSeriesRendering.jsx
+++ b/usage/src/Volume/ImageSeriesRendering.jsx
@@ -1,18 +1,20 @@
-import React, { useState, useContext, useEffect } from 'react';
-import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps.js';
import vtkCollection from '@kitware/vtk.js/Common/DataModel/Collection';
-import vtkImageArrayMapper from '@kitware/vtk.js/Rendering/Core/ImageArrayMapper.js';
-import vtkResourceLoader from '@kitware/vtk.js/IO/Core/ResourceLoader';
+import vtkITKHelper from '@kitware/vtk.js/Common/DataModel/ITKHelper';
import vtkLiteHttpDataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper/LiteHttpDataAccessHelper';
+import vtkResourceLoader from '@kitware/vtk.js/IO/Core/ResourceLoader';
+import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps.js';
+import vtkImageArrayMapper from '@kitware/vtk.js/Rendering/Core/ImageArrayMapper.js';
import { unzipSync } from 'fflate';
-import vtkITKHelper from '@kitware/vtk.js/Common/DataModel/ITKHelper';
+import { useContext, useEffect, useMemo, useState } from 'react';
import {
- View,
+ Contexts,
Dataset,
- ShareDataSet,
+ RegisterDataSet,
+ ShareDataSetRoot,
SliceRepresentation,
- Contexts,
+ UseDataSet,
+ View,
} from 'react-vtk-js';
function Slider(props) {
@@ -110,13 +112,11 @@ function CheckBox(props) {
);
}
-
const loadData = async () => {
console.log('Loading itk module...');
loadData.setStatusText('Loading itk module...');
- if(!window.itk) {
- await vtkResourceLoader
- .loadScript(
+ if (!window.itk) {
+ await vtkResourceLoader.loadScript(
'https://cdn.jsdelivr.net/npm/itk-wasm@1.0.0-b.8/dist/umd/itk-wasm.js'
);
}
@@ -144,7 +144,7 @@ const loadData = async () => {
// Read individual dcm files into an array of vtkImageData.
const imageArray = [];
- if(window.itk) {
+ if (window.itk) {
await Promise.all(
dcmFiles.map(async (filename, index) => {
const { image: itkImage, webWorker } =
@@ -167,12 +167,13 @@ const loadData = async () => {
collection.addItem(img);
}
const totalSlices = imageArray.reduce(
- (accumulator, currImage) => currImage.getDimensions()[2] + accumulator, 0
+ (accumulator, currImage) => currImage.getDimensions()[2] + accumulator,
+ 0
);
loadData.setMaxSlicingValue(totalSlices - 1);
loadData.setStatusText('');
return collection;
-}
+};
function Example(props) {
const [statusText, setStatusText] = useState('Loading data, please wait ...');
@@ -187,34 +188,50 @@ function Example(props) {
loadData.setMaxSlicingValue = setMaxKSlice;
loadData.setStatusText = setStatusText;
- useEffect(
- () => {
- const img = mapper.getImage(kSlice);
- const range = img?.getPointData()?.getScalars()?.getRange();
- if(range && range.length == 2) {
- const maxWidth = range[1] - range[0];
- setColorWindow(maxWidth);
- const center = Math.round((range[0] + range[1]) / 2);
- setColorLevel(center);
- }
- },
- [kSlice]
+ const [imageCollection, setImageCollection] = useState(null);
+
+ useEffect(() => {
+ loadData().then((ds) => {
+ window.ds = ds;
+ setImageCollection(ds);
+ });
+ }, []);
+
+ useEffect(() => {
+ const img = mapper.getImage(kSlice);
+ const range = img?.getPointData()?.getScalars()?.getRange();
+ if (range && range.length == 2) {
+ const maxWidth = range[1] - range[0];
+ setColorWindow(maxWidth);
+ const center = Math.round((range[0] + range[1]) / 2);
+ setColorLevel(center);
+ }
+ }, [kSlice, mapper]);
+
+ const cameraParams = useMemo(
+ () => ({
+ position: [400, 400, -1000],
+ viewUp: [0, -1, 0],
+ viewAngle: 75,
+ directionOfProjection: [0, 0, 1],
+ clippingRange: [-100, 100],
+ parallelProjection: false,
+ }),
+ []
);
return (
+
+
+
+
-
-
-
+
);
}
diff --git a/usage/src/Volume/PET_CT_Overlay.css b/usage/src/Volume/PET_CT_Overlay.css
new file mode 100644
index 0000000..0560c90
--- /dev/null
+++ b/usage/src/Volume/PET_CT_Overlay.css
@@ -0,0 +1,73 @@
+label {
+ color: lightGray;
+}
+
+input[type=range] {
+ -webkit-appearance: none;
+}
+
+input[type=range]::-webkit-slider-runnable-track {
+ width: 300px;
+ height: 3px;
+ background: #ddd;
+ border: none;
+ border-radius: 5px;
+}
+
+
+input[type=range]::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ border: none;
+ height: 25px;
+ width: 8px;
+ border-radius: 50%;
+ background: goldenrod;
+ margin-top: -10px;
+}
+
+
+input[type=range]:focus {
+ outline: none;
+}
+
+input[type=range]:focus::-webkit-slider-runnable-track {
+ background: #ccc;
+}
+
+
+input[type=range][orient='vertical'] {
+ position: absolute;
+ zIndex: 1000;
+ margin: 0;
+ padding: 5;
+ width: 2500%;
+ height: 0.5em;
+ transform: translate(-50%, -50%) rotate(-90deg);
+ background: transparent;
+ font: 1em/1 arial, sans-serif;
+}
+
+.loader {
+ border: 16px solid #f3f3f3;
+ border-radius: 50%;
+ border-top: 16px solid blue;
+ border-right: 16px solid green;
+ border-bottom: 16px solid red;
+ width: 120px;
+ height: 120px;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ -webkit-animation: spin 2s linear infinite;
+ animation: spin 2s linear infinite;
+}
+@-webkit-keyframes spin {
+ 0% { -webkit-transform: rotate(0deg); }
+ 100% { -webkit-transform: rotate(360deg); }
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
diff --git a/usage/src/Volume/PET_CT_Overlay.jsx b/usage/src/Volume/PET_CT_Overlay.jsx
new file mode 100644
index 0000000..81214b2
--- /dev/null
+++ b/usage/src/Volume/PET_CT_Overlay.jsx
@@ -0,0 +1,524 @@
+import vtkITKHelper from '@kitware/vtk.js/Common/DataModel/ITKHelper';
+import vtkLiteHttpDataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper/LiteHttpDataAccessHelper';
+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';
+
+import {
+ Contexts,
+ Dataset,
+ MultiViewRoot,
+ RegisterDataSet,
+ ShareDataSetRoot,
+ SliceRepresentation,
+ UseDataSet,
+ 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.setPTValue) {
+ props.setPTValue(value);
+ }
+ setTimeout(view?.renderView, 0);
+ };
+ return (
+
+ );
+}
+
+function DropDown(props) {
+ const view = useContext(Contexts.ViewContext);
+ function onChange(e) {
+ const value = e.currentTarget.value;
+ props.setValue(value);
+ setTimeout(view?.renderView, 0);
+ }
+ return (
+
+ );
+}
+
+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/itk-wasm@1.0.0-b.8/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...');
+ window.setStatusText('Loading itk module...');
+ if (!window.itk) {
+ await vtkResourceLoader.loadScript(
+ 'https://cdn.jsdelivr.net/npm/itk-wasm@1.0.0-b.8/dist/umd/itk-wasm.js'
+ );
+ }
+
+ console.log('Fetching/downloading the input file, 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!');
+ window.setStatusText('Download complete!');
+
+ const zipFileDataArray = new Uint8Array(zipFileData);
+ const decompressedFiles = unzipSync(zipFileDataArray);
+ const ctDCMFiles = [];
+ const ptDCMFiles = [];
+ const PTRe = /PET AC/;
+ const CTRe = /CT IMAGES/;
+ 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);
+ }
+ }
+ });
+
+ 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);
+};
+
+function Example(props) {
+ const [statusText, setStatusText] = useState('Loading data, please wait ...');
+ const [kSlice, setKSlice] = useState(0);
+ const [ptjSlice, setJSlice] = useState(0);
+ const [ctjSlice, setCTJSlice] = useState(0);
+ const [colorWindow, setColorWindow] = useState(2048);
+ const [colorLevel, setColorLevel] = useState(0);
+ const [ptcolorWindow, setPTColorWindow] = useState(69222);
+ const [ptcolorLevel, setPTColorLevel] = useState(34611);
+ const [colorPreset, setColorPreset] = useState('jet');
+ const [opacity, setOpacity] = useState(0.4);
+ const [maxKSlice, setMaxKSlice] = useState(310);
+ const [maxJSlice, setMaxJSlice] = useState(110);
+ const [resliced, setResliced] = useState(false);
+ window.setMaxKSlice = setMaxKSlice;
+ window.setMaxJSlice = setMaxJSlice;
+ window.setStatusText = setStatusText;
+ window.setPTColorWindow = setPTColorWindow;
+ window.setPTColorLevel = setPTColorLevel;
+ window.setResliced = setResliced;
+
+ useEffect(() => {
+ 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]);
+
+ const cdrMin = ptcolorLevel - ptcolorWindow / 2.0;
+ const cdrMax = ptcolorLevel + ptcolorWindow / 2.0;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default Example;
diff --git a/usage/src/Volume/SliceRendering.jsx b/usage/src/Volume/SliceRendering.jsx
index b0c3d75..3f33e28 100644
--- a/usage/src/Volume/SliceRendering.jsx
+++ b/usage/src/Volume/SliceRendering.jsx
@@ -1,12 +1,14 @@
-import React, { useState, useContext } from 'react';
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps.js';
+import { useContext, useState } from 'react';
import {
- View,
- ShareDataSet,
- SliceRepresentation,
- Reader,
Contexts,
+ Reader,
+ RegisterDataSet,
+ ShareDataSetRoot,
+ SliceRepresentation,
+ UseDataSet,
+ View,
VolumeController,
VolumeRepresentation,
} from 'react-vtk-js';
@@ -66,8 +68,10 @@ function DropDown(props) {
...props.style,
}}
>
- {props.options.map((opt) => (
-
+ {props.options.map((opt, idx) => (
+
))}
);
@@ -106,7 +110,7 @@ function CheckBox(props) {
);
}
-function Example(props) {
+function Example() {
const [iSlice, setISlice] = useState(128);
const [jSlice, setJSlice] = useState(128);
const [kSlice, setKSlice] = useState(47);
@@ -116,120 +120,122 @@ function Example(props) {
const [useLookupTableScalarRange, setUseLookupTableScalarRange] =
useState(false);
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
+
);
}
diff --git a/usage/src/Volume/VolumeRendering.jsx b/usage/src/Volume/VolumeRendering.jsx
index 913a557..7907f50 100644
--- a/usage/src/Volume/VolumeRendering.jsx
+++ b/usage/src/Volume/VolumeRendering.jsx
@@ -1,24 +1,30 @@
-import React from 'react';
-
+import vtkXMLImageDataReader from '@kitware/vtk.js/IO/XML/XMLImageDataReader';
+import { useRef } from 'react';
import {
+ Reader,
View,
- VolumeRepresentation,
VolumeController,
- Reader,
+ VolumeRepresentation,
} from 'react-vtk-js';
-function Example(props) {
- const array = [];
- while (array.length < 1000) {
- array.push(Math.random());
- }
+function Example() {
+ const view = useRef();
+ const run = () => {
+ const v = view.current;
+ const camera = v.getCamera();
+ camera.azimuth(0.5);
+ v.requestRender();
+ requestAnimationFrame(run);
+ };
+
return (
-
+
+
diff --git a/usage/src/main.jsx b/usage/src/main.jsx
index b597a44..04df71c 100644
--- a/usage/src/main.jsx
+++ b/usage/src/main.jsx
@@ -1,5 +1,6 @@
import React from 'react';
-import ReactDOM from 'react-dom';
+import { createRoot } from 'react-dom/client';
import App from './App';
-ReactDOM.render(, document.getElementById('root'));
+const root = createRoot(document.getElementById('root'));
+root.render();
diff --git a/usage/src/styles.css b/usage/src/styles.css
new file mode 100644
index 0000000..47a749b
--- /dev/null
+++ b/usage/src/styles.css
@@ -0,0 +1,3 @@
+html, body, #root {
+ height: 100%;
+}
\ No newline at end of file