Skip to content

Commit 8dd1cd6

Browse files
authored
Merge pull request #5 from mnoergaard/main
refactor fMRIPrep to PETPrep
2 parents 6a6298c + 4f8d8ef commit 8dd1cd6

File tree

8 files changed

+182
-29
lines changed

8 files changed

+182
-29
lines changed

fmriprep/workflows/base.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def init_single_subject_wf(subject_id: str):
146146
147147
"""
148148
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
149-
from niworkflows.interfaces.bids import BIDSDataGrabber, BIDSInfo
149+
from niworkflows.interfaces.bids import BIDSInfo, BIDSDataGrabber
150150
from niworkflows.interfaces.nilearn import NILEARN_VERSION
151151
from niworkflows.interfaces.utility import KeySelect
152152
from niworkflows.utils.bids import collect_data
@@ -209,7 +209,7 @@ def init_single_subject_wf(subject_id: str):
209209
config.execution.bids_dir,
210210
subject_id,
211211
bids_filters=config.execution.bids_filters,
212-
queries=queries
212+
queries=queries,
213213
)[0]
214214

215215

@@ -455,12 +455,12 @@ def init_single_subject_wf(subject_id: str):
455455
)
456456
ds_grayord_metrics_wf = init_ds_grayord_metrics_wf(
457457
bids_root=bids_root,
458-
output_dir=fmriprep_dir,
458+
output_dir=petprep_dir,
459459
metrics=['curv', 'thickness', 'sulc'],
460460
cifti_output=config.workflow.cifti_output,
461461
)
462462
ds_fsLR_surfaces_wf = init_ds_surfaces_wf(
463-
output_dir=fmriprep_dir,
463+
output_dir=petprep_dir,
464464
surfaces=['white', 'pial', 'midthickness'],
465465
entities={
466466
'space': 'fsLR',

fmriprep/workflows/pet/confounds.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
from ...config import DEFAULT_MEMORY_MIN_GB
3737
from ...interfaces import DerivativesDataSink
3838
from ...interfaces.confounds import (
39-
FilterDropped,
4039
PETSummary,
4140
FramewiseDisplacement,
4241
FSLMotionParams,
@@ -148,7 +147,7 @@ def init_pet_confs_wf(
148147
from niworkflows.interfaces.fixes import FixHeaderApplyTransforms as ApplyTransforms
149148
from niworkflows.interfaces.images import SignalExtraction
150149
from niworkflows.interfaces.morphology import BinaryDilation, BinarySubtraction
151-
from niworkflows.interfaces.nibabel import ApplyMask, Binarize
150+
from niworkflows.interfaces.nibabel import Binarize
152151
from niworkflows.interfaces.utility import AddTSVHeader, DictMerge
153152

154153
from ...interfaces.confounds import aCompCorMasks
@@ -262,6 +261,18 @@ def init_pet_confs_wf(
262261
'white_matter',
263262
'csf_wm',
264263
]
264+
get_pet_zooms = pe.Node(niu.Function(function=_get_zooms), name='get_pet_zooms')
265+
acompcor_masks = pe.Node(aCompCorMasks(), name='acompcor_masks')
266+
acompcor_tfm = pe.MapNode(
267+
ApplyTransforms(interpolation='MultiLabel', invert_transform_flags=[True]),
268+
name='acompcor_tfm',
269+
iterfield=['input_image'],
270+
)
271+
acompcor_bin = pe.MapNode(
272+
Binarize(thresh_low=0.99),
273+
name='acompcor_bin',
274+
iterfield=['in_file'],
275+
)
265276
merge_rois = pe.Node(
266277
niu.Merge(3, ravel_inputs=True), name='merge_rois', run_without_submitting=True
267278
)
@@ -372,7 +383,20 @@ def _select_cols(table):
372383
(subtract_mask, outputnode, [('out_mask', 'crown_mask')]),
373384
# Global signals extraction (constrained by anatomy)
374385
(inputnode, signals, [('pet', 'in_file')]),
375-
(inputnode, merge_rois, [('pet_mask', 'in1')]),
386+
(inputnode, get_pet_zooms, [('pet', 'in_file')]),
387+
(inputnode, acompcor_masks, [('t1w_tpms', 'in_vfs')]),
388+
(get_pet_zooms, acompcor_masks, [('out', 'pet_zooms')]),
389+
(acompcor_masks, acompcor_tfm, [('out_masks', 'input_image')]),
390+
(inputnode, acompcor_tfm, [
391+
('pet_mask', 'reference_image'),
392+
('petref2anat_xfm', 'transforms'),
393+
]),
394+
(acompcor_tfm, acompcor_bin, [('output_image', 'in_file')]),
395+
(acompcor_bin, merge_rois, [
396+
(('out_mask', _last), 'in3'),
397+
(('out_mask', lambda masks: masks[0]), 'in1'),
398+
(('out_mask', lambda masks: masks[1]), 'in2'),
399+
]),
376400
(merge_rois, signals, [('out', 'label_files')]),
377401

378402
# Collate computed confounds together

fmriprep/workflows/pet/fit.py

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from .outputs import (
3838
init_ds_hmc_wf,
3939
init_ds_petref_wf,
40+
init_ds_petmask_wf,
4041
init_ds_registration_wf,
4142
init_func_fit_reports_wf,
4243
prepare_timing_parameters,
@@ -176,7 +177,7 @@ def init_pet_fit_wf(
176177
workflow.add_nodes([inputnode])
177178

178179
petref_buffer = pe.Node(
179-
niu.IdentityInterface(fields=['petref', 'pet_file', 'pet_mask']),
180+
niu.IdentityInterface(fields=['petref', 'pet_file']),
180181
name='petref_buffer',
181182
)
182183
hmc_buffer = pe.Node(niu.IdentityInterface(fields=['hmc_xforms']), name='hmc_buffer')
@@ -197,11 +198,7 @@ def init_pet_fit_wf(
197198

198199
summary = pe.Node(
199200
FunctionalSummary(
200-
registration=(
201-
'Precomputed'
202-
if petref2anat_xform
203-
else 'mri_coreg'
204-
),
201+
registration=('Precomputed' if petref2anat_xform else 'mri_coreg'),
205202
registration_dof=config.workflow.pet2anat_dof,
206203
orientation=orientation,
207204
),
@@ -218,7 +215,6 @@ def init_pet_fit_wf(
218215
workflow.connect([
219216
(petref_buffer, outputnode, [
220217
('petref', 'petref'),
221-
('pet_mask', 'pet_mask'),
222218
]),
223219
(hmc_buffer, outputnode, [
224220
('hmc_xforms', 'motion_xfm'),
@@ -252,7 +248,6 @@ def init_pet_fit_wf(
252248
pet_file=pet_file,
253249
reference_frame=config.workflow.reference_frame,
254250
)
255-
petref_wf.inputs.inputnode.dummy_scans = config.workflow.dummy_scans
256251

257252
ds_petref_wf = init_ds_petref_wf(
258253
bids_root=layout.root,
@@ -262,6 +257,21 @@ def init_pet_fit_wf(
262257
)
263258
ds_petref_wf.inputs.inputnode.source_files = [pet_file]
264259

260+
# Ensure all stage-1 workflows were created successfully before
261+
# attempting to connect them. Nipype's ``connect`` call will fail
262+
# with a ``NoneType`` error if any node is undefined.
263+
stage1_nodes = [
264+
petref_wf,
265+
petref_buffer,
266+
ds_petref_wf,
267+
func_fit_reports_wf,
268+
petref_source_buffer,
269+
]
270+
if any(node is None for node in stage1_nodes):
271+
raise RuntimeError(
272+
'PET reference stage could not be built - check inputs and configuration.'
273+
)
274+
265275
workflow.connect([
266276
(petref_wf, petref_buffer, [
267277
('outputnode.pet_file', 'pet_file'),
@@ -344,6 +354,45 @@ def init_pet_fit_wf(
344354
else:
345355
outputnode.inputs.petref2anat_xfm = petref2anat_xform
346356

357+
# Stage 4: Estimate PET brain mask
358+
from niworkflows.interfaces.fixes import FixHeaderApplyTransforms as ApplyTransforms
359+
from niworkflows.interfaces.nibabel import Binarize
360+
361+
from .confounds import _binary_union
362+
363+
t1w_mask_tfm = pe.Node(
364+
ApplyTransforms(interpolation='MultiLabel', invert_transform_flags=[True]),
365+
name='t1w_mask_tfm',
366+
)
367+
petref_mask = pe.Node(Binarize(thresh_low=0.2), name='petref_mask')
368+
merge_mask = pe.Node(niu.Function(function=_binary_union), name='merge_mask')
369+
370+
if not petref2anat_xform:
371+
workflow.connect(
372+
[(pet_reg_wf, t1w_mask_tfm, [('outputnode.itk_pet_to_t1', 'transforms')])]
373+
)
374+
else:
375+
t1w_mask_tfm.inputs.transforms = petref2anat_xform
376+
377+
workflow.connect(
378+
[
379+
(inputnode, t1w_mask_tfm, [('t1w_mask', 'input_image')]),
380+
(petref_buffer, t1w_mask_tfm, [('petref', 'reference_image')]),
381+
(petref_buffer, petref_mask, [('petref', 'in_file')]),
382+
(petref_mask, merge_mask, [('out_mask', 'mask1')]),
383+
(t1w_mask_tfm, merge_mask, [('output_image', 'mask2')]),
384+
(merge_mask, outputnode, [('out', 'pet_mask')]),
385+
]
386+
)
387+
388+
ds_petmask_wf = init_ds_petmask_wf(
389+
output_dir=config.execution.petprep_dir,
390+
desc='brain',
391+
name='ds_petmask_wf',
392+
)
393+
ds_petmask_wf.inputs.inputnode.source_files = [pet_file]
394+
workflow.connect([(merge_mask, ds_petmask_wf, [('out', 'inputnode.petmask')])])
395+
347396
return workflow
348397

349398

@@ -436,18 +485,13 @@ def init_pet_native_wf(
436485
)
437486
outputnode.inputs.metadata = metadata
438487

439-
petbuffer = pe.Node(
440-
niu.IdentityInterface(fields=['pet_file']), name='petbuffer'
441-
)
488+
petbuffer = pe.Node(niu.IdentityInterface(fields=['pet_file']), name='petbuffer')
442489

443490
# PET source: track original PET file(s)
444491
# The Select interface requires an index to choose from ``inlist``. Since
445492
# ``pet_file`` is a single path, explicitly set the index to ``0`` to avoid
446493
# missing mandatory input errors when the node runs.
447-
pet_source = pe.Node(
448-
niu.Select(inlist=[pet_file], index=0),
449-
name='pet_source'
450-
)
494+
pet_source = pe.Node(niu.Select(inlist=[pet_file], index=0), name='pet_source')
451495
validate_pet = pe.Node(ValidateImage(), name='validate_pet')
452496
workflow.connect([
453497
(pet_source, validate_pet, [('out', 'in_file')]),

fmriprep/workflows/pet/reference.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ def init_raw_petref_wf(
137137
]
138138
) # fmt:skip
139139

140+
return workflow
141+
140142

141143
def init_validation_and_dummies_wf(
142144
pet_file=None,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import nibabel as nb
2+
import numpy as np
3+
4+
from ..confounds import init_pet_confs_wf
5+
6+
7+
def test_dvars_connects_pet_mask(tmp_path):
8+
"""Check dvars node connection and execution."""
9+
wf = init_pet_confs_wf(
10+
mem_gb=0.01,
11+
metadata={},
12+
regressors_all_comps=False,
13+
regressors_dvars_th=1.5,
14+
regressors_fd_th=0.5,
15+
)
16+
17+
edge = wf._graph.get_edge_data(wf.get_node('inputnode'), wf.get_node('dvars'))
18+
assert ('pet_mask', 'in_mask') in edge['connect']
19+
20+
img = nb.Nifti1Image(np.random.rand(2, 2, 2, 5), np.eye(4))
21+
mask = nb.Nifti1Image(np.ones((2, 2, 2), dtype=np.uint8), np.eye(4))
22+
pet_file = tmp_path / 'pet.nii.gz'
23+
mask_file = tmp_path / 'mask.nii.gz'
24+
img.to_filename(pet_file)
25+
mask.to_filename(mask_file)
26+
27+
node = wf.get_node('dvars')
28+
node.base_dir = tmp_path
29+
node.inputs.in_file = str(pet_file)
30+
node.inputs.in_mask = str(mask_file)
31+
result = node.run()
32+
33+
assert result.outputs.out_nstd
34+
assert result.outputs.out_std

fmriprep/workflows/pet/tests/test_fit.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,23 @@ def test_pet_native_precomputes(
134134

135135
flatgraph = wf._create_flat_graph()
136136
generate_expanded_graph(flatgraph)
137+
138+
139+
def test_pet_fit_mask_connections(bids_root: Path, tmp_path: Path):
140+
"""Ensure the PET mask is generated and connected correctly."""
141+
pet_file = str(bids_root / 'sub-01' / 'pet' / 'sub-01_task-rest_run-1_pet.nii.gz')
142+
img = nb.Nifti1Image(np.zeros((2, 2, 2, 1)), np.eye(4))
143+
img.to_filename(pet_file)
144+
145+
with mock_config(bids_dir=bids_root):
146+
wf = init_pet_fit_wf(pet_file=pet_file, precomputed={}, omp_nthreads=1)
147+
148+
assert 'merge_mask' in wf.list_node_names()
149+
assert 'ds_petmask_wf.ds_petmask' in wf.list_node_names()
150+
151+
merge_mask = wf.get_node('merge_mask')
152+
edge = wf._graph.get_edge_data(merge_mask, wf.get_node('outputnode'))
153+
assert ('out', 'pet_mask') in edge['connect']
154+
155+
ds_edge = wf._graph.get_edge_data(merge_mask, wf.get_node('ds_petmask_wf'))
156+
assert ('out', 'inputnode.petmask') in ds_edge['connect']
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import nibabel as nb
2+
import numpy as np
3+
from niworkflows.utils.testing import generate_bids_skeleton
4+
5+
from ...tests import mock_config
6+
from ...tests.test_base import BASE_LAYOUT
7+
from ..base import init_pet_wf
8+
9+
10+
def test_pet_mask_flow(tmp_path):
11+
bids_dir = tmp_path / 'bids'
12+
generate_bids_skeleton(bids_dir, BASE_LAYOUT)
13+
img = nb.Nifti1Image(np.zeros((2, 2, 2, 10)), np.eye(4))
14+
pet_file = bids_dir / 'sub-01' / 'pet' / 'sub-01_task-rest_run-1_pet.nii.gz'
15+
img.to_filename(pet_file)
16+
17+
with mock_config(bids_dir=bids_dir):
18+
wf = init_pet_wf(pet_series=str(pet_file), precomputed={})
19+
20+
edge = wf._graph.get_edge_data(
21+
wf.get_node('pet_fit_wf'), wf.get_node('pet_confounds_wf')
22+
)
23+
assert ('pet_mask', 'inputnode.pet_mask') in edge['connect']
24+
25+
conf_wf = wf.get_node('pet_confounds_wf')
26+
conf_edge = conf_wf._graph.get_edge_data(
27+
conf_wf.get_node('inputnode'), conf_wf.get_node('dvars')
28+
)
29+
assert ('pet_mask', 'in_mask') in conf_edge['connect']

fmriprep/workflows/pet/tests/test_reference.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,22 @@
66

77
def test_reference_frame_select(tmp_path):
88
img = nb.Nifti1Image(np.zeros((5, 5, 5, 4)), np.eye(4))
9-
pet_file = tmp_path / "pet.nii.gz"
9+
pet_file = tmp_path / 'pet.nii.gz'
1010
img.to_filename(pet_file)
1111

1212
wf = init_raw_petref_wf(pet_file=str(pet_file), reference_frame=2)
1313
node_names = [n.name for n in wf._get_all_nodes()]
14-
assert "extract_frame" in node_names
15-
assert "gen_avg" not in node_names
16-
node = wf.get_node("extract_frame")
14+
assert 'extract_frame' in node_names
15+
assert 'gen_avg' not in node_names
16+
node = wf.get_node('extract_frame')
1717
assert node.interface.inputs.t_min == 2
1818

1919

2020
def test_reference_frame_average(tmp_path):
2121
img = nb.Nifti1Image(np.zeros((5, 5, 5, 4)), np.eye(4))
22-
pet_file = tmp_path / "pet.nii.gz"
22+
pet_file = tmp_path / 'pet.nii.gz'
2323
img.to_filename(pet_file)
2424

25-
wf = init_raw_petref_wf(pet_file=str(pet_file), reference_frame="average")
25+
wf = init_raw_petref_wf(pet_file=str(pet_file), reference_frame='average')
2626
node_names = [n.name for n in wf._get_all_nodes()]
27-
assert "gen_avg" in node_names
27+
assert 'gen_avg' in node_names

0 commit comments

Comments
 (0)