Skip to content

Commit db15443

Browse files
authored
Merge pull request #178 from nipreps/add_robust_coreg
ENH: Add robust co-registration between PET and MRI
2 parents 153cd71 + d9393f3 commit db15443

File tree

9 files changed

+128
-51
lines changed

9 files changed

+128
-51
lines changed

docs/usage.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,17 @@ Examples: ::
211211
$ petprep /data/bids_root /out participant --hmc-init-frame 10 --hmc-init-frame-fix
212212
$ petprep /data/bids_root /out participant --hmc-off
213213

214+
Anatomical co-registration
215+
--------------------------
216+
*PETPrep* aligns the PET reference volume to the T1-weighted anatomy before
217+
deriving downstream outputs. By default, FreeSurfer's ``mri_coreg`` performs
218+
the alignment, with the :option:`--pet2anat-dof` flag controlling the degrees
219+
of freedom (rigid-body, 6 dof, is the default). When working with low
220+
signal-to-noise references or challenging anatomy, the
221+
:option:`--pet2anat-robust` flag enables ``mri_robust_register`` with an NMI
222+
cost function to improve robustness. This mode is restricted to rigid-body
223+
alignment and therefore requires ``--pet2anat-dof 6``.
224+
214225
Segmentation
215226
----------------
216227
*PETPrep* can segment the brain into different brain regions and extract time activity curves from these regions.

docs/workflows.rst

Lines changed: 13 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -427,42 +427,28 @@ Interpolation uses a Lanczos kernel.
427427

428428
PET to T1w registration
429429
~~~~~~~~~~~~~~~~~~~~~~~
430-
:py:func:`~petprep.workflows.pet.registration.init_bbreg_wf`
430+
:py:func:`~petprep.workflows.pet.registration.init_pet_reg_wf`
431431

432432
.. workflow::
433433
:graph2use: hierarchical
434434
:simple_form: yes
435435

