Skip to content

Commit

Permalink
Add support for FreeSurfer
Browse files Browse the repository at this point in the history
  • Loading branch information
jennydaman committed Feb 27, 2024
1 parent f815e5a commit 367a8c1
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 9 deletions.
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ def get_version(rel_path: str) -> str:
author_email='[email protected]',
url='https://github.com/FNNDSC/pl-visual-dataset',
packages=['visualdataset'],
package_data={
'visualdataset': ['colormaps/*']
},
install_requires=['chris_plugin'],
license='MIT',
entry_points={
Expand Down
17 changes: 12 additions & 5 deletions visualdataset/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env python
import os.path
from argparse import ArgumentParser, Namespace, ArgumentDefaultsHelpFormatter
from pathlib import Path

Expand All @@ -13,12 +14,14 @@
parser = ArgumentParser(description='Prepares a dataset for use with the ChRIS_ui '
'"Visual Datasets" feature.',
formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--matchers', type=str, required=True,
parser.add_argument('--mode', type=str, default='file', choices=['file', 'string', 'freesurfer-7.3.3'],
help='File matching and option selection mode. file=accept JSON files from '
'--matchers and --options. string=accept JSON strings from --matchers and --options. '
'freesurfer=built-in support for the FreeSurfer output files.')
parser.add_argument('--matchers', type=str,
help='Regular expressions used to assign tags to files')
parser.add_argument('--options', type=str,
help='Metadata to go with tag sets')
parser.add_argument('-s', '--string-args', action='store_true',
help='Interpret --matchers and --options as data instead of paths')
parser.add_argument('--first-run-files', type=str, default='[]',
help='List of files to show on first run, '
'as a stringified JSON list of paths relative to inputdir')
Expand All @@ -39,10 +42,14 @@
min_cpu_limit='1000m',
)
def main(options: Namespace, inputdir: Path, outputdir: Path):
matchers, tag_options = parse_args(options.matchers, options.options,
None if options.string_args else inputdir)
matchers, tag_options = parse_args(inputdir, options.mode, options.matchers, options.options)
first_run_files = _LIST_ADAPTER.validate_json(options.first_run_files)
first_run_tags = _DICT_ADAPTER.validate_json(options.first_run_tags)

if not first_run_files and options.mode.lower().startswith('freesurfer'):
if t1 := next(filter(os.path.isfile, inputdir.rglob('T1.mgz')), None):
first_run_files.append(str(t1.relative_to(inputdir)))

print(DISPLAY_TITLE, flush=True)
brain_dataset(inputdir, outputdir, matchers, tag_options, first_run_files, first_run_tags, options.readme)

Expand Down
35 changes: 34 additions & 1 deletion visualdataset/brain_dataset.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import shutil
import sys
from pathlib import Path
from typing import Sequence, Optional, Mapping, Set
from typing import Sequence, Optional, Mapping, Set, Iterator
import importlib.resources

from tqdm import tqdm

Expand Down Expand Up @@ -39,6 +41,8 @@ def brain_dataset(
'for the files of --first-run-files')
sys.exit(1)

copy_colormaplabel_files(options, input_dir, output_dir)

with tqdm(index, desc='Writing outputs') as pbar:
for file in pbar:
output_path = output_dir / file.path
Expand Down Expand Up @@ -91,3 +95,32 @@ def find_first_run_files(
print(f'File was not matched: {file}')
sys.exit(1)
return first_run_index_nums


def copy_colormaplabel_files(options: Sequence[OptionsLink], input_dir: Path, output_dir: Path):
for file in colormaplabel_files_of(options):
shutil.copy(resolve_colormaplabel(file, input_dir), output_dir / file)


def resolve_colormaplabel(file: str, input_dir: Path) -> Path:
"""
If ``file`` is found in ``input_dir``, return its path.
Else, look for the file in this package's files.
"""
path = input_dir / file
if path.is_file():
return path
trav = importlib.resources.files(__package__).joinpath('colormaps', file)
if trav.is_file():
with importlib.resources.as_file(trav) as trav_path:
return trav_path
print(f'colormapLabel not found: "{file}"')
sys.exit(1)


def colormaplabel_files_of(options: Sequence[OptionsLink]) -> Iterator[str]:
return filter(lambda x: x is not None, map(colormaplabel_of, options))


def colormaplabel_of(option: OptionsLink) -> Optional[str]:
return option.options.get('niivue_defaults', {}).get('colormapLabelFile', None)
Empty file.
13 changes: 10 additions & 3 deletions visualdataset/json_arg_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,23 @@

from visualdataset.args_types import Matcher
from visualdataset.manifest import OptionsLink
from visualdataset.wellknown import FREESURFER_MATCHERS, FREESURFER_OPTIONS


def parse_args(matchers: str | None, options: str | None, input_dir: Path | None,
def parse_args(input_dir: Path, mode: str, matchers: str | None, options: str | None
) -> tuple[Sequence[Matcher], Sequence[OptionsLink]]:
if input_dir:
mode = mode.lower()
if mode.startswith('freesurfer'):
return FREESURFER_MATCHERS, FREESURFER_OPTIONS
if mode == 'file':
matchers_str = '[]' if matchers is None else (input_dir / matchers).read_text()
options_str = '[]' if options is None else (input_dir / options).read_text()
else:
elif mode == 'string':
matchers_str = '[]' if matchers is None else matchers
options_str = '[]' if options is None else options
else:
print(f'Unsupported option --mode={mode}')
sys.exit(1)
matchers_list = deserialize_list(matchers_str, Matcher, '--matchers')
options_list = deserialize_list(options_str, OptionsLink, '--options')
return matchers_list, options_list
Expand Down
2 changes: 2 additions & 0 deletions visualdataset/wellknown/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from visualdataset.wellknown.freesurfer import FREESURFER_MATCHERS, FREESURFER_OPTIONS

82 changes: 82 additions & 0 deletions visualdataset/wellknown/freesurfer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""
See https://surfer.nmr.mgh.harvard.edu/fswiki/CorticalParcellation
"""

from typing import Sequence

from visualdataset.args_types import Matcher
from visualdataset.manifest import OptionsLink
from visualdataset.options import ChrisViewerFileOptions, NiivueVolumeSettings

FREESURFER_MATCHERS: Sequence[Matcher] = [
Matcher(key='type', value='T1 MRI', regex=r'mri/T1\.(mgz|nii|nii\.gz)$'),
Matcher(key='name', value='T1 MRI', regex=r'mri/T1\.(mgz|nii|nii\.gz)$'),
Matcher(key='type', value='T1 MRI', regex=r'mri/brainmask\.(mgz|nii|nii\.gz)$'),
Matcher(key='name', value='brain', regex=r'mri/brainmask\.(mgz|nii|nii\.gz)$'),

Matcher(key='type', value='labels', regex=r'mri/wmparc\.(mgz|nii|nii\.gz)$'),
Matcher(key='labels', value='White Matter Parcellation', regex=r'mri/wmparc\.(mgz|nii|nii\.gz)$'),

Matcher(key='type', value='labels', regex=r'mri/aparc\.a2009s\+aseg\.(mgz|nii|nii\.gz)$'),
Matcher(key='labels', value='Destrieux Atlas Parcellation', regex=r'mri/aparc\.a2009s\+aseg\.(mgz|nii|nii\.gz)$'),

Matcher(key='type', value='labels',
regex=r'mri/aparc\.DKTatlas\+aseg(\.(orig|deep|deep\.withCC))?\.(mgz|nii|nii\.gz)$'),
Matcher(key='labels', value='DKTatlas',
regex=r'mri/aparc\.DKTatlas\+aseg(\.(orig|deep|deep\.withCC))?\.(mgz|nii|nii\.gz)$'),
Matcher(key='program', value='FreeSurfer',
regex=r'mri/aparc\.DKTatlas\+aseg\.(mgz|nii|nii\.gz)$'),
Matcher(key='program', value='FastSurfer',
regex=r'mri/aparc\.DKTatlas\+aseg\.deep(\.withCC)?\.(mgz|nii|nii\.gz)$'),
Matcher(key='withCC', value='yes',
regex=r'mri/aparc\.DKTatlas\+aseg\.deep\.withCC\.(mgz|nii|nii\.gz)$'),
Matcher(key='withCC', value='no',
regex=r'mri/aparc\.DKTatlas\+aseg\.deep\.(mgz|nii|nii\.gz)$'),
Matcher(key='orig', value='yes',
regex=r'mri/aparc\.DKTatlas\+aseg\.orig\.(mgz|nii|nii\.gz)$')
]

FREESURFER_OPTIONS: Sequence[OptionsLink] = [
OptionsLink(
match={'type': 'T1 MRI'},
options=ChrisViewerFileOptions(niivue_defaults=NiivueVolumeSettings(colormap='gray'))
),
OptionsLink(
match={'name': 'T1 MRI'},
options=ChrisViewerFileOptions(name='T1 MRI')
),
OptionsLink(
match={'name': 'brain'},
options=ChrisViewerFileOptions(name='Extracted brain (skull-stripped)')
),
OptionsLink(
match={'type': 'labels'},
options=ChrisViewerFileOptions(
niivue_defaults=NiivueVolumeSettings(colormapLabelFile='FreeSurferColorLUT.v7.3.3.json')
)
),
OptionsLink(
match={'labels': 'White Matter Parcellation'},
options=ChrisViewerFileOptions(name='White Matter Parcellation')
),
OptionsLink(
match={'labels': 'Destrieux Atlas Parcellation'},
options=ChrisViewerFileOptions(name='Destrieux Atlas Parcellation')
),
OptionsLink(
match={'labels': 'DKTatlas', 'program': 'FreeSurfer'},
options=ChrisViewerFileOptions(name='Desikan-Killiany Atlas')
),
OptionsLink(
match={'labels': 'DKTatlas', 'program': 'FastSurfer', 'withCC': 'yes'},
options=ChrisViewerFileOptions(name='Desikan-Killiany Atlas (FastSurfer, with corpus callosum)')
),
OptionsLink(
match={'labels': 'DKTatlas', 'program': 'FastSurfer', 'withCC': 'no'},
options=ChrisViewerFileOptions(name='Desikan-Killiany Atlas (FastSurfer)')
),
OptionsLink(
match={'labels': 'DKTatlas', 'orig': 'yes'},
options=ChrisViewerFileOptions(name='Desikan-Killiany Atlas (original)')
),
]

0 comments on commit 367a8c1

Please sign in to comment.