Skip to content

Commit f4a3e0a

Browse files
Merge pull request #460 from jaeilepp/surf-metadata
MRG: Freesurfer surf metadata Parse metadata at end of freesurfer files. Closes #456
2 parents 9959c74 + 45f6052 commit f4a3e0a

File tree

2 files changed

+146
-12
lines changed

2 files changed

+146
-12
lines changed

nibabel/freesurfer/io.py

Lines changed: 112 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from __future__ import division, print_function, absolute_import
22

3+
import warnings
34
import numpy as np
45
import getpass
56
import time
67

7-
8+
from ..externals import OrderedDict
89
from ..externals.six.moves import xrange
910
from ..openers import Opener
1011

@@ -44,24 +45,70 @@ def _fread3_many(fobj, n):
4445
return (b1 << 16) + (b2 << 8) + b3
4546

4647

47-
def read_geometry(filepath):
48+
def _read_volume_info(fobj):
49+
volume_info = OrderedDict()
50+
head = np.fromfile(fobj, '>i4', 1)
51+
if not np.array_equal(head, [20]): # Read two bytes more
52+
head = np.concatenate([head, np.fromfile(fobj, '>i4', 2)])
53+
if not np.array_equal(head, [2, 0, 20]):
54+
warnings.warn("Unknown extension code.")
55+
return volume_info
56+
57+
volume_info['head'] = head
58+
for key in ['valid', 'filename', 'volume', 'voxelsize', 'xras', 'yras',
59+
'zras', 'cras']:
60+
pair = fobj.readline().decode('utf-8').split('=')
61+
if pair[0].strip() != key or len(pair) != 2:
62+
raise IOError('Error parsing volume info.')
63+
if key in ('valid', 'filename'):
64+
volume_info[key] = pair[1].strip()
65+
elif key == 'volume':
66+
volume_info[key] = np.array(pair[1].split()).astype(int)
67+
else:
68+
volume_info[key] = np.array(pair[1].split()).astype(float)
69+
# Ignore the rest
70+
return volume_info
71+
72+
73+
def read_geometry(filepath, read_metadata=False, read_stamp=False):
4874
"""Read a triangular format Freesurfer surface mesh.
4975
5076
Parameters
5177
----------
5278
filepath : str
5379
Path to surface file
80+
read_metadata : bool
81+
Read metadata as key-value pairs.
82+
Valid keys:
83+
* 'head' : array of int
84+
* 'valid' : str
85+
* 'filename' : str
86+
* 'volume' : array of int, shape (3,)
87+
* 'voxelsize' : array of float, shape (3,)
88+
* 'xras' : array of float, shape (3,)
89+
* 'yras' : array of float, shape (3,)
90+
* 'zras' : array of float, shape (3,)
91+
* 'cras' : array of float, shape (3,)
92+
read_stamp : bool
93+
Return the comment from the file
5494
5595
Returns
5696
-------
5797
coords : numpy array
5898
nvtx x 3 array of vertex (x, y, z) coordinates
5999
faces : numpy array
60100
nfaces x 3 array of defining mesh triangles
101+
volume_info : OrderedDict
102+
If read_metadata is true, key-value pairs found in the geometry file
103+
create_stamp : str
104+
If read_stamp is true, the comment added by the program that saved
105+
the file
61106
"""
107+
volume_info = OrderedDict()
108+
62109
with open(filepath, "rb") as fobj:
63110
magic = _fread3(fobj)
64-
if magic == 16777215: # Quad file
111+
if magic in (16777215, 16777213): # Quad file
65112
nvert = _fread3(fobj)
66113
nquad = _fread3(fobj)
67114
coords = np.fromfile(fobj, ">i2", nvert * 3).astype(np.float)
@@ -86,20 +133,33 @@ def read_geometry(filepath):
86133
nface += 1
87134

