11from __future__ import division , print_function , absolute_import
22
3+ import warnings
34import numpy as np
45import getpass
56import time
67
7-
8+ from .. externals import OrderedDict
89from ..externals .six .moves import xrange
910from ..openers import Opener
1011
@@ -44,28 +45,79 @@ 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+ """Helper for reading the footer from a surface file."""
50+ volume_info = OrderedDict ()
51+ head = np .fromfile (fobj , '>i4' , 1 )
52+ if not np .array_equal (head , [20 ]): # Read two bytes more
53+ head = np .concatenate ([head , np .fromfile (fobj , '>i4' , 2 )])
54+ if not np .array_equal (head , [2 , 0 , 20 ]):
55+ warnings .warn ("Unknown extension code." )
56+ return volume_info
57+
58+ volume_info ['head' ] = head
59+ for key in ['valid' , 'filename' , 'volume' , 'voxelsize' , 'xras' , 'yras' ,
60+ 'zras' , 'cras' ]:
61+ pair = fobj .readline ().decode ('utf-8' ).split ('=' )
62+ if pair [0 ].strip () != key or len (pair ) != 2 :
63+ raise IOError ('Error parsing volume info.' )
64+ if key in ('valid' , 'filename' ):
65+ volume_info [key ] = pair [1 ].strip ()
66+ elif key == 'volume' :
67+ volume_info [key ] = np .array (pair [1 ].split ()).astype (int )
68+ else :
69+ volume_info [key ] = np .array (pair [1 ].split ()).astype (float )
70+ # Ignore the rest
71+ return volume_info
72+
73+
74+ def read_geometry (filepath , read_metadata = False , read_stamp = False ):
4875 """Read a triangular format Freesurfer surface mesh.
4976
5077 Parameters
5178 ----------
5279 filepath : str
5380 Path to surface file
81+ read_metadata : bool
82+ Read metadata as key-value pairs.
83+ Valid keys:
84+ * 'head' : array of int
85+ * 'valid' : str
86+ * 'filename' : str
87+ * 'volume' : array of int, shape (3,)
88+ * 'voxelsize' : array of float, shape (3,)
89+ * 'xras' : array of float, shape (3,)
90+ * 'yras' : array of float, shape (3,)
91+ * 'zras' : array of float, shape (3,)
92+ * 'cras' : array of float, shape (3,)
93+ read_stamp : bool
94+ Return the comment from the file
5495
5596 Returns
5697 -------
5798 coords : numpy array
5899 nvtx x 3 array of vertex (x, y, z) coordinates
59100 faces : numpy array
60101 nfaces x 3 array of defining mesh triangles
102+ volume_info : OrderedDict
103+ If read_metadata is true, key-value pairs found in the geometry file
104+ create_stamp : str
105+ If read_stamp is true, the comment added by the program that saved
106+ the file
61107 """
108+ volume_info = OrderedDict ()
109+
110+ TRIANGLE_MAGIC = 16777214
111+ QUAD_MAGIC = 16777215
112+ NEW_QUAD_MAGIC = 16777213
62113 with open (filepath , "rb" ) as fobj :
63114 magic = _fread3 (fobj )
64- if magic == 16777215 : # Quad file
115+ if magic in ( QUAD_MAGIC , NEW_QUAD_MAGIC ) : # Quad file
65116 nvert = _fread3 (fobj )
66117 nquad = _fread3 (fobj )
67- coords = np .fromfile (fobj , ">i2" , nvert * 3 ).astype (np .float )
68- coords = coords .reshape (- 1 , 3 ) / 100.0
118+ (fmt , div ) = (">i2" , 100. ) if magic == QUAD_MAGIC else (">f4" , 1. )
119+ coords = np .fromfile (fobj , fmt , nvert * 3 ).astype (np .float ) / div
120+ coords = coords .reshape (- 1 , 3 )
69121 quads = _fread3_many (fobj , nquad * 4 )
70122 quads = quads .reshape (nquad , 4 )
71123 #
@@ -85,21 +137,34 @@ def read_geometry(filepath):
85137 faces [nface ] = quad [0 ], quad [2 ], quad [3 ]
86138 nface += 1
87139
88- elif magic == 16777214 : # Triangle file
89- fobj .readline () # create_stamp
140+ elif magic == TRIANGLE_MAGIC : # Triangle file
141+ create_stamp = fobj .readline (). rstrip ( b' \n ' ). decode ( 'utf-8' )
90142 fobj .readline ()
91143 vnum = np .fromfile (fobj , ">i4" , 1 )[0 ]
92144 fnum = np .fromfile (fobj , ">i4" , 1 )[0 ]
93145 coords = np .fromfile (fobj , ">f4" , vnum * 3 ).reshape (vnum , 3 )
94146 faces = np .fromfile (fobj , ">i4" , fnum * 3 ).reshape (fnum , 3 )
147+
148+ if read_metadata :
149+ volume_info = _read_volume_info (fobj )
95150 else :
96151 raise ValueError ("File does not appear to be a Freesurfer surface" )
97152
98153 coords = coords .astype (np .float ) # XXX: due to mayavi bug on mac 32bits
99- return coords , faces
100154
155+ ret = (coords , faces )
156+ if read_metadata :
157+ if len (volume_info ) == 0 :
158+ warnings .warn ('No volume information contained in the file' )
159+ ret += (volume_info ,)
160+ if read_stamp :
161+ ret += (create_stamp ,)
101162
102- def write_geometry (filepath , coords , faces , create_stamp = None ):
163+ return ret
164+
165+
166+ def write_geometry (filepath , coords , faces , create_stamp = None ,
167+ volume_info = None ):
103168 """Write a triangular format Freesurfer surface mesh.
104169
105170 Parameters
@@ -112,6 +177,19 @@ def write_geometry(filepath, coords, faces, create_stamp=None):
112177 nfaces x 3 array of defining mesh triangles
113178 create_stamp : str
114179 User/time stamp (default: "created by <user> on <ctime>")
180+ volume_info : dict-like or None
181+ Key-value pairs to encode at the end of the file.
182+ Valid keys:
183+ * 'head' : array of int
184+ * 'valid' : str
185+ * 'filename' : str
186+ * 'volume' : array of int, shape (3,)
187+ * 'voxelsize' : array of float, shape (3,)
188+ * 'xras' : array of float, shape (3,)
189+ * 'yras' : array of float, shape (3,)
190+ * 'zras' : array of float, shape (3,)
191+ * 'cras' : array of float, shape (3,)
192+
115193 """
116194 magic_bytes = np .array ([255 , 255 , 254 ], dtype = np .uint8 )
117195
@@ -129,6 +207,10 @@ def write_geometry(filepath, coords, faces, create_stamp=None):
129207 coords .astype ('>f4' ).reshape (- 1 ).tofile (fobj )
130208 faces .astype ('>i4' ).reshape (- 1 ).tofile (fobj )
131209
210+ # Add volume info, if given
211+ if volume_info is not None and len (volume_info ) > 0 :
212+ fobj .write (_serialize_volume_info (volume_info ))
213+
132214
133215def read_morph_data (filepath ):
134216 """Read a Freesurfer morphometry data file.
@@ -369,3 +451,32 @@ def read_label(filepath, read_scalars=False):
369451 scalar_array = np .loadtxt (filepath , skiprows = 2 , usecols = [- 1 ])
370452 return label_array , scalar_array
371453 return label_array
454+
455+
456+ def _serialize_volume_info (volume_info ):
457+ """Helper for serializing the volume info."""
458+ keys = ['head' , 'valid' , 'filename' , 'volume' , 'voxelsize' , 'xras' , 'yras' ,
459+ 'zras' , 'cras' ]
460+ diff = set (volume_info .keys ()).difference (keys )
461+ if len (diff ) > 0 :
462+ raise ValueError ('Invalid volume info: %s.' % diff .pop ())
463+
464+ strings = list ()
465+ for key in keys :
466+ if key == 'head' :
467+ if not (np .array_equal (volume_info [key ], [20 ]) or np .array_equal (
468+ volume_info [key ], [2 , 0 , 20 ])):
469+ warnings .warn ("Unknown extension code." )
470+ strings .append (np .array (volume_info [key ], dtype = '>i4' ).tostring ())
471+ elif key in ('valid' , 'filename' ):
472+ val = volume_info [key ]
473+ strings .append ('{0} = {1}\n ' .format (key , val ).encode ('utf-8' ))
474+ elif key == 'volume' :
475+ val = volume_info [key ]
476+ strings .append ('{0} = {1} {2} {3}\n ' .format (
477+ key , val [0 ], val [1 ], val [2 ]).encode ('utf-8' ))
478+ else :
479+ val = volume_info [key ]
480+ strings .append ('{0} = {1:0.10g} {2:0.10g} {3:0.10g}\n ' .format (
481+ key .ljust (6 ), val [0 ], val [1 ], val [2 ]).encode ('utf-8' ))
482+ return b'' .join (strings )
0 commit comments