-
Notifications
You must be signed in to change notification settings - Fork 1
/
parse.coffee
89 lines (85 loc) · 3.69 KB
/
parse.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
define 'parse', ['lodash', 'niftiParser', 'ndarray'], (_, niftiParser, ndarray) ->
# Unquoted whitespace matcher (doesn't handle escapes).
WHITESPACE_REGEX = /\s+(?=([^"]*"[^"]*")*[^"]*$)/g
# Parses the image file content. Returns the
# {header, data}, where:
# * *header* is an object {nifti, nrrd, dicom}, where *nifti* is
# is the NIfTI header, NRRD is the NRRD header and *dicom* is
# the embedded DICOM meta-data object
# * *data* is the image binary ndarray in dimension order
# [x, y, z, time], where *z* is the slice index and *time*
# is the volume index.
#
# @buffer the binary image file ArrayBuffer
# @returns the parsed image {header, data} object
(buffer) ->
# Extract the NIfTI header.
niftiHeader = niftiParser.parseNIfTIHeader(buffer)
# Extract the NRRD header.
nrrdHeader = niftiParser.parseNRRDHeader(buffer)
# Extract the embedded DICOM metadata.
extensions = niftiParser.parseHeaderExtensions(buffer)
if extensions.length > 1
throw new Error("The NIfTI file has more than one extension")
if extensions.length == 1
extension = extensions[0]
# Note: the conventional idiom:
# String.fromCharCode.apply(null, data)
# results in a stack overflow. The work-around is to convert
# the characters one byte at a time.
chars = (String.fromCharCode(c) for c in extension.data)
# Get rid of whitespace.
json = chars.join('').replace(WHITESPACE_REGEX, '')
# Note: JSON parse fails with message that it can't find
# a JSON object. The work-around to this work-around bug is
# to search for the substrings we want and hammer together
# a minimal JSON string.
# TODO - unravel this mess.
windowCenterRegex = /("WindowCenter":\[[^\]]+\])/
windowWidthRegex = /("WindowWidth":\[[^\]]+\])/
matches = windowCenterRegex.exec(json)
if matches?
item1 = matches[1]
matches = windowWidthRegex.exec(json)
if matches?
item2 = matches[1]
json = '{' + [item1, item2].join(',') + '}'
# Parse the JSON.
dicom = JSON.parse(json)
else
dicom = null
else
dicom = null
else
dicom = null
# Extract the image binary data.
parsed = niftiParser.parse(buffer)
# The binary image ndarray. Per the nifti-js site, the NIfTI
# header sizes attribute is the inverse of the raw NIfTI
# ndarray order, and suggests reversing the sizes in the
# ndarray constructor argument below. There is (of course)
# no rationale given for this. We dispense with this advice
# and retain the input sizes, which results in a data shape
# consistent with the input shape.
#
# The dcmstack layout is in [time, x, y, z] order. This differs
# from the expected [x, y, z, time] order described
# in, e.g., https://brainder.org/2012/09/23/the-nifti-file-format/,
# We retain the input layout, which has ramifications for the
# array slicing and dicing in sliceDisplay.coffee.
#
# Note: the js ndarray default stride differs from the expected
# nibabel default stride, after accounting for the difference
# in the underlying datum size (byte for nibabel, int16 for js).
# Reversing the sizes as described above still results in an
# incorrect stride. The work-around is to calculate the stride
# by hand.
# TODO - bring this up with the ndarray dev team.
stride = (parsed.sizes[0...i].reduce(_.multiply, 1) for i in [0...parsed.sizes.length])
data = ndarray(parsed.data, parsed.sizes, stride)
# Return the image {header, data} object.
header:
nifti: niftiHeader
nrrd: nrrdHeader
dicom: dicom
data: data