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

Collect templates from TemplateFlow instead of package data #49

Merged
merged 12 commits into from
Aug 19, 2024
Binary file removed qsirecon/data/mni_1mm_t1w_lps.nii.gz
Binary file not shown.
Binary file removed qsirecon/data/mni_1mm_t1w_lps_brain.nii.gz
Binary file not shown.
Binary file removed qsirecon/data/mni_1mm_t1w_lps_brain_infant.nii.gz
Binary file not shown.
Binary file removed qsirecon/data/mni_1mm_t1w_lps_brainmask.nii.gz
Binary file not shown.
Binary file not shown.
Binary file removed qsirecon/data/mni_1mm_t1w_lps_infant.nii.gz
Binary file not shown.
Binary file removed qsirecon/data/mni_1mm_t2w_lps.nii.gz
Binary file not shown.
Binary file removed qsirecon/data/mni_1mm_t2w_lps_brain.nii.gz
Binary file not shown.
Binary file removed qsirecon/data/mni_1mm_t2w_lps_brain_infant.nii.gz
Binary file not shown.
Binary file removed qsirecon/data/mni_1mm_t2w_lps_infant.nii.gz
Binary file not shown.
75 changes: 55 additions & 20 deletions qsirecon/interfaces/anatomical.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,14 @@
traits,
)
from nipype.utils.filemanip import fname_presuffix
from pkg_resources import resource_filename as pkgrf

from ..utils.ingress import ukb_dirname_to_bids
from .images import to_lps

LOGGER = logging.getLogger("nipype.interface")


class QSIReconAnatomicalIngressInputSpec(BaseInterfaceInputSpec):
class QSIPrepAnatomicalIngressInputSpec(BaseInterfaceInputSpec):
recon_input_dir = traits.Directory(
exists=True, mandatory=True, help="directory containing subject results directories"
)
Expand All @@ -42,7 +41,7 @@ class QSIReconAnatomicalIngressInputSpec(BaseInterfaceInputSpec):
infant_mode = traits.Bool(mandatory=True)


class QSIReconAnatomicalIngressOutputSpec(TraitedSpec):
class QSIPrepAnatomicalIngressOutputSpec(TraitedSpec):
# sub-1_desc-aparcaseg_dseg.nii.gz
t1_aparc = File()
# sub-1_dseg.nii.gz
Expand All @@ -65,19 +64,17 @@ class QSIReconAnatomicalIngressOutputSpec(TraitedSpec):
t1_2_mni_reverse_transform = File()
# sub-1_from-T1w_to-MNI152NLin2009cAsym_mode-image_xfm.h5
t1_2_mni_forward_transform = File()
# Generic: what template was used?
template_image = File()


class QSIReconAnatomicalIngress(SimpleInterface):
"""Get only the useful files from a QSIRecon anatomical output.
class QSIPrepAnatomicalIngress(SimpleInterface):
"""Get only the useful files from a QSIPrep anatomical output.

