Skip to content

Commit eac4f79

Browse files
Merge pull request #512 from MarcCote/bf_support_trk_v1
MRG: streamlines support reading TRK (version 1) Add compatibility with trk file version 1; assume identity affine for v1 files.
2 parents 7044ee4 + fcd7376 commit eac4f79

File tree

2 files changed

+79
-73
lines changed

2 files changed

+79
-73
lines changed

nibabel/streamlines/tests/test_trk.py

Lines changed: 57 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import sys
23
import copy
34
import unittest
45
import numpy as np
@@ -99,68 +100,94 @@ def test_load_complex_file(self):
99100
trk = TrkFile.load(DATA['complex_trk_fname'], lazy_load=lazy_load)
100101
assert_tractogram_equal(trk.tractogram, DATA['complex_tractogram'])
101102

103+
def trk_with_bytes(self, trk_key='simple_trk_fname', endian='<'):
104+
""" Return example trk file bytes and struct view onto bytes """
105+
with open(DATA[trk_key], 'rb') as fobj:
106+
trk_bytes = fobj.read()
107+
dt = trk_module.header_2_dtype.newbyteorder(endian)
108+
trk_struct = np.ndarray((1,), dt, buffer=trk_bytes)
109+
trk_struct.flags.writeable = True
110+
return trk_struct, trk_bytes
111+
102112
def test_load_file_with_wrong_information(self):
103113
trk_file = open(DATA['simple_trk_fname'], 'rb').read()
104114

105115
# Simulate a TRK file where `count` was not provided.
106-
count = np.array(0, dtype="int32").tostring()
107-
new_trk_file = trk_file[:1000-12] + count + trk_file[1000-8:]
108-
trk = TrkFile.load(BytesIO(new_trk_file), lazy_load=False)
116+
trk_struct, trk_bytes = self.trk_with_bytes()
117+
trk_struct[Field.NB_STREAMLINES] = 0
118+
trk = TrkFile.load(BytesIO(trk_bytes), lazy_load=False)
109119
assert_tractogram_equal(trk.tractogram, DATA['simple_tractogram'])
110120

111121
# Simulate a TRK where `vox_to_ras` is not recorded (i.e. all zeros).
112-
vox_to_ras = np.zeros((4, 4), dtype=np.float32).tostring()
113-
new_trk_file = trk_file[:440] + vox_to_ras + trk_file[440+64:]
122+
trk_struct, trk_bytes = self.trk_with_bytes()
123+
trk_struct[Field.VOXEL_TO_RASMM] = np.zeros((4, 4))
114124
with clear_and_catch_warnings(record=True, modules=[trk_module]) as w:
115-
trk = TrkFile.load(BytesIO(new_trk_file))
125+
trk = TrkFile.load(BytesIO(trk_bytes))
116126
assert_equal(len(w), 1)
117127
assert_true(issubclass(w[0].category, HeaderWarning))
118128
assert_true("identity" in str(w[0].message))
119129
assert_array_equal(trk.affine, np.eye(4))
120130

121131
# Simulate a TRK where `vox_to_ras` is invalid.
122-
vox_to_ras = np.zeros((4, 4), dtype=np.float32)
123-
vox_to_ras[3, 3] = 1
124-
vox_to_ras = vox_to_ras.tostring()
125-
new_trk_file = trk_file[:440] + vox_to_ras + trk_file[440+64:]
132+
trk_struct, trk_bytes = self.trk_with_bytes()
133+
trk_struct[Field.VOXEL_TO_RASMM] = np.diag([0, 0, 0, 1])
126134
with clear_and_catch_warnings(record=True, modules=[trk_module]) as w:
127-
assert_raises(HeaderError, TrkFile.load, BytesIO(new_trk_file))
135+
assert_raises(HeaderError, TrkFile.load, BytesIO(trk_bytes))
128136

129137
# Simulate a TRK file where `voxel_order` was not provided.
130-
voxel_order = np.zeros(1, dtype="|S3").tostring()
131-
new_trk_file = trk_file[:948] + voxel_order + trk_file[948+3:]
138+
trk_struct, trk_bytes = self.trk_with_bytes()
139+
trk_struct[Field.VOXEL_ORDER] = b''
132140
with clear_and_catch_warnings(record=True, modules=[trk_module]) as w:
133-
TrkFile.load(BytesIO(new_trk_file))
141+
TrkFile.load(BytesIO(trk_bytes))
134142
assert_equal(len(w), 1)
135143
assert_true(issubclass(w[0].category, HeaderWarning))
136144
assert_true("LPS" in str(w[0].message))
137145

138146
# Simulate a TRK file with an unsupported version.
139-
version = np.int32(123).tostring()
140-
new_trk_file = trk_file[:992] + version + trk_file[992+4:]
141-
assert_raises(HeaderError, TrkFile.load, BytesIO(new_trk_file))
147+
trk_struct, trk_bytes = self.trk_with_bytes()
148+
trk_struct['version'] = 123
149+
assert_raises(HeaderError, TrkFile.load, BytesIO(trk_bytes))
142150

