Skip to content

Commit 1e171a3

Browse files
committed
Merge pull request #135 from effigies/master
NF: Write capabilities for freesurfer triangle files Produces files identical to freesurfer up to the point that nibabel reads. There is additional information after a freesurfer generated file, but nibabel reads equivalent data and PySurfer displays these files fine.
2 parents 139953a + 024670e commit 1e171a3

File tree

3 files changed

+66
-2
lines changed

3 files changed

+66
-2
lines changed

nibabel/freesurfer/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"""
33

44
from io import read_geometry, read_morph_data, \
5-
read_annot, read_label
5+
read_annot, read_label, write_geometry
66
from mghformat import load, save, MGHImage

nibabel/freesurfer/io.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from __future__ import with_statement
22

33
import numpy as np
4+
import getpass
5+
import time
46

57

68
def _fread3(fobj):
@@ -93,6 +95,37 @@ def read_geometry(filepath):
9395
return coords, faces
9496

9597

98+
def write_geometry(filepath, coords, faces, create_stamp=None):
99+
"""Write a triangular format Freesurfer surface mesh.
100+
101+
Parameters
102+
----------
103+
filepath : str
104+
Path to surface file
105+
coords : numpy array
106+
nvtx x 3 array of vertex (x, y, z) coordinates
107+
faces : numpy array
108+
nfaces x 3 array of defining mesh triangles
109+
create_stamp : str
110+
User/time stamp (default: "created by <user> on <ctime>")
111+
"""
112+
magic_bytes = np.array([255, 255, 254], dtype=np.uint8)
113+
114+
if create_stamp is None:
115+
create_stamp = "created by %s on %s" % (getpass.getuser(),
116+
time.ctime())
117+
118+
with open(filepath, 'wb') as fobj:
119+
magic_bytes.tofile(fobj)
120+
fobj.write("%s\n\n" % create_stamp)
121+
122+
np.array([coords.shape[0], faces.shape[0]], dtype='>i4').tofile(fobj)
123+
124+
# Coerce types, just to be safe
125+
coords.astype('>f4').reshape(-1).tofile(fobj)
126+
faces.astype('>i4').reshape(-1).tofile(fobj)
127+
128+
96129
def read_morph_data(filepath):
97130
"""Read a Freesurfer morphometry data file.
98131

nibabel/freesurfer/tests/test_io.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1+
from __future__ import with_statement
12
import os
23
from os.path import join as pjoin
4+
import getpass
5+
import time
6+
7+
from nibabel.tmpdirs import InTemporaryDirectory
38

49
from nose.tools import assert_true
510
import numpy as np
611
from numpy.testing import assert_equal
712

8-
from .. import read_geometry, read_morph_data, read_annot, read_label
13+
from .. import read_geometry, read_morph_data, read_annot, read_label, \
14+
write_geometry
915

1016

1117
have_freesurfer = True
@@ -36,6 +42,31 @@ def test_geometry():
3642
assert_equal(0, faces.min())
3743
assert_equal(coords.shape[0], faces.max() + 1)
3844

45+
# Test equivalence of freesurfer- and nibabel-generated triangular files
46+
# with respect to read_geometry()
47+
with InTemporaryDirectory():
48+
surf_path = 'test'
49+
create_stamp = "created by %s on %s" % (getpass.getuser(),
50+
time.ctime())
51+
write_geometry(surf_path, coords, faces, create_stamp)
52+
53+
coords2, faces2 = read_geometry(surf_path)
54+
55+
with open(surf_path, 'rb') as fobj:
56+
magic = np.fromfile(fobj, ">u1", 3)
57+
read_create_stamp = fobj.readline().rstrip('\n')
58+
59+
assert_equal(create_stamp, read_create_stamp)
60+
61+
np.testing.assert_array_equal(coords, coords2)
62+
np.testing.assert_array_equal(faces, faces2)
63+
64+
# Validate byte ordering
65+
coords_swapped = coords.byteswap().newbyteorder()
66+
faces_swapped = faces.byteswap().newbyteorder()
67+
np.testing.assert_array_equal(coords_swapped, coords)
68+
np.testing.assert_array_equal(faces_swapped, faces)
69+
3970

4071
@freesurfer_test
4172
def test_morph_data():

0 commit comments

Comments
 (0)