Skip to content

Commit a9da76c

Browse files
authored
Merge branch 'develop' into fix/ccs-options-brain-masking
2 parents 266f7a4 + 2d9b244 commit a9da76c

File tree

21 files changed

+961
-203
lines changed

21 files changed

+961
-203
lines changed

.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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3333
- `deoblique` field in pipeline config with `warp` and `refit` options to apply `3dWarp` or `3drefit` during data initialization.
3434
- `organism` configuration option.
3535
- 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.
3638

3739
### Changed
3840

@@ -70,6 +72,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7072
- Lingering calls to `cpac_outputs.csv` (was changed to `cpac_outputs.tsv` in v1.8.1).
7173
- 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.
7274
- A bug in `ideal_bandpass` where the frequency mask was incorrectly applied, which caused filter to fail in certain cases.
75+
- A bug where `$ANTSPATH` was unset in C-PAC with FreeSurfer images.
7376

7477
### Upgraded dependencies
7578

CPAC/anat_preproc/anat_preproc.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2189,7 +2189,6 @@ def brain_mask_acpc_freesurfer_fsl_loose(wf, cfg, strat_pool, pipe_num, opt=None
21892189
outputs={
21902190
"desc-preproc_T1w": {"SkullStripped": "True"},
21912191
"desc-brain_T1w": {"SkullStripped": "True"},
2192-
"desc-head_T1w": {"SkullStripped": "False"},
21932192
},
21942193
)
21952194
def brain_extraction(wf, cfg, strat_pool, pipe_num, opt=None):
@@ -2227,7 +2226,6 @@ def brain_extraction(wf, cfg, strat_pool, pipe_num, opt=None):
22272226
outputs = {
22282227
"desc-preproc_T1w": (anat_skullstrip_orig_vol, "out_file"),
22292228
"desc-brain_T1w": (anat_skullstrip_orig_vol, "out_file"),
2230-
"desc-head_T1w": (node_T1w, out_T1w),
22312229
}
22322230

22332231
return (wf, outputs)

CPAC/distortion_correction/distortion_correction.py

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from CPAC.pipeline import nipype_pipeline_engine as pe
3737
from CPAC.pipeline.nodeblock import nodeblock
3838
from CPAC.utils import function
39-
from CPAC.utils.datasource import match_epi_fmaps
39+
from CPAC.utils.datasource import match_epi_fmaps_function_node
4040
from CPAC.utils.interfaces.function import Function
4141

4242