436-
from petprep.workflows.pet.registration import init_bbreg_wf
437-
wf = init_bbreg_wf(
436+
from petprep.workflows.pet.registration import init_pet_reg_wf
437+
wf = init_pet_reg_wf(
438438
omp_nthreads=1,
439-
use_bbr=True,
440-
pet2anat_dof=9,
441-
pet2anat_init='t2w',
439+
pet2anat_dof=6,
440+
mem_gb=3,
441+
use_robust_register=False,
442442
)
443443

444-
``pet2anat_init`` selects the initialization strategy for PET-to-anatomical
445-
registration. The default ``'auto'`` setting uses available metadata to choose
446-
between header information and intensity-based approaches.
444+
The PET reference volume is aligned to the skull-stripped anatomical image
445+
using FreeSurfer's ``mri_coreg`` with the number of degrees of freedom set via
446+
the :option:`--pet2anat-dof` flag. The resulting affine is converted to ITK
447+
format for downstream application, along with its inverse.
447448

448-
The alignment between the reference :abbr:`EPI (echo-planar imaging)` image
449-
of each run and the reconstructed subject using the gray/white matter boundary
450-
(FreeSurfer's ``?h.white`` surfaces) is calculated by the ``bbregister`` routine.
451-
See :func:`petprep.workflows.pet.registration.init_bbreg_wf` for further details.
452-
453-
.. figure:: _static/EPIT1Normalization.svg
454-
455-
Animation showing :abbr:`EPI (echo-planar imaging)` to T1w registration (FreeSurfer ``bbregister``)
456-
457-
If FreeSurfer processing is disabled, FSL ``flirt`` is run with the
458-
:abbr:`BBR (boundary-based registration)` cost function, using the
459-
``fast`` segmentation to establish the gray/white matter boundary.
460-
See :func:`petprep.workflows.pet.registration.init_fsl_bbr_wf` for further details.
461-
462-
After either :abbr:`BBR (boundary-based registration)` workflow is run, the resulting affine
463-
transform will be compared to the initial transform found by FLIRT.
464-
Excessive deviation will result in rejecting the BBR refinement and accepting the
465-
original, affine registration.
449+
If co-registration proves challenging, the :option:`--pet2anat-robust` flag
450+
switches the workflow to FreeSurfer's ``mri_robust_register`` with an NMI cost function and restricted
451+
to rigid-body (6 dof) transforms. This method is more robust to large initial misalignments.
466452

467453
Resampling PET runs onto standard spaces
468454
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

petprep/cli/parser.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,12 @@ def _bids_filter(value, parser):
357357
help='Degrees of freedom when registering PET to anatomical images. '
358358
'6 degrees (rotation and translation) are used by default.',
359359
)
360+
g_conf.add_argument(
361+
'--pet2anat-robust',
362+
action='store_true',
363+
help='Use FreeSurfer mri_robust_register with an NMI cost function for'
364+
'PET-to-T1w co-registration. This option is limited to 6 dof.',
365+
)
360366
g_conf.add_argument(
361367
'--force-bbr',
362368
action=DeprecatedAction,
@@ -752,6 +758,9 @@ def parse_args(args=None, namespace=None):
752758
parser = _build_parser()
753759
opts = parser.parse_args(args, namespace)
754760

761+
if getattr(opts, 'pet2anat_robust', False) and opts.pet2anat_dof != 6:
762+
parser.error('--pet2anat-robust requires --pet2anat-dof=6.')
763+
755764
if opts.config_file:
756765
skip = {} if opts.reports_only else {'execution': ('run_uuid',)}
757766
config.load(opts.config_file, skip=skip, init=False)

petprep/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,8 @@ class workflow(_Config):
556556
"""Degrees of freedom of the PET-to-anatomical registration steps."""
557557
pet2anat_init = 'auto'
558558
"""Initial transform for PET-to-anatomical registration."""
559+
pet2anat_robust = False
560+
"""Use ``mri_robust_register`` for PET-to-anatomical alignment."""
559561
cifti_output = None
560562
"""Generate HCP Grayordinates, accepts either ``'91k'`` (default) or ``'170k'``."""
561563
hires = None

petprep/interfaces/reports.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ def _generate_segment(self):
227227
class FunctionalSummaryInputSpec(TraitedSpec):
228228
registration = traits.Enum(
229229
'mri_coreg',
230+
'mri_robust_register',
230231
'Precomputed',
231232
mandatory=True,
232233
desc='PET/anatomical registration method',
@@ -246,8 +247,10 @@ def _generate_segment(self):
246247
# TODO: Add a note about registration_init below?
247248
if self.inputs.registration == 'Precomputed':
248249
reg = 'Precomputed affine transformation'
249-
else:
250+
elif self.inputs.registration == 'mri_coreg':
250251
reg = f'FreeSurfer <code>mri_coreg</code> - {dof} dof'
252+
else:
253+
reg = 'FreeSurfer <code>mri_robust_register</code> (ROBENT cost)'
251254

252255
meta = self.inputs.metadata or {}
253256
time_zero = meta.get('TimeZero', None)

petprep/interfaces/tests/test_reports.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,15 @@ def test_subject_summary_handles_missing_task(tmp_path):
7474
assert 'Task: <none> (1 run)' in segment
7575

7676

77-
def test_functional_summary_with_metadata():
77+
@pytest.mark.parametrize(
78+
'registration',
79+
['mri_coreg', 'mri_robust_register'],
80+
)
81+
def test_functional_summary_with_metadata(registration):
7882
from ..reports import FunctionalSummary
7983

8084
summary = FunctionalSummary(
81-
registration='mri_coreg',
85+
registration=registration,
8286
registration_dof=6,
8387
orientation='RAS',
8488
metadata={
@@ -92,6 +96,7 @@ def test_functional_summary_with_metadata():
9296
)
9397

9498
segment = summary._generate_segment()
99+
assert registration in segment
95100
assert 'Radiotracer: [11C]DASB' in segment
96101
assert 'Injected dose: 100 MBq' in segment
97102
assert 'Number of frames: 2' in segment

petprep/workflows/pet/fit.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,11 @@ def init_pet_fit_wf(
286286
'Please check your BIDS JSON sidecar.'
287287
)
288288

289+
registration_method = 'Precomputed'
290+
if not petref2anat_xform:
291+
registration_method = (
292+
'mri_robust_register' if config.workflow.pet2anat_robust else 'mri_coreg'
293+
)
289294
hmc_disabled = bool(config.workflow.hmc_off)
290295
if hmc_disabled:
291296
config.execution.work_dir.mkdir(parents=True, exist_ok=True)
@@ -314,7 +319,7 @@ def init_pet_fit_wf(
314319

315320
summary = pe.Node(
316321
FunctionalSummary(
317-
registration=('Precomputed' if petref2anat_xform else 'mri_coreg'),
322+
registration=registration_method,
318323
registration_dof=config.workflow.pet2anat_dof,
319324
orientation=orientation,
320325
metadata=metadata,
@@ -423,6 +428,7 @@ def init_pet_fit_wf(
423428
pet2anat_dof=config.workflow.pet2anat_dof,
424429
omp_nthreads=omp_nthreads,
425430
mem_gb=mem_gb['resampled'],
431+
use_robust_register=config.workflow.pet2anat_robust,
426432
sloppy=config.execution.sloppy,
427433
)
428434

petprep/workflows/pet/registration.py

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def init_pet_reg_wf(
4141
pet2anat_dof: AffineDOF,
4242
mem_gb: float,
4343
omp_nthreads: int,
44+
use_robust_register: bool = False,
4445
name: str = 'pet_reg_wf',
4546
sloppy: bool = False,
4647
):
@@ -69,6 +70,10 @@ def init_pet_reg_wf(
6970
Size of PET file in GB
7071
omp_nthreads : :obj:`int`
7172
Maximum number of threads an individual process may use
73+
use_robust_register : :obj:`bool`
74+
Run FreeSurfer ``mri_robust_register`` with an NMI cost function for
75+
PET-to-anatomical alignment. Only rigid-body (6 dof) alignment is
76+
supported in this mode.
7277
name : :obj:`str`
7378
Name of workflow (default: ``pet_reg_wf``)
7479
@@ -90,7 +95,7 @@ def init_pet_reg_wf(
9095
Affine transform from anatomical space to PET space (ITK format)
9196
9297
"""
93-
from nipype.interfaces.freesurfer import MRICoreg
98+
from nipype.interfaces.freesurfer import MRICoreg, RobustRegister
9499
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
95100
from niworkflows.interfaces.nibabel import ApplyMask
96101
from niworkflows.interfaces.nitransforms import ConcatenateXFMs
@@ -107,26 +112,56 @@ def init_pet_reg_wf(
107112
)
108113

109114
mask_brain = pe.Node(ApplyMask(), name='mask_brain')
110-
mri_coreg = pe.Node(
111-
MRICoreg(dof=pet2anat_dof, sep=[4], ftol=0.0001, linmintol=0.01),
112-
name='mri_coreg',
113-
n_procs=omp_nthreads,
114-
mem_gb=5,
115-
)
115+
if use_robust_register:
116+
coreg = pe.Node(
117+
RobustRegister(
118+
auto_sens=False,
119+
est_int_scale=False,
120+
init_orient=True,
121+
args='--cost NMI',
122+
max_iterations=10,
123+
high_iterations=20,
124+
iteration_thresh=0.01,
125+
),
126+
name='mri_robust_register',
127+
n_procs=omp_nthreads,
128+
mem_gb=5,
129+
)
130+
coreg_target = 'target_file'
131+
coreg_output = 'out_reg_file'
132+
else:
133+
coreg = pe.Node(
134+
MRICoreg(dof=pet2anat_dof, sep=[4], ftol=0.0001, linmintol=0.01),
135+
name='mri_coreg',
136+
n_procs=omp_nthreads,
137+
mem_gb=5,
138+
)
139+
coreg_target = 'reference_file'
140+
coreg_output = 'out_lta_file'
116141
convert_xfm = pe.Node(ConcatenateXFMs(inverse=True), name='convert_xfm')
117142

118-
workflow.connect([
119-
(inputnode, mask_brain, [
120-
('anat_preproc', 'in_file'),
121-
('anat_mask', 'in_mask'),
122-
]),
123-
(inputnode, mri_coreg, [('ref_pet_brain', 'source_file')]),
124-
(mask_brain, mri_coreg, [('out_file', 'reference_file')]),
125-
(mri_coreg, convert_xfm, [('out_lta_file', 'in_xfms')]),
126-
(convert_xfm, outputnode, [
127-
('out_xfm', 'itk_pet_to_t1'),
128-
('out_inv', 'itk_t1_to_pet'),
129-
]),
130-
]) # fmt:skip
143+
connections = [
144+
(
145+
inputnode,
146+
mask_brain,
147+
[
148+
('anat_preproc', 'in_file'),
149+
('anat_mask', 'in_mask'),
150+
],
151+
),
152+
(inputnode, coreg, [('ref_pet_brain', 'source_file')]),
153+
(mask_brain, coreg, [('out_file', coreg_target)]),
154+
(coreg, convert_xfm, [(coreg_output, 'in_xfms')]),
155+
(
156+
convert_xfm,
157+
outputnode,
158+
[
159+
('out_xfm', 'itk_pet_to_t1'),
160+
('out_inv', 'itk_t1_to_pet'),
161+
],
162+
),
163+
]
164+
165+
workflow.connect(connections) # fmt:skip
131166

132167
return workflow

petprep/workflows/pet/tests/test_fit.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,26 @@ def test_pet_fit_stage1_inclusion(bids_root: Path, tmp_path: Path):
288288
assert not any(name.startswith('pet_hmc_wf') for name in wf2.list_node_names())
289289

290290

291+
def test_pet_fit_robust_registration(bids_root: Path, tmp_path: Path):
292+
"""Robust PET-to-anatomical registration swaps in mri_robust_register."""
293+
pet_series = [str(bids_root / 'sub-01' / 'pet' / 'sub-01_task-rest_run-1_pet.nii.gz')]
294+
img = nb.Nifti1Image(np.zeros((2, 2, 2, 1)), np.eye(4))
295+
for path in pet_series:
296+
img.to_filename(path)
297+
Path(path).with_suffix('').with_suffix('.json').write_text(
298+
'{"FrameTimesStart": [0], "FrameDuration": [1]}'
299+
)
300+
301+
with mock_config(bids_dir=bids_root):
302+
config.workflow.pet2anat_robust = True
303+
config.workflow.pet2anat_dof = 6
304+
wf = init_pet_fit_wf(pet_series=pet_series, precomputed={}, omp_nthreads=1)
305+
306+
node_names = wf.list_node_names()
307+
assert 'pet_reg_wf.mri_robust_register' in node_names
308+
assert 'pet_reg_wf.mri_coreg' not in node_names
309+
310+
291311
def test_pet_fit_requires_both_derivatives(bids_root: Path, tmp_path: Path):
292312
"""Supplying only one of petref or HMC transforms should raise an error."""
293313
pet_series = [str(bids_root / 'sub-01' / 'pet' / 'sub-01_task-rest_run-1_pet.nii.gz')]

0 commit comments

Comments
 (0)