Skip to content

Commit fc0e075

Browse files
author
Alexis Girault
committed
WIP: read volume (3d image) data
Limitations: - does not support compressed/encapsulated pixel data (see non-raw dicom transfer syntaxes, ex: JPEG2000) - assumes slicethickness is spacing - harcode pixel type and dimension in imageType
1 parent 6e30276 commit fc0e075

File tree

2 files changed

+213
-49
lines changed

2 files changed

+213
-49
lines changed

examples/Dicom/src/index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,21 @@ const outputFileInformation = curry(async function outputFileInformation (output
1717
// Select DICOM serie
1818
outputTextArea.textContent = "Please select serie..."
1919
setupDicomForm(patients, async (serie) => {
20+
console.time('customRead:')
21+
const image1 = serie.getImageData()
22+
console.log(image1)
23+
console.warn(image1.data.length)
24+
console.timeEnd('customRead:')
2025
outputTextArea.textContent = "Loading..."
2126

2227
// Read DICOM serie
28+
console.time('itkRead:')
2329
const files = Object.values(serie.images).map((image) => image.file)
2430
const { image, webWorker } = await readImageDICOMFileSeries(null, files)
2531
webWorker.terminate()
32+
console.log(image)
33+
console.warn(image.data.length)
34+
console.timeEnd('itkRead:')
2635

2736
// Display
2837
function replacer (key, value) {

examples/Dicom/src/parseDicomFiles.js

Lines changed: 204 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ import "regenerator-runtime/runtime";
55

66
import 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+
820
class DICOMEntity {
921
constructor() {
1022
this.metaData = {}
@@ -120,6 +132,138 @@ class DICOMSeries extends DICOMEntity {
120132
}
121133
this.images[imageNumber] = new DICOMImage(metaData, file)
122134
}
135+
136+
getImageData() {
137+
function numArrayFromString(str, separator = '\\') {
138+
const strArray = str.split(separator)
139+
return strArray.map(Number)
140+
}
141+
142+
const slices = Object.values(this.images)
143+
const meta = slices[0].metaData
144+
145+
// Origin
146+
let origin = [0, 0, 0]
147+
if (meta.ImagePositionPatient !== undefined) {
148+
origin = numArrayFromString(meta.ImagePositionPatient)
149+
}
150+
151+
// Spacing
152+
let spacing = [1, 1];
153+
if (meta.PixelSpacing !== undefined) {
154+
spacing = numArrayFromString(meta.PixelSpacing);
155+
}
156+
if (meta.SliceThickness !== undefined) {
157+
spacing.push(Number(meta.SliceThickness))
158+
} else {
159+
spacing.push(1);
160+
}
161+
162+
// Dimensions
163+
const size = [
164+
meta.Rows,
165+
meta.Columns,
166+
Object.keys(this.images).length,
167+
]
168+
169+
// Direction matrix (3x3)
170+
let directionCosines = [1, 0, 0, 0, 1, 0]
171+
if (meta.ImageOrientationPatient !== undefined) {
172+
directionCosines = numArrayFromString(meta.ImageOrientationPatient)
173+
}
174+
const iDirCos = directionCosines.slice(0, 3)
175+
const jDirCos = directionCosines.slice(3, 6)
176+
const kDirCos = [
177+
iDirCos[1] * jDirCos[2] - iDirCos[2] * jDirCos[1],
178+
iDirCos[2] * jDirCos[0] - iDirCos[0] * jDirCos[2],
179+
iDirCos[0] * jDirCos[1] - iDirCos[1] * jDirCos[0],
180+
]
181+
const direction = {
182+
rows: 3,
183+
columns: 3,
184+
data: [
185+
iDirCos[0], jDirCos[0], kDirCos[0],
186+
iDirCos[1], jDirCos[1], kDirCos[1],
187+
iDirCos[2], jDirCos[2], kDirCos[2],
188+
],
189+
}
190+
191+
// Pixel data type
192+
let slope = 1
193+
if (meta.RescaleSlope !== undefined) {
194+
slope = Number(meta.RescaleSlope)
195+
}
196+
let intercept = 0
197+
if (meta.RescaleIntercept !== undefined) {
198+
intercept = Number(meta.RescaleIntercept)
199+
}
200+
let smallestValue = 0
201+
if (meta.SmallestImagePixelValue !== undefined) {
202+
smallestValue = Number(meta.SmallestImagePixelValue)
203+
}
204+
const hasNegativeValues = (slope < 0) || (smallestValue + intercept) < 0;
205+
const unsigned = meta.PixelRepresentation === 0 && !hasNegativeValues;
206+
const bits = meta.BitsAllocated
207+
let ArrayType
208+
let intType
209+
switch (bits) {
210+
case 8:
211+
ArrayType = unsigned ? Uint8Array : Int8Array
212+
intType = unsigned ? 'uint8_t' : 'int8_t'
213+
break
214+
case 16:
215+
ArrayType = unsigned ? Uint16Array : Int16Array
216+
intType = unsigned ? 'uint16_t' : 'int16_t'
217+
break
218+
case 32:
219+
ArrayType = unsigned ? Uint32Array : Int32Array
220+
intType = unsigned ? 'uint32_t' : 'int32_t'
221+
break
222+
default:
223+
throw Error(`Unknown pixel bit type (${bits})`)
224+
}
225+
226+
// Image info
227+
const imageType = {
228+
dimension: 3,
229+
componentType: intType,
230+
pixelType: 1, // TODO: based on meta.PhotometricInterpretation?
231+
components: meta.SamplesPerPixel,
232+
}
233+
234+
// Dataview on pixel data
235+
const pixelDataArrays = slices.map((image) => {
236+
const value = image.metaData.PixelData
237+
return new ArrayType(value.buffer, value.offset)
238+
})
239+
240+
// Concatenate all pixel data
241+
const data = concatenate(ArrayType, pixelDataArrays)
242+
243+
// Rescale
244+
const b = Number(meta.RescaleIntercept)
245+
const m = Number(meta.RescaleSlope)
246+
const hasIntercept = !Number.isNaN(b) && b !== 0
247+
const hasSlope = !Number.isNaN(m) && m !== 1
248+
let rescaleFunction
249+
if (hasIntercept && hasSlope) {
250+
data = data.map((value) => m * value + b)
251+
} else if (hasIntercept) {
252+
data = data.map((value) => value + b)
253+
} else if (hasSlope) {
254+
data = data.map((value) => m * value)
255+
}
256+
257+
return {
258+
imageType,
259+
name: "Image",
260+
origin,
261+
spacing,
262+
direction,
263+
size,
264+
data,
265+
}
266+
}
123267
}
124268

125269
class DICOMImage extends DICOMEntity {
@@ -147,6 +291,9 @@ class DICOMImage extends DICOMEntity {
147291
'BitsStored',
148292
'HighBit',
149293
'PixelRepresentation',
294+
'PixelData',
295+
'RescaleIntercept',
296+
'RescaleSlope',
150297
]
151298
}
152299

@@ -223,60 +370,68 @@ async function parseDicomFiles(fileList, ignoreFailedFiles = false) {
223370
return
224371
}
225372

226-
if (element.fragments) {
227-
console.warn(`${tagName} contains fragments which isn't supported`)
228-
return
229-
}
373+
let value = undefined
230374

231-
let vr = element.vr
232-
if (vr === undefined) {
233-
if (tagInfo === undefined || tagInfo.vr === undefined) {
234-
console.warn(`${tagName} vr is unknown, skipping`)
375+
if (tagName === 'PixelData') {
376+
if (element.fragments) {
377+
throw new Error('Fragments/encapsulated pixel data is not supported.');
378+
} else {
379+
value = {
380+
buffer: dataSet.byteArray.buffer,
381+
offset: element.dataOffset,
382+
length: element.length,
383+
}
384+
}
385+
} else {
386+
let vr = element.vr
387+
if (vr === undefined) {
388+
if (tagInfo === undefined || tagInfo.vr === undefined) {
389+
console.warn(`${tagName} vr is unknown, skipping`)
390+
} else {
391+
vr = tagInfo.vr
392+
}
235393
}
236-
vr = tagInfo.vr
237-
}
238394

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) {
395+
switch (vr) {
396+
case 'US':
269397
value = dataSet.uint16(tag)
270-
} else if (element.length === 4) {
398+
break
399+
case 'SS':
400+
value = dataSet.int16(tag)
401+
break
402+
case 'UL':
271403
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
404+
break
405+
case 'US':
406+
value = dataSet.int32(tag)
407+
break
408+
case 'FD':
409+
value = dataSet.double(tag)
410+
break
411+
case 'FL':
412+
value = dataSet.float(tag)
413+
break
414+
case 'AT':
415+
value = `(${dataSet.uint16(tag, 0)},${dataSet.uint16(tag, 1)})`
416+
break
417+
case 'OB':
418+
case 'OW':
419+
case 'UN':
420+
case 'OF':
421+
case 'UT':
422+
// TODO: binary data? is this correct?
423+
if (element.length === 2) {
424+
value = dataSet.uint16(tag)
425+
} else if (element.length === 4) {
426+
value = dataSet.uint32(tag)
427+
} else {
428+
return
429+
}
430+
break
431+
default: //string
432+
value = dataSet.string(tag)
433+
break
434+
}
280435
}
281436

282437
metaData[tagName] = value

0 commit comments

Comments
 (0)