@@ -406,23 +406,7 @@ def distcor_blip_afni_qwarp(wf, cfg, strat_pool, pipe_num, opt=None):
406406
3dQWarp. The output of this can then proceed to
407407
func_preproc.
408408
"""
409-
match_epi_imports = ["import json"]
410-
match_epi_fmaps_node = pe.Node(
411-
Function(
412-
input_names=[
413-
"bold_pedir",
414-
"epi_fmap_one",
415-
"epi_fmap_params_one",
416-
"epi_fmap_two",
417-
"epi_fmap_params_two",
418-
],
419-
output_names=["opposite_pe_epi", "same_pe_epi"],
420-
function=match_epi_fmaps,
421-
imports=match_epi_imports,
422-
as_module=True,
423-
),
424-
name=f"match_epi_fmaps_{pipe_num}",
425-
)
409+
match_epi_fmaps_node = match_epi_fmaps_function_node(f"match_epi_fmaps_{pipe_num}")
426410

427411
node, out = strat_pool.get_data("epi-1")
428412
wf.connect(node, out, match_epi_fmaps_node, "epi_fmap_one")

CPAC/func_preproc/func_preproc.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (C) 2012-2023 C-PAC Developers
1+
# Copyright (C) 2012-2025 C-PAC Developers
22

33
# This file is part of C-PAC.
44

@@ -16,13 +16,15 @@
1616
# License along with C-PAC. If not, see <https://www.gnu.org/licenses/>.
1717
"""Functional preprocessing."""
1818

19+
from typing import TYPE_CHECKING
20+
1921
# pylint: disable=ungrouped-imports,wrong-import-order,wrong-import-position
2022
from nipype.interfaces import afni, ants, fsl, utility as util
2123
from nipype.interfaces.afni import preprocess, utils as afni_utils
2224

2325
from CPAC.func_preproc.utils import get_num_slices, interpolate_slice_timing, nullify
2426
from CPAC.pipeline import nipype_pipeline_engine as pe
25-
from CPAC.pipeline.nodeblock import nodeblock
27+
from CPAC.pipeline.nodeblock import nodeblock, NODEBLOCK_RETURN, POOL_RESOURCE_DICT
2628
from CPAC.utils.interfaces import Function
2729
from CPAC.utils.interfaces.ants import (
2830
AI, # niworkflows
@@ -31,6 +33,10 @@
3133
)
3234
from CPAC.utils.utils import add_afni_prefix, afni_3dwarp
3335

36+
if TYPE_CHECKING:
37+
from CPAC.pipeline.engine import ResourcePool
38+
from CPAC.utils.configuration import Configuration
39+
3440

3541
def collect_arguments(*args):
3642
"""Collect arguments."""
@@ -1890,7 +1896,9 @@ def bold_masking(wf, cfg, strat_pool, pipe_num, opt=None):
18901896
["functional_preproc", "run"],
18911897
["functional_preproc", "template_space_func_masking", "run"],
18921898
],
1893-
inputs=[("space-template_desc-preproc_bold", "space-template_desc-bold_mask")],
1899+
inputs=[
1900+
("space-template_desc-preproc_bold", "space-template_desc-bold_mask"),
1901+
],
18941902
outputs={
18951903
"space-template_desc-preproc_bold": {
18961904
"Description": "The skull-stripped BOLD time-series.",
@@ -1906,7 +1914,13 @@ def bold_masking(wf, cfg, strat_pool, pipe_num, opt=None):
19061914
},
19071915
},
19081916
)
1909-
def template_space_bold_masking(wf, cfg, strat_pool, pipe_num, opt=None):
1917+
def template_space_bold_masking(
1918+
wf: pe.Workflow,
1919+
cfg: "Configuration",
1920+
strat_pool: "ResourcePool",
1921+
pipe_num: int,
1922+
opt: None = None,
1923+
) -> NODEBLOCK_RETURN:
19101924
"""Mask the bold in template space."""
19111925
func_apply_mask = pe.Node(
19121926
interface=afni_utils.Calc(),
@@ -1924,13 +1938,13 @@ def template_space_bold_masking(wf, cfg, strat_pool, pipe_num, opt=None):
19241938
node, out = strat_pool.get_data("space-template_desc-bold_mask")
19251939
wf.connect(node, out, func_apply_mask, "in_file_b")
19261940

1927-
outputs = {
1941+
outputs: POOL_RESOURCE_DICT = {
19281942
"space-template_desc-preproc_bold": (func_apply_mask, "out_file"),
19291943
"space-template_desc-brain_bold": (func_apply_mask, "out_file"),
19301944
"space-template_desc-head_bold": (node_head_bold, out_head_bold),
19311945
}
19321946

1933-
return (wf, outputs)
1947+
return wf, outputs
19341948

19351949

19361950
@nodeblock(

CPAC/nuisance/nuisance.py

Lines changed: 51 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@
4040
TR_string_to_float,
4141
)
4242
from CPAC.pipeline import nipype_pipeline_engine as pe
43-
from CPAC.pipeline.engine import ResourcePool
44-
from CPAC.pipeline.nodeblock import nodeblock
43+
from CPAC.pipeline.engine import NodeData, ResourcePool
44+
from CPAC.pipeline.nodeblock import nodeblock, NODEBLOCK_RETURN, POOL_RESOURCE_DICT
4545
from CPAC.registration.registration import (
4646
apply_transform,
4747
warp_timeseries_to_EPItemplate,
@@ -2457,8 +2457,15 @@ def nuisance_regressors_generation_EPItemplate(wf, cfg, strat_pool, pipe_num, op
24572457
inputs=[
24582458
(
24592459
"desc-preproc_bold",
2460-
"space-bold_desc-brain_mask",
2460+
"desc-reorient_bold",
2461+
"sbref",
2462+
[
2463+
"space-bold_desc-brain_mask",
2464+
"space-template_desc-bold_mask",
2465+
"space-template_desc-brain_mask",
2466+
],
24612467
"from-bold_to-T1w_mode-image_desc-linear_xfm",
2468+
"from-template_to-bold_mode-image_xfm",
24622469
"desc-movementParameters_motion",
24632470
"framewise-displacement-jenkinson",
24642471
"framewise-displacement-power",
@@ -2486,7 +2493,13 @@ def nuisance_regressors_generation_EPItemplate(wf, cfg, strat_pool, pipe_num, op
24862493
"lateral-ventricles-mask",
24872494
"TR",
24882495
],
2489-
outputs=["desc-confounds_timeseries", "censor-indices"],
2496+
outputs={
2497+
"desc-confounds_timeseries": {},
2498+
"censor-indices": {},
2499+
"space-bold_desc-brain_mask": {
2500+
"Description": "Binary brain mask of the BOLD functional time-series, transformed from template space."
2501+
},
2502+
},
24902503
)
24912504
def nuisance_regressors_generation_T1w(wf, cfg, strat_pool, pipe_num, opt=None):
24922505
return nuisance_regressors_generation(wf, cfg, strat_pool, pipe_num, opt, "T1w")
@@ -2499,38 +2512,37 @@ def nuisance_regressors_generation(
24992512
pipe_num: int,
25002513
opt: dict,
25012514
space: Literal["T1w", "bold"],
2502-
) -> tuple[Workflow, dict]:
2503-
"""Generate nuisance regressors.
2504-
2505-
Parameters
2506-
----------
2507-
wf : ~nipype.pipeline.engine.workflows.Workflow
2508-
2509-
cfg : ~CPAC.utils.configuration.Configuration
2510-
2511-
strat_pool : ~CPAC.pipeline.engine.ResourcePool
2512-
2513-
pipe_num : int
2514-
2515-
opt : dict
2516-
2517-
space : str
2518-
T1w or bold
2519-
2520-
Returns
2521-
-------
2522-
wf : nipype.pipeline.engine.workflows.Workflow
2515+
) -> NODEBLOCK_RETURN:
2516+
"""Generate nuisance regressors."""
2517+
from CPAC.nuisance.utils.xfm import transform_bold_mask_to_native
25232518

