Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🥅 Add registration guardrails #1814

Draft
wants to merge 72 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
99a0ea8
:construction: WIP :goal_net: Add registration guardrail
shnizzedy Sep 26, 2022
8cc3f5a
:recycle: Modularize mask parameter checking
shnizzedy Sep 29, 2022
fe7413b
:construction: WIP :goal_net: Create registration guardrail
shnizzedy Sep 29, 2022
3aea371
:wrench: Add 'quality_thresholds' to 'registration_workflows' in pipe…
shnizzedy Sep 29, 2022
8fa5c23
:wrench: Set global registration QC thresholds on config load
shnizzedy Sep 29, 2022
b891928
:goal_net: Pass registrations through guardrail before continuing
shnizzedy Sep 29, 2022
3fb78f0
:goal_net: Plug in registration guardrails
shnizzedy Sep 29, 2022
c12e7c4
:recycle: Convert random.log to random.tsv
shnizzedy Oct 4, 2022
b7fabf4
:sparkles: Afford retrying registration with a different random seed
shnizzedy Oct 4, 2022
d3bdfa8
:goal_net: Add registration guardrails (with retries) to anatomical p…
shnizzedy Oct 4, 2022
db3d555
:page_facing_up: Add license notice to modified files
shnizzedy Oct 4, 2022
b9a7ad0
:recycle: Plug in ants guardrail
shnizzedy Oct 4, 2022
d9d0748
:fire: Remove empty file
shnizzedy Oct 5, 2022
d61569f
:art: Initialze guardrail before conditional
shnizzedy Oct 5, 2022
58811df
:bug: Fix concat-then-guardrail logic
shnizzedy Oct 5, 2022
fe139cc
:recycle: Give each guardrail a unique name
shnizzedy Oct 5, 2022
07a91cd
:art: Keep random seed in bounds
shnizzedy Oct 5, 2022
cf8272b
:bug: Add imports to guardrail node initialization
shnizzedy Oct 7, 2022
cfd7a91
:necktie: Change guardrail retry default to `True` and retry even if …
shnizzedy Oct 7, 2022
ed81db0
:bug: Handle list values returned from `qc_masks`
shnizzedy Oct 7, 2022
e9359c2
:bug: Log failed metric instead of boolean
shnizzedy Oct 7, 2022
6de7c6d
:twisted_rightwards_arrows: Merge 'develop' into 'enh/guardrails'
shnizzedy Oct 18, 2022
f6b8540
:pencil2: Fix typo (octal, not hexadecimal)
shnizzedy Oct 18, 2022
6c81eec
:construction: WIP :goal_net: Iterate guardrail installation
shnizzedy Oct 19, 2022
492c823
:recycle: Rewire coreg for bbreg fallback options
shnizzedy Oct 20, 2022
e8f0c32
:adhesive_bandage: ~~el~~if
shnizzedy Oct 20, 2022
1190804
:rotating_light: Minor linting
shnizzedy Oct 20, 2022
66e4ce6
:recycle: Move logger import + declaration into guardrail function
shnizzedy Oct 20, 2022
328b569
:twisted_rightwards_arrows: Merge dotgraph into enh/guardrails
shnizzedy Oct 20, 2022
837533e
:recycle: Better handling of global thresholds
shnizzedy Oct 21, 2022
e32644b
:goal_net: Guardrail: Log error on first try, raise on second
shnizzedy Oct 21, 2022
88b5589
:technologist: Add decorator for retry __doc__s
shnizzedy Oct 21, 2022
c83ed62
:recycle: Rewire guardrail for `anat_mni_ants_register`
shnizzedy Oct 22, 2022
fcd8a90
:recycle: Use guardrails to inform warp selection
shnizzedy Oct 24, 2022
88de7d6
:recycle: Import registration imports for retry registration
shnizzedy Oct 24, 2022
fa87a92
:goal_net: More specific error handling
shnizzedy Oct 24, 2022
e115d2e
:recycle: Pass failed_qc status to retry_calculate_ants_warp
shnizzedy Oct 25, 2022
ef7d540
:art: Increment seed for ANTs retry
shnizzedy Oct 25, 2022
e28b1a7
:recycle: Refactor guardrails for create_register_func_to_anat_use_T2
shnizzedy Oct 25, 2022
be69c25
:recycle: Refactor bbreg guardrails
shnizzedy Oct 25, 2022
ec62b6a
:goal_net: Guardrail `create_fsl_flirt_linear_reg`
shnizzedy Oct 25, 2022
94f0092
:goal_net: Guardrail
shnizzedy Oct 25, 2022
883a56a
:recycle: Make `connect_retries` a `Workflow` method
shnizzedy Oct 25, 2022
f2b9115
:goal_net: Guardrail `create_fsl_fnirt_nonlinear_reg_nhp`
shnizzedy Oct 25, 2022
fb90455
:recycle: Refactor guardrails for `create_wf_calculate_ants_warp`
shnizzedy Oct 25, 2022
c167384
:recycle: Refactor guardrail for `create_register_func_to_anat`
shnizzedy Oct 25, 2022
fd5e27d
:goal_net: Guardrail `acpc_alignment`
shnizzedy Oct 26, 2022
162cf6a
:art: Simplify calls to `nodes_and_guardrails`
shnizzedy Oct 26, 2022
c0e67c8
:recycle: Semi-abstract connecting guardrailed nodes
shnizzedy Oct 26, 2022
a398b18
:goal_net: Guardrail `unet_brain_connector`
shnizzedy Oct 26, 2022
056afa2
:memo: Fix registration docstring workflow graphs
shnizzedy Oct 27, 2022
dda6eb0
:page_facing_up: Move license declaration from docstring to comments
shnizzedy Oct 27, 2022
65b7bf2
:rewind: Remove prototype guardrails from `freesurfer*connector` func…
shnizzedy Oct 28, 2022
6bd09c3
:goal_net: Guardrail `freesurfer_fsl_brain_connector`
shnizzedy Oct 28, 2022
243fdd9
:goal_net: Guardrail `mask_T2`
shnizzedy Oct 28, 2022
1cbf464
:goal_net: Guardrail `fnirt_based_brain_extraction`
shnizzedy Oct 28, 2022
611066f
:goal_net: Guardrail `freesurfer_abcd_preproc`
shnizzedy Oct 28, 2022
00c3116
:rewind: Remove prototype guardrails from `correct_restore_brain_inte…
shnizzedy Oct 28, 2022
20e87e4
:goal_net: Guardrail `init_brain_extraction_wf`
shnizzedy Oct 28, 2022
eed56b3
:wrench: Add `fail_fast` configuration option to expose Nipype's `sto…
shnizzedy Oct 28, 2022
5cc28cf
:fire: Remove prototype guardrail definitions
shnizzedy Oct 28, 2022
50261d4
:memo: Expand guardrail docstrings
shnizzedy Oct 28, 2022
d4b7d02
:goal_net: Guardrail `distcor_blip_afni_qwarp`
shnizzedy Oct 28, 2022
8f917a7
:goal_net: Guardrail 'distcor_blip_fsl_topup'
shnizzedy Oct 28, 2022
744411f
:goal_net: Guardrail `linear_reg_func_to_anat`
shnizzedy Oct 28, 2022
51051d9
:goal_net: Guardrail `anat_refined_mask`
shnizzedy Oct 28, 2022
3cce875
:goal_net: Guardrail `bold_mask_anatomical_based`
shnizzedy Oct 28, 2022
cde0c7c
:goal_net: Guardrail `bold_mask_css`
shnizzedy Oct 28, 2022
e0e2138
:goal_net: Guardrail `dual_regression`
shnizzedy Oct 28, 2022
97fd7fb
:goal_net: Guardrail `spatial_regression`
shnizzedy Oct 28, 2022
307979e
:white_check_mark: Update random seed test
shnizzedy Oct 28, 2022
02887af
fixup! :wrench: Add `fail_fast` configuration option to expose Nipype…
shnizzedy Oct 28, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
generated-members=CPAC.utils.configuration.configuration.Configuration.*

# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
Expand Down Expand Up @@ -435,6 +435,7 @@ good-names=c,
ex,
nb,
Run,
TR,
v,
wf,
_,
Expand All @@ -443,6 +444,7 @@ good-names=c,
# they will always be accepted
good-names-rgxs=^_version_(extra|m[a-n]{2}[or]{2})$, # version parts in info.py
.*EPI.*,
.*TE.*,
.*T1.*,
.*T2.*

Expand Down
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,33 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [unreleased]

### Added
- Added the ability to downsample to 10K or 2K resolution for freesurfer runs
- Added the ability to ingress TotalReadoutTime from epi field map meta-data from the JSON sidecars.
- Added the ability to use TotalReadoutTime of epi field maps in the calculation of FSL topup distortion correction.
- Added ability to set minimum quality measure thresholds to all registration steps
- Difference method (``-``) for ``CPAC.utils.configuration.Configuration`` instances
- Added ``fail_fast`` configuration setting and CLI flag

### Changed
- Added a level of depth to `working` directories to match `log` and `output` directory structure
- Renamed participant-pipeline-level `output` directory prefix to `pipeline_` to match `log` and `working` paths
- Changed the 1mm atlases chosen in the rbc-options preconfig to the 2mm versions
- For Nilearn-generated correlation matrices, diagonals are now set to all `1`s (were all `0`s)
- Added ability to apply nusiance correction to template-space BOLD images
- Removed ability to run single-step-resampling on motion-corrected BOLD data
- Moved default pipeline config into directory with other preconfigs
- Added crash messages from during and before graph building to logs
- Added data-config-specific hash string to C-PAC-generated config files
- Updated `rbc-options` preconfig to use `fmriprep-options` preprocessing
- Changed `random.log` to `random.tsv` and updated logic to log random seed when not specified

