diff --git a/CHANGELOG.md b/CHANGELOG.md index fecdc489f3..a82254901b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Switch `sink_native_transforms` under `registration_workflows` to output all `.mat` files in ANTs and FSL Transforms. - `deoblique` field in pipeline config with `warp` and `refit` options to apply `3dWarp` or `3drefit` during data initialization. - `organism` configuration option. +- Functionality to convert `space-T1w_desc-loose_brain_mask` and `space-T1w_desc-tight_brain_mask` into generic brain mask `space-T1w_desc-brain_mask` to use in brain extraction nodeblock downstream. - `desc-ABCDpreproc_T1w` to the outputs - `bc` to `lite` container images. diff --git a/CPAC/anat_preproc/anat_preproc.py b/CPAC/anat_preproc/anat_preproc.py index 2b51139505..4e9639c5b5 100644 --- a/CPAC/anat_preproc/anat_preproc.py +++ b/CPAC/anat_preproc/anat_preproc.py @@ -1303,7 +1303,7 @@ def freesurfer_fsl_brain_connector(wf, cfg, strat_pool, pipe_num, opt): # fslmaths tmp_mask.nii.gz -mas ${CCSDIR}/templates/MNI152_T1_1mm_first_brain_mask.nii.gz tmp_mask.nii.gz apply_mask = pe.Node(interface=fsl.maths.ApplyMask(), name=f"apply_mask_{node_id}") - wf.connect(skullstrip, "out_file", apply_mask, "in_file") + wf.connect(skullstrip, "mask_file", apply_mask, "in_file") node, out = strat_pool.get_data("T1w-brain-template-mask-ccs") wf.connect(node, out, apply_mask, "mask_file") @@ -1347,36 +1347,18 @@ def freesurfer_fsl_brain_connector(wf, cfg, strat_pool, pipe_num, opt): wf.connect(combine_mask, "out_file", binarize_combined_mask, "in_file") - # CCS brain mask is in FS space, transfer it back to native T1 space - fs_fsl_brain_mask_to_native = pe.Node( - interface=freesurfer.ApplyVolTransform(), - name=f"fs_fsl_brain_mask_to_native_{node_id}", - ) - fs_fsl_brain_mask_to_native.inputs.reg_header = True - fs_fsl_brain_mask_to_native.inputs.interp = "nearest" - - wf.connect( - binarize_combined_mask, "out_file", fs_fsl_brain_mask_to_native, "source_file" - ) - - node, out = strat_pool.get_data("pipeline-fs_raw-average") - wf.connect(node, out, fs_fsl_brain_mask_to_native, "target_file") - - node, out = strat_pool.get_data("freesurfer-subject-dir") - wf.connect(node, out, fs_fsl_brain_mask_to_native, "subjects_dir") - if opt == "FreeSurfer-BET-Tight": outputs = { "space-T1w_desc-tight_brain_mask": ( - fs_fsl_brain_mask_to_native, - "transformed_file", + binarize_combined_mask, + "out_file", ) } elif opt == "FreeSurfer-BET-Loose": outputs = { "space-T1w_desc-loose_brain_mask": ( - fs_fsl_brain_mask_to_native, - "transformed_file", + binarize_combined_mask, + "out_file", ) } @@ -2058,11 +2040,21 @@ def brain_mask_freesurfer_abcd(wf, cfg, strat_pool, pipe_num, opt=None): "T1w-brain-template-mask-ccs", "T1w-ACPC-template", ], - outputs=["space-T1w_desc-tight_brain_mask"], + outputs={ + "space-T1w_desc-brain_mask": { + "Description": "Brain mask extracted using FreeSurfer-BET-Tight method", + "Method": "FreeSurfer-BET-Tight", + "Threshold": "tight", + } + }, ) def brain_mask_freesurfer_fsl_tight(wf, cfg, strat_pool, pipe_num, opt=None): wf, outputs = freesurfer_fsl_brain_connector(wf, cfg, strat_pool, pipe_num, opt) + # Convert the tight brain mask to generic brain mask + outputs["space-T1w_desc-brain_mask"] = outputs.pop( + "space-T1w_desc-tight_brain_mask" + ) return (wf, outputs) @@ -2107,11 +2099,21 @@ def brain_mask_acpc_freesurfer_abcd(wf, cfg, strat_pool, pipe_num, opt=None): "T1w-brain-template-mask-ccs", "T1w-ACPC-template", ], - outputs=["space-T1w_desc-loose_brain_mask"], + outputs={ + "space-T1w_desc-brain_mask": { + "Description": "Brain mask extracted using FreeSurfer-BET-Loose method", + "Method": "FreeSurfer-BET-Loose", + "Threshold": "loose", + } + }, ) def brain_mask_freesurfer_fsl_loose(wf, cfg, strat_pool, pipe_num, opt=None): wf, outputs = freesurfer_fsl_brain_connector(wf, cfg, strat_pool, pipe_num, opt) + # Convert the loose brain mask to generic brain mask + outputs["space-T1w_desc-brain_mask"] = outputs.pop( + "space-T1w_desc-loose_brain_mask" + ) return (wf, outputs) diff --git a/CPAC/anat_preproc/tests/test_anat_preproc.py b/CPAC/anat_preproc/tests/test_anat_preproc.py index 60bc42cead..829a3acd77 100755 --- a/CPAC/anat_preproc/tests/test_anat_preproc.py +++ b/CPAC/anat_preproc/tests/test_anat_preproc.py @@ -5,6 +5,11 @@ import nibabel as nib from .. import anat_preproc +from unittest.mock import Mock, patch +from ..anat_preproc import ( + brain_mask_freesurfer_fsl_loose, + brain_mask_freesurfer_fsl_tight, +) class TestAnatPreproc: @@ -269,3 +274,71 @@ def test_anat_brain(self): # print 'correlation: ', correlation assert correlation[0, 1] >= 0.97 + + +@patch("CPAC.anat_preproc.anat_preproc.freesurfer_fsl_brain_connector") +def test_brain_mask_freesurfer_fsl_loose(mock_connector): + """Test that brain_mask_freesurfer_fsl_loose correctly renames output key.""" + + mock_wf = Mock() + mock_cfg = Mock() + mock_strat_pool = Mock() + pipe_num = 1 + + mock_outputs = { + "space-T1w_desc-loose_brain_mask": "brain_mask_data", + "other_output": "other_data", + } + + mock_connector.return_value = (mock_wf, mock_outputs) + + result_wf, result_outputs = brain_mask_freesurfer_fsl_loose( + mock_wf, mock_cfg, mock_strat_pool, pipe_num + ) + + mock_connector.assert_called_once_with( + mock_wf, mock_cfg, mock_strat_pool, pipe_num, None + ) + + # Assert workflow returned unchanged + assert result_wf == mock_wf + + # Assert output key was renamed correctly + assert "space-T1w_desc-brain_mask" in result_outputs + assert "space-T1w_desc-loose_brain_mask" not in result_outputs + assert result_outputs["space-T1w_desc-brain_mask"] == "brain_mask_data" + assert result_outputs["other_output"] == "other_data" + + +@patch("CPAC.anat_preproc.anat_preproc.freesurfer_fsl_brain_connector") +def test_brain_mask_freesurfer_fsl_tight(mock_connector): + """Test that brain_mask_freesurfer_fsl_tight correctly renames output key.""" + + mock_wf = Mock() + mock_cfg = Mock() + mock_strat_pool = Mock() + pipe_num = 1 + + mock_outputs = { + "space-T1w_desc-tight_brain_mask": "brain_mask_data", + "other_output": "other_data", + } + + mock_connector.return_value = (mock_wf, mock_outputs) + + result_wf, result_outputs = brain_mask_freesurfer_fsl_tight( + mock_wf, mock_cfg, mock_strat_pool, pipe_num + ) + + mock_connector.assert_called_once_with( + mock_wf, mock_cfg, mock_strat_pool, pipe_num, None + ) + + # Assert workflow returned unchanged + assert result_wf == mock_wf + + # Assert output key was renamed correctly + assert "space-T1w_desc-brain_mask" in result_outputs + assert "space-T1w_desc-tight_brain_mask" not in result_outputs + assert result_outputs["space-T1w_desc-brain_mask"] == "brain_mask_data" + assert result_outputs["other_output"] == "other_data"