2524-
outputs : dict
2525-
"""
25262519
prefixes = [f"space-{space}_"] * 2
25272520
reg_tool = None
2521+
outputs: POOL_RESOURCE_DICT = {}
2522+
2523+
brain_mask = (
2524+
strat_pool.node_data("space-bold_desc-brain_mask")
2525+
if strat_pool.check_rpool("space-bold_desc-brain_mask")
2526+
else NodeData()
2527+
)
25282528
if space == "T1w":
25292529
prefixes[0] = ""
25302530
if strat_pool.check_rpool("from-template_to-T1w_mode-image_desc-linear_xfm"):
25312531
reg_tool = strat_pool.reg_tool(
25322532
"from-template_to-T1w_mode-image_desc-linear_xfm"
25332533
)
2534+
if brain_mask.node is NotImplemented:
2535+
if reg_tool and strat_pool.check_rpool(
2536+
["space-template_desc-bold_mask", "space-template_desc-brain_mask"]
2537+
):
2538+
outputs["space-bold_desc-brain_mask"] = (
2539+
transform_bold_mask_to_native(
2540+
wf, strat_pool, cfg, pipe_num, reg_tool
2541+
)
2542+
)
2543+
brain_mask.node, brain_mask.out = outputs[
2544+
"space-bold_desc-brain_mask"
2545+
]
25342546
elif space == "bold":
25352547
reg_tool = strat_pool.reg_tool(
25362548
"from-EPItemplate_to-bold_mode-image_desc-linear_xfm"
@@ -2575,8 +2587,12 @@ def nuisance_regressors_generation(
25752587
node, out = strat_pool.get_data("desc-preproc_bold")
25762588
wf.connect(node, out, regressors, "inputspec.functional_file_path")
25772589

2578-
node, out = strat_pool.get_data("space-bold_desc-brain_mask")
2579-
wf.connect(node, out, regressors, "inputspec.functional_brain_mask_file_path")
2590+
wf.connect(
2591+
brain_mask.node,
2592+
brain_mask.out,
2593+
regressors,
2594+
"inputspec.functional_brain_mask_file_path",
2595+
)
25802596

25812597
if strat_pool.check_rpool(f"desc-brain_{space}"):
25822598
node, out = strat_pool.get_data(f"desc-brain_{space}")
@@ -2738,12 +2754,13 @@ def nuisance_regressors_generation(
27382754
node, out = strat_pool.get_data("TR")
27392755
wf.connect(node, out, regressors, "inputspec.tr")
27402756

2741-
outputs = {
2742-
"desc-confounds_timeseries": (regressors, "outputspec.regressors_file_path"),
2743-
"censor-indices": (regressors, "outputspec.censor_indices"),
2744-
}
2757+
outputs["desc-confounds_timeseries"] = (
2758+
regressors,
2759+
"outputspec.regressors_file_path",
2760+
)
2761+
outputs["censor-indices"] = (regressors, "outputspec.censor_indices")
27452762

2746-
return (wf, outputs)
2763+
return wf, outputs
27472764

27482765

27492766
def nuisance_regression(wf, cfg, strat_pool, pipe_num, opt, space, res=None):

CPAC/nuisance/utils/xfm.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
"""Transformation utilities for nuisance regression."""
18+
19+
from typing import cast, Literal
20+
21+
from nipype.pipeline.engine import Workflow
22+
23+
from CPAC.pipeline.engine import ResourcePool
24+
from CPAC.registration.registration import apply_transform
25+
from CPAC.utils.configuration import Configuration
26+
27+
28+
def transform_bold_mask_to_native(
29+
wf: Workflow,
30+
strat_pool: ResourcePool,
31+
cfg: Configuration,
32+
pipe_num: int,
33+
reg_tool: Literal["ants", "fsl"],
34+
) -> tuple[Workflow, str]:
35+
"""Transform a template-space BOLD mask to native space."""
36+
num_cpus = cast(
37+
int, cfg["pipeline_setup", "system_config", "max_cores_per_participant"]
38+
)
39+
num_ants_cores = cast(
40+
int, cfg["pipeline_setup", "system_config", "num_ants_threads"]
41+
)
42+
apply_xfm = apply_transform(
43+
f"xfm_from-template_to-bold_mask_{pipe_num}",
44+
reg_tool,
45+
time_series=True,
46+
num_cpus=num_cpus,
47+
num_ants_cores=num_ants_cores,
48+
)
49+
apply_xfm.inputs.inputspec.interpolation = cfg[
50+
"registration_workflows",
51+
"functional_registration",
52+
"func_registration_to_template",
53+
f"{'ANTs' if reg_tool == 'ants' else 'FNIRT'}_pipelines",
54+
"interpolation",
55+
]
56+
sbref = strat_pool.node_data("sbref")
57+
bold_mask = strat_pool.node_data(
58+
["space-template_desc-bold_mask", "space-template_desc-brain_mask"]
59+
)
60+
xfm = strat_pool.node_data("from-template_to-bold_mode-image_xfm")
61+
wf.connect(
62+
[
63+
(bold_mask.node, apply_xfm, [(bold_mask.out, "inputspec.input_image")]),
64+
(sbref.node, apply_xfm, [(sbref.out, "inputspec.reference")]),
65+
(xfm.node, apply_xfm, [(xfm.out, "inputspec.transform")]),
66+
]
67+
)
68+
69+
return apply_xfm, "outputspec.output_image"

0 commit comments

Comments
 (0)