Many of the preprocessed outputs aren't useful for reconstruction
(mainly anything that has been mapped forward into template space).
"""

input_spec = QSIReconAnatomicalIngressInputSpec
output_spec = QSIReconAnatomicalIngressOutputSpec
input_spec = QSIPrepAnatomicalIngressInputSpec
output_spec = QSIPrepAnatomicalIngressOutputSpec

def _run_interface(self, runtime):
# The path to the output from the qsirecon run
Expand Down Expand Up @@ -137,15 +134,6 @@ def _run_interface(self, runtime):
"t1_2_mni_forward_transform",
"%s/sub-%s*_from-T*w_to-MNI152NLin2009cAsym_mode-image_xfm.h5" % (anat_root, sub),
)
if not self.inputs.infant_mode:
self._results["template_image"] = pkgrf(
"qsirecon",
"data/mni_1mm_t1w_lps_brain.nii.gz",
)
else:
self._results["template_image"] = pkgrf(
"qsirecon", "data/mni_1mm_t1w_lps_brain_infant.nii.gz"
)
tsalo marked this conversation as resolved.
Show resolved Hide resolved

return runtime

Expand All @@ -163,13 +151,13 @@ def _get_if_exists(self, name, pattern, excludes=None):
self._results[name] = files[0]


class UKBAnatomicalIngressInputSpec(QSIReconAnatomicalIngressInputSpec):
class UKBAnatomicalIngressInputSpec(QSIPrepAnatomicalIngressInputSpec):
recon_input_dir = traits.Directory(
exists=True, mandatory=True, help="directory containing a single subject's results"
)


class UKBAnatomicalIngress(QSIReconAnatomicalIngress):
class UKBAnatomicalIngress(QSIPrepAnatomicalIngress):
input_spec = UKBAnatomicalIngressInputSpec

def _run_interface(self, runtime):
Expand Down Expand Up @@ -344,3 +332,50 @@ def _run_interface(self, runtime):

self._results["voxel_size"] = voxel_size
return runtime


class _GetTemplateInputSpec(BaseInterfaceInputSpec):
template_name = traits.Enum(
"MNI152NLin2009cAsym",
"MNIInfant",
mandatory=True,
)


class _GetTemplateOutputSpec(BaseInterfaceInputSpec):
template_file = File(exists=True)
mask_file = File(exists=True)


class GetTemplate(SimpleInterface):
input_spec = _GetTemplateInputSpec
output_spec = _GetTemplateOutputSpec

def _run_interface(self, runtime):
from templateflow.api import get as get_template

template_file = str(
get_template(
self.inputs.template_name,
cohort=[None, "2"],
resolution="1",
desc=None,
suffix="T1w",
extension=".nii.gz",
),
)
mask_file = str(
get_template(
self.inputs.template_name,
cohort=[None, "2"],
resolution="1",
desc="brain",
suffix="mask",
extension=".nii.gz",
),
)

self._results["template_file"] = template_file
self._results["mask_file"] = mask_file

return runtime
14 changes: 7 additions & 7 deletions qsirecon/interfaces/ingress.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
LOGGER = logging.getLogger("nipype.interface")


class QsiReconDWIIngressInputSpec(BaseInterfaceInputSpec):
class QSIPrepDWIIngressInputSpec(BaseInterfaceInputSpec):
# DWI files
dwi_file = File(exists=True)
bval_file = File(exists=True)
Expand All @@ -52,7 +52,7 @@ class QsiReconDWIIngressInputSpec(BaseInterfaceInputSpec):
atlas_names = traits.List()


class QsiReconDWIIngressOutputSpec(TraitedSpec):
class QSIPrepDWIIngressOutputSpec(TraitedSpec):
subject_id = traits.Str()
session_id = traits.Str()
space_id = traits.Str()
Expand All @@ -73,9 +73,9 @@ class QsiReconDWIIngressOutputSpec(TraitedSpec):
slice_qc_file = File(exists=True)


class QsiReconDWIIngress(SimpleInterface):
input_spec = QsiReconDWIIngressInputSpec
output_spec = QsiReconDWIIngressOutputSpec
class QSIPrepDWIIngress(SimpleInterface):
input_spec = QSIPrepDWIIngressInputSpec
output_spec = QSIPrepDWIIngressOutputSpec

def _run_interface(self, runtime):
params = get_bids_params(self.inputs.dwi_file)
Expand Down Expand Up @@ -121,7 +121,7 @@ def _get_qc_filename(self, out_root, params, desc, suffix):
return out_root + "/" + fname + "_desc-%s_dwi.%s" % (desc, suffix)


class _UKBioBankDWIIngressInputSpec(QsiReconDWIIngressInputSpec):
class _UKBioBankDWIIngressInputSpec(QSIPrepDWIIngressInputSpec):
dwi_file = File(exists=False, help="The name of what a BIDS dwi file may have been")
data_dir = traits.Directory(
exists=True, help="The UKB data directory for a subject. Must contain DTI/ and T1/"
Expand All @@ -130,7 +130,7 @@ class _UKBioBankDWIIngressInputSpec(QsiReconDWIIngressInputSpec):

class UKBioBankDWIIngress(SimpleInterface):
input_spec = _UKBioBankDWIIngressInputSpec
output_spec = QsiReconDWIIngressOutputSpec
output_spec = QSIPrepDWIIngressOutputSpec

def _run_interface(self, runtime):
runpath = Path(runtime.cwd)
Expand Down
10 changes: 5 additions & 5 deletions qsirecon/interfaces/interchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
traits,
)

from qsirecon.interfaces.anatomical import QSIReconAnatomicalIngress
from qsirecon.interfaces.ingress import QsiReconDWIIngress
from qsirecon.interfaces.anatomical import QSIPrepAnatomicalIngress
from qsirecon.interfaces.ingress import QSIPrepDWIIngress

# Anatomical (t1w/t2w) slots
FS_FILES_TO_REGISTER = [
Expand All @@ -23,9 +23,9 @@
"fs_to_qsiprep_transform_mrtrix",
]

# These come directly from QSIRecon outputs. They're aligned to the DWIs in AC-PC
# These come directly from QSIPrep outputs. They're aligned to the DWIs in AC-PC
qsiprep_highres_anatomical_ingressed_fields = (
QSIReconAnatomicalIngress.output_spec.class_editable_traits()
QSIPrepAnatomicalIngress.output_spec.class_editable_traits()
)

# The init_recon_anatomical anatomical workflow can create additional
Expand All @@ -38,7 +38,7 @@
)

# These are read directly from QSIRecon's dwi results.
qsiprep_output_names = QsiReconDWIIngress().output_spec.class_editable_traits()
qsiprep_output_names = QSIPrepDWIIngress().output_spec.class_editable_traits()

# dMRI + registered anatomical fields
recon_workflow_anatomical_input_fields = anatomical_workflow_outputs + [
Expand Down
4 changes: 2 additions & 2 deletions qsirecon/workflows/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def init_single_subject_recon_wf(subject_id):
subject_id : str
Single subject label
"""
from ..interfaces.ingress import QsiReconDWIIngress, UKBioBankDWIIngress
from ..interfaces.ingress import QSIPrepDWIIngress, UKBioBankDWIIngress
from ..interfaces.interchange import (
ReconWorkflowInputs,
anatomical_workflow_outputs,
Expand Down Expand Up @@ -152,7 +152,7 @@ def init_single_subject_recon_wf(subject_id):
# Get the preprocessed DWI and all the related preprocessed images
if config.workflow.recon_input_pipeline == "qsiprep":
dwi_ingress_nodes[dwi_file] = pe.Node(
QsiReconDWIIngress(dwi_file=dwi_file), name=wf_name + "_ingressed_dwi_data"
QSIPrepDWIIngress(dwi_file=dwi_file), name=wf_name + "_ingressed_dwi_data"
)

elif config.workflow.recon_input_pipeline == "ukb":
Expand Down
83 changes: 31 additions & 52 deletions qsirecon/workflows/recon/anatomical.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
from nipype.interfaces.base import traits
from nipype.pipeline import engine as pe
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
from niworkflows.interfaces.reportlets.registration import SpatialNormalizationRPT
from pkg_resources import resource_filename as pkgrf

from ... import config
from ...interfaces.anatomical import (
QSIReconAnatomicalIngress,
GetTemplate,
QSIPrepAnatomicalIngress,
UKBAnatomicalIngress,
VoxelSizeChooser,
)
Expand Down Expand Up @@ -272,18 +272,18 @@ def gather_qsiprep_anatomical_data(subject_id):
"has_freesurfer": False,
}
recon_input_dir = config.execution.bids_dir
# Check to see if we have a T1w preprocessed by QSIRecon
# Check to see if we have a T1w preprocessed by QSIPrep
tsalo marked this conversation as resolved.
Show resolved Hide resolved
missing_qsiprep_anats = check_qsiprep_anatomical_outputs(recon_input_dir, subject_id, "T1w")
has_qsiprep_t1w = not missing_qsiprep_anats
status["has_qsiprep_t1w"] = has_qsiprep_t1w
if missing_qsiprep_anats:
config.loggers.workflow.info(
"Missing T1w QSIRecon outputs found: %s", " ".join(missing_qsiprep_anats)
"Missing T1w QSIPrep outputs found: %s", " ".join(missing_qsiprep_anats)
)
else:
config.loggers.workflow.info("Found usable QSIRecon-preprocessed T1w image and mask.")
config.loggers.workflow.info("Found usable QSIPrep-preprocessed T1w image and mask.")
anat_ingress = pe.Node(
QSIReconAnatomicalIngress(subject_id=subject_id, recon_input_dir=recon_input_dir),
QSIPrepAnatomicalIngress(subject_id=subject_id, recon_input_dir=recon_input_dir),
name="qsiprep_anat_ingress",
)

