__init__.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. """
  2. Python Library to Read FreeSurfer's cortical parcellation anatomical statistics
  3. ([lh]h.aparc(.*)?.stats)
  4. Freesurfer
  5. https://surfer.nmr.mgh.harvard.edu/
  6. >>> from freesurfer_stats import CorticalParcellationStats
  7. >>> stats = CorticalParcellationStats.read('tests/subjects/fabian/stats/lh.aparc.DKTatlas.stats')
  8. >>> stats.headers['CreationTime'].isoformat()
  9. '2019-05-09T21:05:54+00:00'
  10. >>> stats.headers['cvs_version']
  11. 'Id: mris_anatomical_stats.c,v 1.79 2016/03/14 15:15:34 greve Exp'
  12. >>> stats.headers['cmdline'][:64]
  13. 'mris_anatomical_stats -th3 -mgz -cortex ../label/lh.cortex.label'
  14. >>> stats.hemisphere
  15. >>> stats.whole_brain_measurements['Estimated Total Intracranial Volume']
  16. (1670487.274486, 'mm^3')
  17. >>> stats.whole_brain_measurements['White Surface Total Area']
  18. (98553.0, 'mm^2')
  19. >>> stats.structure_measurements['postcentral']
  20. {'Structure Name': 'postcentral',
  21. 'Number of Vertices': 8102,
  22. 'Surface Area': 5258.0,
  23. 'Gray Matter Volume': 12037.0,
  24. 'Average Thickness': 2.109,
  25. 'Thickness StdDev': 0.568,
  26. ...}
  27. >>> stats.structure_measurement_units
  28. {'Structure Name': None,
  29. 'Number of Vertices': None,
  30. 'Surface Area': 'mm^2',
  31. 'Gray Matter Volume': 'mm^3',
  32. 'Average Thickness': 'mm',
  33. 'Thickness StdDev': 'mm',
  34. ...}
  35. """
  36. import datetime
  37. import re
  38. import typing
  39. import pandas
  40. from freesurfer_stats.version import __version__
  41. class CorticalParcellationStats:
  42. _HEMISPHERE_PREFIX_TO_SIDE = {'lh': 'left', 'rh': 'right'}
  43. _GENERAL_MEASUREMENTS_REGEX = re.compile(
  44. r'^Measure \S+, ([^,\s]+),? ([^,]+), ([\d\.]+), (\S+)$')
  45. def __init__(self):
  46. self.headers \
  47. = {} # type: typing.Dict[str, typing.Union[str, datetime.datetime]]
  48. self.whole_brain_measurements \
  49. = {} # type: typing.Dict[str, typing.Tuple[float, int]]
  50. self.structure_measurements \
  51. = {} # type: typing.Dict[str, typing.Dict[str, typing.Union[str, int, float]]]
  52. self.structure_measurement_units \
  53. = {} # type: typing.Dict[str, typing.Union[str, None]]
  54. @property
  55. def hemisphere(self) -> str:
  56. return self._HEMISPHERE_PREFIX_TO_SIDE[self.headers['hemi']]
  57. @staticmethod
  58. def _read_header_line(stream: typing.TextIO) -> str:
  59. line = stream.readline()
  60. assert line.startswith('# ')
  61. return line[2:].rstrip()
  62. @classmethod
  63. def _read_column_header_line(cls, stream: typing.TextIO) -> typing.Tuple[int, str, str]:
  64. line = cls._read_header_line(stream)
  65. assert line.startswith('TableCol'), line
  66. line = line[len('TableCol '):].lstrip()
  67. index, key, value = line.split(maxsplit=2)
  68. return int(index), key, value
  69. def _read_headers(self, stream: typing.TextIO) -> None:
  70. self.headers = {}
  71. while True:
  72. line = self._read_header_line(stream)
  73. if line.startswith('Measure'):
  74. break
  75. elif line:
  76. attr_name, attr_value = line.split(' ', maxsplit=1)
  77. attr_value = attr_value.lstrip()
  78. if attr_name in ['cvs_version', 'mrisurf.c-cvs_version']:
  79. attr_value = attr_value.strip('$').rstrip()
  80. if attr_name == 'CreationTime':
  81. attr_dt = datetime.datetime.strptime(
  82. attr_value, '%Y/%m/%d-%H:%M:%S-%Z')
  83. if attr_dt.tzinfo is None:
  84. assert attr_value.endswith('-GMT')
  85. attr_dt = attr_dt.replace(tzinfo=datetime.timezone.utc)
  86. attr_value = attr_dt
  87. if attr_name == 'AnnotationFileTimeStamp':
  88. attr_value = datetime.datetime.strptime(
  89. attr_value, '%Y/%m/%d %H:%M:%S')
  90. self.headers[attr_name] = attr_value
  91. @staticmethod
  92. def _filter_unit(unit: str) -> typing.Union[str, None]:
  93. if unit in ['unitless', 'NA']:
  94. return None
  95. return unit
  96. @classmethod
  97. def _read_column_attributes(cls, num: int, stream: typing.TextIO) \
  98. -> typing.List[typing.Dict[str, str]]:
  99. columns = []
  100. for column_index in range(1, int(num) + 1):
  101. column_attrs = {}
  102. for _ in range(3):
  103. column_index_line, key, value \
  104. = cls._read_column_header_line(stream)
  105. assert column_index_line == column_index
  106. assert key not in column_attrs
  107. column_attrs[key] = value
  108. columns.append(column_attrs)
  109. return columns
  110. def _read(self, stream: typing.TextIO) -> None:
  111. assert stream.readline().rstrip() \
  112. == '# Table of FreeSurfer cortical parcellation anatomical statistics'
  113. assert stream.readline().rstrip() == '#'
  114. self._read_headers(stream)
  115. self.whole_brain_measurements = {}
  116. line = self._read_header_line(stream)
  117. while not line.startswith('NTableCols'):
  118. key, name, value, unit \
  119. = self._GENERAL_MEASUREMENTS_REGEX.match(line).groups()
  120. if key == 'SupraTentorialVolNotVent' and name.lower() == 'supratentorial volume':
  121. name += ' Without Ventricles'
  122. assert name not in self.whole_brain_measurements, \
  123. (key, name, self.whole_brain_measurements)
  124. self.whole_brain_measurements[name] = (float(value), unit)
  125. line = self._read_header_line(stream)
  126. columns = self._read_column_attributes(
  127. int(line[len('NTableCols '):]), stream)
  128. assert self._read_header_line(stream) \
  129. == 'ColHeaders ' + ' '.join(c['ColHeader'] for c in columns)
  130. assert columns[0]['ColHeader'] == 'StructName'
  131. column_names = [c['FieldName'] for c in columns]
  132. self.structure_measurements = {}
  133. for line in stream:
  134. values = line.rstrip().split()
  135. assert len(values) == len(column_names)
  136. struct_name = values[0]
  137. assert struct_name not in self.structure_measurements
  138. for column_index, column_attrs in enumerate(columns):
  139. if column_attrs['ColHeader'] in ['NumVert', 'FoldInd']:
  140. values[column_index] = int(values[column_index])
  141. elif column_attrs['ColHeader'] != 'StructName':
  142. values[column_index] = float(values[column_index])
  143. self.structure_measurements[struct_name] \
  144. = dict(zip(column_names, values))
  145. self.structure_measurement_units = {
  146. c['FieldName']: self._filter_unit(c['Units']) for c in columns}
  147. @classmethod
  148. def read(cls, path: str) -> 'CorticalParcellationStats':
  149. stats = cls()
  150. with open(path, 'r') as stream:
  151. # pylint: disable=protected-access
  152. stats._read(stream)
  153. return stats