Skip to content

Commit 646083a

Browse files
authored
Merge pull request #134 from nipreps/speedup_hmc
ENH: Speed up head motion correction workflow
2 parents 12f3247 + f81f36a commit 646083a

File tree

10 files changed

+158
-16
lines changed

10 files changed

+158
-16
lines changed

docs/usage.rst

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,19 @@ registration (mri_robust_register) algorithm utilized settings optimized for PET
192192
scaling was enabled, automated sensitivity detection was activated, and the Frobenius norm threshold
193193
for convergence was set at 0.0001, ensuring precise and consistent alignment across frames.
194194

195-
To edit the motion correction parameters and run the workflow, use: ::
195+
By default, *PETPrep* evaluates the frames acquired after
196+
:option:`--hmc-start-time` and initializes motion correction with the
197+
frame exhibiting the highest tracer uptake. Provide a zero-based index
198+
with :option:`--hmc-init-frame` to override this choice. Adding
199+
:option:`--hmc-init-frame-fix` keeps whichever frame is selected (automatic or
200+
manual) fixed during robust template estimation to improve reproducibility.
201+
Iterations are automatically disabled to reduce runtime when :option:`--hmc-init-frame-fix` is
202+
used.
203+
204+
Examples: ::
196205

197206
$ petprep /data/bids_root /out participant --hmc-fwhm 8 --hmc-start-time 60
207+
$ petprep /data/bids_root /out participant --hmc-init-frame 10 --hmc-init-frame-fix
198208

199209
Segmentation
200210
----------------

docs/workflows.rst

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,8 +391,13 @@ parameters for each time-step are passed on to the
391391

392392
The smoothing kernel width and onset of motion estimation can be
393393
customized via the :option:`--hmc-fwhm` and :option:`--hmc-start-time`
394-
command line options. By default a 10 mm FWHM Gaussian is applied and
395-
estimation begins at 120 s.
394+
command line options. By default, PETPrep initializes registration with
395+
the frame showing the highest tracer uptake after this start time. An
396+
explicit zero-based frame index can be provided with
397+
:option:`--hmc-init-frame`. Adding :option:`--hmc-init-frame-fix` keeps the chosen
398+
frame fixed during robust template estimation and disables iterations to
399+
reduce runtime. A 10 mm FWHM Gaussian is applied and estimation begins at
400+
120 s unless otherwise specified.
396401

397402
Pre-processed PET in native space
398403
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

petprep/cli/parser.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ def _min_one(value, parser):
9292
raise parser.error("Argument can't be less than one.")
9393
return value
9494

95+
def _int_or_auto(value):
96+
"""Parse an integer value or the special 'auto' keyword."""
97+
return 'auto' if value == 'auto' else int(value)
98+
9599
def _to_gb(value):
96100
scale = {'G': 1, 'T': 10**3, 'M': 1e-3, 'K': 1e-6, 'B': 1e-9}
97101
digits = ''.join([c for c in value if c.isdigit()])
@@ -536,6 +540,24 @@ def _bids_filter(value, parser):
536540
type=float,
537541
help='Time (in seconds) after which head-motion estimation is performed.',
538542
)
543+
g_hmc.add_argument(
544+
'--hmc-init-frame',
545+
dest='hmc_init_frame',
546+
nargs='?',
547+
const='auto',
548+
default='auto',
549+
type=_int_or_auto,
550+
help=(
551+
"Initial frame index for head-motion estimation; omit or use 'auto' "
552+
'to select the frame with highest uptake.'
553+
),
554+
)
555+
g_hmc.add_argument(
556+
'--hmc-init-frame-fix',
557+
dest='hmc_fix_frame',
558+
action='store_true',
559+
help=('Keep the chosen initial reference frame fixed during head-motion estimation.'),
560+
)
539561

