From 07133c2d8feed88d4a70d1a3fc4fd15bd87c6034 Mon Sep 17 00:00:00 2001 From: Mathias Goncalves Date: Fri, 6 Dec 2024 11:12:11 -0500 Subject: [PATCH 1/8] ENH: Use 2 step registration for adult templates --- nibabies/interfaces/patches.py | 86 +++++++- nibabies/workflows/anatomical/fit.py | 166 ++++++++++++++- nibabies/workflows/anatomical/registration.py | 201 +++++++++++++++++- 3 files changed, 439 insertions(+), 14 deletions(-) diff --git a/nibabies/interfaces/patches.py b/nibabies/interfaces/patches.py index d7e56c05..ddbba8e1 100644 --- a/nibabies/interfaces/patches.py +++ b/nibabies/interfaces/patches.py @@ -1,5 +1,10 @@ -import nipype.interfaces.freesurfer as fs -from nipype.interfaces.base import File, traits +from pathlib import Path + +from nipype.interfaces import ( + freesurfer as fs, +) +from nipype.interfaces.ants.base import ANTSCommand, ANTSCommandInputSpec +from nipype.interfaces.base import File, InputMultiObject, TraitedSpec, traits class _MRICoregInputSpec(fs.registration.MRICoregInputSpec): @@ -23,3 +28,80 @@ class MRICoreg(fs.MRICoreg): """ input_spec = _MRICoregInputSpec + + +class ConcatXFMInputSpec(ANTSCommandInputSpec): + transforms = InputMultiObject( + traits.Either(File(exists=True), 'identity'), + argstr='%s', + mandatory=True, + desc='transform files: will be applied in reverse order. For ' + 'example, the last specified transform will be applied first.', + ) + out_xfm = traits.File( + 'concat_xfm.h5', + usedefault=True, + argstr='--output [ %s, 1 ]', + desc='output file name', + ) + reference_image = File( + argstr='--reference-image %s', + mandatory=True, + desc='reference image space that you wish to warp INTO', + exists=True, + ) + invert_transform_flags = InputMultiObject(traits.Bool()) + + +class ConcatXFMOutputSpec(TraitedSpec): + out_xfm = File(desc='Combined transform') + + +class ConcatXFM(ANTSCommand): + """ + Streamed use of antsApplyTransforms to combine nonlinear xfms into a single file + + Examples + -------- + + >>> from nibabies.interfaces.patches import ConcatXFM + >>> cxfm = ConcatXFM() + >>> cxfm.inputs.transforms = ['xfm1.h5', 'xfm0.h5'] + >>> cxfm.inputs.reference_image = 'sub-01_T1w.nii.gz' + >>> cxfm.cmdline + 'antsApplyTransforms --output [ concat_xfm.h5, 1 ] --transform .../xfm1.h5 \ +--transform .../xfm0.h5 --reference_image .../sub-01_T1w.nii.gz' + + """ + + _cmd = 'antsApplyTransforms' + input_spec = ConcatXFMInputSpec + output_spec = ConcatXFMOutputSpec + + def _get_transform_filenames(self): + retval = [] + invert_flags = self.inputs.invert_transform_flags + if not invert_flags: + invert_flags = [False] * len(self.inputs.transforms) + elif len(self.inputs.transforms) != len(invert_flags): + raise ValueError( + 'ERROR: The invert_transform_flags list must have the same number ' + 'of entries as the transforms list.' + ) + + for transform, invert in zip(self.inputs.transforms, invert_flags, strict=False): + if invert: + retval.append(f'--transform [ {transform}, 1 ]') + else: + retval.append(f'--transform {transform}') + return ' '.join(retval) + + def _format_arg(self, opt, spec, val): + if opt == 'transforms': + return self._get_transform_filenames() + return super()._format_arg(opt, spec, val) + + def _list_outputs(self): + outputs = self._outputs().get() + outputs['out_xfm'] = Path(self.inputs.out_xfm).absolute() + return outputs diff --git a/nibabies/workflows/anatomical/fit.py b/nibabies/workflows/anatomical/fit.py index d0fe552d..24bd7b04 100644 --- a/nibabies/workflows/anatomical/fit.py +++ b/nibabies/workflows/anatomical/fit.py @@ -9,6 +9,7 @@ from niworkflows.interfaces.fixes import FixHeaderApplyTransforms as ApplyTransforms from niworkflows.interfaces.header import ValidateImage from niworkflows.interfaces.nibabel import ApplyMask, Binarize +from niworkflows.interfaces.utility import KeySelect from niworkflows.utils.connections import pop_file from smriprep.workflows.anatomical import ( _is_skull_stripped, @@ -37,7 +38,10 @@ from nibabies.workflows.anatomical.brain_extraction import init_infant_brain_extraction_wf from nibabies.workflows.anatomical.outputs import init_anat_reports_wf from nibabies.workflows.anatomical.preproc import init_anat_preproc_wf, init_csf_norm_wf -from nibabies.workflows.anatomical.registration import init_coregistration_wf +from nibabies.workflows.anatomical.registration import ( + init_concat_registrations_wf, + init_coregistration_wf, +) from nibabies.workflows.anatomical.segmentation import init_segmentation_wf from nibabies.workflows.anatomical.surfaces import init_mcribs_dhcp_wf @@ -228,9 +232,9 @@ def init_infant_anat_fit_wf( name='seg_buffer', ) # Stage 5 - collated template names, forward and reverse transforms - template_buffer = pe.Node(niu.Merge(2), name='template_buffer') - anat2std_buffer = pe.Node(niu.Merge(2), name='anat2std_buffer') - std2anat_buffer = pe.Node(niu.Merge(2), name='std2anat_buffer') + template_buffer = pe.Node(niu.Merge(3), name='template_buffer') + anat2std_buffer = pe.Node(niu.Merge(3), name='anat2std_buffer') + std2anat_buffer = pe.Node(niu.Merge(3), name='std2anat_buffer') # Stage 6 results: Refined stage 2 results; may be direct copy if no refinement refined_buffer = pe.Node( @@ -875,15 +879,42 @@ def init_infant_anat_fit_wf( seg_buffer.inputs.anat_tpms = anat_tpms # Stage 5: Normalization + + # If selected adult templates are requested (MNI152 6th Gen or 2009) + # opt to concatenate transforms first from native -> infant template (MNIInfant), + # and then use a previously computed MNIInfant -> MNI transform + # this minimizes the chance of a bad registration. + templates = [] + concat_xfms = [] found_xfms = {} + intermediate = None # The intermediate space when concatenating xfms - includes cohort + intermediate_targets = { + 'MNI152NLin6Asym', + } # TODO: 'MNI152NLin2009cAsym' + for template in spaces.get_spaces(nonstandard=False, dim=(3,)): + # resolution / spec will not differentiate here + if template.startswith('MNIInfant'): + intermediate = template xfms = precomputed.get('transforms', {}).get(template, {}) if set(xfms) != {'forward', 'reverse'}: - templates.append(template) + if template in intermediate_targets: + concat_xfms.append(template) + else: + templates.append(template) else: found_xfms[template] = xfms + # Create another set of buffers to handle the case where we aggregate found and generated + # xfms to be concatenated + concat_template_buffer = pe.Node(niu.Merge(2), name='concat_template_buffer') + concat_template_buffer.inputs.in1 = list(found_xfms) + concat_anat2std_buffer = pe.Node(niu.Merge(2), name='concat_anat2std_buffer') + concat_anat2std_buffer.inputs.in1 = [xfm['forward'] for xfm in found_xfms.values()] + concat_std2anat_buffer = pe.Node(niu.Merge(2), name='concat_std2anat_buffer') + concat_std2anat_buffer.inputs.in1 = [xfm['reverse'] for xfm in found_xfms.values()] + template_buffer.inputs.in1 = list(found_xfms) anat2std_buffer.inputs.in1 = [xfm['forward'] for xfm in found_xfms.values()] std2anat_buffer.inputs.in1 = [xfm['reverse'] for xfm in found_xfms.values()] @@ -898,7 +929,7 @@ def init_infant_anat_fit_wf( ]) # fmt:skip if templates: - LOGGER.info(f'ANAT Stage 5: Preparing normalization workflow for {templates}') + LOGGER.info(f'ANAT Stage 5a: Preparing normalization workflow for {templates}') register_template_wf = init_register_template_wf( sloppy=sloppy, omp_nthreads=omp_nthreads, @@ -925,11 +956,53 @@ def init_infant_anat_fit_wf( ('outputnode.std2anat_xfm', 'inputnode.std2anat_xfm'), ]), (register_template_wf, template_buffer, [('outputnode.template', 'in2')]), + (register_template_wf, concat_template_buffer, [('outputnode.template', 'in2')]), (register_template_wf, std2anat_buffer, [('outputnode.std2anat_xfm', 'in2')]), + (register_template_wf, concat_std2anat_buffer, [('outputnode.std2anat_xfm', 'in2')]), (register_template_wf, anat2std_buffer, [('outputnode.anat2std_xfm', 'in2')]), + (register_template_wf, concat_anat2std_buffer, [('outputnode.anat2std_xfm', 'in2')]), ]) # fmt:skip + + if concat_xfms: + LOGGER.info(f'ANAT Stage 5b: Concatenating normalization for {concat_xfms}') + # 1. Select intermediate's transforms + select_infant_mni = pe.Node( + KeySelect(fields=['template', 'anat2std_xfm', 'std2anat_xfm'], key=intermediate), + name='select_infant_mni', + run_without_submitting=True, + ) + concat_reg_wf = init_concat_registrations_wf(templates=concat_xfms) + ds_concat_reg_wf = init_ds_template_registration_wf( + output_dir=str(output_dir), + image_type=reference_anat, + name='ds_concat_registration_wf', + ) + + workflow.connect([ + (concat_template_buffer, select_infant_mni, [('out', 'template')]), + (concat_anat2std_buffer, select_infant_mni, [('out', 'anat2std_xfm')]), + (concat_std2anat_buffer, select_infant_mni, [('out', 'std2anat_xfm')]), + (select_infant_mni, concat_reg_wf, [ + ('template', 'inputnode.intermediate'), + ('anat2std_xfm', 'inputnode.anat2std_xfm'), + ('std2anat_xfm', 'inputnode.std2anat_xfm'), + ]), + (anat_buffer, concat_reg_wf, [('anat_preproc', 'inputnode.anat_preproc')]), + (sourcefile_buffer, ds_concat_reg_wf, [ + ('anat_source_files', 'inputnode.source_files') + ]), + (concat_reg_wf, ds_concat_reg_wf, [ + ('outputnode.template', 'inputnode.template'), + ('outputnode.anat2std_xfm', 'inputnode.anat2std_xfm'), + ('outputnode.std2anat_xfm', 'inputnode.std2anat_xfm'), + ]), + (concat_reg_wf, template_buffer, [('outputnode.template', 'in3')]), + (concat_reg_wf, anat2std_buffer, [('outputnode.anat2std_xfm', 'in3')]), + (concat_reg_wf, std2anat_buffer, [('outputnode.std2anat_xfm', 'in3')]), + ]) # fmt:skip + if found_xfms: - LOGGER.info(f'ANAT Stage 5: Found pre-computed registrations for {found_xfms}') + LOGGER.info(f'ANAT Stage 5c: Found pre-computed registrations for {found_xfms}') # Only refine mask if necessary if anat_mask or recon_method is None or not refine_mask: @@ -1404,9 +1477,9 @@ def init_infant_single_anat_fit_wf( name='seg_buffer', ) # Stage 4 - collated template names, forward and reverse transforms - template_buffer = pe.Node(niu.Merge(2), name='template_buffer') - anat2std_buffer = pe.Node(niu.Merge(2), name='anat2std_buffer') - std2anat_buffer = pe.Node(niu.Merge(2), name='std2anat_buffer') + template_buffer = pe.Node(niu.Merge(3), name='template_buffer') + anat2std_buffer = pe.Node(niu.Merge(3), name='anat2std_buffer') + std2anat_buffer = pe.Node(niu.Merge(3), name='std2anat_buffer') # Stage 5 results: Refined stage 2 results; may be direct copy if no refinement refined_buffer = pe.Node( @@ -1720,15 +1793,41 @@ def init_infant_single_anat_fit_wf( seg_buffer.inputs.anat_tpms = anat_tpms # Stage 4: Normalization + # If selected adult templates are requested (MNI152 6th Gen or 2009) + # opt to concatenate transforms first from native -> infant template (MNIInfant), + # and then use a previously computed MNIInfant -> MNI transform + # this minimizes the chance of a bad registration. + templates = [] + concat_xfms = [] found_xfms = {} + intermediate = None # The intermediate space when concatenating xfms - includes cohort + intermediate_targets = { + 'MNI152NLin6Asym', + } # TODO: 'MNI152NLin2009cAsym' + for template in spaces.get_spaces(nonstandard=False, dim=(3,)): + # resolution / spec will not differentiate here + if template.startswith('MNIInfant'): + intermediate = template xfms = precomputed.get('transforms', {}).get(template, {}) if set(xfms) != {'forward', 'reverse'}: - templates.append(template) + if template in intermediate_targets: + concat_xfms.append(template) + else: + templates.append(template) else: found_xfms[template] = xfms + # Create another set of buffers to handle the case where we aggregate found and generated + # xfms to be concatenated + concat_template_buffer = pe.Node(niu.Merge(2), name='concat_template_buffer') + concat_template_buffer.inputs.in1 = list(found_xfms) + concat_anat2std_buffer = pe.Node(niu.Merge(2), name='concat_anat2std_buffer') + concat_anat2std_buffer.inputs.in1 = [xfm['forward'] for xfm in found_xfms.values()] + concat_std2anat_buffer = pe.Node(niu.Merge(2), name='concat_std2anat_buffer') + concat_std2anat_buffer.inputs.in1 = [xfm['reverse'] for xfm in found_xfms.values()] + template_buffer.inputs.in1 = list(found_xfms) anat2std_buffer.inputs.in1 = [xfm['forward'] for xfm in found_xfms.values()] std2anat_buffer.inputs.in1 = [xfm['reverse'] for xfm in found_xfms.values()] @@ -1770,9 +1869,54 @@ def init_infant_single_anat_fit_wf( ('outputnode.std2anat_xfm', 'inputnode.std2anat_xfm'), ]), (register_template_wf, template_buffer, [('outputnode.template', 'in2')]), + (register_template_wf, concat_template_buffer, [('outputnode.template', 'in2')]), (register_template_wf, std2anat_buffer, [('outputnode.std2anat_xfm', 'in2')]), + (register_template_wf, concat_std2anat_buffer, [('outputnode.std2anat_xfm', 'in2')]), (register_template_wf, anat2std_buffer, [('outputnode.anat2std_xfm', 'in2')]), + (register_template_wf, concat_anat2std_buffer, [('outputnode.anat2std_xfm', 'in2')]), + ]) # fmt:skip + + if concat_xfms: + LOGGER.info(f'ANAT Stage 5b: Concatenating normalization for {concat_xfms}') + # 1. Select intermediate's transforms + select_infant_mni = pe.Node( + KeySelect(fields=['template', 'anat2std_xfm', 'std2anat_xfm']), + name='select_infant_mni', + run_without_submitting=True, + ) + select_infant_mni.inputs.key = intermediate + + concat_reg_wf = init_concat_registrations_wf(templates=concat_xfms) + ds_concat_reg_wf = init_ds_template_registration_wf( + output_dir=str(output_dir), + image_type=reference_anat, + name='ds_concat_registration_wf', + ) + + workflow.connect([ + (concat_template_buffer, select_infant_mni, [('out', 'keys')]), + (concat_template_buffer, select_infant_mni, [('out', 'template')]), + (concat_anat2std_buffer, select_infant_mni, [('out', 'anat2std_xfm')]), + (concat_std2anat_buffer, select_infant_mni, [('out', 'std2anat_xfm')]), + (select_infant_mni, concat_reg_wf, [ + ('template', 'inputnode.intermediate'), + ('anat2std_xfm', 'inputnode.anat2std_xfm'), + ('std2anat_xfm', 'inputnode.std2anat_xfm'), + ]), + (anat_buffer, concat_reg_wf, [('anat_preproc', 'inputnode.anat_preproc')]), + (sourcefile_buffer, ds_concat_reg_wf, [ + ('anat_source_files', 'inputnode.source_files') + ]), + (concat_reg_wf, ds_concat_reg_wf, [ + ('outputnode.template', 'inputnode.template'), + ('outputnode.anat2std_xfm', 'inputnode.anat2std_xfm'), + ('outputnode.std2anat_xfm', 'inputnode.std2anat_xfm'), + ]), + (concat_reg_wf, template_buffer, [('outputnode.template', 'in3')]), + (concat_reg_wf, anat2std_buffer, [('outputnode.anat2std_xfm', 'in3')]), + (concat_reg_wf, std2anat_buffer, [('outputnode.std2anat_xfm', 'in3')]), ]) # fmt:skip + if found_xfms: LOGGER.info(f'ANAT Stage 4: Found pre-computed registrations for {found_xfms}') diff --git a/nibabies/workflows/anatomical/registration.py b/nibabies/workflows/anatomical/registration.py index f9a437c2..0c3229fb 100644 --- a/nibabies/workflows/anatomical/registration.py +++ b/nibabies/workflows/anatomical/registration.py @@ -4,9 +4,25 @@ from __future__ import annotations -from nipype.interfaces import utility as niu +from collections import defaultdict + +from nipype.interfaces import ( + utility as niu, +) +from nipype.interfaces.ants.base import Info as ANTsInfo from nipype.pipeline import engine as pe +from niworkflows.engine.workflows import LiterateWorkflow as Workflow from niworkflows.interfaces.fixes import FixHeaderApplyTransforms as ApplyTransforms +from smriprep.workflows.fit.registration import ( + TemplateDesc, + TemplateFlowSelect, + _fmt_cohort, + get_metadata, + tf_ver, +) + +from nibabies.config import DEFAULT_MEMORY_MIN_GB +from nibabies.interfaces.patches import ConcatXFM def init_coregistration_wf( @@ -288,3 +304,186 @@ def init_coregister_derivatives_wf( ]) # fmt:on return workflow + + +def init_concat_registrations_wf( + *, + templates, + name='concat_registrations_wf', +): + """ + Concatenate two transforms to produce a single transform, from native to ``template``. + + Parameters + ---------- + templates : :obj:`list` of :obj:`str` + List of standard space fullnames (e.g., ``MNI152NLin6Asym`` + or ``MNIPediatricAsym:cohort-4``) which are targets for spatial + normalization. + + Inputs + ------ + anat_preproc + The anatomical reference, to be used as a reference image for std2anat_xfm + intermediate + Standard space fullname (usually ``MNIInfant:cohort-X``) which serves as + the intermediate space between native and *template* + anat2std_xfm + The incoming anat2std transform (from native to MNIInfant) + std2anat_xfm + The incoming std2anat transform (from MNIInfant to native) + + Outputs + ------- + anat2std_xfm + Anatomical -> MNIInfant -> template transform. + std2anat_xfm + The template -> MNIInfant -> anatomical transform. + template + Template name extracted from the input parameter ``template``, for further + use in downstream nodes. + template_spec + Template specifications extracted from the input parameter ``template``, for + further use in downstream nodes. + + """ + ntpls = len(templates) + workflow = Workflow(name=name) + + if templates: + workflow.__desc__ = """\ +Volume-based spatial normalization to {targets} ({targets_id}) was performed by +concatenating two registrations with `antsRegistration` (ANTs {ants_ver}). First, the +anatomical reference was registered to the Infant MNI templates (@mniinfant). Separately, +the Infant MNI templates were registered to {targets}, with the saved transform to template +stored for reuse and accessed with *TemplateFlow* [{tf_ver}, @templateflow]: +""".format( + ants_ver=ANTsInfo.version() or '(version unknown)', + targets='{} standard space{}'.format( + defaultdict('several'.format, {1: 'one', 2: 'two', 3: 'three', 4: 'four'})[ntpls], + 's' * (ntpls != 1), + ), + targets_id=', '.join(templates), + tf_ver=tf_ver, + ) + + # Append template citations to description + for template in templates: + template_meta = get_metadata(template.split(':')[0]) + template_refs = ['@{}'.format(template.split(':')[0].lower())] + + if template_meta.get('RRID', None): + template_refs += [f'RRID:{template_meta["RRID"]}'] + + workflow.__desc__ += """\ +*{template_name}* [{template_refs}; TemplateFlow ID: {template}]""".format( + template=template, + template_name=template_meta['Name'], + template_refs=', '.join(template_refs), + ) + workflow.__desc__ += '.\n' if template == templates[-1] else ', ' + + inputnode = pe.Node( + niu.IdentityInterface( + fields=['template', 'anat_preproc', 'anat2std_xfm', 'intermediate', 'std2anat_xfm'] + ), + name='inputnode', + ) + inputnode.inputs.template = templates + + out_fields = [ + 'anat2std_xfm', + 'std2anat_xfm', + 'template', + 'template_spec', + ] + outputnode = pe.Node(niu.IdentityInterface(fields=out_fields), name='outputnode') + + intermed_xfms = pe.MapNode( + niu.Function( + function=_load_intermediate_xfms, output_names=['int2std_xfm', 'std2int_xfm'] + ), + name='intermed_xfms', + iterfield=['std'], + run_without_submitting=True, + ) + + split_desc = pe.MapNode( + TemplateDesc(), run_without_submitting=True, iterfield='template', name='split_desc' + ) + + tf_select = pe.MapNode( + TemplateFlowSelect(resolution=1), + name='tf_select', + run_without_submitting=True, + iterfield=['template', 'template_spec'], + ) + + merge_anat2std = pe.MapNode( + niu.Merge(2), name='merge_anat2std', iterfield=['in1', 'in2'], run_without_submitting=True + ) + merge_std2anat = merge_anat2std.clone('merge_std2anat') + + concat_anat2std = pe.MapNode( + ConcatXFM(), + name='concat_anat2std', + mem_gb=DEFAULT_MEMORY_MIN_GB, + iterfield=['transforms', 'reference_image'], + ) + concat_std2anat = pe.MapNode( + ConcatXFM(), + name='concat_std2anat', + mem_gb=DEFAULT_MEMORY_MIN_GB, + iterfield=['transforms', 'reference_image'], + ) + + fmt_cohort = pe.MapNode( + niu.Function(function=_fmt_cohort, output_names=['template', 'spec']), + name='fmt_cohort', + run_without_submitting=True, + iterfield=['template', 'spec'], + ) + + workflow.connect([ + (inputnode, merge_anat2std, [('anat2std_xfm', 'in2')]), + (inputnode, merge_std2anat, [('std2anat_xfm', 'in2')]), + (inputnode, concat_std2anat, [('anat_preproc', 'reference_image')]), + (inputnode, intermed_xfms, [('intermediate', 'intermediate')]), + (inputnode, intermed_xfms, [('template', 'std')]), + + (intermed_xfms, merge_anat2std, [('int2std_xfm', 'in1')]), + (intermed_xfms, merge_std2anat, [('std2int_xfm', 'in1')]), + + (merge_anat2std, concat_anat2std, [('out', 'transforms')]), + (merge_std2anat, concat_std2anat, [('out', 'transforms')]), + + (inputnode, split_desc, [('template', 'template')]), + (split_desc, tf_select, [ + ('name', 'template'), + ('spec', 'template_spec'), + ]), + (tf_select, concat_anat2std, [('t1w_file', 'reference_image')]), + (split_desc, fmt_cohort, [ + ('name', 'template'), + ('spec', 'spec'), + ]), + (fmt_cohort, outputnode, [ + ('template', 'template'), + ('spec', 'template_spec'), + ]), + (concat_anat2std, outputnode, [('out_xfm', 'anat2std_xfm')]), + (concat_std2anat, outputnode, [('out_xfm', 'std2anat_xfm')]), + ]) # fmt:skip + + return workflow + + +def _load_intermediate_xfms(intermediate, std): + from nibabies.data import load + + # MNIInfant:cohort-1 -> MNIInfant+1 + intmed = intermediate.replace(':cohort-', '+') + + int2std = load.readable(f'tpl_xfms/from-{intmed}_to-{std}_xfm.h5') + std2int = load.readable(f'tpl_xfms/from-{std}_to-{intmed}_xfm.h5') + return int2std, std2int From 71ee9a2f86e1ed5a622380932acc24c743f6c3bc Mon Sep 17 00:00:00 2001 From: Mathias Goncalves Date: Tue, 10 Dec 2024 16:49:04 -0500 Subject: [PATCH 2/8] ENH: Store XFMs in OSF, add manifest for easy retrieval --- nibabies/data/xfm_manifest.json | 90 +++++++++++++++++++ nibabies/workflows/anatomical/registration.py | 23 ++++- pyproject.toml | 1 + 3 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 nibabies/data/xfm_manifest.json diff --git a/nibabies/data/xfm_manifest.json b/nibabies/data/xfm_manifest.json new file mode 100644 index 00000000..e95b1f29 --- /dev/null +++ b/nibabies/data/xfm_manifest.json @@ -0,0 +1,90 @@ +{ + "from-MNI152NLin6Asym_to-MNIInfant+10_xfm.h5": { + "url": "https://osf.io/download/jf6vz/", + "hash": "md5:df6d40e5bbdca85f083866ad26507796" + }, + "from-MNI152NLin6Asym_to-MNIInfant+11_xfm.h5": { + "url": "https://osf.io/download/zmjyn/", + "hash": "md5:ff8435f06c0a44be88e050492b90ffea" + }, + "from-MNI152NLin6Asym_to-MNIInfant+1_xfm.h5": { + "url": "https://osf.io/download/kx7ny/", + "hash": "md5:c27d35dff75d59d605c8d786c985594e" + }, + "from-MNI152NLin6Asym_to-MNIInfant+2_xfm.h5": { + "url": "https://osf.io/download/6758aa0c67a7782b00f73c77/", + "hash": "md5:81fabdc70c200bf099893bd1377ef0f7" + }, + "from-MNI152NLin6Asym_to-MNIInfant+3_xfm.h5": { + "url": "https://osf.io/download/6758aa1c76bfbc22cbf73b0a/", + "hash": "md5:c8f5b79b95f9aa65add5524e88601cc6" + }, + "from-MNI152NLin6Asym_to-MNIInfant+4_xfm.h5": { + "url": "https://osf.io/download/6758aa1bb96fd819c41e25a4/", + "hash": "md5:1e4b927115a76b031c46e6180fc76a30" + }, + "from-MNI152NLin6Asym_to-MNIInfant+5_xfm.h5": { + "url": "https://osf.io/download/6758aa146e0cd8ca5f563b2b/", + "hash": "md5:25bfd0837a88db267762974c0a530535" + }, + "from-MNI152NLin6Asym_to-MNIInfant+6_xfm.h5": { + "url": "https://osf.io/download/6758ab50b95a2e75b11e23f0/", + "hash": "md5:7ed4732832ed6dd45dd2259d8b4454e7" + }, + "from-MNI152NLin6Asym_to-MNIInfant+7_xfm.h5": { + "url": "https://osf.io/download/6758ab5667a7782b00f73cfa/", + "hash": "md5:a1244c38b7b4825abefc5834d0398b08" + }, + "from-MNI152NLin6Asym_to-MNIInfant+8_xfm.h5": { + "url": "https://osf.io/download/rq2an/", + "hash": "md5:50d11fdac22c6589af8a7f61e4b3e41a" + }, + "from-MNI152NLin6Asym_to-MNIInfant+9_xfm.h5": { + "url": "https://osf.io/download/6758ab67eacdd8b34803d991/", + "hash": "md5:3184d91f8b3a386ca3ec913c365651d8" + }, + "from-MNIInfant+10_to-MNI152NLin6Asym_xfm.h5": { + "url": "https://osf.io/download/4xh9q/", + "hash": "md5:a1f3dd3c0ac8b05efbaf893cca6f9641" + }, + "from-MNIInfant+11_to-MNI152NLin6Asym_xfm.h5": { + "url": "https://osf.io/download/6758a5026e0cd8ca5f56380d/", + "hash": "md5:0d1aadef884574e54065d4e2cdb8e398" + }, + "from-MNIInfant+1_to-MNI152NLin6Asym_xfm.h5": { + "url": "https://osf.io/download/7ge2b/", + "hash": "md5:d5e4272140c6f582f64b7f39b31ca837" + }, + "from-MNIInfant+2_to-MNI152NLin6Asym_xfm.h5": { + "url": "https://osf.io/download/6758a3c1a678894ad71e2422/", + "hash": "md5:b03651dae4d378410c44f1c6c63dbea0" + }, + "from-MNIInfant+3_to-MNI152NLin6Asym_xfm.h5": { + "url": "https://osf.io/download/6758a3c3bc61ce5912662055/", + "hash": "md5:7cc099e26647e670c8e75ead2cfe39a6" + }, + "from-MNIInfant+4_to-MNI152NLin6Asym_xfm.h5": { + "url": "https://osf.io/download/6758a3bc040c053b58f73b47/", + "hash": "md5:e92e9150f2ad4d2730f005aa9750438d" + }, + "from-MNIInfant+5_to-MNI152NLin6Asym_xfm.h5": { + "url": "https://osf.io/download/6758a3bdea7294dbdd66161a/", + "hash": "md5:9cf6cf3fb500c229da15490c9080201a" + }, + "from-MNIInfant+6_to-MNI152NLin6Asym_xfm.h5": { + "url": "https://osf.io/download/6758a3bf040c053b58f73b4b/", + "hash": "md5:2212fdb57b85e8a0f7fa9feea5b0dd1b" + }, + "from-MNIInfant+7_to-MNI152NLin6Asym_xfm.h5": { + "url": "https://osf.io/download/6758a4f78af26d0a97661ca9/", + "hash": "md5:6913f8191201350311ff61525fae8a21" + }, + "from-MNIInfant+8_to-MNI152NLin6Asym_xfm.h5": { + "url": "https://osf.io/download/6758a500f82c189df71e256f/", + "hash": "md5:809455af8416cd61c1693b5c7eafbd13" + }, + "from-MNIInfant+9_to-MNI152NLin6Asym_xfm.h5": { + "url": "https://osf.io/download/6758a4ff040c053b58f73bd1/", + "hash": "md5:49317cbb038c399d4df7428f07d36983" + } +} diff --git a/nibabies/workflows/anatomical/registration.py b/nibabies/workflows/anatomical/registration.py index 0c3229fb..77be3371 100644 --- a/nibabies/workflows/anatomical/registration.py +++ b/nibabies/workflows/anatomical/registration.py @@ -479,11 +479,30 @@ def init_concat_registrations_wf( def _load_intermediate_xfms(intermediate, std): + import json + + import pooch + from nibabies.data import load + xfms = json.loads(load('xfm_manifest.json').read_text()) # MNIInfant:cohort-1 -> MNIInfant+1 intmed = intermediate.replace(':cohort-', '+') - int2std = load.readable(f'tpl_xfms/from-{intmed}_to-{std}_xfm.h5') - std2int = load.readable(f'tpl_xfms/from-{std}_to-{intmed}_xfm.h5') + int2std_name = f'from-{intmed}_to-{std}_xfm.h5' + int2std_meta = xfms[int2std_name] + int2std = pooch.retrieve( + url=int2std_meta['url'], + known_hash=int2std_meta['hash'], + fname=int2std_name, + ) + + std2int_name = f'from-{std}_to-{intmed}_xfm.h5' + std2int_meta = xfms[std2int_name] + std2int = pooch.retrieve( + url=std2int_meta['url'], + known_hash=std2int_meta['hash'], + fname=std2int_name, + ) + return int2std, std2int diff --git a/pyproject.toml b/pyproject.toml index 5316ab6c..cc0dbd51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ dependencies = [ "numpy >= 1.21.0", "packaging", "pandas", + "pooch", "psutil >= 5.4", "pybids >= 0.15.0", "requests", From 0199735cf10e79827a685d20064db4a413f61435 Mon Sep 17 00:00:00 2001 From: Mathias Goncalves Date: Tue, 10 Dec 2024 21:30:37 -0500 Subject: [PATCH 3/8] TST: Fix doctest --- nibabies/interfaces/conftest.py | 2 ++ nibabies/interfaces/patches.py | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/nibabies/interfaces/conftest.py b/nibabies/interfaces/conftest.py index a0decec4..a3707a73 100644 --- a/nibabies/interfaces/conftest.py +++ b/nibabies/interfaces/conftest.py @@ -33,6 +33,8 @@ def _chdir(path): 'sub-01_run-01_echo-1_bold.nii.gz', 'sub-01_run-01_echo-2_bold.nii.gz', 'sub-01_run-01_echo-3_bold.nii.gz', + 'xfm0.h5', + 'xfm1.h5', ) diff --git a/nibabies/interfaces/patches.py b/nibabies/interfaces/patches.py index ddbba8e1..e80394c1 100644 --- a/nibabies/interfaces/patches.py +++ b/nibabies/interfaces/patches.py @@ -59,18 +59,18 @@ class ConcatXFMOutputSpec(TraitedSpec): class ConcatXFM(ANTSCommand): """ - Streamed use of antsApplyTransforms to combine nonlinear xfms into a single file + Streamed use of antsApplyTransforms to combine multiple xfms into a single file Examples -------- >>> from nibabies.interfaces.patches import ConcatXFM >>> cxfm = ConcatXFM() - >>> cxfm.inputs.transforms = ['xfm1.h5', 'xfm0.h5'] - >>> cxfm.inputs.reference_image = 'sub-01_T1w.nii.gz' - >>> cxfm.cmdline - 'antsApplyTransforms --output [ concat_xfm.h5, 1 ] --transform .../xfm1.h5 \ ---transform .../xfm0.h5 --reference_image .../sub-01_T1w.nii.gz' + >>> cxfm.inputs.transforms = [testdir / 'xfm0.h5', testdir / 'xfm1.h5'] + >>> cxfm.inputs.reference_image = testdir / 'anatomical.nii' + >>> cxfm.cmdline # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE + 'antsApplyTransforms --output [ concat_xfm.h5, 1 ] --reference-image .../anatomical.nii \ +--transform .../xfm0.h5 --transform .../xfm1.h5' """ From aaa7cc935d42d4cef51b41d60c9b13656968a2e2 Mon Sep 17 00:00:00 2001 From: Mathias Goncalves Date: Tue, 10 Dec 2024 23:17:14 -0500 Subject: [PATCH 4/8] DOC: Add a docstring --- nibabies/workflows/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nibabies/workflows/base.py b/nibabies/workflows/base.py index eca233a3..3c2bf859 100644 --- a/nibabies/workflows/base.py +++ b/nibabies/workflows/base.py @@ -856,6 +856,11 @@ def init_workflow_spaces(execution_spaces: SpatialReferences, age_months: int): def init_execution_spaces(): + """Initialize the spaces to be saved. + + Either invoked by ``--output-spaces``, + or an empty :py:class:`~niworkflows.utils.spaces.SpatialReferences` + """ from niworkflows.utils.spaces import Reference, SpatialReferences spaces = config.execution.output_spaces or SpatialReferences() From 5eee41a14054739547a903ac690c7452d2aa350c Mon Sep 17 00:00:00 2001 From: Mathias Goncalves Date: Thu, 12 Dec 2024 21:00:40 -0500 Subject: [PATCH 5/8] FEAT: Make multi-step registration optional --- nibabies/cli/parser.py | 6 ++++++ nibabies/config.py | 3 +++ nibabies/workflows/anatomical/fit.py | 10 +++++++--- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/nibabies/cli/parser.py b/nibabies/cli/parser.py index e7b1b470..4b73662a 100644 --- a/nibabies/cli/parser.py +++ b/nibabies/cli/parser.py @@ -758,6 +758,12 @@ def _str_none(val): action='store_true', help='Replace low intensity voxels in CSF mask with average', ) + g_baby.add_argument( + '--multi-step-reg', + action='store_true', + help='For certain adult templates (MNI152NLin6Asym), perform two step ' + 'registrations (native -> MNIInfant -> template) and concatenate into a single xfm', + ) return parser diff --git a/nibabies/config.py b/nibabies/config.py index f3bb698f..f8c4cc83 100644 --- a/nibabies/config.py +++ b/nibabies/config.py @@ -578,6 +578,9 @@ class workflow(_Config): """Run FreeSurfer ``recon-all`` with the ``-logitudinal`` flag.""" medial_surface_nan = None """Fill medial surface with :abbr:`NaNs (not-a-number)` when sampling.""" + multi_step_reg = False + """Perform multiple registrations (native -> MNIInfant -> template) and concatenate into a + single transform""" norm_csf = False """Replace low intensity voxels in CSF mask with average.""" project_goodvoxels = False diff --git a/nibabies/workflows/anatomical/fit.py b/nibabies/workflows/anatomical/fit.py index 24bd7b04..44e40f95 100644 --- a/nibabies/workflows/anatomical/fit.py +++ b/nibabies/workflows/anatomical/fit.py @@ -889,9 +889,13 @@ def init_infant_anat_fit_wf( concat_xfms = [] found_xfms = {} intermediate = None # The intermediate space when concatenating xfms - includes cohort - intermediate_targets = { - 'MNI152NLin6Asym', - } # TODO: 'MNI152NLin2009cAsym' + intermediate_targets = ( + { + 'MNI152NLin6Asym', # TODO: 'MNI152NLin2009cAsym' + } + if config.workflow.multi_step_reg + else set() + ) for template in spaces.get_spaces(nonstandard=False, dim=(3,)): # resolution / spec will not differentiate here From 7878af82e8f9dafb2206b2c8797eab1e54c02035 Mon Sep 17 00:00:00 2001 From: Mathias Goncalves Date: Thu, 12 Dec 2024 23:45:58 -0500 Subject: [PATCH 6/8] FIX: Apply to single anat, fix select / preproc connections --- nibabies/workflows/anatomical/fit.py | 30 +++++++++++++++------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/nibabies/workflows/anatomical/fit.py b/nibabies/workflows/anatomical/fit.py index 44e40f95..230738ee 100644 --- a/nibabies/workflows/anatomical/fit.py +++ b/nibabies/workflows/anatomical/fit.py @@ -969,9 +969,9 @@ def init_infant_anat_fit_wf( if concat_xfms: LOGGER.info(f'ANAT Stage 5b: Concatenating normalization for {concat_xfms}') - # 1. Select intermediate's transforms + select_infant_mni = pe.Node( - KeySelect(fields=['template', 'anat2std_xfm', 'std2anat_xfm'], key=intermediate), + KeySelect(fields=['anat2std_xfm', 'std2anat_xfm'], key=intermediate), name='select_infant_mni', run_without_submitting=True, ) @@ -983,15 +983,15 @@ def init_infant_anat_fit_wf( ) workflow.connect([ - (concat_template_buffer, select_infant_mni, [('out', 'template')]), + (concat_template_buffer, select_infant_mni, [('out', 'keys')]), (concat_anat2std_buffer, select_infant_mni, [('out', 'anat2std_xfm')]), (concat_std2anat_buffer, select_infant_mni, [('out', 'std2anat_xfm')]), (select_infant_mni, concat_reg_wf, [ - ('template', 'inputnode.intermediate'), + ('key', 'inputnode.intermediate'), ('anat2std_xfm', 'inputnode.anat2std_xfm'), ('std2anat_xfm', 'inputnode.std2anat_xfm'), ]), - (anat_buffer, concat_reg_wf, [('anat_preproc', 'inputnode.anat_preproc')]), + (anat_preproc_buffer, concat_reg_wf, [('anat_preproc', 'inputnode.anat_preproc')]), (sourcefile_buffer, ds_concat_reg_wf, [ ('anat_source_files', 'inputnode.source_files') ]), @@ -1806,9 +1806,13 @@ def init_infant_single_anat_fit_wf( concat_xfms = [] found_xfms = {} intermediate = None # The intermediate space when concatenating xfms - includes cohort - intermediate_targets = { - 'MNI152NLin6Asym', - } # TODO: 'MNI152NLin2009cAsym' + intermediate_targets = ( + { + 'MNI152NLin6Asym', # TODO: 'MNI152NLin2009cAsym' + } + if config.workflow.multi_step_reg + else set() + ) for template in spaces.get_spaces(nonstandard=False, dim=(3,)): # resolution / spec will not differentiate here @@ -1882,13 +1886,12 @@ def init_infant_single_anat_fit_wf( if concat_xfms: LOGGER.info(f'ANAT Stage 5b: Concatenating normalization for {concat_xfms}') - # 1. Select intermediate's transforms + select_infant_mni = pe.Node( - KeySelect(fields=['template', 'anat2std_xfm', 'std2anat_xfm']), + KeySelect(fields=['anat2std_xfm', 'std2anat_xfm'], key=intermediate), name='select_infant_mni', run_without_submitting=True, ) - select_infant_mni.inputs.key = intermediate concat_reg_wf = init_concat_registrations_wf(templates=concat_xfms) ds_concat_reg_wf = init_ds_template_registration_wf( @@ -1899,15 +1902,14 @@ def init_infant_single_anat_fit_wf( workflow.connect([ (concat_template_buffer, select_infant_mni, [('out', 'keys')]), - (concat_template_buffer, select_infant_mni, [('out', 'template')]), (concat_anat2std_buffer, select_infant_mni, [('out', 'anat2std_xfm')]), (concat_std2anat_buffer, select_infant_mni, [('out', 'std2anat_xfm')]), (select_infant_mni, concat_reg_wf, [ - ('template', 'inputnode.intermediate'), + ('key', 'inputnode.intermediate'), ('anat2std_xfm', 'inputnode.anat2std_xfm'), ('std2anat_xfm', 'inputnode.std2anat_xfm'), ]), - (anat_buffer, concat_reg_wf, [('anat_preproc', 'inputnode.anat_preproc')]), + (anat_preproc_buffer, concat_reg_wf, [('anat_preproc', 'inputnode.anat_preproc')]), (sourcefile_buffer, ds_concat_reg_wf, [ ('anat_source_files', 'inputnode.source_files') ]), From 57c2fccf9da5bf6aff8c569507b32f8363d50747 Mon Sep 17 00:00:00 2001 From: Mathias Goncalves Date: Thu, 12 Dec 2024 23:57:37 -0500 Subject: [PATCH 7/8] TST: Add new parameters to workflow construction test --- nibabies/workflows/tests/test_base.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/nibabies/workflows/tests/test_base.py b/nibabies/workflows/tests/test_base.py index b603a597..7da27b86 100644 --- a/nibabies/workflows/tests/test_base.py +++ b/nibabies/workflows/tests/test_base.py @@ -137,6 +137,8 @@ def _make_params( surface_recon_method: str | None = 'auto', ignore: list[str] = None, bids_filters: dict = None, + norm_csf: bool = False, + multi_step_reg: bool = False, ): if ignore is None: ignore = [] @@ -157,6 +159,8 @@ def _make_params( surface_recon_method, ignore, bids_filters, + norm_csf, + multi_step_reg, ) @@ -178,6 +182,8 @@ def _make_params( 'surface_recon_method', 'ignore', 'bids_filters', + 'norm_csf', + 'multi_step_reg', ), [ _make_params(), @@ -211,6 +217,8 @@ def _make_params( # _make_params(freesurfer=False, bold2anat_init="header", use_bbr=False), # Regression test for gh-3154: _make_params(bids_filters={'sbref': {'suffix': 'sbref'}}), + _make_params(norm_csf=True), + _make_params(multi_step_reg=True), ], ) def test_init_nibabies_wf( @@ -233,6 +241,8 @@ def test_init_nibabies_wf( surface_recon_method: str | None, ignore: list[str], bids_filters: dict, + norm_csf: bool, + multi_step_reg: bool, ): monkeypatch.setenv('SUBJECTS_DIR', '/opt/freesurfer/subjects') with mock_config(bids_dir=bids_root): @@ -244,6 +254,8 @@ def test_init_nibabies_wf( config.execution.me_output_echos = me_output_echos config.workflow.medial_surface_nan = medial_surface_nan config.workflow.project_goodvoxels = project_goodvoxels + config.workflow.norm_csf = norm_csf + config.workflow.multi_step_reg = multi_step_reg # config.workflow.run_msmsulc = run_msmsulc config.workflow.skull_strip_anat = skull_strip_anat config.workflow.cifti_output = cifti_output From 63afae12a4b13fafa9c9d1a27767438455077992 Mon Sep 17 00:00:00 2001 From: Mathias Goncalves Date: Fri, 13 Dec 2024 00:25:03 -0500 Subject: [PATCH 8/8] MAINT: Bump requirements while at it [skip ci] --- requirements.txt | 113 +++++++++++++++++++++++++++++------------------ 1 file changed, 69 insertions(+), 44 deletions(-) diff --git a/requirements.txt b/requirements.txt index 37e5201c..8515238c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,9 +4,10 @@ # # pip-compile --extra=container --output-file=requirements.txt --strip-extras pyproject.toml # -acres==0.1.0 +acres==0.2.0 # via # nibabies (pyproject.toml) + # nipype # niworkflows # smriprep annexremote==1.6.6 @@ -26,21 +27,23 @@ backports-tarfile==1.2.0 # via jaraco-context bids-validator==1.14.7.post0 # via pybids -bidsschematools==0.11.3 +bidsschematools==1.0.0 # via bids-validator bokeh==3.5.2 # via tedana -boto3==1.35.32 +boto3==1.35.80 # via datalad -botocore==1.35.32 +botocore==1.35.80 # via # boto3 # s3transfer certifi==2024.8.30 # via requests +cffi==1.17.1 + # via cryptography chardet==5.2.0 # via datalad -charset-normalizer==3.3.2 +charset-normalizer==3.4.0 # via requests ci-info==0.3.0 # via @@ -51,13 +54,15 @@ click==8.1.7 # bidsschematools # nipype # pybids -contourpy==1.3.0 +contourpy==1.3.1 # via # bokeh # matplotlib +cryptography==44.0.0 + # via secretstorage cycler==0.12.1 # via matplotlib -datalad==1.1.3 +datalad==1.1.4 # via # datalad-next # datalad-osf @@ -76,27 +81,31 @@ fasteners==0.19 # via datalad filelock==3.16.1 # via nipype -fonttools==4.54.1 +fonttools==4.55.3 # via matplotlib formulaic==0.5.2 # via pybids -fsspec==2024.9.0 +fsspec==2024.10.0 # via universal-pathlib +greenlet==3.1.1 + # via sqlalchemy h5py==3.12.1 # via nitransforms -humanize==4.10.0 +humanize==4.11.0 # via # datalad # datalad-next idna==3.10 # via requests -imageio==2.35.1 +imageio==2.36.1 # via scikit-image importlib-metadata==8.5.0 # via keyring importlib-resources==6.4.5 - # via nireports -indexed-gzip==1.8.7 + # via + # nibabel + # nireports +indexed-gzip==1.9.4 # via smriprep interface-meta==1.3.0 # via formulaic @@ -114,6 +123,10 @@ jaraco-context==6.0.1 # keyrings-alt jaraco-functools==4.1.0 # via keyring +jeepney==0.8.0 + # via + # keyring + # secretstorage jinja2==3.1.4 # via # bokeh @@ -129,9 +142,9 @@ joblib==1.4.2 # scikit-learn jsonschema==4.23.0 # via bidsschematools -jsonschema-specifications==2023.12.1 +jsonschema-specifications==2024.10.1 # via jsonschema -keyring==25.4.1 +keyring==25.5.0 # via datalad keyrings-alt==5.0.2 # via datalad @@ -156,9 +169,9 @@ lxml==5.3.0 # svgutils mapca==0.0.5 # via tedana -markupsafe==2.1.5 +markupsafe==3.0.2 # via jinja2 -matplotlib==3.9.2 +matplotlib==3.9.3 # via # nireports # nitime @@ -177,7 +190,7 @@ more-itertools==10.5.0 # jaraco-functools msgpack==1.1.0 # via datalad -networkx==3.3 +networkx==3.4.2 # via # nipype # prov @@ -201,30 +214,30 @@ nilearn==0.10.4 # nireports # niworkflows # tedana -nipype==1.8.6 +nipype==1.9.1 # via # nibabies (pyproject.toml) # nireports # niworkflows # sdcflows # smriprep -nireports==24.0.2 +nireports==24.0.3 # via nibabies (pyproject.toml) nitime==0.11 # via nibabies (pyproject.toml) -nitransforms==24.0.2 +nitransforms==24.1.0 # via # nibabies (pyproject.toml) # niworkflows # sdcflows -niworkflows==1.11.0 +niworkflows==1.12.1 # via # nibabies (pyproject.toml) # sdcflows # smriprep num2words==0.5.13 # via pybids -numpy==1.26.4 +numpy==2.1.1 # via # bokeh # contourpy @@ -255,7 +268,7 @@ numpy==1.26.4 # transforms3d osfclient==0.0.5 # via datalad-osf -packaging==24.1 +packaging==24.2 # via # bokeh # datalad @@ -267,6 +280,7 @@ packaging==24.1 # nilearn # nipype # niworkflows + # pooch # scikit-image # smriprep pandas==2.2.2 @@ -281,21 +295,27 @@ pandas==2.2.2 # robustica # seaborn # tedana -patool==3.0.0 +patool==3.1.0 # via datalad -pillow==10.4.0 +pillow==11.0.0 # via # bokeh # imageio # matplotlib # scikit-image platformdirs==4.3.6 - # via datalad + # via + # datalad + # pooch +pooch==1.8.2 + # via nibabies (pyproject.toml) prov==2.0.1 # via nipype -psutil==6.0.0 +psutil==6.1.0 # via nibabies (pyproject.toml) -pybids==0.17.2 +puremagic==1.28 + # via nipype +pybids==0.18.1 # via # nibabies (pyproject.toml) # nireports @@ -307,9 +327,11 @@ pybtex==0.24.0 # via tedana pybtex-apa-style==1.3 # via tedana -pydot==3.0.2 +pycparser==2.22 + # via cffi +pydot==3.0.3 # via nipype -pyparsing==3.1.4 +pyparsing==3.2.0 # via # matplotlib # pydot @@ -321,7 +343,7 @@ python-dateutil==2.9.0.post0 # nipype # pandas # prov -python-gitlab==4.12.2 +python-gitlab==5.1.0 # via datalad pytz==2024.2 # via pandas @@ -348,6 +370,7 @@ requests==2.32.3 # nibabies (pyproject.toml) # nilearn # osfclient + # pooch # python-gitlab # requests-toolbelt # templateflow @@ -355,11 +378,11 @@ requests-toolbelt==1.0.0 # via python-gitlab robustica==0.1.4 # via tedana -rpds-py==0.20.0 +rpds-py==0.22.3 # via # jsonschema # referencing -s3transfer==0.10.2 +s3transfer==0.10.4 # via boto3 scikit-image==0.24.0 # via @@ -392,17 +415,19 @@ seaborn==0.13.2 # via # nireports # niworkflows +secretstorage==3.3.3 + # via keyring simplejson==3.19.3 # via nipype -six==1.16.0 +six==1.17.0 # via # isodate # osfclient # pybtex # python-dateutil -smriprep==0.16.1 +smriprep @ git+https://github.com/nipreps/smriprep.git@dev-nibabies # via nibabies (pyproject.toml) -sqlalchemy==2.0.35 +sqlalchemy==2.0.36 # via pybids svgutils==0.3.4 # via @@ -421,15 +446,15 @@ threadpoolctl==3.5.0 # via # scikit-learn # tedana -tifffile==2024.9.20 +tifffile==2024.12.12 # via scikit-image toml==0.10.2 # via # nibabies (pyproject.toml) # sdcflows -tornado==6.4.1 +tornado==6.4.2 # via bokeh -tqdm==4.66.5 +tqdm==4.67.1 # via # datalad # osfclient @@ -439,25 +464,25 @@ tqdm==4.66.5 traits==6.3.2 # via # nipype - # niworkflows # sdcflows transforms3d==0.4.2 # via niworkflows typing-extensions==4.12.2 # via # formulaic + # nibabel # sqlalchemy tzdata==2024.2 # via pandas -universal-pathlib==0.2.5 +universal-pathlib==0.2.6 # via pybids urllib3==2.2.3 # via # botocore # requests -wrapt==1.16.0 +wrapt==1.17.0 # via formulaic xyzservices==2024.9.0 # via bokeh -zipp==3.20.2 +zipp==3.21.0 # via importlib-metadata