Expand All @@ -296,7 +296,7 @@ def gather_qsiprep_anatomical_data(subject_id):

if missing_qsiprep_transforms:
config.loggers.workflow.info(
"Missing T1w QSIRecon outputs: %s", " ".join(missing_qsiprep_transforms)
"Missing T1w QSIPrep outputs: %s", " ".join(missing_qsiprep_transforms)
)

return anat_ingress, status
Expand Down Expand Up @@ -523,13 +523,32 @@ def _get_status():
"has_qsiprep_t1w_transforms": has_qsiprep_t1w_transforms,
}

# XXX: This is a temporary solution until QSIRecon supports flexible output spaces.
get_template = pe.Node(
GetTemplate(
template_name="MNI152NLin2009cAsym" if not config.workflow.infant else "MNIInfant",
),
name="get_template",
)
mask_template = pe.Node(
afni.Calc(expr="a*b", outputtype="NIFTI_GZ"),
name="mask_template",
)
reorient_to_lps = pe.Node(
afni.Resample(orientation="RAI", outputtype="NIFTI_GZ"),
name="reorient_to_lps",
)

reference_grid_wf = init_output_grid_wf()
workflow.connect([
(inputnode, reference_grid_wf, [
('template_image', 'inputnode.template_image'),
('dwi_ref', 'inputnode.input_image')]),
(reference_grid_wf, buffernode, [
('outputnode.grid_image', 'resampling_template')])
(get_template, mask_template, [
('template_file', 'in_file_a'),
('mask_file', 'in_file_b'),
]),
(mask_template, reorient_to_lps, [('out_file', 'in_file')]),
(inputnode, reference_grid_wf, [('dwi_ref', 'inputnode.input_image')]),
(reorient_to_lps, reference_grid_wf, [('out_file', 'inputnode.template_image')]),
(reference_grid_wf, buffernode, [('outputnode.grid_image', 'resampling_template')]),
]) # fmt:skip

