Skip to content

Commit 83a530d

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 83a530d

File tree

2 files changed

+207
-49
lines changed

2 files changed

+207
-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: 198 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,114 @@ 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+
// Rescale
221+
const b = Number(meta.RescaleIntercept)
222+
const m = Number(meta.RescaleSlope)
223+
const hasIntercept = !Number.isNaN(b) && b !== 0
224+
const hasSlope = !Number.isNaN(m) && m !== 1
225+
let rescaleFunction
226+
if (hasIntercept && hasSlope) {
227+
data = data.map((value) => m * value + b)
228+
} else if (hasIntercept) {
229+
data = data.map((value) => value + b)
230+
} else if (hasSlope) {
231+
data = data.map((value) => m * value)
232+
}
233+
234+
return {
235+
imageType,
236+
origin,
237+
spacing,
238+
direction,
239+
size,
240+
data,
241+
}
242+
}
123243
}
124244

125245
class DICOMImage extends DICOMEntity {
@@ -147,6 +267,9 @@ class DICOMImage extends DICOMEntity {
147267
'BitsStored',
148268
'HighBit',
149269
'PixelRepresentation',
270+
'PixelData',
271+
'RescaleIntercept',
272+
'RescaleSlope',
150273
]
151274
}
152275

@@ -223,60 +346,86 @@ async function parseDicomFiles(fileList, ignoreFailedFiles = false) {
223346
return
224347
}
225348

226-
if (element.fragments) {
227-
console.warn(`${tagName} contains fragments which isn't supported`)
228-
return
229-
}
349+
let value = undefined
350+
351+
if (tagName === 'PixelData') {
352+
if (element.fragments) {
353+
let bot = element.basicOffsetTable
354+
// if basic offset table is empty, calculate it
355+
if (bot.length === 0) {
356+
bot = dicomParser.createJPEGBasicOffsetTable(dataSet, element)
357+
}
230358

231-
let vr = element.vr
232-
if (vr === undefined) {
233-
if (tagInfo === undefined || tagInfo.vr === undefined) {
234-
console.warn(`${tagName} vr is unknown, skipping`)
359+
const imageFrames = []
360+
for (let frameIndex = 0; frameIndex < bot.length; frameIndex += 1) {
361+
imageFrames.push(dicomParser.readEncapsulatedImageFrame(dataSet, element, 0, bot))
362+
}
363+
const buffer = imageFrames.length === 1
364+
? imageFrames[0]
365+
: concatenate(imageFrames[0].constructor, imageFrames)
366+
value = {
367+
buffer,
368+
offset: 0,
369+
length: buffer.length,
370+
encapsulated: true,
371+
}
372+
} else {
373+
value = {
374+
buffer: dataSet.byteArray.buffer,
375+
offset: element.dataOffset,
376+
length: element.length,
377+
encapsulated: false,
378+
}
379+
}
380+
} else {
381+
let vr = element.vr
382+
if (vr === undefined) {
383+
if (tagInfo === undefined || tagInfo.vr === undefined) {
384+
console.warn(`${tagName} vr is unknown, skipping`)
385+
}
386+
vr = tagInfo.vr
235387
}
236-
vr = tagInfo.vr
237-
}
238388

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

282431
metaData[tagName] = value

0 commit comments

Comments
 (0)