Skip to content

Commit dd03b20

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 8fa5c7f commit dd03b20

File tree

2 files changed

+233
-49
lines changed

2 files changed

+233
-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: 224 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,140 @@ 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+
const origin = numArrayFromString(meta.ImagePositionPatient)
147+
148+
// Spacing
149+
const spacing = numArrayFromString(meta.PixelSpacing)
150+
spacing.push(Number(meta.SliceThickness)) // TODO: or SpacingBetweenSlices?
151+
152+
// Dimensions
153+
const size = [
154+
meta.Rows,
155+
meta.Columns,
156+
Object.keys(this.images).length,
157+
]
158+
159+
// Direction matrix (3x3)
160+
const directionCosines = numArrayFromString(meta.ImageOrientationPatient)
161+
const iDirCos = directionCosines.slice(0, 3)
162+
const jDirCos = directionCosines.slice(3, 6)
163+
const kDirCos = [
164+
iDirCos[1] * jDirCos[2] - iDirCos[2] * jDirCos[1],
165+
iDirCos[2] * jDirCos[0] - iDirCos[0] * jDirCos[2],
166+
iDirCos[0] * jDirCos[1] - iDirCos[1] * jDirCos[0],
167+
]
168+
const direction = {
169+
data: [
170+
iDirCos[0], jDirCos[0], kDirCos[0],
171+
iDirCos[1], jDirCos[1], kDirCos[1],
172+
iDirCos[2], jDirCos[2], kDirCos[2],
173+
],
174+
}
175+
176+
// Pixel data type
177+
const unsigned = (meta.PixelRepresentation === 0) && (meta.RescaleIntercept > 0)
178+
const bits = meta.BitsAllocated
179+
let ArrayType
180+
let intType
181+
switch (bits) {
182+
case 8:
183+
ArrayType = unsigned ? Uint8Array : Int8Array
184+
intType = unsigned ? 'uint8_t' : 'int8_t'
185+
break
186+
case 16:
187+
ArrayType = unsigned ? Uint16Array : Int16Array
188+
intType = unsigned ? 'uint16_t' : 'int16_t'
189+
break
190+
case 32:
191+
ArrayType = unsigned ? Uint32Array : Int32Array
192+
intType = unsigned ? 'uint32_t' : 'int32_t'
193+
break
194+
default:
195+
throw Error(`Unknown pixel bit type (${bits})`)
196+
}
197+
198+
// Image info
199+
const imageType = {
200+
dimension: 3,
201+
componentType: intType,
202+
pixelType: 1, // TODO: based on meta.PhotometricInterpretation?
203+
components: meta.SamplesPerPixel,
204+
}
205+
206+
// Dataview on pixel data
207+
const pixelDataArrays = slices.map((image) => {
208+
const value = image.metaData.PixelData
209+
if (value.buffer.constructor === ArrayType && value.offset === 0) {
210+
return value.buffer
211+
}
212+
return new ArrayType(value.buffer, value.offset)
213+
})
214+
215+
// Concatenate all pixel data
216+
const data = pixelDataArrays.length === 1
217+
? pixelDataArrays[0]
218+
: concatenate(ArrayType, pixelDataArrays)
219+
220+
// Masking bits
221+
let maskFunction
222+
if (meta.BitsStored !== bits) {
223+
let mask = ''
224+
for (let i = 0; i < bits; i += 1) {
225+
if (i < meta.HighBit - meta.BitsStored || i > meta.HighBit) {
226+
mask += '0'
227+
} else {
228+
mask += '1'
229+
}
230+
}
231+
maskFunction = (value, index) => { data[index] = value & mask }
232+
}
233+
234+
// Rescale
235+
const b = Number(meta.RescaleIntercept)
236+
const m = Number(meta.RescaleSlope)
237+
const hasIntercept = !Number.isNaN(b) && b !== 0
238+
const hasSlope = !Number.isNaN(m) && m !== 1
239+
let rescaleFunction
240+
if (hasIntercept && hasSlope) {
241+
rescaleFunction = (value, index) => { data[index] = m * value + b }
242+
} else if (hasIntercept) {
243+
rescaleFunction = (value, index) => { data[index] = value + b }
244+
} else if (hasSlope) {
245+
rescaleFunction = (value, index) => { data[index] = m * value }
246+
}
247+
248+
// Apply transformations if needed
249+
if (maskFunction && rescaleFunction) {
250+
data.forEach((_, index) => {
251+
maskFunction(data[index], index)
252+
rescaleFunction(data[index], index)
253+
})
254+
} else if (maskFunction) {
255+
data.forEach(maskFunction)
256+
} else if (rescaleFunction) {
257+
data.forEach(rescaleFunction)
258+
}
259+
260+
return {
261+
imageType,
262+
origin,
263+
spacing,
264+
direction,
265+
size,
266+
data,
267+
}
268+
}
123269
}
124270

