__init__.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  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. >>>
  16. >>> surface.write_triangular('somewhere/else/lh.pial')
  17. """
  18. import collections
  19. import contextlib
  20. import datetime
  21. import locale
  22. import re
  23. import struct
  24. import typing
  25. try:
  26. from freesurfer_surface.version import __version__
  27. except ImportError: # pragma: no cover
  28. __version__ = None
  29. class UnsupportedLocaleSettingError(locale.Error):
  30. pass
  31. @contextlib.contextmanager
  32. def setlocale(temporary_locale):
  33. primary_locale = locale.setlocale(locale.LC_ALL)
  34. try:
  35. yield locale.setlocale(locale.LC_ALL, temporary_locale)
  36. except locale.Error as exc:
  37. if str(exc) == 'unsupported locale setting':
  38. raise UnsupportedLocaleSettingError(temporary_locale)
  39. raise exc
  40. finally:
  41. locale.setlocale(locale.LC_ALL, primary_locale)
  42. Vertex = collections.namedtuple('Vertex', ['right', 'anterior', 'superior'])
  43. class Surface:
  44. _MAGIC_NUMBER = b'\xff\xff\xfe'
  45. _TAG_CMDLINE = b'\x00\x00\x00\x03'
  46. _TAG_OLD_SURF_GEOM = b'\x00\x00\x00\x14'
  47. _TAG_OLD_USEREALRAS = b'\x00\x00\x00\x02'
  48. _DATETIME_FORMAT = '%a %b %d %H:%M:%S %Y'
  49. def __init__(self):
  50. self.creator = None
  51. self.creation_datetime = None
  52. self.vertices = []
  53. self.triangles_vertex_indices = []
  54. self.using_old_real_ras = False
  55. self.volume_geometry_info = None
  56. self.command_lines = []
  57. @classmethod
  58. def _read_cmdlines(cls, stream: typing.BinaryIO) -> typing.Iterator[str]:
  59. while True:
  60. tag = stream.read(4)
  61. if not tag:
  62. return
  63. assert tag == cls._TAG_CMDLINE # might be TAG_GROUP_AVG_SURFACE_AREA
  64. # TAGwrite
  65. # https://github.com/freesurfer/freesurfer/blob/release_6_0_0/utils/tags.c#L94
  66. str_length, = struct.unpack('>Q', stream.read(8))
  67. yield stream.read(str_length - 1)
  68. assert stream.read(1) == b'\x00'
  69. def _read_triangular(self, stream: typing.BinaryIO):
  70. assert stream.read(3) == self._MAGIC_NUMBER
  71. self.creator, creation_dt_str = re.match(rb'^created by (\w+) on (.* \d{4})\n',
  72. stream.readline()).groups()
  73. with setlocale('C'):
  74. self.creation_datetime = datetime.datetime.strptime(creation_dt_str.decode(),
  75. self._DATETIME_FORMAT)
  76. assert stream.read(1) == b'\n'
  77. # fwriteInt
  78. # https://github.com/freesurfer/freesurfer/blob/release_6_0_0/utils/fio.c#L290
  79. vertices_num, triangles_num = struct.unpack('>II', stream.read(4 * 2))
  80. self.vertices = [Vertex(*struct.unpack('>fff', stream.read(4 * 3)))
  81. for _ in range(vertices_num)]
  82. self.triangles_vertex_indices = [struct.unpack('>III', stream.read(4 * 3))
  83. for _ in range(triangles_num)]
  84. assert all(vertex_idx < vertices_num
  85. for triangle_vertex_index in self.triangles_vertex_indices
  86. for vertex_idx in triangle_vertex_index)
  87. assert stream.read(4) == self._TAG_OLD_USEREALRAS
  88. using_old_real_ras, = struct.unpack('>I', stream.read(4))
  89. assert using_old_real_ras in [0, 1], using_old_real_ras
  90. self.using_old_real_ras = bool(using_old_real_ras)
  91. assert stream.read(4) == self._TAG_OLD_SURF_GEOM
  92. # writeVolGeom
  93. # https://github.com/freesurfer/freesurfer/blob/release_6_0_0/utils/transform.c#L368
  94. self.volume_geometry_info = tuple(stream.readline() for _ in range(8))
  95. self.command_lines = list(self._read_cmdlines(stream))
  96. @classmethod
  97. def read_triangular(cls, surface_file_path: str) -> 'Surface':
  98. surface = cls()
  99. with open(surface_file_path, 'rb') as surface_file:
  100. # pylint: disable=protected-access
  101. surface._read_triangular(surface_file)
  102. return surface
  103. def _triangular_creation_datetime_strftime(self) -> bytes:
  104. fmt = self._DATETIME_FORMAT.replace('%d', '{:>2}'.format(self.creation_datetime.day))
  105. with setlocale('C'):
  106. return self.creation_datetime.strftime(fmt).encode()
  107. def write_triangular(self, surface_file_path: str,
  108. creation_datetime: typing.Optional[datetime.datetime] = None):
  109. if creation_datetime is None:
  110. self.creation_datetime = datetime.datetime.now()
  111. else:
  112. self.creation_datetime = creation_datetime
  113. with open(surface_file_path, 'wb') as surface_file:
  114. surface_file.write(
  115. self._MAGIC_NUMBER
  116. + b'created by ' + self.creator
  117. + b' on ' + self._triangular_creation_datetime_strftime()
  118. + b'\n\n'
  119. + struct.pack('>II', len(self.vertices), len(self.triangles_vertex_indices))
  120. )
  121. for vertex in self.vertices:
  122. surface_file.write(struct.pack('>fff', *vertex))
  123. for triangle_vertex_indices in self.triangles_vertex_indices:
  124. surface_file.write(struct.pack('>III', *triangle_vertex_indices))
  125. surface_file.write(self._TAG_OLD_USEREALRAS
  126. + struct.pack('>I', 1 if self.using_old_real_ras else 0))
  127. surface_file.write(self._TAG_OLD_SURF_GEOM
  128. + b''.join(self.volume_geometry_info))
  129. for command_line in self.command_lines:
  130. surface_file.write(self._TAG_CMDLINE + struct.pack('>Q', len(command_line) + 1)
  131. + command_line + b'\0')
  132. def add_vertex(self, vertex: Vertex) -> int:
  133. self.vertices.append(vertex)
  134. return len(self.vertices) - 1