Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion freesurfer_post/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@

__version__ = '0.1.0'
__author__ = 'Your Name'
__email__ = 'your.email@example.com'
__email__ = 'your.email@example.com'
20 changes: 17 additions & 3 deletions freesurfer_post/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,18 @@
@click.option('--subject-id', '-s', help='Subject ID to process')
@click.option('--session-id', '-x', help='Session ID to process')
@click.option('--working-dir', '-w', help='Path to working directory')
def main(verbose, input_path, output_path, processing_level, subjects_dir, subject_id, session_id, working_dir):
@click.option('--fs-license-file', '-l', help='Path to license file')
def main(
verbose,
input_path,
output_path,
processing_level,
subjects_dir,
subject_id,
session_id,
working_dir,
fs_license_file,
):
"""FreeSurfer Post-processing Tools.

A command line interface for post-processing FreeSurfer outputs.
Expand All @@ -41,6 +52,7 @@ def main(verbose, input_path, output_path, processing_level, subjects_dir, subje
click.echo(f'Subject directory: {subject_fs_dir}')
click.echo(f'Processing level: {processing_level}')
click.echo(f'Working directory: {working_dir}')
click.echo(f'FreeSurfer license file: {fs_license_file}')

workflow = build_workflow(
subject_id=subject_id,
Expand All @@ -50,8 +62,10 @@ def main(verbose, input_path, output_path, processing_level, subjects_dir, subje
working_dir=working_dir,
)

workflow.config['execution'] = {
'stop_on_first_crash': 'true'}
import os

os.environ['FS_LICENSE'] = fs_license_file
workflow.config['execution'] = {'stop_on_first_crash': 'true'}
workflow.run()


Expand Down
2 changes: 2 additions & 0 deletions freesurfer_post/interfaces/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ class _WaitingInputSpec(TraitedSpec):
mandatory=True,
)


class _WaitingOutputSpec(TraitedSpec):
no_args = traits.Str(
desc='No args',
mandatory=True,
)


class Waiting(SimpleInterface):
# Force a bunch of nodes to finish and then retuen an empty string
# to put in "args" of the dependent workflow
Expand Down
8 changes: 5 additions & 3 deletions freesurfer_post/interfaces/tabular.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@


def statsfile_to_df(stats_fname, hemi, atlas, column_suffix=''):
with open(stats_fname, 'r') as fo:
with open(stats_fname) as fo:
data = fo.readlines()

idx = [i for i, l in enumerate(data) if l.startswith('# ColHeaders ')]

Check failure on line 21 in freesurfer_post/interfaces/tabular.py

View workflow job for this annotation

GitHub Actions / lint (3.12)

Ruff (E741)

freesurfer_post/interfaces/tabular.py:21:21: E741 Ambiguous variable name: `l`
assert len(idx) == 1
idx = idx[0]

Expand Down Expand Up @@ -137,7 +137,9 @@
output_dir = Path(self.inputs.output_dir) / subject_id
output_dir.mkdir(parents=True, exist_ok=True)
output_prefix = f'{subject_id}_{session_id}' if session_id else subject_id
cleaned_atlas_name = atlas.replace('.', '').replace('_order', '').replace('_', '')
cleaned_atlas_name = (
atlas.replace('.', '').replace('_order', '').replace('_', '')
)
out_df.to_csv(
output_dir / f'{output_prefix}_atlas-{cleaned_atlas_name}_surfacestats.tsv',
sep='\t',
Expand Down Expand Up @@ -204,7 +206,7 @@
(header,) = [line for line in lines if header_tag in line]
header = header[len(header_tag) :].strip().split()

stats_df = pd.read_csv(str(stats_file), sep='\s+', comment='#', names=header).melt(
stats_df = pd.read_csv(str(stats_file), sep=r'\s+', comment='#', names=header).melt(
id_vars=['StructName'], ignore_index=True
)
stats_df = stats_df[stats_df['variable'] != 'Index']
Expand Down
13 changes: 9 additions & 4 deletions freesurfer_post/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"""Utility functions for FreeSurfer post-processing."""

import warnings
from pathlib import Path


def find_freesurfer_dir(subjects_dir: str | Path, subject_id: str, session_id: str | None = None) -> Path:
def find_freesurfer_dir(
subjects_dir: str | Path, subject_id: str, session_id: str | None = None
) -> Path:
"""Find a valid FreeSurfer subject directory in a directory.

