Skip to content

Commit 4c992f7

Browse files
authored
Merge branch 'develop' into fix/check_s3
2 parents 6b0356f + 592c10b commit 4c992f7

File tree

3 files changed

+161
-1
lines changed

3 files changed

+161
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
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.
3636
- `desc-ABCDpreproc_T1w` to the outputs
3737
- `bc` to `lite` container images.
38+
- validation node to match the pixdim4 of CPAC processed bold outputs with the original raw bold sources.
3839

3940
### Changed
4041

CPAC/pipeline/engine.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
MOVEMENT_FILTER_KEYS,
4646
name_fork,
4747
source_set,
48+
validate_outputs,
4849
)
4950
from CPAC.registration.registration import transform_derivative
5051
from CPAC.resources.templates.lookup_table import lookup_identifier
@@ -1441,7 +1442,37 @@ def gather_pipes(self, wf, cfg, all=False, add_incl=None, add_excl=None):
14411442
subdir=out_dct["subdir"],
14421443
),
14431444
)
1444-
wf.connect(nii_name, "out_file", ds, f'{out_dct["subdir"]}.@data')
1445+
if resource.endswith("_bold"):
1446+
# Node to validate TR (and other scan parameters)
1447+
validate_bold_header = pe.Node(
1448+
Function(
1449+
input_names=["input_bold", "RawSource_bold"],
1450+
output_names=["output_bold"],
1451+
function=validate_outputs,
1452+
imports=[
1453+
"from CPAC.pipeline.utils import find_pixdim4, update_pixdim4"
1454+
],
1455+
),
1456+
name=f"validate_bold_header_{resource_idx}_{pipe_x}",
1457+
)
1458+
raw_source, raw_out = self.get_data("bold")
1459+
wf.connect(
1460+
[
1461+
(nii_name, validate_bold_header, [(out, "input_bold")]),
1462+
(
1463+
raw_source,
1464+
validate_bold_header,
1465+
[(raw_out, "RawSource_bold")],
1466+
),
1467+
(
1468+
validate_bold_header,
1469+
ds,
1470+
[("output_bold", f'{out_dct["subdir"]}.@data')],
1471+
),
1472+
]
1473+
)
1474+
else:
1475+
wf.connect(nii_name, "out_file", ds, f'{out_dct["subdir"]}.@data')
14451476
wf.connect(write_json, "json_file", ds, f'{out_dct["subdir"]}.@json')
14461477
outputs_logger.info(expected_outputs)
14471478

CPAC/pipeline/utils.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@
2121
import subprocess
2222
from typing import Optional, TYPE_CHECKING
2323

24+
import nibabel as nib
25+
2426
from CPAC.func_preproc.func_motion import motion_estimate_filter
2527
from CPAC.utils.bids_utils import insert_entity
28+
from CPAC.utils.monitoring import IFLOGGER
2629

2730
if TYPE_CHECKING:
2831
from CPAC.pipeline.nodeblock import POOL_RESOURCE_MAPPING
@@ -44,6 +47,131 @@ def get_shell() -> str:
4447
return shell
4548

4649

