@@ -5,6 +5,18 @@ import "regenerator-runtime/runtime";
55
66import DICOM_TAG_DICT from './dicomTags'
77
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+
820class DICOMEntity {
921 constructor ( ) {
1022 this . metaData = { }
@@ -110,6 +122,7 @@ class DICOMSeries extends DICOMEntity {
110122 this . images = { }
111123 this . extractTags ( metaData )
112124 this . addMetaData ( metaData , file )
125+ this . constructedImageData = 0
113126 }
114127
115128 addMetaData ( metaData , file ) {
@@ -120,6 +133,112 @@ class DICOMSeries extends DICOMEntity {
120133 }
121134 this . images [ imageNumber ] = new DICOMImage ( metaData , file )
122135 }
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+ }
123242}
124243
125244class DICOMImage extends DICOMEntity {
@@ -147,6 +266,9 @@ class DICOMImage extends DICOMEntity {
147266 'BitsStored' ,
148267 'HighBit' ,
149268 'PixelRepresentation' ,
269+ 'PixelData' ,
270+ 'RescaleIntercept' ,
271+ 'RescaleSlope' ,
150272 ]
151273 }
152274
@@ -228,55 +350,63 @@ async function parseDicomFiles(fileList, ignoreFailedFiles = false) {
228350 return
229351 }
230352
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
235368 }
236- vr = tagInfo . vr
237- }
238369
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' :
269372 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' :
271378 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+ }
280410 }
281411
282412 metaData [ tagName ] = value
0 commit comments