Skip to content

Commit bfa1ccd

Browse files
authored
Merge pull request #1925 from FCP-INDI/develop
Update main with recent changes to develop (FS ingress fix)
2 parents 7f94c3a + a37fbd9 commit bfa1ccd

37 files changed

+438
-347
lines changed

.github/workflows/smoke_test_participant.yml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -122,14 +122,6 @@ jobs:
122122
- ''
123123
participant:
124124
- 032102 032106 032164 032167 032130 032128 2215 2312 032191 032195
125-
include:
126-
# requires T2w
127-
- preconfig: monkey-ABCD
128-
variant: ''
129-
participant: 032102 032106 032130 032128 032191 032195
130-
- preconfig: monkey-ABCD
131-
variant: ABCD-HCP
132-
participant: 032102 032106 032130 032128 032191 032195
133125
steps:
134126
- name: Get C-PAC
135127
run: |

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3030
- Added NodeBlock information to `pypeline.log` when verbose debugging is on
3131
- Added the ability to ingress FreeSurfer data into CPAC
3232
- Added the ability to toggle FreeSurfer derived masks for brain extraction
33+
- Added an optional volume center to FD-J calculation
34+
- Added new preconfig `abcd-prep`, which performs minimal preprocessing on the T1w data in preparation for Freesurfer Recon-All
3335

3436
### Changed
3537
- Freesurfer output directory ingress moved to the data configuration YAML

CPAC/anat_preproc/anat_preproc.py

Lines changed: 61 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
mri_convert, \
1717
wb_command, \
1818
fslmaths_command, \
19-
VolumeRemoveIslands
19+
VolumeRemoveIslands, \
20+
normalize_wmparc
2021
from CPAC.pipeline.engine import flatten_list
2122
from CPAC.utils.docs import docstring_parameter
2223
from CPAC.utils.interfaces.fsl import Merge as fslMerge
@@ -914,16 +915,44 @@ def freesurfer_abcd_brain_connector(wf, cfg, strat_pool, pipe_num, opt):
914915
### ABCD harmonization - anatomical brain mask generation ###
915916
# Ref: https://github.com/DCAN-Labs/DCAN-HCP/blob/master/PostFreeSurfer/PostFreeSurferPipeline.sh#L151-L156
916917

918+
917919
wmparc_to_nifti = pe.Node(util.Function(input_names=['in_file',
918920
'reslice_like',
919921
'args'],
920922
output_names=['out_file'],
921923
function=mri_convert),
922924
name=f'wmparc_to_nifti_{pipe_num}')
923-
wmparc_to_nifti.inputs.args = '-rt nearest'
925+
926+
# Register wmparc file if ingressing FreeSurfer data
927+
if strat_pool.check_rpool('pipeline-fs_xfm'):
928+
929+
wmparc_to_native = pe.Node(util.Function(input_names=['source_file',
930+
'target_file',
931+
'xfm',
932+
'out_file'],
933+
output_names=['transformed_file'],
934+
function=normalize_wmparc),
935+
name=f'wmparc_to_native_{pipe_num}')
936+
937+
wmparc_to_native.inputs.out_file = 'wmparc_warped.mgz'
938+
939+
node, out = strat_pool.get_data('pipeline-fs_wmparc')
940+
wf.connect(node, out, wmparc_to_native, 'source_file')
941+
942+
node, out = strat_pool.get_data('pipeline-fs_raw-average')
943+
wf.connect(node, out, wmparc_to_native, 'target_file')
924944

925-
node, out = strat_pool.get_data('pipeline-fs_wmparc')
926-
wf.connect(node, out, wmparc_to_nifti, 'in_file')
945+
node, out = strat_pool.get_data('pipeline-fs_xfm')
946+
wf.connect(node, out, wmparc_to_native, 'xfm')
947+
948+
wf.connect(wmparc_to_native, 'transformed_file', wmparc_to_nifti, 'in_file')
949+
950+
else:
951+
952+
node, out = strat_pool.get_data('pipeline-fs_wmparc')
953+
wf.connect(node, out, wmparc_to_nifti, 'in_file')
954+
955+
wmparc_to_nifti.inputs.args = '-rt nearest'
927956