540562
g_seg = parser.add_argument_group('Segmentation options')
541563
g_seg.add_argument(

petprep/cli/tests/test_parser.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,3 +305,23 @@ def test_reference_mask_options(tmp_path, minimal_bids, monkeypatch):
305305
assert config.workflow.ref_mask_name == 'cerebellum'
306306
assert config.workflow.ref_mask_index == (3, 4)
307307
_reset_config()
308+
309+
310+
def test_hmc_init_frame_parsing(tmp_path):
311+
"""Ensure --hmc-init-frame accepts optional integers and defaults to auto."""
312+
datapath = tmp_path / 'data'
313+
outpath = tmp_path / 'out'
314+
datapath.mkdir()
315+
316+
parser = _build_parser()
317+
base_args = [str(datapath), str(outpath), 'participant']
318+
319+
opts = parser.parse_args(base_args)
320+
assert opts.hmc_init_frame == 'auto'
321+
322+
opts = parser.parse_args(base_args + ['--hmc-init-frame'])
323+
assert opts.hmc_init_frame == 'auto'
324+
325+
opts = parser.parse_args(base_args + ['--hmc-init-frame', '3', '--hmc-init-frame-fix'])
326+
assert opts.hmc_init_frame == 3
327+
assert opts.hmc_fix_frame is True

petprep/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,10 @@ class workflow(_Config):
596596
"""FWHM for Gaussian smoothing prior to head-motion estimation."""
597597
hmc_start_time: float = 120.0
598598
"""Time point (in seconds) at which head-motion estimation starts."""
599+
hmc_init_frame: int | str | None = 'auto'
600+
"""Index of initial frame for head-motion estimation ('auto' selects highest uptake)."""
601+
hmc_fix_frame: bool = False
602+
"""Whether to fix the reference frame during head-motion estimation."""
599603
seg = 'gtm'
600604
"""Segmentation approach ('gtm', 'brainstem', 'thalamicNuclei',
601605
'hippocampusAmygdala', 'wm', 'raphe', 'limbic')."""

petprep/data/nipreps.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -181,13 +181,13 @@
181181
"sub-{subject}[/ses-{session}]/{datatype<perf>|perf}/sub-{subject}[_ses-{session}][_task-{task}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_space-{space}][_atlas-{atlas}][_cohort-{cohort}][_desc-{desc}]_{suffix<timeseries>}{extension<.json|.tsv>|.tsv}",
182182
"sub-{subject}[/ses-{session}]/{datatype<perf>|perf}/sub-{subject}[_ses-{session}][_task-{task}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_space-{space}][_atlas-{atlas}][_cohort-{cohort}][_desc-{desc}]_{suffix<asl|aslref|att|cbf|coverage|mask>}{extension<.nii|.nii.gz|.json|.tsv>|.tsv}",
183183
"sub-{subject}[/ses-{session}]/{datatype<fmap>|fmap}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_dir-{direction}][_run-{run}][_part-{part}][_space-{space}][_cohort-{cohort}][_res-{resolution}][_fmapid-{fmapid}][_desc-{desc}]_{suffix<fieldmap>}{extension<.nii|.nii.gz|.json>|.nii.gz}",
184-
"sub-{subject}[/ses-{session}]/{datatype<pet>|pet}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_trc-{tracer}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_res-{resolution}][_desc-{desc}]_{suffix<pet|petref>}{extension<.nii|.nii.gz|.json>|.nii.gz}",
185-
"sub-{subject}[/ses-{session}]/{datatype<pet>|pet}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_trc-{tracer}][_rec-{reconstruction}][_run-{run}][_hemi-{hemi<L|R>}]_from-{from}_to-{to}_mode-{mode<image|points>|image}_{suffix<xfm>|xfm}{extension<.txt|.h5>}",
186-
"sub-{subject}[/ses-{session}]/{datatype<pet>|pet}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_trc-{tracer}][_rec-{reconstruction}][_run-{run}]_hemi-{hemi<L|R>}[_space-{space}][_cohort-{cohort}][_den-{density}][_desc-{desc}]_{suffix<white|smoothwm|pial|midthickness|inflated|vinflated|sphere|flat|sulc|curv|thickness>}{extension<.surf.gii|.shape.gii>}",
187-
"sub-{subject}[/ses-{session}]/{datatype<pet>|pet}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_trc-{tracer}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_den-{density}][_desc-{desc}]_{suffix<sulc|curv|thickness>}{extension<.dscalar.nii|.json>}",
188-
"sub-{subject}[/ses-{session}]/{datatype<pet>|pet}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_trc-{tracer}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_res-{resolution}]_desc-{desc}_{suffix<mask>|mask}{extension<.nii|.nii.gz|.json>|.nii.gz}",
189-
"sub-{subject}[/ses-{session}]/{datatype<pet>|pet}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_trc-{tracer}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_res-{resolution}]_label-{label}[_desc-{desc}]_{suffix<probseg>|probseg}{extension<.nii|.nii.gz|.json>|.nii.gz}",
190-
"sub-{subject}[/ses-{session}]/{datatype<pet>|pet}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_trc-{tracer}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_part-{part}][_space-{space}][_atlas-{atlas}][_cohort-{cohort}][_desc-{desc}]_{suffix<timeseries|regressors>|timeseries}{extension<.json|.tsv>|.tsv}",
184+
"sub-{subject}[/ses-{session}]/{datatype<pet>|pet}/sub-{subject}[_ses-{session}][_task-{task}][_acq-{acquisition}][_ce-{ceagent}][_trc-{tracer}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_res-{resolution}][_desc-{desc}]_{suffix<pet|petref>}{extension<.nii|.nii.gz|.json>|.nii.gz}",
185+
"sub-{subject}[/ses-{session}]/{datatype<pet>|pet}/sub-{subject}[_ses-{session}][_task-{task}][_acq-{acquisition}][_ce-{ceagent}][_trc-{tracer}][_rec-{reconstruction}][_run-{run}][_hemi-{hemi<L|R>}]_from-{from}_to-{to}_mode-{mode<image|points>|image}_{suffix<xfm>|xfm}{extension<.txt|.h5>}",
186+
"sub-{subject}[/ses-{session}]/{datatype<pet>|pet}/sub-{subject}[_ses-{session}][_task-{task}][_acq-{acquisition}][_ce-{ceagent}][_trc-{tracer}][_rec-{reconstruction}][_run-{run}]_hemi-{hemi<L|R>}[_space-{space}][_cohort-{cohort}][_den-{density}][_desc-{desc}]_{suffix<white|smoothwm|pial|midthickness|inflated|vinflated|sphere|flat|sulc|curv|thickness>}{extension<.surf.gii|.shape.gii>}",
187+
"sub-{subject}[/ses-{session}]/{datatype<pet>|pet}/sub-{subject}[_ses-{session}][_task-{task}][_acq-{acquisition}][_ce-{ceagent}][_trc-{tracer}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_den-{density}][_desc-{desc}]_{suffix<sulc|curv|thickness>}{extension<.dscalar.nii|.json>}",
188+
"sub-{subject}[/ses-{session}]/{datatype<pet>|pet}/sub-{subject}[_ses-{session}][_task-{task}][_acq-{acquisition}][_ce-{ceagent}][_trc-{tracer}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_res-{resolution}]_desc-{desc}_{suffix<mask>|mask}{extension<.nii|.nii.gz|.json>|.nii.gz}",
189+
"sub-{subject}[/ses-{session}]/{datatype<pet>|pet}/sub-{subject}[_ses-{session}][_task-{task}][_acq-{acquisition}][_ce-{ceagent}][_trc-{tracer}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_res-{resolution}]_label-{label}[_desc-{desc}]_{suffix<probseg>|probseg}{extension<.nii|.nii.gz|.json>|.nii.gz}",
190+
"sub-{subject}[/ses-{session}]/{datatype<pet>|pet}/sub-{subject}[_ses-{session}][_task-{task}][_acq-{acquisition}][_ce-{ceagent}][_trc-{tracer}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_part-{part}][_space-{space}][_atlas-{atlas}][_cohort-{cohort}][_desc-{desc}]_{suffix<timeseries|regressors>|timeseries}{extension<.json|.tsv>|.tsv}",
191191
"sub-{subject}/{datatype<figures>}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_desc-{desc}]_{suffix<T1w|T2w|T1rho|T1map|T2map|T2star|FLAIR|FLASH|PDmap|PD|PDT2|inplaneT[12]|angio|dseg|mask|T2starw|MTw|TSE|pet>}{extension<.html|.svg>|.svg}",
192192
"sub-{subject}/{datatype<figures>}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_fmapid-{fmapid}][_desc-{desc}]_{suffix<fieldmap>}{extension<.html|.svg>|.svg}",
193193
"sub-{subject}/{datatype<figures>}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_echo-{echo}][_part-{part}][_space-{space}][_cohort-{cohort}][_desc-{desc}]_{suffix<dwi|epi|epiref|pet>}{extension<.html|.svg>|.svg}",

petprep/workflows/pet/fit.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,8 @@ def init_pet_fit_wf(
281281
start_time=config.workflow.hmc_start_time,
282282
frame_durations=frame_durations,
283283
frame_start_times=frame_start_times,
284+
initial_frame=config.workflow.hmc_init_frame,
285+
fixed_frame=config.workflow.hmc_fix_frame,
284286
)
285287

286288
ds_hmc_wf = init_ds_hmc_wf(

petprep/workflows/pet/hmc.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,14 @@ def lta_list(in_file):
100100
return lta_list
101101

102102

103+
def _find_highest_uptake_frame(in_files: list[str]) -> int:
104+
import nibabel as nb
105+
import numpy as np
106+
107+
uptake = [np.sum(nb.load(f).get_fdata(dtype=np.float32)) for f in in_files]
108+
return int(np.argmax(uptake)) + 1
109+
110+
103111
class _LTAList2ITKInputSpec(BaseInterfaceInputSpec):
104112
in_xforms = InputMultiObject(File(exists=True), mandatory=True)
105113
in_reference = File(exists=True, mandatory=True)
@@ -139,6 +147,8 @@ def init_pet_hmc_wf(
139147
start_time: float = 120.0,
140148
frame_durations: Sequence[float] | None = None,
141149
frame_start_times: Sequence[float] | None = None,
150+
initial_frame: int | str | None = 'auto',
151+
fixed_frame: bool = False,
142152
name: str = 'pet_hmc_wf',
143153
):
144154
r"""
@@ -177,6 +187,15 @@ def init_pet_hmc_wf(
177187
frame_start_times : :class:`~typing.Sequence`\[:obj:`float`] or ``None``
178188
Optional list of frame onset times used together with
179189
``frame_durations`` to locate the start frame.
190+
initial_frame : :obj:`int`, ``'auto'`` or ``None``
191+
0-based index of the frame used to initialize motion correction. If ``'auto'`` or
192+
``None`` (default), the frame with the highest uptake is selected automatically.
193+
FreeSurfer's ``initial_timepoint`` is 1-based; this workflow applies the
194+
required offset internally.
195+
fixed_frame : :obj:`bool`
196+
Whether to keep the initial time point fixed during robust template
197+
estimation (``fs.RobustTemplate``'s ``fixtp`` parameter). If ``True``,
198+
iterations are skipped to reduce runtime.
180199
name : :obj:`str`
181200
Name of workflow (default: ``pet_hmc_wf``)
182201
@@ -274,6 +293,17 @@ def num_files(filelist):
274293
name='create_lta_list',
275294
)
276295

296+
auto_init_frame = initial_frame in (None, 'auto')
297+
if auto_init_frame:
298+
find_highest_uptake_frame = pe.Node(
299+
niu.Function(
300+
input_names=['in_files'],
301+
output_names=['index'],
302+
function=_find_highest_uptake_frame,
303+
),
304+
name='find_highest_uptake_frame',
305+
)
306+
277307
# Motion estimation
278308
robust_template = pe.Node(
279309
fs.RobustTemplate(
@@ -282,9 +312,13 @@ def num_files(filelist):
282312
average_metric='mean',
283313
args='--cras',
284314
num_threads=omp_nthreads,
315+
fixed_timepoint=fixed_frame,
316+
no_iteration=fixed_frame,
285317
),
286318
name='est_robust_hmc',
287319
)
320+
if not auto_init_frame:
321+
robust_template.inputs.initial_timepoint = int(initial_frame) + 1
288322
upd_xfm = pe.Node(
289323
niu.Function(
290324
input_names=['xforms', 'idx'],
@@ -324,4 +358,12 @@ def num_files(filelist):
324358
(ref_to_nii, outputnode, [('out_file', 'petref')]),
325359
]) # fmt:skip
326360

361+
if auto_init_frame:
362+
workflow.connect(
363+
[
364+
(thresh, find_highest_uptake_frame, [('out_file', 'in_files')]),
365+
(find_highest_uptake_frame, robust_template, [('index', 'initial_timepoint')]),
366+
]
367+
)
368+
327369
return workflow

petprep/workflows/pet/tests/test_hmc.py

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
from ..hmc import get_start_frame, init_pet_hmc_wf, update_list_transforms
1+
import nibabel as nb
2+
import numpy as np
3+
import pytest
4+
5+
from ..hmc import (
6+
_find_highest_uptake_frame,
7+
get_start_frame,
8+
init_pet_hmc_wf,
9+
update_list_transforms,
10+
)
211

312

413
def test_get_start_frame_basic():
@@ -26,11 +35,8 @@ def test_update_list_transforms_padding():
2635
assert update_list_transforms(xforms, 0) == xforms
2736

2837

29-
import pytest
30-
31-
3238
def test_update_list_transforms_empty():
33-
with pytest.raises(ValueError):
39+
with pytest.raises(ValueError, match='cannot be empty'):
3440
update_list_transforms([], 1)
3541

3642

@@ -40,3 +46,34 @@ def test_init_pet_hmc_wf_nodes():
4046
assert 'split_frames' in names
4147
assert 'est_robust_hmc' in names
4248
assert 'convert_ref' in names
49+
50+
51+
def test_init_pet_hmc_wf_auto_inittp():
52+
wf = init_pet_hmc_wf(mem_gb=1, omp_nthreads=1, initial_frame='auto')
53+
names = wf.list_node_names()
54+
assert 'find_highest_uptake_frame' in names
55+
56+
57+
def test_init_pet_hmc_wf_specific_inittp():
58+
wf = init_pet_hmc_wf(mem_gb=1, omp_nthreads=1, initial_frame=2, fixed_frame=True)
59+
names = wf.list_node_names()
60+
assert 'find_highest_uptake_frame' not in names
61+
node = wf.get_node('est_robust_hmc')
62+
initial_frame = 2
63+
assert node.inputs.initial_timepoint == initial_frame + 1
64+
assert node.inputs.fixed_timepoint is True
65+
assert node.inputs.no_iteration is True
66+
67+
68+
def test_find_highest_uptake_frame(tmp_path):
69+
data = [np.ones((2, 2, 2)) * i for i in (1, 2, 3)]
70+
files = []
71+
for idx, arr in enumerate(data):
72+
img = nb.Nifti1Image(arr, np.eye(4))
73+
fname = tmp_path / f'frame{idx}.nii.gz'
74+
img.to_filename(fname)
75+
files.append(str(fname))
76+
77+
expected = np.argmax([arr.sum() for arr in data]) + 1
78+
result = _find_highest_uptake_frame(files)
79+
assert result == expected

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ dependencies = [
2727
"nireports >= 24.1.0",
2828
"nitime >= 0.9",
2929
"nitransforms >= 24.1.1",
30-
"niworkflows >= 1.14.0",
30+
"niworkflows @ git+https://github.com/nipreps/niworkflows.git@master",
3131
"numpy >= 1.24",
3232
"packaging >= 24",
3333
"pandas >= 1.2",

0 commit comments

Comments
 (0)