| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- """
- Python Library to Read FreeSurfer's cortical parcellation anatomical statistics
- ([lh]h.aparc(.*)?.stats)
- Freesurfer
- https://surfer.nmr.mgh.harvard.edu/
- >>> from freesurfer_stats import CorticalParcellationStats
- >>> stats = CorticalParcellationStats.read('tests/subjects/fabian/stats/lh.aparc.DKTatlas.stats')
- >>> stats.headers['CreationTime'].isoformat()
- '2019-05-09T21:05:54+00:00'
- >>> stats.headers['cvs_version']
- 'Id: mris_anatomical_stats.c,v 1.79 2016/03/14 15:15:34 greve Exp'
- >>> stats.headers['cmdline'][:64]
- 'mris_anatomical_stats -th3 -mgz -cortex ../label/lh.cortex.label'
- >>> stats.hemisphere
- >>> stats.whole_brain_measurements['Estimated Total Intracranial Volume']
- (1670487.274486, 'mm^3')
- >>> stats.whole_brain_measurements['White Surface Total Area']
- (98553.0, 'mm^2')
- >>> stats.structure_measurements['postcentral']
- {'Structure Name': 'postcentral',
- 'Number of Vertices': 8102,
- 'Surface Area': 5258.0,
- 'Gray Matter Volume': 12037.0,
- 'Average Thickness': 2.109,
- 'Thickness StdDev': 0.568,
- ...}
- >>> stats.structure_measurement_units
- {'Structure Name': None,
- 'Number of Vertices': None,
- 'Surface Area': 'mm^2',
- 'Gray Matter Volume': 'mm^3',
- 'Average Thickness': 'mm',
- 'Thickness StdDev': 'mm',
- ...}
- """
- import datetime
- import re
- import typing
- import pandas
- from freesurfer_stats.version import __version__
- class CorticalParcellationStats:
- _HEMISPHERE_PREFIX_TO_SIDE = {'lh': 'left', 'rh': 'right'}
- _GENERAL_MEASUREMENTS_REGEX = re.compile(
- r'^Measure \S+, ([^,\s]+),? ([^,]+), ([\d\.]+), (\S+)$')
- def __init__(self):
- self.headers \
- = {} # type: typing.Dict[str, typing.Union[str, datetime.datetime]]
- self.whole_brain_measurements \
- = {} # type: typing.Dict[str, typing.Tuple[float, int]]
- self.structure_measurements \
- = {} # type: typing.Dict[str, typing.Dict[str, typing.Union[str, int, float]]]
- self.structure_measurement_units \
- = {} # type: typing.Dict[str, typing.Union[str, None]]
- @property
- def hemisphere(self) -> str:
- return self._HEMISPHERE_PREFIX_TO_SIDE[self.headers['hemi']]
- @staticmethod
- def _read_header_line(stream: typing.TextIO) -> str:
- line = stream.readline()
- assert line.startswith('# ')
- return line[2:].rstrip()
- @classmethod
- def _read_column_header_line(cls, stream: typing.TextIO) -> typing.Tuple[int, str, str]:
- line = cls._read_header_line(stream)
- assert line.startswith('TableCol'), line
- line = line[len('TableCol '):].lstrip()
- index, key, value = line.split(maxsplit=2)
- return int(index), key, value
- def _read_headers(self, stream: typing.TextIO) -> None:
- self.headers = {}
- while True:
- line = self._read_header_line(stream)
- if line.startswith('Measure'):
- break
- elif line:
- attr_name, attr_value = line.split(' ', maxsplit=1)
- attr_value = attr_value.lstrip()
- if attr_name in ['cvs_version', 'mrisurf.c-cvs_version']:
- attr_value = attr_value.strip('$').rstrip()
- if attr_name == 'CreationTime':
- attr_dt = datetime.datetime.strptime(
- attr_value, '%Y/%m/%d-%H:%M:%S-%Z')
- if attr_dt.tzinfo is None:
- assert attr_value.endswith('-GMT')
- attr_dt = attr_dt.replace(tzinfo=datetime.timezone.utc)
- attr_value = attr_dt
- if attr_name == 'AnnotationFileTimeStamp':
- attr_value = datetime.datetime.strptime(
- attr_value, '%Y/%m/%d %H:%M:%S')
- self.headers[attr_name] = attr_value
- @staticmethod
- def _filter_unit(unit: str) -> typing.Union[str, None]:
- if unit in ['unitless', 'NA']:
- return None
- return unit
- @classmethod
- def _read_column_attributes(cls, num: int, stream: typing.TextIO) \
- -> typing.List[typing.Dict[str, str]]:
- columns = []
- for column_index in range(1, int(num) + 1):
- column_attrs = {}
- for _ in range(3):
- column_index_line, key, value \
- = cls._read_column_header_line(stream)
- assert column_index_line == column_index
- assert key not in column_attrs
- column_attrs[key] = value
- columns.append(column_attrs)
- return columns
- def _read(self, stream: typing.TextIO) -> None:
- assert stream.readline().rstrip() \
- == '# Table of FreeSurfer cortical parcellation anatomical statistics'
- assert stream.readline().rstrip() == '#'
- self._read_headers(stream)
- self.whole_brain_measurements = {}
- line = self._read_header_line(stream)
- while not line.startswith('NTableCols'):
- key, name, value, unit \
- = self._GENERAL_MEASUREMENTS_REGEX.match(line).groups()
- if key == 'SupraTentorialVolNotVent' and name.lower() == 'supratentorial volume':
- name += ' Without Ventricles'
- assert name not in self.whole_brain_measurements, \
- (key, name, self.whole_brain_measurements)
- self.whole_brain_measurements[name] = (float(value), unit)
- line = self._read_header_line(stream)
- columns = self._read_column_attributes(
- int(line[len('NTableCols '):]), stream)
- assert self._read_header_line(stream) \
- == 'ColHeaders ' + ' '.join(c['ColHeader'] for c in columns)
- assert columns[0]['ColHeader'] == 'StructName'
- column_names = [c['FieldName'] for c in columns]
- self.structure_measurements = {}
- for line in stream:
- values = line.rstrip().split()
- assert len(values) == len(column_names)
- struct_name = values[0]
- assert struct_name not in self.structure_measurements
- for column_index, column_attrs in enumerate(columns):
- if column_attrs['ColHeader'] in ['NumVert', 'FoldInd']:
- values[column_index] = int(values[column_index])
- elif column_attrs['ColHeader'] != 'StructName':
- values[column_index] = float(values[column_index])
- self.structure_measurements[struct_name] \
- = dict(zip(column_names, values))
- self.structure_measurement_units = {
- c['FieldName']: self._filter_unit(c['Units']) for c in columns}
- @classmethod
- def read(cls, path: str) -> 'CorticalParcellationStats':
- stats = cls()
- with open(path, 'r') as stream:
- # pylint: disable=protected-access
- stats._read(stream)
- return stats
|