125271
class DICOMImage extends DICOMEntity {
@@ -147,6 +293,9 @@ class DICOMImage extends DICOMEntity {
147293
'BitsStored',
148294
'HighBit',
149295
'PixelRepresentation',
296+
'PixelData',
297+
'RescaleIntercept',
298+
'RescaleSlope',
150299
]
151300
}
152301

@@ -223,60 +372,86 @@ async function parseDicomFiles(fileList, ignoreFailedFiles = false) {
223372
return
224373
}
225374

226-
if (element.fragments) {
227-
console.warn(`${tagName} contains fragments which isn't supported`)
228-
return
229-
}
375+
let value = undefined
376+
377+
if (tagName === 'PixelData') {
378+
if (element.fragments) {
379+
let bot = element.basicOffsetTable
380+
// if basic offset table is empty, calculate it
381+
if (bot.length === 0) {
382+
bot = dicomParser.createJPEGBasicOffsetTable(dataSet, element)
383+
}
230384

231-
let vr = element.vr
232-
if (vr === undefined) {
233-
if (tagInfo === undefined || tagInfo.vr === undefined) {
234-
console.warn(`${tagName} vr is unknown, skipping`)
385+
const imageFrames = []
386+
for (let frameIndex = 0; frameIndex < bot.length; frameIndex += 1) {
387+
imageFrames.push(dicomParser.readEncapsulatedImageFrame(dataSet, element, 0, bot))
388+
}
389+
const buffer = imageFrames.length === 1
390+
? imageFrames[0]
391+
: concatenate(imageFrames[0].constructor, imageFrames)
392+
value = {
393+
buffer,
394+
offset: 0,
395+
length: buffer.length,
396+
encapsulated: true,
397+
}
398+
} else {
399+
value = {
400+
buffer: dataSet.byteArray.buffer,
401+
offset: element.dataOffset,
402+
length: element.length,
403+
encapsulated: false,
404+
}
405+
}
406+
} else {
407+
let vr = element.vr
408+
if (vr === undefined) {
409+
if (tagInfo === undefined || tagInfo.vr === undefined) {
410+
console.warn(`${tagName} vr is unknown, skipping`)
411+
}
412+
vr = tagInfo.vr
235413
}
236-
vr = tagInfo.vr
237-
}
238414

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) {
415+
switch (vr) {
416+
case 'US':
269417
value = dataSet.uint16(tag)
270-
} else if (element.length === 4) {
418+
break
419+
case 'SS':
420+
value = dataSet.int16(tag)
421+
break
422+
case 'UL':
271423
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
424+
break
425+
case 'US':
426+
value = dataSet.int32(tag)
427+
break
428+
case 'FD':
429+
value = dataSet.double(tag)
430+
break
431+
case 'FL':
432+
value = dataSet.float(tag)
433+
break
434+
case 'AT':
435+
value = `(${dataSet.uint16(tag, 0)},${dataSet.uint16(tag, 1)})`
436+
break
437+
case 'OB':
438+
case 'OW':
439+
case 'UN':
440+
case 'OF':
441+
case 'UT':
442+
// TODO: binary data? is this correct?
443+
if (element.length === 2) {
444+
value = dataSet.uint16(tag)
445+
} else if (element.length === 4) {
446+
value = dataSet.uint32(tag)
447+
} else {
448+
return
449+
}
450+
break
451+
default: //string
452+
value = dataSet.string(tag)
453+
break
454+
}
280455
}
281456

282457
metaData[tagName] = value

0 commit comments

Comments
 (0)