88135
elif magic == 16777214: # Triangle file
89-
fobj.readline() # create_stamp
136+
create_stamp = fobj.readline().rstrip(b'\n').decode('utf-8')
90137
fobj.readline()
91138
vnum = np.fromfile(fobj, ">i4", 1)[0]
92139
fnum = np.fromfile(fobj, ">i4", 1)[0]
93140
coords = np.fromfile(fobj, ">f4", vnum * 3).reshape(vnum, 3)
94141
faces = np.fromfile(fobj, ">i4", fnum * 3).reshape(fnum, 3)
142+
143+
if read_metadata:
144+
volume_info = _read_volume_info(fobj)
95145
else:
96146
raise ValueError("File does not appear to be a Freesurfer surface")
97147

98148
coords = coords.astype(np.float) # XXX: due to mayavi bug on mac 32bits
99-
return coords, faces
100149

150+
ret = (coords, faces)
151+
if read_metadata:
152+
if len(volume_info) == 0:
153+
warnings.warn('No volume information contained in the file')
154+
ret += (volume_info,)
155+
if read_stamp:
156+
ret += (create_stamp,)
101157

102-
def write_geometry(filepath, coords, faces, create_stamp=None):
158+
return ret
159+
160+
161+
def write_geometry(filepath, coords, faces, create_stamp=None,
162+
volume_info=None):
103163
"""Write a triangular format Freesurfer surface mesh.
104164
105165
Parameters
@@ -112,6 +172,19 @@ def write_geometry(filepath, coords, faces, create_stamp=None):
112172
nfaces x 3 array of defining mesh triangles
113173
create_stamp : str
114174
User/time stamp (default: "created by <user> on <ctime>")
175+
volume_info : dict-like or None
176+
Key-value pairs to encode at the end of the file.
177+
Valid keys:
178+
* 'head' : array of int
179+
* 'valid' : str
180+
* 'filename' : str
181+
* 'volume' : array of int, shape (3,)
182+
* 'voxelsize' : array of float, shape (3,)
183+
* 'xras' : array of float, shape (3,)
184+
* 'yras' : array of float, shape (3,)
185+
* 'zras' : array of float, shape (3,)
186+
* 'cras' : array of float, shape (3,)
187+
115188
"""
116189
magic_bytes = np.array([255, 255, 254], dtype=np.uint8)
117190

@@ -129,6 +202,10 @@ def write_geometry(filepath, coords, faces, create_stamp=None):
129202
coords.astype('>f4').reshape(-1).tofile(fobj)
130203
faces.astype('>i4').reshape(-1).tofile(fobj)
131204

205+
# Add volume info, if given
206+
if volume_info is not None and len(volume_info) > 0:
207+
fobj.write(_serialize_volume_info(volume_info))
208+
132209

