@@ -3,6 +3,7 @@ import vtkLiteHttpDataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelpe
3
3
import vtkResourceLoader from '@kitware/vtk.js/IO/Core/ResourceLoader' ;
4
4
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps.js' ;
5
5
import { BlendMode } from '@kitware/vtk.js/Rendering/Core/VolumeMapper/Constants.js' ;
6
+ import vtkMath from '@kitware/vtk.js/Common/Core/Math' ;
6
7
import { unzipSync } from 'fflate' ;
7
8
import { useContext , useEffect , useState } from 'react' ;
8
9
import './PET_CT_Overlay.css' ;
@@ -18,15 +19,15 @@ import {
18
19
View ,
19
20
VolumeRepresentation ,
20
21
} 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' ;
21
24
22
25
function Slider ( props ) {
23
26
const view = useContext ( Contexts . ViewContext ) ;
24
27
const onChange = ( e ) => {
25
28
const value = Number ( e . currentTarget . value ) ;
26
29
props . setValue ( value ) ;
27
- if ( props . setCTValue ) {
28
- props . setCTValue ( value * 4 ) ;
29
- }
30
+ props . setPTValue ( value ) ;
30
31
setTimeout ( view ?. renderView , 0 ) ;
31
32
} ;
32
33
return (
@@ -96,23 +97,134 @@ function DropDown(props) {
96
97
) ;
97
98
}
98
99
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 = / P T / ;
174
+ const CTRe = / C T / ;
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
+
99
211
const loadData = async ( ) => {
100
212
console . log ( 'Loading itk module...' ) ;
101
- loadData . setStatusText ( 'Loading itk module...' ) ;
213
+ window . setStatusText ( 'Loading itk module...' ) ;
102
214
if ( ! window . itk ) {
103
215
await vtkResourceLoader . loadScript (
104
216
'https://cdn.jsdelivr.net/npm/[email protected] /dist/umd/itk-wasm.js'
105
217
) ;
106
218
}
107
219
108
220
console . log ( 'Fetching/downloading the input file, please wait...' ) ;
109
- loadData . setStatusText ( 'Loading data, please wait...' ) ;
221
+ window . setStatusText ( 'Loading data, please wait...' ) ;
110
222
const zipFileData = await vtkLiteHttpDataAccessHelper . fetchBinary (
111
223
'https://data.kitware.com/api/v1/folder/661ad10a5165b19d36c87220/download'
112
224
) ;
113
225
114
226
console . log ( 'Fetching/downloading input file done!' ) ;
115
- loadData . setStatusText ( 'Download complete!' ) ;
227
+ window . setStatusText ( 'Download complete!' ) ;
116
228
117
229
const zipFileDataArray = new Uint8Array ( zipFileData ) ;
118
230
const decompressedFiles = unzipSync ( zipFileDataArray ) ;
@@ -142,20 +254,13 @@ const loadData = async () => {
142
254
ptWebWorkers . terminateWorkers ( ) ;
143
255
ptImageData = vtkITKHelper . convertItkToVtkImage ( ptitkImage ) ;
144
256
}
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 ) ;
153
258
} ;
154
259
155
260
function Example ( props ) {
156
261
const [ statusText , setStatusText ] = useState ( 'Loading data, please wait ...' ) ;
157
262
const [ kSlice , setKSlice ] = useState ( 0 ) ;
158
- const [ jSlice , setJSlice ] = useState ( 0 ) ;
263
+ const [ ptjSlice , setJSlice ] = useState ( 0 ) ;
159
264
const [ ctjSlice , setCTJSlice ] = useState ( 0 ) ;
160
265
const [ colorWindow , setColorWindow ] = useState ( 2048 ) ;
161
266
const [ colorLevel , setColorLevel ] = useState ( 0 ) ;
@@ -165,24 +270,28 @@ function Example(props) {
165
270
const [ opacity , setOpacity ] = useState ( 0.4 ) ;
166
271
const [ maxKSlice , setMaxKSlice ] = useState ( 310 ) ;
167
272
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 ;
173
280
174
281
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 ] ) ;
183
290
184
291
return (
185
292
< 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 } />
186
295
< ShareDataSetRoot >
187
296
< RegisterDataSet id = 'ctData' >
188
297
< Dataset dataset = { window . ctData } />
@@ -215,7 +324,7 @@ function Example(props) {
215
324
max = { 4095 }
216
325
value = { colorLevel }
217
326
setValue = { setColorLevel }
218
- style = { { top : '60px' , left : '205px ' } }
327
+ style = { { top : '60px' , left : '5px ' } }
219
328
/>
220
329
< Slider
221
330
label = 'Color Window'
@@ -238,7 +347,7 @@ function Example(props) {
238
347
options = { vtkColorMaps . rgbPresetNames }
239
348
value = { colorPreset }
240
349
setValue = { setColorPreset }
241
- style = { { top : '30px ' , left : '305px ' } }
350
+ style = { { top : '10px ' , left : '405px ' } }
242
351
/>
243
352
< div className = 'loader' id = 'loader' />
244
353
< div
@@ -250,10 +359,10 @@ function Example(props) {
250
359
} }
251
360
>
252
361
< View
253
- id = '0 '
362
+ id = 'axial '
254
363
camera = { {
255
364
position : [ 0 , 0 , 0 ] ,
256
- focalPoint : [ 0 , 0 , - 1 ] ,
365
+ focalPoint : [ 0 , 0 , 1 ] ,
257
366
viewUp : [ 0 , - 1 , 0 ] ,
258
367
parallelProjection : true ,
259
368
} }
@@ -306,10 +415,10 @@ function Example(props) {
306
415
} }
307
416
>
308
417
< View
309
- id = '0 '
418
+ id = 'coronal '
310
419
camera = { {
311
420
position : [ 0 , 0 , 0 ] ,
312
- focalPoint : [ 0 , - 1 , 0 ] ,
421
+ focalPoint : [ 0 , 1 , 0 ] ,
313
422
viewUp : [ 0 , 0 , 1 ] ,
314
423
parallelProjection : true ,
315
424
} }
@@ -318,15 +427,15 @@ function Example(props) {
318
427
< Slider
319
428
label = 'Slice'
320
429
max = { maxJSlice }
321
- value = { jSlice }
322
- setValue = { setJSlice }
323
- setCTValue = { setCTJSlice }
430
+ value = { ctjSlice }
431
+ setValue = { setCTJSlice }
432
+ setPTValue = { setJSlice }
433
+ resliced = { resliced }
324
434
orient = 'vertical'
325
435
style = { { top : '50%' , left : '5%' } }
326
436
/>
327
437
< SliceRepresentation
328
- id = 'pt'
329
- jSlice = { jSlice }
438
+ jSlice = { ptjSlice }
330
439
mapper = { {
331
440
resolveCoincidentTopology : 'Polygon' ,
332
441
resolveCoincidentTopologyPolygonOffsetParameters : {
@@ -364,7 +473,7 @@ function Example(props) {
364
473
} }
365
474
>
366
475
< View
367
- id = '0 '
476
+ id = 'mip3D '
368
477
camera = { {
369
478
position : [ 0 , 0 , 0 ] ,
370
479
focalPoint : [ 0 , 1 , 0 ] ,
0 commit comments