Skip to content

Commit 8c4d05a

Browse files
authored
Merge branch 'develop' into fix/check_s3
2 parents 8d4e08e + 61960ac commit 8c4d05a

34 files changed

+1279
-500
lines changed

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ cpac_runs
44
.env*
55
.git
66
.github
7+
!.github/CODEOWNERS
78
!.github/scripts
89
*.tar.gz

.github/CODEOWNERS

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Copyright (C) 2025 C-PAC Developers
2+
3+
# This file is part of C-PAC.
4+
5+
# C-PAC is free software: you can redistribute it and/or modify it under
6+
# the terms of the GNU Lesser General Public License as published by the
7+
# Free Software Foundation, either version 3 of the License, or (at your
8+
# option) any later version.
9+
10+
# C-PAC is distributed in the hope that it will be useful, but WITHOUT
11+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
13+
# License for more details.
14+
15+
# You should have received a copy of the GNU Lesser General Public
16+
# License along with C-PAC. If not, see <https://www.gnu.org/licenses/>.
17+
18+
# Global maintenance
19+
* @FCP-INDI/Maintenance
20+
21+
# DevOps
22+
/pyproject.toml @FCP-INDI/DevOps
23+
/requirements.txt @FCP-INDI/DevOps
24+
/setup.py @FCP-INDI/DevOps
25+
/dev @FCP-INDI/DevOps
26+
/scripts @FCP-INDI/DevOps
27+
/.* @FCP-INDI/DevOps
28+
/.circleci @FCP-INDI/DevOps
29+
/.github @FCP-INDI/DevOps
30+
**/*Dockerfile @FCP-INDI/DevOps

.github/Dockerfiles/base-lite.Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ COPY --from=fsl /usr/lib/x86_64-linux-gnu /usr/lib/x86_64-linux-gnu
5252
COPY --from=fsl /usr/bin /usr/bin
5353
COPY --from=fsl /usr/local/bin /usr/local/bin
5454
COPY --from=fsl /usr/share/fsl /usr/share/fsl
55+
RUN apt-get update \
56+
&& apt-get install --no-install-recommends -y bc
5557

5658
# Installing C-PAC dependencies
5759
COPY requirements.txt /opt/requirements.txt

.github/Dockerfiles/base-standard.Dockerfile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,14 @@ Standard software dependencies for C-PAC standard images"
2222
LABEL org.opencontainers.image.source=https://github.com/FCP-INDI/C-PAC
2323
USER root
2424

25+
# Installing ANTs
26+
ENV LANG="en_US.UTF-8" \
27+
LC_ALL="en_US.UTF-8" \
28+
ANTSPATH=/usr/lib/ants/bin \
29+
PATH=/usr/lib/ants/bin:$PATH
30+
2531
# Installing FreeSurfer
2632
RUN apt-get update \
27-
&& apt-get install --no-install-recommends -y bc \
2833
&& yes | mamba install tcsh \
2934
&& yes | mamba clean --all \
3035
&& cp -l `which tcsh` /bin/tcsh \

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3232
- Switch `sink_native_transforms` under `registration_workflows` to output all `.mat` files in ANTs and FSL Transforms.
3333
- `deoblique` field in pipeline config with `warp` and `refit` options to apply `3dWarp` or `3drefit` during data initialization.
3434
- `organism` configuration option.
35+
- 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.
36+
- `desc-ABCDpreproc_T1w` to the outputs
37+
- `bc` to `lite` container images.
3538

3639
### Changed
3740

@@ -55,6 +58,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5558
- Introduced a new `template_space_func_masking` section in the pipeline config for template-space-only methods.
5659
- Moved `Anatomical_Resampled` masking method from `func_masking` to the `template_space_func_masking`.
5760
- Upgraded resource retrieval to `importlib.resources`.
61+
- Turned `On` boundary_based_registration for abcd-options preconfig.
62+
- Refactored `transform_timeseries_to_T1template_abcd` nodeblock removing unnecessary nodes, changing `desc-preproc_T1w` inputs as reference to `desc-head_T1w`.
63+
- Appended `T1w to Template` FOV match transform to the XFM.
5864

5965
### Upgraded
6066

@@ -70,6 +76,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7076
- Lingering calls to `cpac_outputs.csv` (was changed to `cpac_outputs.tsv` in v1.8.1).
7177
- A bug in the `freesurfer_abcd_preproc` nodeblock where the `Template` image was incorrectly used as `reference` during the `inverse_warp` step. Replacing it with the subject-specific `T1w` image resolved the issue of the `desc-restoreBrain_T1w` being chipped off.
7278
- A bug in `ideal_bandpass` where the frequency mask was incorrectly applied, which caused filter to fail in certain cases.
79+
- `Freesufer-ABCD` brain masking strategy to create mask as per the original DCAN script.
80+
- A bug where `$ANTSPATH` was unset in C-PAC with FreeSurfer images.
7381

7482
### Upgraded dependencies
7583

CPAC/anat_preproc/anat_preproc.py

Lines changed: 33 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,37 +1128,12 @@ def freesurfer_abcd_brain_connector(wf, cfg, strat_pool, pipe_num, opt):
11281128
name=f"wmparc_to_nifti_{pipe_num}",
11291129
)
11301130

1131-
# Register wmparc file if ingressing FreeSurfer data
1132-
if strat_pool.check_rpool("pipeline-fs_xfm"):
1133-
wmparc_to_native = pe.Node(
1134-
Function(
1135-
input_names=["source_file", "target_file", "xfm", "out_file"],
1136-
output_names=["transformed_file"],
1137-
function=normalize_wmparc,
1138-
),
1139-
name=f"wmparc_to_native_{pipe_num}",
1140-
)
1141-
1142-
wmparc_to_native.inputs.out_file = "wmparc_warped.mgz"
1143-
1144-
node, out = strat_pool.get_data("pipeline-fs_wmparc")
1145-
wf.connect(node, out, wmparc_to_native, "source_file")
1146-
1147-
node, out = strat_pool.get_data("pipeline-fs_raw-average")
1148-
wf.connect(node, out, wmparc_to_native, "target_file")
1149-
1150-
node, out = strat_pool.get_data("pipeline-fs_xfm")
1151-
wf.connect(node, out, wmparc_to_native, "xfm")
1152-
1153-
wf.connect(wmparc_to_native, "transformed_file", wmparc_to_nifti, "in_file")
1154-
1155-
else:
1156-
node, out = strat_pool.get_data("pipeline-fs_wmparc")
1157-
wf.connect(node, out, wmparc_to_nifti, "in_file")
1131+
node, out = strat_pool.get_data("pipeline-fs_wmparc")
1132+
wf.connect(node, out, wmparc_to_nifti, "in_file")
11581133

11591134
wmparc_to_nifti.inputs.args = "-rt nearest"
11601135

1161-
node, out = strat_pool.get_data("desc-preproc_T1w")
1136+
node, out = strat_pool.get_data(["desc-restore_T1w", "desc-preproc_T1w"])
11621137
wf.connect(node, out, wmparc_to_nifti, "reslice_like")
11631138

11641139
binary_mask = pe.Node(
@@ -1194,7 +1169,7 @@ def freesurfer_abcd_brain_connector(wf, cfg, strat_pool, pipe_num, opt):
11941169

11951170
wf.connect(binary_filled_mask, "out_file", brain_mask_to_t1_restore, "in_file")
11961171

1197-
node, out = strat_pool.get_data("desc-preproc_T1w")
1172+
node, out = strat_pool.get_data(["desc-restore_T1w", "desc-preproc_T1w"])
11981173
wf.connect(node, out, brain_mask_to_t1_restore, "ref_file")
11991174

12001175
outputs = {"space-T1w_desc-brain_mask": (brain_mask_to_t1_restore, "out_file")}
@@ -1303,7 +1278,7 @@ def freesurfer_fsl_brain_connector(wf, cfg, strat_pool, pipe_num, opt):
13031278
# fslmaths tmp_mask.nii.gz -mas ${CCSDIR}/templates/MNI152_T1_1mm_first_brain_mask.nii.gz tmp_mask.nii.gz
13041279
apply_mask = pe.Node(interface=fsl.maths.ApplyMask(), name=f"apply_mask_{node_id}")
13051280

1306-
wf.connect(skullstrip, "out_file", apply_mask, "in_file")
1281+
wf.connect(skullstrip, "mask_file", apply_mask, "in_file")
13071282

13081283
node, out = strat_pool.get_data("T1w-brain-template-mask-ccs")
13091284
wf.connect(node, out, apply_mask, "mask_file")
@@ -1347,36 +1322,18 @@ def freesurfer_fsl_brain_connector(wf, cfg, strat_pool, pipe_num, opt):
13471322

13481323
wf.connect(combine_mask, "out_file", binarize_combined_mask, "in_file")
13491324

1350-
# CCS brain mask is in FS space, transfer it back to native T1 space
1351-
fs_fsl_brain_mask_to_native = pe.Node(
1352-
interface=freesurfer.ApplyVolTransform(),
1353-
name=f"fs_fsl_brain_mask_to_native_{node_id}",
1354-
)
1355-
fs_fsl_brain_mask_to_native.inputs.reg_header = True
1356-
fs_fsl_brain_mask_to_native.inputs.interp = "nearest"
1357-
1358-
wf.connect(
1359-
binarize_combined_mask, "out_file", fs_fsl_brain_mask_to_native, "source_file"
1360-
)
1361-
1362-
node, out = strat_pool.get_data("pipeline-fs_raw-average")
1363-
wf.connect(node, out, fs_fsl_brain_mask_to_native, "target_file")
1364-
1365-
node, out = strat_pool.get_data("freesurfer-subject-dir")
1366-
wf.connect(node, out, fs_fsl_brain_mask_to_native, "subjects_dir")
1367-
13681325
if opt == "FreeSurfer-BET-Tight":
13691326
outputs = {
13701327
"space-T1w_desc-tight_brain_mask": (
1371-
fs_fsl_brain_mask_to_native,
1372-
"transformed_file",
1328+
binarize_combined_mask,
1329+
"out_file",
13731330
)
13741331
}
13751332
elif opt == "FreeSurfer-BET-Loose":
13761333
outputs = {
13771334
"space-T1w_desc-loose_brain_mask": (
1378-
fs_fsl_brain_mask_to_native,
1379-
"transformed_file",
1335+
binarize_combined_mask,
1336+
"out_file",
13801337
)
13811338
}
13821339

@@ -2028,10 +1985,9 @@ def brain_mask_acpc_freesurfer(wf, cfg, strat_pool, pipe_num, opt=None):
20281985
option_key=["anatomical_preproc", "brain_extraction", "using"],
20291986
option_val="FreeSurfer-ABCD",
20301987
inputs=[
2031-
"desc-preproc_T1w",
1988+
["desc-restore_T1w", "desc-preproc_T1w"],
20321989
"pipeline-fs_wmparc",
20331990
"pipeline-fs_raw-average",
2034-
"pipeline-fs_xfm",
20351991
"freesurfer-subject-dir",
20361992
],
20371993
outputs=["space-T1w_desc-brain_mask"],
@@ -2058,11 +2014,21 @@ def brain_mask_freesurfer_abcd(wf, cfg, strat_pool, pipe_num, opt=None):
20582014
"T1w-brain-template-mask-ccs",
20592015
"T1w-ACPC-template",
20602016
],
2061-
outputs=["space-T1w_desc-tight_brain_mask"],
2017+
outputs={
2018+
"space-T1w_desc-brain_mask": {
2019+
"Description": "Brain mask extracted using FreeSurfer-BET-Tight method",
2020+
"Method": "FreeSurfer-BET-Tight",
2021+
"Threshold": "tight",
2022+
}
2023+
},
20622024
)
20632025
def brain_mask_freesurfer_fsl_tight(wf, cfg, strat_pool, pipe_num, opt=None):
20642026
wf, outputs = freesurfer_fsl_brain_connector(wf, cfg, strat_pool, pipe_num, opt)
20652027

2028+
# Convert the tight brain mask to generic brain mask
2029+
outputs["space-T1w_desc-brain_mask"] = outputs.pop(
2030+
"space-T1w_desc-tight_brain_mask"
2031+
)
20662032
return (wf, outputs)
20672033

20682034

@@ -2075,10 +2041,9 @@ def brain_mask_freesurfer_fsl_tight(wf, cfg, strat_pool, pipe_num, opt=None):
20752041
option_key=["anatomical_preproc", "brain_extraction", "using"],
20762042
option_val="FreeSurfer-ABCD",
20772043
inputs=[
2078-
"desc-preproc_T1w",
2044+
["desc-restore_T1w", "desc-preproc_T1w"],
20792045
"pipeline-fs_wmparc",
20802046
"pipeline-fs_raw-average",
2081-
"pipeline-fs_xfm",
20822047
"freesurfer-subject-dir",
20832048
],
20842049
outputs=["space-T1w_desc-acpcbrain_mask"],
@@ -2107,11 +2072,21 @@ def brain_mask_acpc_freesurfer_abcd(wf, cfg, strat_pool, pipe_num, opt=None):
21072072
"T1w-brain-template-mask-ccs",
21082073
"T1w-ACPC-template",
21092074
],
2110-
outputs=["space-T1w_desc-loose_brain_mask"],
2075+
outputs={
2076+
"space-T1w_desc-brain_mask": {
2077+
"Description": "Brain mask extracted using FreeSurfer-BET-Loose method",
2078+
"Method": "FreeSurfer-BET-Loose",
2079+
"Threshold": "loose",
2080+
}
2081+
},
21112082
)
21122083
def brain_mask_freesurfer_fsl_loose(wf, cfg, strat_pool, pipe_num, opt=None):
21132084
wf, outputs = freesurfer_fsl_brain_connector(wf, cfg, strat_pool, pipe_num, opt)
21142085

2086+
# Convert the loose brain mask to generic brain mask
2087+
outputs["space-T1w_desc-brain_mask"] = outputs.pop(
2088+
"space-T1w_desc-loose_brain_mask"
2089+
)
21152090
return (wf, outputs)
21162091

21172092

@@ -2187,7 +2162,6 @@ def brain_mask_acpc_freesurfer_fsl_loose(wf, cfg, strat_pool, pipe_num, opt=None
21872162
outputs={
21882163
"desc-preproc_T1w": {"SkullStripped": "True"},
21892164
"desc-brain_T1w": {"SkullStripped": "True"},
2190-
"desc-head_T1w": {"SkullStripped": "False"},
21912165
},
21922166
)
21932167
def brain_extraction(wf, cfg, strat_pool, pipe_num, opt=None):
@@ -2225,7 +2199,6 @@ def brain_extraction(wf, cfg, strat_pool, pipe_num, opt=None):
22252199
outputs = {
22262200
"desc-preproc_T1w": (anat_skullstrip_orig_vol, "out_file"),
22272201
"desc-brain_T1w": (anat_skullstrip_orig_vol, "out_file"),
2228-
"desc-head_T1w": (node_T1w, out_T1w),
22292202
}
22302203

22312204
return (wf, outputs)

CPAC/anat_preproc/tests/test_anat_preproc.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
import nibabel as nib
66

77
from .. import anat_preproc
8+
from unittest.mock import Mock, patch
9+
from ..anat_preproc import (
10+
brain_mask_freesurfer_fsl_loose,
11+
brain_mask_freesurfer_fsl_tight,
12+
)
813

914

1015
class TestAnatPreproc:
@@ -269,3 +274,71 @@ def test_anat_brain(self):
269274
# print 'correlation: ', correlation
270275

271276
assert correlation[0, 1] >= 0.97
277+
278+
279+
@patch("CPAC.anat_preproc.anat_preproc.freesurfer_fsl_brain_connector")
280+
def test_brain_mask_freesurfer_fsl_loose(mock_connector):
281+
"""Test that brain_mask_freesurfer_fsl_loose correctly renames output key."""
282+
283+
mock_wf = Mock()
284+
mock_cfg = Mock()
285+
mock_strat_pool = Mock()
286+
pipe_num = 1
287+
288+
mock_outputs = {
289+
"space-T1w_desc-loose_brain_mask": "brain_mask_data",
290+
"other_output": "other_data",
291+
}
292+
293+
mock_connector.return_value = (mock_wf, mock_outputs)
294+
295+
result_wf, result_outputs = brain_mask_freesurfer_fsl_loose(
296+
mock_wf, mock_cfg, mock_strat_pool, pipe_num
297+
)
298+
299+
mock_connector.assert_called_once_with(
300+
mock_wf, mock_cfg, mock_strat_pool, pipe_num, None
301+
)
302+
303+
# Assert workflow returned unchanged
304+
assert result_wf == mock_wf
305+
306+
# Assert output key was renamed correctly
307+
assert "space-T1w_desc-brain_mask" in result_outputs
308+
assert "space-T1w_desc-loose_brain_mask" not in result_outputs
309+
assert result_outputs["space-T1w_desc-brain_mask"] == "brain_mask_data"
310+
assert result_outputs["other_output"] == "other_data"
311+
312+
313+
@patch("CPAC.anat_preproc.anat_preproc.freesurfer_fsl_brain_connector")
314+
def test_brain_mask_freesurfer_fsl_tight(mock_connector):
315+
"""Test that brain_mask_freesurfer_fsl_tight correctly renames output key."""
316+
317+
mock_wf = Mock()
318+
mock_cfg = Mock()
319+
mock_strat_pool = Mock()
320+
pipe_num = 1
321+
322+
mock_outputs = {
323+
"space-T1w_desc-tight_brain_mask": "brain_mask_data",
324+
"other_output": "other_data",
325+
}
326+
327+
mock_connector.return_value = (mock_wf, mock_outputs)
328+
329+
result_wf, result_outputs = brain_mask_freesurfer_fsl_tight(
330+
mock_wf, mock_cfg, mock_strat_pool, pipe_num
331+
)
332+
333+
mock_connector.assert_called_once_with(
334+
mock_wf, mock_cfg, mock_strat_pool, pipe_num, None
335+
)
336+
337+
# Assert workflow returned unchanged
338+
assert result_wf == mock_wf
339+
340+
# Assert output key was renamed correctly
341+
assert "space-T1w_desc-brain_mask" in result_outputs
342+
assert "space-T1w_desc-tight_brain_mask" not in result_outputs
343+
assert result_outputs["space-T1w_desc-brain_mask"] == "brain_mask_data"
344+
assert result_outputs["other_output"] == "other_data"

CPAC/anat_preproc/utils.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,9 @@ def mri_convert(in_file, reslice_like=None, out_file=None, args=None):
487487
import os
488488

489489
if out_file is None:
490-
out_file = in_file.replace(".mgz", ".nii.gz")
490+
out_file = os.path.join(
491+
os.getcwd(), os.path.basename(in_file).replace(".mgz", ".nii.gz")
492+
)
491493

492494
cmd = "mri_convert %s %s" % (in_file, out_file)
493495

@@ -525,7 +527,9 @@ def mri_convert_reorient(in_file, orientation, out_file=None):
525527
import os
526528

527529
if out_file is None:
528-
out_file = in_file.split(".")[0] + "_reoriented.mgz"
530+
out_file = os.path.join(
531+
os.getcwd(), os.path.basename(in_file).split(".")[0] + "_reoriented.mgz"
532+
)
529533

530534
cmd = "mri_convert %s %s --out_orientation %s" % (in_file, out_file, orientation)
531535

0 commit comments

Comments
 (0)