Parameters
Expand All @@ -27,8 +30,8 @@ def find_freesurfer_dir(subjects_dir: str | Path, subject_id: str, session_id: s

warn_no_session = False
if session_id is not None:
if (subjects_dir / f"{subject_id}_{session_id}").exists():
return subjects_dir / f"{subject_id}_{session_id}"
if (subjects_dir / f'{subject_id}_{session_id}').exists():
return subjects_dir / f'{subject_id}_{session_id}'
warn_no_session = True

if (subjects_dir / subject_id).exists():
Expand All @@ -39,4 +42,6 @@ def find_freesurfer_dir(subjects_dir: str | Path, subject_id: str, session_id: s
stacklevel=2,
)
return subjects_dir / subject_id
raise FileNotFoundError(f'No directory found for subject: {subject_id}, session: {session_id}')
raise FileNotFoundError(
f'No directory found for subject: {subject_id}, session: {session_id}'
)
82 changes: 51 additions & 31 deletions freesurfer_post/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ def build_workflow(

inputnode = pe.Node(
niu.IdentityInterface(
fields=['subject_id', 'session_id', 'fs_subjects_dir', 'output_dir']),
fields=['subject_id', 'session_id', 'fs_subjects_dir', 'output_dir']
),
name='inputnode',
)
inputnode.inputs.subject_id = subject_id
Expand All @@ -73,29 +74,44 @@ def build_workflow(
subject_freesurfer_dir=subject_freesurfer_dir,
parc_name=parc_name,
)
workflow.connect([
(inputnode, parc_wf, [('subject_id', 'inputnode.subject_id'),
('session_id', 'inputnode.session_id'),
('fs_subjects_dir', 'inputnode.fs_subjects_dir'),
('output_dir', 'inputnode.output_dir')]),
])
workflow.connect(
[
(
inputnode,
parc_wf,
[
('subject_id', 'inputnode.subject_id'),
('session_id', 'inputnode.session_id'),
('fs_subjects_dir', 'inputnode.fs_subjects_dir'),
('output_dir', 'inputnode.output_dir'),
],
),
]
)

# Get the segmentation stats and euler number
fs_stats = pe.Node(FSStats(), name='fs_stats')
workflow.connect([
(inputnode, fs_stats, [('subject_id', 'subject_id'),
('session_id', 'session_id'),
('fs_subjects_dir', 'subjects_dir'),
('output_dir', 'output_dir')]),
])
workflow.connect(
[
(
inputnode,
fs_stats,
[
('subject_id', 'subject_id'),
('session_id', 'session_id'),
('fs_subjects_dir', 'subjects_dir'),
('output_dir', 'output_dir'),
],
),
]
)

return workflow


def init_parcellation_wf(
subject_id: str,
subject_freesurfer_dir: str | Path,
parc_name: str):
subject_id: str, subject_freesurfer_dir: str | Path, parc_name: str
):
"""
Initialize a workflow to process a single parcellation.

Expand Down Expand Up @@ -126,7 +142,8 @@ def init_parcellation_wf(
"""
inputnode = pe.Node(
niu.IdentityInterface(
fields=['subject_id', 'session_id', 'fs_subjects_dir', 'output_dir']),
fields=['subject_id', 'session_id', 'fs_subjects_dir', 'output_dir']
),
name='inputnode',
)
clean_parc_name = parc_name.replace('.', '').replace('_', '')
Expand All @@ -138,25 +155,32 @@ def init_parcellation_wf(
),
name='collect_stats',
)
workflow.connect([
(inputnode, collect_stats, [
('subject_id', 'subject_id'),
('session_id', 'session_id'),
('fs_subjects_dir', 'subjects_dir'),
('output_dir', 'output_dir'),
]),
])
workflow.connect(
[
(
inputnode,
collect_stats,
[
('subject_id', 'subject_id'),
('session_id', 'session_id'),
('fs_subjects_dir', 'subjects_dir'),
('output_dir', 'output_dir'),
],
),
]
)
transform_nodes = {}
parc_stats_nodes = {}
gwr_seg_stats_nodes = {}
for hemi in ['lh', 'rh']:

fsaverage_annot = ANNOTS_DIR / f'{hemi}.{parc_name}.annot'
# native annot is created by SurfaceTransform if it's not a NATIVE_PARCELLATION
# otherwise it's already present in the subject's directory
native_annot = subject_freesurfer_dir / 'label' / f'{hemi}.{parc_name}.annot'
stats_file = subject_freesurfer_dir / 'stats' / f'{hemi}.{parc_name}.stats'
gwr_stats_file = subject_freesurfer_dir / 'stats' / f'{hemi}.{parc_name}.g-w.pct.stats'
gwr_stats_file = (
subject_freesurfer_dir / 'stats' / f'{hemi}.{parc_name}.g-w.pct.stats'
)
if parc_name in AVAILABLE_PARCELLATIONS:
transform_nodes[hemi] = pe.Node(
fs.SurfaceTransform(
Expand Down Expand Up @@ -229,7 +253,3 @@ def init_parcellation_wf(
]) # fmt: off

return workflow




4 changes: 3 additions & 1 deletion tests/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ docker run --rm -ti --entrypoint /bin/bash \
-v /Users/mcieslak/Desktop/fmriprep_anat:/data \
-v /Users/mcieslak/Desktop/freesurfer_post:/output \
-v /Users/mcieslak/Desktop/fspost_work:/work \
--mount type=bind,source=/Users/mcieslak/Desktop/license.txt,target=/opt/freesurfer/license.txt \
--mount type=bind,source=/Users/mcieslak/Desktop/license.txt,target=/opt/fs_license.txt \
-v /Users/mcieslak/projects/freesurfer-post/freesurfer_post:/opt/conda/envs/freesurfer-post/lib/python3.12/site-packages/freesurfer_post \
pennlinc/freesurfer-post:unstable \
/data \
Expand All @@ -18,5 +18,7 @@ freesurfer-post \
participant \
--subjects-dir /data/sourcedata/freesurfer \
--subject-id sub-colornest001 \
--fs-license-file /opt/fs_license.txt \
--session-id ses-01 \
-w /work