50+
def find_pixdim4(file_path):
51+
"""Find the pixdim4 value of a NIfTI file.
52+
53+
Parameters
54+
----------
55+
file_path : str
56+
Path to the NIfTI file.
57+
58+
Returns
59+
-------
60+
float
61+
The pixdim4 value of the NIfTI file.
62+
63+
Raises
64+
------
65+
FileNotFoundError
66+
If the file does not exist.
67+
nibabel.filebasedimages.ImageFileError
68+
If there is an error loading the NIfTI file.
69+
IndexError
70+
If pixdim4 is not found in the header.
71+
"""
72+
if not os.path.isfile(file_path):
73+
error_message = f"File not found: {file_path}"
74+
raise FileNotFoundError(file_path)
75+
76+
try:
77+
nii = nib.load(file_path)
78+
header = nii.header
79+
pixdim = header.get_zooms()
80+
return pixdim[3]
81+
except nib.filebasedimages.ImageFileError as e:
82+
error_message = f"Error loading the NIfTI file: {e}"
83+
raise nib.filebasedimages.ImageFileError(error_message)
84+
except IndexError as e:
85+
error_message = f"pixdim4 not found in the header: {e}"
86+
raise IndexError(error_message)
87+
88+
89+
def update_pixdim4(file_path, new_pixdim4):
90+
"""Update the pixdim4 value of a NIfTI file using 3drefit.
91+
92+
Parameters
93+
----------
94+
file_path : str
95+
Path to the NIfTI file.
96+
new_pixdim4 : float
97+
New pixdim4 value to update the NIfTI file with.
98+
99+
Raises
100+
------
101+
FileNotFoundError
102+
If the file does not exist.
103+
subprocess.CalledProcessError
104+
If there is an error running the subprocess.
105+
106+
Notes
107+
-----
108+
The pixdim4 value is the Repetition Time (TR) of the NIfTI file.
109+
110+
"""
111+
if not os.path.isfile(file_path):
112+
error_message = f"File not found: {file_path}"
113+
raise FileNotFoundError(error_message)
114+
115+
# Print the current pixdim4 value for verification
116+
IFLOGGER.info(f"Updating {file_path} with new pixdim[4] value: {new_pixdim4}")
117+
118+
# Construct the command to update the pixdim4 value using 3drefit
119+
command = ["3drefit", "-TR", str(new_pixdim4), file_path]
120+
121+
try:
122+
subprocess.run(command, check=True)
123+
IFLOGGER.info(f"Successfully updated TR to {new_pixdim4} seconds.")
124+
except subprocess.CalledProcessError as e:
125+
error_message = f"Error occurred while updating the file: {e}"
126+
raise subprocess.CalledProcessError(error_message)
127+
128+
129+
def validate_outputs(input_bold, RawSource_bold):
130+
"""Match pixdim4/TR of the input_bold with RawSource_bold.
131+
132+
Parameters
133+
----------
134+
input_bold : str
135+
Path to the input BOLD file.
136+
RawSource_bold : str
137+
Path to the RawSource BOLD file.
138+
139+
Returns
140+
-------
141+
output_bold : str
142+
Path to the output BOLD file.
143+
144+
Raises
145+
------
146+
Exception
147+
If there is an error in finding or updating pixdim4.
148+
"""
149+
try:
150+
output_bold = input_bold
151+
output_pixdim4 = find_pixdim4(output_bold)
152+
source_pixdim4 = find_pixdim4(RawSource_bold)
153+
154+
if output_pixdim4 != source_pixdim4:
155+
IFLOGGER.info(
156+
"TR mismatch detected between output_bold and RawSource_bold."
157+
)
158+
IFLOGGER.info(f"output_bold TR: {output_pixdim4} seconds")
159+
IFLOGGER.info(f"RawSource_bold TR: {source_pixdim4} seconds")
160+
IFLOGGER.info(
161+
"Attempting to update the TR of output_bold to match RawSource_bold."
162+
)
163+
update_pixdim4(output_bold, source_pixdim4)
164+
else:
165+
IFLOGGER.debug("TR match detected between output_bold and RawSource_bold.")
166+
IFLOGGER.debug(f"output_bold TR: {output_pixdim4} seconds")
167+
IFLOGGER.debug(f"RawSource_bold TR: {source_pixdim4} seconds")
168+
return output_bold
169+
except Exception as e:
170+
error_message = f"Error in validating outputs: {e}"
171+
IFLOGGER.error(error_message)
172+
return output_bold
173+
174+
47175
def name_fork(resource_idx, cfg, json_info, out_dct):
48176
"""Create and insert entities for forkpoints.
49177

0 commit comments

Comments
 (0)