143151
# Simulate a TRK file with a wrong hdr_size.
144-
hdr_size = np.int32(1234).tostring()
145-
new_trk_file = trk_file[:996] + hdr_size + trk_file[996+4:]
146-
assert_raises(HeaderError, TrkFile.load, BytesIO(new_trk_file))
152+
trk_struct, trk_bytes = self.trk_with_bytes()
153+
trk_struct['hdr_size'] = 1234
154+
assert_raises(HeaderError, TrkFile.load, BytesIO(trk_bytes))
147155

148156
# Simulate a TRK file with a wrong scalar_name.
149-
trk_file = open(DATA['complex_trk_fname'], 'rb').read()
150-
noise = np.int32(42).tostring()
151-
new_trk_file = trk_file[:47] + noise + trk_file[47+4:]
152-
assert_raises(HeaderError, TrkFile.load, BytesIO(new_trk_file))
157+
trk_struct, trk_bytes = self.trk_with_bytes('complex_trk_fname')
158+
trk_struct['scalar_name'][0, 0] = b'colors\x003\x004'
159+
assert_raises(HeaderError, TrkFile.load, BytesIO(trk_bytes))
153160

154161
# Simulate a TRK file with a wrong property_name.
155-
noise = np.int32(42).tostring()
156-
new_trk_file = trk_file[:254] + noise + trk_file[254+4:]
157-
assert_raises(HeaderError, TrkFile.load, BytesIO(new_trk_file))
162+
trk_struct, trk_bytes = self.trk_with_bytes('complex_trk_fname')
163+
trk_struct['property_name'][0, 0] = b'colors\x003\x004'
164+
assert_raises(HeaderError, TrkFile.load, BytesIO(trk_bytes))
165+
166+
def test_load_trk_version_1(self):
167+
# Simulate and test a TRK (version 1).
168+
# First check that setting the RAS affine works in version 2.
169+
trk_struct, trk_bytes = self.trk_with_bytes()
170+
trk_struct[Field.VOXEL_TO_RASMM] = np.diag([2, 3, 4, 1])
171+
trk = TrkFile.load(BytesIO(trk_bytes))
172+
assert_array_equal(trk.affine, np.diag([2, 3, 4, 1]))
173+
# Next check that affine assumed identity if version 1.
174+
trk_struct['version'] = 1
175+
with clear_and_catch_warnings(record=True, modules=[trk_module]) as w:
176+
trk = TrkFile.load(BytesIO(trk_bytes))
177+
assert_equal(len(w), 1)
178+
assert_true(issubclass(w[0].category, HeaderWarning))
179+
assert_true("identity" in str(w[0].message))
180+
assert_array_equal(trk.affine, np.eye(4))
181+
assert_array_equal(trk.header['version'], 1)
158182

159183
def test_load_complex_file_in_big_endian(self):
160-
trk_file = open(DATA['complex_trk_big_endian_fname'], 'rb').read()
184+
trk_struct, trk_bytes = self.trk_with_bytes(
185+
'complex_trk_big_endian_fname', endian='>')
161186
# We use hdr_size as an indicator of little vs big endian.
162-
hdr_size_big_endian = np.array(1000, dtype=">i4").tostring()
163-
assert_equal(trk_file[996:996+4], hdr_size_big_endian)
187+
good_orders = '>' if sys.byteorder == 'little' else '>='
188+
hdr_size = trk_struct['hdr_size']
189+
assert_true(hdr_size.dtype.byteorder in good_orders)
190+
assert_equal(hdr_size, 1000)
164191