928957
node, out = strat_pool.get_data('desc-preproc_T1w')
929958
wf.connect(node, out, wmparc_to_nifti, 'reslice_like')
@@ -1244,21 +1273,24 @@ def acpc_align_head(wf, cfg, strat_pool, pipe_num, opt=None):
12441273
["anatomical_preproc", "run"]],
12451274
"option_key": "None",
12461275
"option_val": "None",
1247-
"inputs": ["desc-head_T1w",
1248-
"desc-preproc_T1w",
1276+
"inputs": ["desc-preproc_T1w",
1277+
"desc-head_T1w",
12491278
"T1w-ACPC-template"],
12501279
"outputs": ["desc-head_T1w",
12511280
"desc-preproc_T1w",
12521281
"from-T1w_to-ACPC_mode-image_desc-aff2rig_xfm"]}
12531282
'''
1254-
1283+
1284+
# NOTE: desc-preproc_T1w and desc-head_T1w are both needed in this
1285+
# nodeblock despite the list called in get_data() below
1286+
12551287
acpc_align = acpc_alignment(config=cfg,
12561288
acpc_target=cfg.anatomical_preproc[
12571289
'acpc_alignment']['acpc_target'],
12581290
mask=False,
12591291
wf_name=f'acpc_align_{pipe_num}')
12601292

1261-
node, out = strat_pool.get_data(['desc-head_T1w', 'desc-preproc_T1w'])
1293+
node, out = strat_pool.get_data(['desc-preproc_T1w','desc-head_T1w'])
12621294
wf.connect(node, out, acpc_align, 'inputspec.anat_leaf')
12631295

12641296
node, out = strat_pool.get_data('T1w-ACPC-template')
@@ -1772,6 +1804,8 @@ def brain_mask_freesurfer_abcd(wf, cfg, strat_pool, pipe_num, opt=None):
17721804
"option_val": "FreeSurfer-ABCD",
17731805
"inputs": ["desc-preproc_T1w",
17741806
"pipeline-fs_wmparc",
1807+
"pipeline-fs_raw-average",
1808+
"pipeline-fs_xfm",
17751809
"freesurfer-subject-dir"],
17761810
"outputs": ["space-T1w_desc-brain_mask"]}
17771811
'''
@@ -1813,6 +1847,8 @@ def brain_mask_acpc_freesurfer_abcd(wf, cfg, strat_pool, pipe_num, opt=None):
18131847
"option_val": "FreeSurfer-ABCD",
18141848
"inputs": ["desc-preproc_T1w",
18151849
"pipeline-fs_wmparc",
1850+
"pipeline-fs_raw-average",
1851+
"pipeline-fs_xfm",
18161852
"freesurfer-subject-dir"],
18171853
"outputs": ["space-T1w_desc-acpcbrain_mask"]}
18181854
'''
@@ -2533,10 +2569,10 @@ def brain_extraction_temp_T2(wf, cfg, strat_pool, pipe_num, opt=None):
25332569
def freesurfer_abcd_preproc(wf, cfg, strat_pool, pipe_num, opt=None):
25342570
'''
25352571
{"name": "freesurfer_abcd_preproc",
2536-
"config": ["anatomical_preproc", "brain_extraction"],
2537-
"switch": "None",
2538-
"option_key": "using",
2539-
"option_val": "FreeSurfer-ABCD",
2572+
"config": ["surface_analysis", "abcd_prefreesurfer_prep"],
2573+
"switch": ["run"],
2574+
"option_key": "None",
2575+
"option_val": "None",
25402576
"inputs": ["desc-preproc_T1w",
25412577
"T1w-template",
25422578
"T1w-brain-template-mask",
@@ -2545,16 +2581,17 @@ def freesurfer_abcd_preproc(wf, cfg, strat_pool, pipe_num, opt=None):
25452581
"freesurfer-subject-dir"],
25462582
"outputs": ["desc-restore_T1w",
25472583
"desc-restore-brain_T1w",
2584+
"desc-ABCDpreproc_T1w",
25482585
"pipeline-fs_desc-fast_biasfield",
25492586
"pipeline-fs_hemi-L_desc-surface_curv",
2550-
"pipeline-fs_hemi-R_desc-surface_curv",
2587+
"pipeline-fs_hemi-R_desc-surface_curv",
25512588
"pipeline-fs_hemi-L_desc-surfaceMesh_pial",
25522589
"pipeline-fs_hemi-R_desc-surfaceMesh_pial",
25532590
"pipeline-fs_hemi-L_desc-surfaceMesh_smoothwm",
25542591
"pipeline-fs_hemi-R_desc-surfaceMesh_smoothwm",
25552592
"pipeline-fs_hemi-L_desc-surfaceMesh_sphere",
25562593
"pipeline-fs_hemi-R_desc-surfaceMesh_sphere",
2557-
"pipeline-fs_hemi-L_desc-surfaceMap_sulc",
2594+
"pipeline-fs_hemi-L_desc-surfaceMap_sulc",
25582595
"pipeline-fs_hemi-R_desc-surfaceMap_sulc",
25592596
"pipeline-fs_hemi-L_desc-surfaceMap_thickness",
25602597
"pipeline-fs_hemi-R_desc-surfaceMap_thickness",
@@ -2565,7 +2602,6 @@ def freesurfer_abcd_preproc(wf, cfg, strat_pool, pipe_num, opt=None):
25652602
"pipeline-fs_wmparc",
25662603
"freesurfer-subject-dir"]}
25672604
'''
2568-
25692605
# fnirt-based brain extraction
25702606
brain_extraction = fnirt_based_brain_extraction(config=cfg,
25712607
wf_name=f'fnirt_based_brain_extraction_{pipe_num}')
@@ -2657,52 +2693,15 @@ def freesurfer_abcd_preproc(wf, cfg, strat_pool, pipe_num, opt=None):
26572693
wf.connect(average_brain, 'out_stat',
26582694
normalize_head, 'number')
26592695

2660-
if strat_pool.check_rpool('freesurfer-subject-dir'):
2661-
outputs = {
2696+
outputs = {
26622697
'desc-restore_T1w': (fast_correction, 'outputspec.anat_restore'),
26632698
'desc-restore-brain_T1w': (fast_correction,
26642699
'outputspec.anat_brain_restore'),
2665-
'pipeline-fs_desc-fast_biasfield': (fast_correction, 'outputspec.bias_field')}
2666-
return (wf, outputs)
2667-
2668-
else:
2669-
### recon-all -all step ###
2670-
reconall = pe.Node(interface=freesurfer.ReconAll(),
2671-
name=f'anat_freesurfer_{pipe_num}',
2672-
mem_gb=2.7)
2673-
2674-
sub_dir = cfg.pipeline_setup['working_directory']['path']
2675-
freesurfer_subject_dir = os.path.join(sub_dir,
2676-
'cpac_'+cfg['subject_id'],
2677-
f'anat_preproc_freesurfer_{pipe_num}',
2678-
'anat_freesurfer')
2679-
2680-
# create the directory for FreeSurfer node
2681-
if not os.path.exists(freesurfer_subject_dir):
2682-
os.makedirs(freesurfer_subject_dir)
2683-
2684-
reconall.inputs.directive = 'all'
2685-
reconall.inputs.subjects_dir = freesurfer_subject_dir
2686-
reconall.inputs.openmp = cfg.pipeline_setup['system_config']['num_OMP_threads']
2687-
2688-
wf.connect(normalize_head, 'out_file',
2689-
reconall, 'T1_files')
2690-
2691-
wf, hemisphere_outputs = freesurfer_hemispheres(wf, reconall, pipe_num)
2692-
2693-
outputs = {
2694-
'desc-restore_T1w': (fast_correction, 'outputspec.anat_restore'),
2695-
'desc-restore-brain_T1w': (fast_correction,
2696-
'outputspec.anat_brain_restore'),
2697-
'pipeline-fs_desc-fast_biasfield': (fast_correction, 'outputspec.bias_field'),
2698-
'pipeline-fs_wmparc': (reconall, 'wmparc'),
2699-
'freesurfer-subject-dir': (reconall, 'subjects_dir'),
2700-
**hemisphere_outputs
2701-
}
2702-
2700+
'pipeline-fs_desc-fast_biasfield': (fast_correction, 'outputspec.bias_field'),
2701+
'desc-ABCDpreproc_T1w': (normalize_head, 'out_file')
2702+
}
27032703
return (wf, outputs)
27042704

2705-
27062705
# we're grabbing the postproc outputs and appending them to
27072706
# the reconall outputs
27082707
@docstring_parameter(postproc_outputs=str(flatten_list(freesurfer_abcd_preproc, 'outputs')
@@ -2715,7 +2714,8 @@ def freesurfer_reconall(wf, cfg, strat_pool, pipe_num, opt=None):
27152714
"switch": ["run_reconall"],
27162715
"option_key": "None",
27172716
"option_val": "None",
2718-
"inputs": ["desc-preproc_T1w"],
2717+
"inputs": [["desc-ABCDpreproc_T1w",
2718+
"desc-preproc_T1w"]],
27192719
"outputs": ["freesurfer-subject-dir",
27202720
"pipeline-fs_raw-average",
27212721
"pipeline-fs_subcortical-seg",
@@ -2748,7 +2748,7 @@ def freesurfer_reconall(wf, cfg, strat_pool, pipe_num, opt=None):
27482748
reconall.inputs.args = cfg.surface_analysis['freesurfer'][
27492749
'reconall_args']
27502750

2751-
node, out = strat_pool.get_data("desc-preproc_T1w")
2751+
node, out = strat_pool.get_data(["desc-ABCDpreproc_T1w","desc-preproc_T1w"])
27522752
wf.connect(node, out, reconall, 'T1_files')
27532753

27542754
wf, hemisphere_outputs = freesurfer_hemispheres(wf, reconall, pipe_num)
@@ -2983,9 +2983,9 @@ def fast_bias_field_correction(config=None, wf_name='fast_bias_field_correction'
29832983
def correct_restore_brain_intensity_abcd(wf, cfg, strat_pool, pipe_num, opt=None):
29842984
'''
29852985
{"name": "correct_restore_brain_intensity_abcd",
2986-
"config": ["anatomical_preproc", "brain_extraction"],
2987-
"switch": "None",
2988-
"option_key": "using",
2986+
"config": "None",
2987+
"switch": ["anatomical_preproc", "brain_extraction", "run"],
2988+
"option_key": ["anatomical_preproc", "brain_extraction", "using"],
29892989
"option_val": "FreeSurfer-ABCD",
29902990
"inputs": [("desc-preproc_T1w",
29912991
"desc-n4_T1w",

CPAC/anat_preproc/utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,15 @@ def fslmaths_command(in_file, number, out_file_suffix):
420420

421421
return out_file
422422

423+
def normalize_wmparc(source_file, target_file, xfm, out_file):
424+
from CPAC.utils.monitoring.custom_logging import log_subprocess
425+
import os
426+
427+
cmd = ['mri_vol2vol', '--mov', source_file, \
428+
'--targ', target_file, '--o', out_file, '--lta', xfm]
429+
log_subprocess(cmd)
430+
output = os.path.join(os.getcwd(), out_file)
431+
return output
423432

424433
"""This module provides interfaces for workbench -volume-remove-islands commands"""
425434
from nipype.interfaces.base import TraitedSpec, File, traits, CommandLineInputSpec

CPAC/generate_motion_statistics/generate_motion_statistics.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -324,21 +324,31 @@ def calculate_FD_P(in_file):
324324
return out_file
325325

326326

327-
def calculate_FD_J(in_file, motion_correct_tool='3dvolreg'):
327+
def calculate_FD_J(in_file, motion_correct_tool='3dvolreg', center=None):
328328
"""
329329
Method to calculate framewise displacement as per Jenkinson et al. 2002
330330
331331
Parameters
332332
----------
333333
in_file : string
334-
matrix transformations from volume alignment file path
334+
matrix transformations from volume alignment file path if
335+
motion_correct_tool is '3dvolreg', or FDRMS (*_rel.rms) output if
336+
motion_correct_tool is 'mcflirt'.
337+
motion_correct_tool : string
338+
motion correction tool used, '3dvolreg' or 'mcflirt'.
339+
center : ndarray
340+
optional volume center for the calculation.
335341
336342
Returns
337343
-------
338344
out_file : string
339345
Frame-wise displacement file path
340346
341347
"""
348+
if center is None:
349+
center = np.zeros((3, 1))
350+
else:
351+
center = np.asarray(center).reshape((3, 1))
342352

343353
if motion_correct_tool == '3dvolreg':
344354
pm_ = np.genfromtxt(in_file)
@@ -359,7 +369,7 @@ def calculate_FD_J(in_file, motion_correct_tool='3dvolreg'):
359369

360370
M = np.dot(T_rb, np.linalg.inv(T_rb_prev)) - np.eye(4)
361371
A = M[0:3, 0:3]
362-
b = M[0:3, 3]
372+
b = M[0:3, 3:4] + A @ center
363373

364374
fd[i] = np.sqrt(
365375
(rmax * rmax / 5) * np.trace(np.dot(A.T, A)) + np.dot(b.T, b)
@@ -370,13 +380,39 @@ def calculate_FD_J(in_file, motion_correct_tool='3dvolreg'):
370380
elif motion_correct_tool == 'mcflirt':
371381
rel_rms = np.loadtxt(in_file)
372382
fd = np.append(0, rel_rms)
383+
384+
else:
385+
raise ValueError(f"motion_correct_tool {motion_correct_tool} not supported")
373386

374387
out_file = os.path.join(os.getcwd(), 'FD_J.1D')
375388
np.savetxt(out_file, fd, fmt='%.8f')
376389

377390
return out_file
378391

379392

393+
def find_volume_center(img_file):
394+
"""
395+
Find the center of mass of a Nifti image volume
396+
397+
Parameters
398+
----------
399+
img_file : string (nifti file)
400+
path to nifti volume image
401+
402+
Returns
403+
-------
404+
center : ndarray
405+
volume center of mass vector
406+
"""
407+
img = nb.load(img_file)
408+
dim = np.array(img.header["dim"][1:4])
409+
pixdim = np.array(img.header["pixdim"][1:4])
410+
# Calculation follows MCFLIRT
411+
# https://github.com/fithisux/FSL/blob/7aa2932949129f5c61af912ea677d4dbda843895/src/mcflirt/mcflirt.cc#L479
412+
center = 0.5 * (dim - 1) * pixdim
413+
return center
414+
415+
380416
def gen_motion_parameters(subject_id, scan_id, movement_parameters,
381417
max_displacement, motion_correct_tool):
382418
"""

CPAC/pipeline/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
x in ALL_PIPELINE_CONFIGS if 'pipeline_config' in x]
2626
ALL_PIPELINE_CONFIGS.sort()
2727
AVAILABLE_PIPELINE_CONFIGS = [preconfig for preconfig in ALL_PIPELINE_CONFIGS
28-
if preconfig != 'benchmark-ANTS' and
28+
if preconfig not in
29+
['benchmark-ANTS', 'monkey-ABCD'] and
2930
not preconfig.startswith('regtest-')]
3031

3132

0 commit comments

Comments
 (0)