133210
def read_morph_data(filepath):
134211
"""Read a Freesurfer morphometry data file.
@@ -369,3 +446,32 @@ def read_label(filepath, read_scalars=False):
369446
scalar_array = np.loadtxt(filepath, skiprows=2, usecols=[-1])
370447
return label_array, scalar_array
371448
return label_array
449+
450+
451+
def _serialize_volume_info(volume_info):
452+
"""Helper for serializing the volume info."""
453+
keys = ['head', 'valid', 'filename', 'volume', 'voxelsize', 'xras', 'yras',
454+
'zras', 'cras']
455+
diff = set(volume_info.keys()).difference(keys)
456+
if len(diff) > 0:
457+
raise ValueError('Invalid volume info: %s.' % diff.pop())
458+
459+
strings = list()
460+
for key in keys:
461+
if key == 'head':
462+
if not (np.array_equal(volume_info[key], [20]) or np.array_equal(
463+
volume_info[key], [2, 0, 20])):
464+
warnings.warn("Unknown extension code.")
465+
strings.append(np.array(volume_info[key], dtype='>i4').tostring())
466+
elif key in ('valid', 'filename'):
467+
val = volume_info[key]
468+
strings.append('{0} = {1}\n'.format(key, val).encode('utf-8'))
469+
elif key == 'volume':
470+
val = volume_info[key]
471+
strings.append('{0} = {1} {2} {3}\n'.format(
472+
key, val[0], val[1], val[2]).encode('utf-8'))
473+
else:
474+
val = volume_info[key]
475+
strings.append('{0} = {1:0.10g} {2:0.10g} {3:0.10g}\n'.format(
476+
key.ljust(6), val[0], val[1], val[2]).encode('utf-8'))
477+
return b''.join(strings)

nibabel/freesurfer/tests/test_io.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,21 @@
44
import getpass
55
import time
66
import hashlib
7+
import warnings
78

89

910
from ...tmpdirs import InTemporaryDirectory
1011

1112
from nose.tools import assert_true
1213
import numpy as np
13-
from numpy.testing import assert_equal, assert_raises, dec
14+
from numpy.testing import assert_equal, assert_raises, dec, assert_allclose
1415

1516
from .. import (read_geometry, read_morph_data, read_annot, read_label,
1617
write_geometry, write_morph_data, write_annot)
1718

1819
from ...tests.nibabel_data import get_nibabel_data
1920
from ...fileslice import strided_scalar
20-
21+
from ...testing import clear_and_catch_warnings
2122

2223
DATA_SDIR = 'fsaverage'
2324

@@ -54,26 +55,53 @@ def test_geometry():
5455
assert_equal(0, faces.min())
5556
assert_equal(coords.shape[0], faces.max() + 1)
5657

57-
# Test quad with sphere
5858
surf_path = pjoin(data_path, "surf", "%s.%s" % ("lh", "sphere"))
59-
coords, faces = read_geometry(surf_path)
59+
coords, faces, volume_info, create_stamp = read_geometry(
60+
surf_path, read_metadata=True, read_stamp=True)
61+
6062
assert_equal(0, faces.min())
6163
assert_equal(coords.shape[0], faces.max() + 1)
64+
assert_equal(9, len(volume_info))
65+
assert_equal([2, 0, 20], volume_info['head'])
66+
assert_equal(u'created by greve on Thu Jun 8 19:17:51 2006',
67+
create_stamp)
6268

6369
# Test equivalence of freesurfer- and nibabel-generated triangular files
6470
# with respect to read_geometry()
6571
with InTemporaryDirectory():
6672
surf_path = 'test'
6773
create_stamp = "created by %s on %s" % (getpass.getuser(),
6874
time.ctime())
69-
write_geometry(surf_path, coords, faces, create_stamp)
75+
volume_info['cras'] = [1., 2., 3.]
76+
write_geometry(surf_path, coords, faces, create_stamp, volume_info)
7077

71-
coords2, faces2 = read_geometry(surf_path)
78+
coords2, faces2, volume_info2 = \
79+
read_geometry(surf_path, read_metadata=True)
7280

81+
for key in ('xras', 'yras', 'zras', 'cras'):
82+
assert_allclose(volume_info2[key], volume_info[key],
83+
rtol=1e-7, atol=1e-30)
84+
assert_equal(volume_info2['cras'], volume_info['cras'])
7385
with open(surf_path, 'rb') as fobj:
7486
np.fromfile(fobj, ">u1", 3)
7587
read_create_stamp = fobj.readline().decode().rstrip('\n')
7688

89+
# now write an incomplete file
90+
write_geometry(surf_path, coords, faces)
91+
with clear_and_catch_warnings() as w:
92+
warnings.filterwarnings('always', category=DeprecationWarning)
93+
read_geometry(surf_path, read_metadata=True)
94+
assert_true(any('volume information contained' in str(ww.message)
95+
for ww in w))
96+
assert_true(any('extension code' in str(ww.message) for ww in w))
97+
volume_info['head'] = [1, 2]
98+
with clear_and_catch_warnings() as w:
99+
write_geometry(surf_path, coords, faces, create_stamp, volume_info)
100+
assert_true(any('Unknown extension' in str(ww.message) for ww in w))
101+
volume_info['a'] = 0
102+
assert_raises(ValueError, write_geometry, surf_path, coords,
103+
faces, create_stamp, volume_info)
104+
77105
assert_equal(create_stamp, read_create_stamp)
78106

79107
np.testing.assert_array_equal(coords, coords2)

0 commit comments

Comments
 (0)