__init__.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. """
  2. Python Library to Read and Write Surface Files in Freesurfer's TriangularSurface Format
  3. compatible with Freesurfer's MRISwriteTriangularSurface()
  4. https://github.com/freesurfer/freesurfer/blob/release_6_0_0/include/mrisurf.h#L1281
  5. https://github.com/freesurfer/freesurfer/blob/release_6_0_0/utils/mrisurf.c
  6. https://raw.githubusercontent.com/freesurfer/freesurfer/release_6_0_0/utils/mrisurf.c
  7. Freesurfer
  8. https://surfer.nmr.mgh.harvard.edu/
  9. >>> from freesurfer_surface import Surface, Vertex
  10. >>>
  11. >>> surface = Surface.read_triangular('bert/surf/lh.pial'))
  12. >>>
  13. >>> vertex_index = surface.add_vertex(Vertex(0.0, -3.14, 21.42))
  14. >>> print(surface.vertices[vertex_index])
  15. >>> surface.write_triangular('somewhere/else/lh.pial')
  16. >>>
  17. >>> surface.load_annotation_file('bert/label/lh.aparc.annot')
  18. >>> print([label.name for label in surface.annotation.labels])
  19. """
  20. import collections
  21. import contextlib
  22. import datetime
  23. import locale
  24. import re
  25. import struct
  26. import typing
  27. try:
  28. from freesurfer_surface.version import __version__
  29. except ImportError: # pragma: no cover
  30. __version__ = None
  31. class UnsupportedLocaleSettingError(locale.Error):
  32. pass
  33. @contextlib.contextmanager
  34. def setlocale(temporary_locale):
  35. primary_locale = locale.setlocale(locale.LC_ALL)
  36. try:
  37. yield locale.setlocale(locale.LC_ALL, temporary_locale)
  38. except locale.Error as exc:
  39. if str(exc) == 'unsupported locale setting':
  40. raise UnsupportedLocaleSettingError(temporary_locale)
  41. raise exc
  42. finally:
  43. locale.setlocale(locale.LC_ALL, primary_locale)
  44. Vertex = collections.namedtuple('Vertex', ['right', 'anterior', 'superior'])
  45. class Label:
  46. # pylint: disable=too-few-public-methods
  47. index: int
  48. name: str
  49. red: int
  50. blue: int
  51. green: int
  52. transparency: int
  53. @property
  54. def color_code(self):
  55. return int.from_bytes((self.transparency, self.red, self.green, self.blue),
  56. byteorder='big', signed=False)
  57. class Annotation:
  58. # pylint: disable=too-few-public-methods
  59. _TAG_OLD_COLORTABLE = b'\0\0\0\x01'
  60. vertex_values: typing.Dict[int, int] = {}
  61. colortable_path: typing.Optional[bytes] = None
  62. # TODO dict
  63. labels: typing.List[Label] = None
  64. @staticmethod
  65. def _read_label(stream: typing.BinaryIO) -> Label:
  66. label = Label()
  67. label.index, name_length = struct.unpack('>II', stream.read(4*2))
  68. label.name = stream.read(name_length - 1).decode()
  69. assert stream.read(1) == b'\0'
  70. label.red, label.blue, label.green, label.transparency \
  71. = struct.unpack('>IIII', stream.read(4 * 4))
  72. return label
  73. def _read(self, stream: typing.BinaryIO) -> None:
  74. # https://surfer.nmr.mgh.harvard.edu/fswiki/LabelsClutsAnnotationFiles
  75. annotations_num, = struct.unpack('>I', stream.read(4))
  76. annotations = (struct.unpack('>II', stream.read(4 * 2))
  77. for _ in range(annotations_num))
  78. self.vertex_values = {vertex_index: annotation_value
  79. for vertex_index, annotation_value in annotations}
  80. assert all((annotation_value >> (8 * 3)) == 0
  81. for annotation_value in self.vertex_values.values())
  82. assert stream.read(4) == self._TAG_OLD_COLORTABLE
  83. colortable_version, _, filename_length = struct.unpack('>III', stream.read(4 * 3))
  84. assert colortable_version > 0 # new version
  85. self.colortable_path = stream.read(filename_length - 1)
  86. assert stream.read(1) == b'\0'
  87. labels_num, = struct.unpack('>I', stream.read(4))
  88. self.labels = [self._read_label(stream) for _ in range(labels_num)]
  89. assert not stream.read(1)
  90. @classmethod
  91. def read(cls, annotation_file_path: str) -> 'Annotation':
  92. annotation = cls()
  93. with open(annotation_file_path, 'rb') as annotation_file:
  94. # pylint: disable=protected-access
  95. annotation._read(annotation_file)
  96. return annotation
  97. class Surface:
  98. # pylint: disable=too-many-instance-attributes
  99. _MAGIC_NUMBER = b'\xff\xff\xfe'
  100. _TAG_CMDLINE = b'\x00\x00\x00\x03'
  101. _TAG_OLD_SURF_GEOM = b'\x00\x00\x00\x14'
  102. _TAG_OLD_USEREALRAS = b'\x00\x00\x00\x02'
  103. _DATETIME_FORMAT = '%a %b %d %H:%M:%S %Y'
  104. creator: typing.Optional[bytes] = None
  105. creation_datetime: typing.Optional[datetime.datetime] = None
  106. vertices: typing.List[Vertex] = []
  107. triangles_vertex_indices: typing.List[typing.Tuple[int]] = []
  108. using_old_real_ras: bool = False
  109. volume_geometry_info: typing.Optional[typing.Tuple[bytes]] = None
  110. command_lines: typing.List[bytes] = []
  111. annotation: typing.Optional[Annotation] = None
  112. @classmethod
  113. def _read_cmdlines(cls, stream: typing.BinaryIO) -> typing.Iterator[str]:
  114. while True:
  115. tag = stream.read(4)
  116. if not tag:
  117. return
  118. assert tag == cls._TAG_CMDLINE # might be TAG_GROUP_AVG_SURFACE_AREA
  119. # TAGwrite
  120. # https://github.com/freesurfer/freesurfer/blob/release_6_0_0/utils/tags.c#L94
  121. str_length, = struct.unpack('>Q', stream.read(8))
  122. yield stream.read(str_length - 1)
  123. assert stream.read(1) == b'\x00'
  124. def _read_triangular(self, stream: typing.BinaryIO):
  125. assert stream.read(3) == self._MAGIC_NUMBER
  126. self.creator, creation_dt_str = re.match(rb'^created by (\w+) on (.* \d{4})\n',
  127. stream.readline()).groups()
  128. with setlocale('C'):
  129. self.creation_datetime = datetime.datetime.strptime(creation_dt_str.decode(),
  130. self._DATETIME_FORMAT)
  131. assert stream.read(1) == b'\n'
  132. # fwriteInt
  133. # https://github.com/freesurfer/freesurfer/blob/release_6_0_0/utils/fio.c#L290
  134. vertices_num, triangles_num = struct.unpack('>II', stream.read(4 * 2))
  135. self.vertices = [Vertex(*struct.unpack('>fff', stream.read(4 * 3)))
  136. for _ in range(vertices_num)]
  137. self.triangles_vertex_indices = [struct.unpack('>III', stream.read(4 * 3))
  138. for _ in range(triangles_num)]
  139. assert all(vertex_idx < vertices_num
  140. for triangle_vertex_index in self.triangles_vertex_indices
  141. for vertex_idx in triangle_vertex_index)
  142. assert stream.read(4) == self._TAG_OLD_USEREALRAS
  143. using_old_real_ras, = struct.unpack('>I', stream.read(4))
  144. assert using_old_real_ras in [0, 1], using_old_real_ras
  145. self.using_old_real_ras = bool(using_old_real_ras)
  146. assert stream.read(4) == self._TAG_OLD_SURF_GEOM
  147. # writeVolGeom
  148. # https://github.com/freesurfer/freesurfer/blob/release_6_0_0/utils/transform.c#L368
  149. self.volume_geometry_info = tuple(stream.readline() for _ in range(8))
  150. self.command_lines = list(self._read_cmdlines(stream))
  151. @classmethod
  152. def read_triangular(cls, surface_file_path: str) -> 'Surface':
  153. surface = cls()
  154. with open(surface_file_path, 'rb') as surface_file:
  155. # pylint: disable=protected-access
  156. surface._read_triangular(surface_file)
  157. return surface
  158. def _triangular_creation_datetime_strftime(self) -> bytes:
  159. fmt = self._DATETIME_FORMAT.replace('%d', '{:>2}'.format(self.creation_datetime.day))
  160. with setlocale('C'):
  161. return self.creation_datetime.strftime(fmt).encode()
  162. def write_triangular(self, surface_file_path: str,
  163. creation_datetime: typing.Optional[datetime.datetime] = None):
  164. if creation_datetime is None:
  165. self.creation_datetime = datetime.datetime.now()
  166. else:
  167. self.creation_datetime = creation_datetime
  168. with open(surface_file_path, 'wb') as surface_file:
  169. surface_file.write(
  170. self._MAGIC_NUMBER
  171. + b'created by ' + self.creator
  172. + b' on ' + self._triangular_creation_datetime_strftime()
  173. + b'\n\n'
  174. + struct.pack('>II', len(self.vertices), len(self.triangles_vertex_indices))
  175. )
  176. for vertex in self.vertices:
  177. surface_file.write(struct.pack('>fff', *vertex))
  178. for triangle_vertex_indices in self.triangles_vertex_indices:
  179. surface_file.write(struct.pack('>III', *triangle_vertex_indices))
  180. surface_file.write(self._TAG_OLD_USEREALRAS
  181. + struct.pack('>I', 1 if self.using_old_real_ras else 0))
  182. surface_file.write(self._TAG_OLD_SURF_GEOM
  183. + b''.join(self.volume_geometry_info))
  184. for command_line in self.command_lines:
  185. surface_file.write(self._TAG_CMDLINE + struct.pack('>Q', len(command_line) + 1)
  186. + command_line + b'\0')
  187. def load_annotation_file(self, annotation_file_path: str) -> None:
  188. annotation = Annotation.read(annotation_file_path)
  189. assert len(annotation.vertex_values) <= len(self.vertices)
  190. assert all(0 <= vertex_index < len(self.vertices)
  191. for vertex_index in annotation.vertex_values.keys())
  192. self.annotation = annotation
  193. def add_vertex(self, vertex: Vertex) -> int:
  194. self.vertices.append(vertex)
  195. return len(self.vertices) - 1