165192
for lazy_load in [False, True]:
166193
trk = TrkFile.load(DATA['complex_trk_big_endian_fname'],

nibabel/streamlines/trk.py

Lines changed: 22 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -26,36 +26,11 @@
2626
MAX_NB_NAMED_SCALARS_PER_POINT = 10
2727
MAX_NB_NAMED_PROPERTIES_PER_STREAMLINE = 10
2828

29-
# See http://docs.scipy.org/doc/numpy/reference/arrays.dtypes.html
30-
header_1_dtd = [(Field.MAGIC_NUMBER, 'S6'),
31-
(Field.DIMENSIONS, 'h', 3),
32-
(Field.VOXEL_SIZES, 'f4', 3),
33-
(Field.ORIGIN, 'f4', 3),
34-
(Field.NB_SCALARS_PER_POINT, 'h'),
35-
('scalar_name', 'S20', MAX_NB_NAMED_SCALARS_PER_POINT),
36-
(Field.NB_PROPERTIES_PER_STREAMLINE, 'h'),
37-
('property_name', 'S20',
38-
MAX_NB_NAMED_PROPERTIES_PER_STREAMLINE),
39-
('reserved', 'S508'),
40-
(Field.VOXEL_ORDER, 'S4'),
41-
('pad2', 'S4'),
42-
('image_orientation_patient', 'f4', 6),
43-
('pad1', 'S2'),
44-
('invert_x', 'S1'),
45-
('invert_y', 'S1'),
46-
('invert_z', 'S1'),
47-
('swap_xy', 'S1'),
48-
('swap_yz', 'S1'),
49-
('swap_zx', 'S1'),
50-
(Field.NB_STREAMLINES, 'i4'),
51-
('version', 'i4'),
52-
('hdr_size', 'i4'),
53-
]
54-
55-
# Version 2 adds a 4x4 matrix giving the affine transformtation going
29+
# Version 2 adds a 4x4 matrix giving the affine transformation going
5630
# from voxel coordinates in the referenced 3D voxel matrix, to xyz
5731
# coordinates (axes L->R, P->A, I->S). If (0 based) value [3, 3] from
5832
# this matrix is 0, this means the matrix is not recorded.
33+
# See http://docs.scipy.org/doc/numpy/reference/arrays.dtypes.html
5934
header_2_dtd = [(Field.MAGIC_NUMBER, 'S6'),
6035
(Field.DIMENSIONS, 'h', 3),
6136
(Field.VOXEL_SIZES, 'f4', 3),
@@ -83,7 +58,6 @@
8358
]
8459

8560
# Full header numpy dtypes
86-
header_1_dtype = np.dtype(header_1_dtd)
8761
header_2_dtype = np.dtype(header_2_dtd)
8862

8963

@@ -341,8 +315,8 @@ def load(cls, fileobj, lazy_load=False):
341315
data_per_point_slice = {}
342316
if hdr[Field.NB_SCALARS_PER_POINT] > 0:
343317
cpt = 0
344-
for scalar_name in hdr['scalar_name']:
345-
scalar_name, nb_scalars = decode_value_from_name(scalar_name)
318+
for scalar_field in hdr['scalar_name']:
319+
scalar_name, nb_scalars = decode_value_from_name(scalar_field)
346320

347321
if nb_scalars == 0:
348322
continue
@@ -358,8 +332,8 @@ def load(cls, fileobj, lazy_load=False):
358332
data_per_streamline_slice = {}
359333
if hdr[Field.NB_PROPERTIES_PER_STREAMLINE] > 0:
360334
cpt = 0
361-
for property_name in hdr['property_name']:
362-
results = decode_value_from_name(property_name)
335+
for property_field in hdr['property_name']:
336+
results = decode_value_from_name(property_field)
363337
property_name, nb_properties = results
364338

365339
if nb_properties == 0:
@@ -584,8 +558,8 @@ def _read_header(fileobj):
584558
TrkFile.HEADER_SIZE))
585559

586560
if header_rec['version'] == 1:
587-
header_rec = np.fromstring(string=header_str,
588-
dtype=header_1_dtype)
561+
# There is no 4x4 matrix for voxel to RAS transformation.
562+
header_rec[Field.VOXEL_TO_RASMM] = np.zeros((4, 4))
589563
elif header_rec['version'] == 2:
590564
pass # Nothing more to do.
591565
else:
@@ -724,12 +698,17 @@ def __str__(self):
724698
hdr_field = getattr(Field, attr)
725699
if hdr_field in vars:
726700
vars[attr] = vars[hdr_field]
727-
vars['scalar_names'] = '\n '.join([asstr(s)
728-
for s in vars['scalar_name']
729-
if len(s) > 0])
730-
vars['property_names'] = "\n ".join([asstr(s)
731-
for s in vars['property_name']
732-
if len(s) > 0])
701+
702+
nb_scalars = self.header[Field.NB_SCALARS_PER_POINT]
703+
scalar_names = [asstr(s)
704+
for s in vars['scalar_name'][:nb_scalars]
705+
if len(s) > 0]
706+
vars['scalar_names'] = '\n '.join(scalar_names)
707+
nb_properties = self.header[Field.NB_PROPERTIES_PER_STREAMLINE]
708+
property_names = [asstr(s)
709+
for s in vars['property_name'][:nb_properties]
710+
if len(s) > 0]
711+
vars['property_names'] = "\n ".join(property_names)
733712
# Make all byte strings into strings
734713
# Fixes recursion error on Python 3.3
735714
vars = dict((k, asstr(v) if hasattr(v, 'decode') else v)
@@ -739,11 +718,11 @@ def __str__(self):
739718
v.{version}
740719
dim: {DIMENSIONS}
741720
voxel_sizes: {VOXEL_SIZES}
742-
orgin: {ORIGIN}
721+
origin: {ORIGIN}
743722
nb_scalars: {NB_SCALARS_PER_POINT}
744-
scalar_name:\n {scalar_names}
723+
scalar_names:\n {scalar_names}
745724
nb_properties: {NB_PROPERTIES_PER_STREAMLINE}
746-
property_name:\n {property_names}
725+
property_names:\n {property_names}
747726
vox_to_world:\n{VOXEL_TO_RASMM}
748727
voxel_order: {VOXEL_ORDER}
749728
image_orientation_patient: {image_orientation_patient}

0 commit comments

Comments
 (0)