### Fixed
- Fixed [bug](https://github.com/FCP-INDI/C-PAC/issues/1795) that was causing `cpac run` to fail when passing a manual random seed via `--random_seed`.
- Replaces ``DwellTime`` with ``EffectiveEchoSpacing`` for FSL usage of the term
- Fixed an issue that was causing some epi field maps to not be ingressed if the BIDS tags were not in the correct order.

## [v1.8.4] - 2022-06-27

Expand Down
6 changes: 3 additions & 3 deletions CPAC/alff/alff.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,8 @@ def alff_falff(wf, cfg, strat_pool, pipe_num, opt=None):
"switch": ["run"],
"option_key": "None",
"option_val": "None",
"inputs": [["desc-cleaned_bold", "desc-brain_bold", "desc-preproc_bold",
"bold"],
"inputs": [["desc-cleanedNofilt_bold", "desc-brain_bold",
"desc-preproc_bold", "bold"],
"space-bold_desc-brain_mask"],
"outputs": ["alff",
"falff"]}
Expand All @@ -262,7 +262,7 @@ def alff_falff(wf, cfg, strat_pool, pipe_num, opt=None):
alff.get_node('hp_input').iterables = ('hp', alff.inputs.hp_input.hp)
alff.get_node('lp_input').iterables = ('lp', alff.inputs.lp_input.lp)

node, out = strat_pool.get_data(["desc-cleaned_bold", "desc-brain_bold",
node, out = strat_pool.get_data(["desc-cleanedNofilt_bold", "desc-brain_bold",
"desc-preproc_bold", "bold"])
wf.connect(node, out, alff, 'inputspec.rest_res')

Expand Down
613 changes: 329 additions & 284 deletions CPAC/anat_preproc/anat_preproc.py

Large diffs are not rendered by default.

131 changes: 69 additions & 62 deletions CPAC/anat_preproc/ants.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,21 @@
from packaging.version import parse as parseversion, Version

# nipype
# pylint: disable=wrong-import-order
import CPAC.pipeline.nipype_pipeline_engine as pe
from nipype.interfaces import utility as niu
from nipype.interfaces.fsl.maths import ApplyMask
from nipype.interfaces.ants import N4BiasFieldCorrection, Atropos, MultiplyImages

from ..utils.misc import get_template_specs
from nipype.interfaces.ants import (N4BiasFieldCorrection, Atropos,
MultiplyImages)
from CPAC.registration.guardrails import guardrail_selection, retry_clone
# niworkflows
from ..utils.interfaces.ants import (
ImageMath,
ResampleImageBySpacing,
AI,
ThresholdImage,
)
from CPAC.utils.interfaces.ants import (ImageMath,
ResampleImageBySpacing,
AI,
ThresholdImage)
from CPAC.utils.interfaces.fixes import (
FixHeaderRegistration as Registration,
FixHeaderApplyTransforms as ApplyTransforms,
)
FixHeaderApplyTransforms as ApplyTransforms)
from CPAC.utils.interfaces.utils import CopyXForm


Expand Down Expand Up @@ -114,8 +112,9 @@ def init_brain_extraction_wf(tpl_target_path,
Estimated peak memory consumption of the most hungry nodes
in the workflow
bids_suffix : str
Sequence type of the first input image. For a list of acceptable values
see https://bids-specification.readthedocs.io/en/latest/\
Sequence type of the first input image. For a list of
acceptable values see
https://bids-specification.readthedocs.io/en/latest/\
04-modality-specific-files/01-magnetic-resonance-imaging-data.html#anatomy-imaging-data
atropos_refine : bool
Enables or disables the whole ATROPOS sub-workflow
Expand Down Expand Up @@ -146,12 +145,14 @@ def init_brain_extraction_wf(tpl_target_path,
computation to a specific region.
**Outputs**
out_file
Skull-stripped and :abbr:`INU (intensity non-uniformity)`-corrected ``in_files``
Skull-stripped and :abbr:`INU (intensity non-uniformity)
`-corrected ``in_files``
out_mask
Calculated brain mask
bias_corrected
The ``in_files`` input images, after :abbr:`INU (intensity non-uniformity)`
correction, before skull-stripping.
The ``in_files`` input images, after
:abbr:`INU (intensity non-uniformity)` correction, before
skull-stripping.
bias_image
The :abbr:`INU (intensity non-uniformity)` field estimated for each
input in ``in_files``
Expand Down Expand Up @@ -187,7 +188,8 @@ def init_brain_extraction_wf(tpl_target_path,
mem_gb=1.3, mem_x=(3811976743057169 / 302231454903657293676544,
'hdr_file'))

trunc = pe.MapNode(ImageMath(operation='TruncateImageIntensity', op2='0.01 0.999 256'),
trunc = pe.MapNode(ImageMath(operation='TruncateImageIntensity',
op2='0.01 0.999 256'),
name='truncate_images', iterfield=['op1'])
inu_n4 = pe.MapNode(
N4BiasFieldCorrection(
Expand Down Expand Up @@ -226,6 +228,7 @@ def init_brain_extraction_wf(tpl_target_path,
_ants_version = Registration().version
if _ants_version and parseversion(_ants_version) >= Version('2.3.0'):
init_aff.inputs.search_grid = (40, (0, 40, 40))
init_aff_nodes = (init_aff, retry_clone(init_aff))

# Set up spatial normalization
settings_file = 'antsBrainExtraction_%s.json' if use_laplacian \
Expand All @@ -241,10 +244,12 @@ def init_brain_extraction_wf(tpl_target_path,
if _ants_version and parseversion(_ants_version) >= Version('2.2.0'):
fixed_mask_trait += 's'

norm_nodes, norm_guardrails = wf.nodes_and_guardrails(
norm, registered='warped_image')

map_brainmask = pe.Node(
ApplyTransforms(interpolation='Gaussian', float=True),
name='map_brainmask'
)
name='map_brainmask')
map_brainmask.inputs.input_image = str(tpl_mask_path)

thr_brainmask = pe.Node(ThresholdImage(
Expand All @@ -267,24 +272,34 @@ def init_brain_extraction_wf(tpl_target_path,
n_procs=omp_nthreads, name='inu_n4_final', iterfield=['input_image'])

# Apply mask
apply_mask = pe.MapNode(ApplyMask(), iterfield=['in_file'], name='apply_mask')

apply_mask = pe.MapNode(ApplyMask(), iterfield=['in_file'],
name='apply_mask')

wf.connect_retries(init_aff_nodes, [
(inputnode, 'in_mask', 'fixed_image_mask'),
(res_tmpl, 'output_image', 'fixed_image'),
(res_target, 'output_image', 'moving_image')])
for i, node in enumerate(norm_nodes):
wf.connect(init_aff_nodes[i], 'output_transform',
node, 'initial_moving_transform')
wf.connect_retries(norm_nodes, [
(inputnode, 'in_mask', fixed_mask_trait)])
norm_rtransforms = guardrail_selection(wf, *norm_nodes,
'reverse_transforms',
norm_guardrails[0])
norm_rinvert_flags = guardrail_selection(wf, *norm_nodes,
'reverse_invert_flags',
norm_guardrails[0])
wf.connect([
(inputnode, trunc, [('in_files', 'op1')]),
(inputnode, copy_xform, [(('in_files', _pop), 'hdr_file')]),
(inputnode, inu_n4_final, [('in_files', 'input_image')]),
(inputnode, init_aff, [('in_mask', 'fixed_image_mask')]),
(inputnode, norm, [('in_mask', fixed_mask_trait)]),
(inputnode, map_brainmask, [(('in_files', _pop), 'reference_image')]),
(trunc, inu_n4, [('output_image', 'input_image')]),
(inu_n4, res_target, [
(('output_image', _pop), 'input_image')]),
(res_tmpl, init_aff, [('output_image', 'fixed_image')]),
(res_target, init_aff, [('output_image', 'moving_image')]),
(init_aff, norm, [('output_transform', 'initial_moving_transform')]),
(norm, map_brainmask, [
('reverse_transforms', 'transforms'),
('reverse_invert_flags', 'invert_transform_flags')]),
(inu_n4, res_target, [(('output_image', _pop), 'input_image')]),
(norm_rtransforms, map_brainmask, [('out', 'transforms')]),
(norm_rinvert_flags, map_brainmask, [
('out', 'invert_transform_flags')]),
(map_brainmask, thr_brainmask, [('output_image', 'input_image')]),
(thr_brainmask, dil_brainmask, [('output_image', 'op1')]),
(dil_brainmask, get_brainmask, [('output_image', 'op1')]),
Expand All @@ -294,12 +309,10 @@ def init_brain_extraction_wf(tpl_target_path,
(apply_mask, copy_xform, [('out_file', 'out_file')]),
(inu_n4_final, copy_xform, [('output_image', 'bias_corrected'),
('bias_image', 'bias_image')]),
(copy_xform, outputnode, [
('out_file', 'out_file'),
('out_mask', 'out_mask'),
('bias_corrected', 'bias_corrected'),
('bias_image', 'bias_image')]),
])
(copy_xform, outputnode, [('out_file', 'out_file'),
('out_mask', 'out_mask'),
('bias_corrected', 'bias_corrected'),
('bias_image', 'bias_image')])])

if use_laplacian:
lap_tmpl = pe.Node(ImageMath(operation='Laplacian', op2='1.5 1'),
Expand All @@ -311,29 +324,28 @@ def init_brain_extraction_wf(tpl_target_path,
mrg_tmpl.inputs.in1 = tpl_target_path
mrg_target = pe.Node(niu.Merge(2), name='mrg_target')
wf.connect([
(inu_n4, lap_target, [
(('output_image', _pop), 'op1')]),
(inu_n4, lap_target, [(('output_image', _pop), 'op1')]),
(lap_tmpl, mrg_tmpl, [('output_image', 'in2')]),
(inu_n4, mrg_target, [('output_image', 'in1')]),
(lap_target, mrg_target, [('output_image', 'in2')]),
(mrg_tmpl, norm, [('out', 'fixed_image')]),
(mrg_target, norm, [('out', 'moving_image')]),
])
(lap_target, mrg_target, [('output_image', 'in2')])])
wf.connect_retries(norm_nodes, [(mrg_tmpl, 'out', 'fixed_image'),
(mrg_target, 'out', 'moving_image')])
wf.connect_retries(norm_guardrails, [(mrg_tmpl, 'out', 'reference')])
else:
norm.inputs.fixed_image = tpl_target_path
wf.connect([
(inu_n4, norm, [
(('output_image', _pop), 'moving_image')]),
])
for i, node in enumerate(norm_nodes):
node.inputs.fixed_image = tpl_target_path
norm_guardrails[i].inputs.reference = tpl_target_path
wf.connect_retries(norm_nodes, [
(inu_n4, ('output_image', _pop), 'moving_image')])

if atropos_refine:
atropos_model = atropos_model or list(ATROPOS_MODELS[bids_suffix].values())
atropos_model = atropos_model or list(
ATROPOS_MODELS[bids_suffix].values())
atropos_wf = init_atropos_wf(
use_random_seed=atropos_use_random_seed,
omp_nthreads=omp_nthreads,
mem_gb=mem_gb,
in_segmentation_model=atropos_model,
)
in_segmentation_model=atropos_model)
sel_wm = pe.Node(niu.Select(index=atropos_model[-1] - 1),
name='sel_wm',
run_without_submitting=True,
Expand All @@ -343,24 +355,19 @@ def init_brain_extraction_wf(tpl_target_path,

wf.disconnect([
(get_brainmask, apply_mask, [('output_image', 'mask_file')]),
(copy_xform, outputnode, [('out_mask', 'out_mask')]),
])
(copy_xform, outputnode, [('out_mask', 'out_mask')])])
wf.connect([
(inu_n4, atropos_wf, [
('output_image', 'inputnode.in_files')]),
(inu_n4, atropos_wf, [('output_image', 'inputnode.in_files')]),
(thr_brainmask, atropos_wf, [
('output_image', 'inputnode.in_mask')]),
(get_brainmask, atropos_wf, [
('output_image', 'inputnode.in_mask_dilated')]),
(atropos_wf, sel_wm, [('outputnode.out_tpms', 'inlist')]),
(sel_wm, inu_n4_final, [('out', 'weight_image')]),
(atropos_wf, apply_mask, [
('outputnode.out_mask', 'mask_file')]),
(atropos_wf, outputnode, [
('outputnode.out_mask', 'out_mask'),
('outputnode.out_segm', 'out_segm'),
('outputnode.out_tpms', 'out_tpms')]),
])
(atropos_wf, apply_mask, [('outputnode.out_mask', 'mask_file')]),
(atropos_wf, outputnode, [('outputnode.out_mask', 'out_mask'),
('outputnode.out_segm', 'out_segm'),
('outputnode.out_tpms', 'out_tpms')])])
return wf


Expand Down
Loading