# Missing Freesurfer AND QSIRecon T1ws, or the user wants a DWI-based mask
Expand Down Expand Up @@ -818,46 +837,6 @@ def _get_resampled(atlas_configs, atlas_name, to_retrieve):
return atlas_configs[atlas_name][to_retrieve]


def get_t1w_registration_node(infant_mode, sloppy, omp_nthreads):
mattcieslak marked this conversation as resolved.
Show resolved Hide resolved

# Gets an ants interface for t1w-based normalization
if sloppy:
config.loggers.workflow.info("Using QuickSyN")
# Requires a warp file: make an inaccurate one
settings = pkgrf("qsirecon", "data/quick_syn.json")
t1_2_mni = pe.Node(
SpatialNormalizationRPT(
float=True,
generate_report=True,
settings=[settings],
),
name="t1_2_mni",
n_procs=omp_nthreads,
mem_gb=2,
)
else:
t1_2_mni = pe.Node(
SpatialNormalizationRPT(
float=True,
generate_report=True,
flavor="precise",
),
name="t1_2_mni",
n_procs=omp_nthreads,
mem_gb=2,
)
# Get the template image
if not infant_mode:
ref_img_brain = pkgrf("qsirecon", "data/mni_1mm_t1w_lps_brain.nii.gz")
else:
ref_img_brain = pkgrf("qsirecon", "data/mni_1mm_t1w_lps_brain_infant.nii.gz")

t1_2_mni.inputs.template = "MNI152NLin2009cAsym"
t1_2_mni.inputs.reference_image = ref_img_brain
t1_2_mni.inputs.orientation = "LPS"
return t1_2_mni


def init_output_grid_wf() -> Workflow:
"""Generate a non-oblique, uniform voxel-size grid around a brain."""
workflow = Workflow(name="output_grid_wf")
Expand Down
4 changes: 2 additions & 2 deletions qsirecon/workflows/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from .. import config
from ..interfaces import DerivativesDataSink
from ..interfaces.ingress import QsiReconDWIIngress
from ..interfaces.ingress import QSIPrepDWIIngress
from ..interfaces.interchange import qsiprep_output_names, recon_workflow_input_fields
from ..interfaces.reports import InteractiveReport

Expand Down Expand Up @@ -86,7 +86,7 @@ def init_single_subject_json_report_wf(subject_id, name):
niu.IdentityInterface(fields=recon_workflow_input_fields), name="inputnode"
)
qsirecon_preprocessed_dwi_data = pe.Node(
QsiReconDWIIngress(), name="qsirecon_preprocessed_dwi_data"
QSIPrepDWIIngress(), name="qsirecon_preprocessed_dwi_data"
)

# For doctests
Expand Down