@@ -5,6 +5,18 @@ import "regenerator-runtime/runtime";
5
5
6
6
import DICOM_TAG_DICT from './dicomTags'
7
7
8
+ function concatenate ( resultConstructor , arrays ) {
9
+ const totalLength = arrays . reduce ( ( total , arr ) => {
10
+ return total + arr . length
11
+ } , 0 ) ;
12
+ const result = new resultConstructor ( totalLength ) ;
13
+ arrays . reduce ( ( offset , arr ) => {
14
+ result . set ( arr , offset ) ;
15
+ return offset + arr . length ;
16
+ } , 0 ) ;
17
+ return result ;
18
+ }
19
+
8
20
class DICOMEntity {
9
21
constructor ( ) {
10
22
this . metaData = { }
@@ -110,6 +122,7 @@ class DICOMSeries extends DICOMEntity {
110
122
this . images = { }
111
123
this . extractTags ( metaData )
112
124
this . addMetaData ( metaData , file )
125
+ this . constructedImageData = 0
113
126
}
114
127
115
128
addMetaData ( metaData , file ) {
@@ -120,6 +133,112 @@ class DICOMSeries extends DICOMEntity {
120
133
}
121
134
this . images [ imageNumber ] = new DICOMImage ( metaData , file )
122
135
}
136
+
137
+ getImageData ( ) {
138
+ if ( this . constructedImageData === 1 ) {
139
+ console . warn (
140
+ 'DICOMSeries.getImageData was called more than once. ' +
141
+ 'Since DICOMSeries does not store the image data ' +
142
+ '(to save memory while keeping access to its metadata), ' +
143
+ 'getImageData recomputes the image data each time it is called. ' +
144
+ 'Users of this API should cache the resulting image data ' +
145
+ 'if they need to access it again to improve performance.'
146
+ )
147
+ }
148
+
149
+ function numArrayFromString ( str , separator = '\\' ) {
150
+ const strArray = str . split ( separator )
151
+ return strArray . map ( Number )
152
+ }
153
+
154
+ const slices = Object . values ( this . images )
155
+ const meta = slices [ 0 ] . metaData
156
+
157
+ // Origin
158
+ const origin = numArrayFromString ( meta . ImagePositionPatient )
159
+
160
+ // Spacing
161
+ const spacing = numArrayFromString ( meta . PixelSpacing )
162
+ spacing . push ( Number ( meta . SliceThickness ) )
163
+
164
+ // Dimensions
165
+ const size = [
166
+ meta . Rows ,
167
+ meta . Columns ,
168
+ Object . keys ( this . images ) . length
169
+ ]
170
+
171
+ // Direction matrix (3x3)
172
+ const directionCosines = numArrayFromString ( meta . ImageOrientationPatient )
173
+ const iDirCos = directionCosines . slice ( 0 , 3 )
174
+ const jDirCos = directionCosines . slice ( 3 , 6 )
175
+ const kDirCos = [
176
+ iDirCos [ 1 ] * jDirCos [ 2 ] - iDirCos [ 2 ] * jDirCos [ 1 ] ,
177
+ iDirCos [ 2 ] * jDirCos [ 0 ] - iDirCos [ 0 ] * jDirCos [ 2 ] ,
178
+ iDirCos [ 0 ] * jDirCos [ 1 ] - iDirCos [ 1 ] * jDirCos [ 0 ] ,
179
+ ]
180
+ const direction = [
181
+ iDirCos [ 0 ] , jDirCos [ 0 ] , kDirCos [ 0 ] ,
182
+ iDirCos [ 1 ] , jDirCos [ 1 ] , kDirCos [ 1 ] ,
183
+ iDirCos [ 2 ] , jDirCos [ 2 ] , kDirCos [ 2 ] ,
184
+ ]
185
+
186
+ // Image info
187
+ const imageType = {
188
+ // TODO: should be based on PhotometricInterpretation instead?
189
+ // pixelType: meta.PixelRepresentation,
190
+ components : meta . SamplesPerPixel
191
+ }
192
+
193
+ // Pixel data type
194
+ const unsigned = ( meta . PixelRepresentation === 0 )
195
+ const bits = meta . BitsAllocated // TODO: or stored?
196
+ let ArrayType
197
+ switch ( bits ) {
198
+ case 8 :
199
+ ArrayType = unsigned ? Uint8Array : Int8Array
200
+ break
201
+ case 16 :
202
+ ArrayType = unsigned ? Uint16Array : Int16Array
203
+ break
204
+ case 32 :
205
+ ArrayType = unsigned ? Uint32Array : Int32Array
206
+ break
207
+ default :
208
+ throw Error ( `Unknown pixel bit type (${ bits } )` )
209
+ }
210
+
211
+ // Pixel data
212
+ const pixelDataArrays = slices . map ( ( image ) => {
213
+ const value = image . metaData . PixelData
214
+ return new ArrayType ( value . buffer , value . offset )
215
+ } )
216
+ let data = concatenate ( ArrayType , pixelDataArrays )
217
+
218
+ // Rescale
219
+ // TODO: ArrayType can change sign with this
220
+ const b = Number ( meta . RescaleIntercept )
221
+ const m = Number ( meta . RescaleSlope )
222
+ const hasIntercept = ! isNaN ( b ) && b !== 0
223
+ const hasSlope = ! isNaN ( m ) && m !== 1
224
+ if ( hasIntercept && hasSlope ) {
225
+ data = data . map ( ( SV ) => m * SV + b )
226
+ } else if ( hasIntercept ) {
227
+ data = data . map ( ( SV ) => SV + b )
228
+ } else if ( hasSlope ) {
229
+ data = data . map ( ( SV ) => m * SV )
230
+ }
231
+
232
+ this . constructedImageData = true
233
+ return {
234
+ imageType,
235
+ origin,
236
+ spacing,
237
+ direction,
238
+ size,
239
+ data
240
+ }
241
+ }
123
242
}
124
243
125
244
class DICOMImage extends DICOMEntity {
@@ -147,6 +266,9 @@ class DICOMImage extends DICOMEntity {
147
266
'BitsStored' ,
148
267
'HighBit' ,
149
268
'PixelRepresentation' ,
269
+ 'PixelData' ,
270
+ 'RescaleIntercept' ,
271
+ 'RescaleSlope' ,
150
272
]
151
273
}
152
274
@@ -228,55 +350,63 @@ async function parseDicomFiles(fileList, ignoreFailedFiles = false) {
228
350
return
229
351
}
230
352
231
- let vr = element . vr
232
- if ( vr === undefined ) {
233
- if ( tagInfo === undefined || tagInfo . vr === undefined ) {
234
- console . warn ( `${ tagName } vr is unknown, skipping` )
353
+ let value = undefined
354
+
355
+ if ( tagName === 'PixelData' ) {
356
+ value = {
357
+ buffer : dataSet . byteArray . buffer ,
358
+ offset : element . dataOffset ,
359
+ length : element . length
360
+ }
361
+ } else {
362
+ let vr = element . vr
363
+ if ( vr === undefined ) {
364
+ if ( tagInfo === undefined || tagInfo . vr === undefined ) {
365
+ console . warn ( `${ tagName } vr is unknown, skipping` )
366
+ }
367
+ vr = tagInfo . vr
235
368
}
236
- vr = tagInfo . vr
237
- }
238
369
239
- let value = undefined
240
- switch ( vr ) {
241
- case 'US' :
242
- value = dataSet . uint16 ( tag )
243
- break
244
- case 'SS' :
245
- value = dataSet . int16 ( tag )
246
- break
247
- case 'UL' :
248
- value = dataSet . uint32 ( tag )
249
- break
250
- case 'US' :
251
- value = dataSet . int32 ( tag )
252
- break
253
- case 'FD' :
254
- value = dataSet . double ( tag )
255
- break
256
- case 'FL' :
257
- value = dataSet . float ( tag )
258
- break
259
- case 'AT' :
260
- value = `(${ dataSet . uint16 ( tag , 0 ) } ,${ dataSet . uint16 ( tag , 1 ) } )`
261
- break
262
- case 'OB' :
263
- case 'OW' :
264
- case 'UN' :
265
- case 'OF' :
266
- case 'UT' :
267
- // TODO: binary data? is this correct?
268
- if ( element . length === 2 ) {
370
+ switch ( vr ) {
371
+ case 'US' :
269
372
value = dataSet . uint16 ( tag )
270
- } else if ( element . length === 4 ) {
373
+ break
374
+ case 'SS' :
375
+ value = dataSet . int16 ( tag )
376
+ break
377
+ case 'UL' :
271
378
value = dataSet . uint32 ( tag )
272
- } else {
273
- // don't store binary data, only meta data
274
- return
275
- }
276
- break
277
- default : //string
278
- value = dataSet . string ( tag )
279
- break
379
+ break
380
+ case 'US' :
381
+ value = dataSet . int32 ( tag )
382
+ break
383
+ case 'FD' :
384
+ value = dataSet . double ( tag )
385
+ break
386
+ case 'FL' :
387
+ value = dataSet . float ( tag )
388
+ break
389
+ case 'AT' :
390
+ value = `(${ dataSet . uint16 ( tag , 0 ) } ,${ dataSet . uint16 ( tag , 1 ) } )`
391
+ break
392
+ case 'OB' :
393
+ case 'OW' :
394
+ case 'UN' :
395
+ case 'OF' :
396
+ case 'UT' :
397
+ // TODO: binary data? is this correct?
398
+ if ( element . length === 2 ) {
399
+ value = dataSet . uint16 ( tag )
400
+ } else if ( element . length === 4 ) {
401
+ value = dataSet . uint32 ( tag )
402
+ } else {
403
+ return
404
+ }
405
+ break
406
+ default : //string
407
+ value = dataSet . string ( tag )
408
+ break
409
+ }
280
410
}
281
411
282
412
metaData [ tagName ] = value
0 commit comments