Skip to content

Commit 83056b0

Browse files
authored
Merge pull request #252 from mgxd/enh/t2star-output
ENH: Output T2star maps for multiecho data
2 parents 691ee6f + 153d59c commit 83056b0

File tree

7 files changed

+456
-49
lines changed

7 files changed

+456
-49
lines changed

.circleci/bcp_full_outputs.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ sub-01/ses-1mo/func/sub-01_ses-1mo_task-rest_acq-PA_run-001_desc-confounds_times
5454
sub-01/ses-1mo/func/sub-01_ses-1mo_task-rest_acq-PA_run-001_desc-confounds_timeseries.tsv
5555
sub-01/ses-1mo/func/sub-01_ses-1mo_task-rest_acq-PA_run-001_desc-preproc_bold.json
5656
sub-01/ses-1mo/func/sub-01_ses-1mo_task-rest_acq-PA_run-001_desc-preproc_bold.nii.gz
57+
sub-01/ses-1mo/func/sub-01_ses-1mo_task-rest_acq-PA_run-001_from-scanner_to-boldref_mode-image_xfm.txt
5758
sub-01/ses-1mo/func/sub-01_ses-1mo_task-rest_acq-PA_run-001_from-scanner_to-T1w_mode-image_xfm.txt
5859
sub-01/ses-1mo/func/sub-01_ses-1mo_task-rest_acq-PA_run-001_from-T1w_to-scanner_mode-image_xfm.txt
5960
sub-01/ses-1mo/func/sub-01_ses-1mo_task-rest_acq-PA_run-001_space-MNIInfant_cohort-1_boldref.nii.gz

nibabies/interfaces/maths.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""A module for interfaces """
2+
import os
3+
4+
import numpy as np
5+
from nipype.interfaces.base import File, SimpleInterface, TraitedSpec, traits
6+
from nipype.utils.filemanip import fname_presuffix
7+
8+
9+
class ClipInputSpec(TraitedSpec):
10+
in_file = File(exists=True, mandatory=True, desc="Input imaging file")
11+
out_file = File(desc="Output file name")
12+
minimum = traits.Float(
13+
-np.inf, usedefault=True, desc="Values under minimum are set to minimum"
14+
)
15+
maximum = traits.Float(np.inf, usedefault=True, desc="Values over maximum are set to maximum")
16+
17+
18+
class ClipOutputSpec(TraitedSpec):
19+
out_file = File(desc="Output file name")
20+
21+
22+
class Clip(SimpleInterface):
23+
"""Simple clipping interface that clips values to specified minimum/maximum
24+
If no values are outside the bounds, nothing is done and the in_file is passed
25+
as the out_file without copying.
26+
"""
27+
28+
input_spec = ClipInputSpec
29+
output_spec = ClipOutputSpec
30+
31+
def _run_interface(self, runtime):
32+
import nibabel as nb
33+
34+
img = nb.load(self.inputs.in_file)
35+
data = img.get_fdata()
36+
37+
out_file = self.inputs.out_file
38+
if out_file:
39+
out_file = os.path.join(runtime.cwd, out_file)
40+
41+
if np.any((data < self.inputs.minimum) | (data > self.inputs.maximum)):
42+
if not out_file:
43+
out_file = fname_presuffix(
44+
self.inputs.in_file, suffix="_clipped", newpath=runtime.cwd
45+
)
46+
np.clip(data, self.inputs.minimum, self.inputs.maximum, out=data)
47+
img.__class__(data, img.affine, img.header).to_filename(out_file)
48+
elif not out_file:
49+
out_file = self.inputs.in_file
50+
51+
self._results["out_file"] = out_file
52+
return runtime
53+
54+
55+
class Label2MaskInputSpec(TraitedSpec):
56+
in_file = File(exists=True, mandatory=True, desc="Input label file")
57+
label_val = traits.Int(mandatory=True, dec="Label value to create mask from")
58+
59+
60+
class Label2MaskOutputSpec(TraitedSpec):
61+
out_file = File(desc="Output file name")
62+
63+
64+
class Label2Mask(SimpleInterface):
65+
"""Create mask file for a label from a multi-label segmentation"""
66+
67+
input_spec = Label2MaskInputSpec
68+
output_spec = Label2MaskOutputSpec
69+
70+
def _run_interface(self, runtime):
71+
import nibabel as nb
72+
73+
img = nb.load(self.inputs.in_file)
74+
75+
mask = np.uint16(img.dataobj) == self.inputs.label_val
76+
out_img = img.__class__(mask, img.affine, img.header)
77+
out_img.set_data_dtype(np.uint8)
78+
79+
out_file = fname_presuffix(self.inputs.in_file, suffix="_mask", newpath=runtime.cwd)
80+
81+
out_img.to_filename(out_file)
82+
83+
self._results["out_file"] = out_file
84+
return runtime

nibabies/interfaces/reports.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
isdefined,
2020
traits,
2121
)
22+
from niworkflows.interfaces.reportlets import base as nrb
2223
from smriprep.interfaces.freesurfer import ReconAll
2324

2425
LOGGER = logging.getLogger("nipype.interface")
@@ -296,6 +297,49 @@ def _generate_segment(self):
296297
)
297298

298299

300+
class LabeledHistogramInputSpec(nrb._SVGReportCapableInputSpec):
301+
in_file = traits.File(exists=True, mandatory=True, desc="Image containing values to plot")
302+
label_file = traits.File(
303+
exists=True,
304+
desc="Mask or label image where non-zero values will be used to extract data from in_file",
305+
)
306+
mapping = traits.Dict(desc="Map integer label values onto names of voxels")
307+
xlabel = traits.Str("voxels", usedefault=True, desc="Description of values plotted")
308+
309+
310+
class LabeledHistogram(nrb.ReportingInterface):
311+
input_spec = LabeledHistogramInputSpec
312+
313+
def _generate_report(self):
314+
import nibabel as nb
315+
import numpy as np
316+
import seaborn as sns
317+
from matplotlib import pyplot as plt
318+
from nilearn.image import resample_to_img
319+
320+
report_file = self._out_report
321+
img = nb.load(self.inputs.in_file)
322+
data = img.get_fdata(dtype=np.float32)
323+
324+
if self.inputs.label_file:
325+
label_img = nb.load(self.inputs.label_file)
326+
if label_img.shape != img.shape[:3] or not np.allclose(label_img.affine, img.affine):
327+
label_img = resample_to_img(label_img, img, interpolation="nearest")
328+
labels = np.uint16(label_img.dataobj)
329+
else:
330+
labels = np.uint8(data > 0)
331+
332+
uniq_labels = np.unique(labels[labels > 0])
333+
label_map = self.inputs.mapping or {label: label for label in uniq_labels}
334+
335+
rois = {label_map.get(label, label): data[labels == label] for label in label_map}
336+
with sns.axes_style('whitegrid'):
337+
fig = sns.histplot(rois, bins=50)
338+
fig.set_xlabel(self.inputs.xlabel)
339+
plt.savefig(report_file)
340+
plt.close()
341+
342+
299343
def get_world_pedir(ornt, pe_direction):
300344
"""Return world direction of phase encoding"""
301345
# TODO: Move to niworkflows

nibabies/workflows/bold/base.py

Lines changed: 75 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
init_bold_surf_wf,
7070
)
7171
from .stc import init_bold_stc_wf
72-
from .t2s import init_bold_t2s_wf
72+
from .t2s import init_bold_t2s_wf, init_t2s_reporting_wf
7373

7474

7575
def init_func_preproc_wf(bold_file, has_fieldmap=False, existing_derivatives=None):
@@ -346,10 +346,9 @@ def init_func_preproc_wf(bold_file, has_fieldmap=False, existing_derivatives=Non
346346
outputnode = pe.Node(
347347
niu.IdentityInterface(
348348
fields=[
349+
"anat2bold_xfm",
349350
"bold_anat",
350351
"bold_anat_ref",
351-
"bold2anat_xfm",
352-
"anat2bold_xfm",
353352
"bold_mask_anat",
354353
"bold_aseg_anat",
355354
"bold_aparc_anat",
@@ -363,6 +362,7 @@ def init_func_preproc_wf(bold_file, has_fieldmap=False, existing_derivatives=Non
363362
"bold_mask_native",
364363
"bold_echos_native",
365364
"bold_cifti",
365+
"bold2anat_xfm",
366366
"cifti_variant",
367367
"cifti_metadata",
368368
"cifti_density",
@@ -372,6 +372,10 @@ def init_func_preproc_wf(bold_file, has_fieldmap=False, existing_derivatives=Non
372372
"melodic_mix",
373373
"nonaggr_denoised_file",
374374
"confounds_metadata",
375+
"t2star_bold",
376+
"t2star_t1",
377+
"t2star_std",
378+
"hmc_xforms",
375379
]
376380
),
377381
name="outputnode",
@@ -433,6 +437,7 @@ def init_func_preproc_wf(bold_file, has_fieldmap=False, existing_derivatives=Non
433437
('bold_anat_ref', 'inputnode.bold_t1_ref'),
434438
('bold2anat_xfm', 'inputnode.bold2anat_xfm'),
435439
('anat2bold_xfm', 'inputnode.anat2bold_xfm'),
440+
('hmc_xforms', 'inputnode.hmc_xforms'),
436441
('bold_aseg_anat', 'inputnode.bold_aseg_t1'),
437442
('bold_aparc_anat', 'inputnode.bold_aparc_t1'),
438443
('bold_mask_anat', 'inputnode.bold_mask_t1'),
@@ -449,6 +454,9 @@ def init_func_preproc_wf(bold_file, has_fieldmap=False, existing_derivatives=Non
449454
('cifti_variant', 'inputnode.cifti_variant'),
450455
('cifti_metadata', 'inputnode.cifti_metadata'),
451456
('cifti_density', 'inputnode.cifti_density'),
457+
('t2star_bold', 'inputnode.t2star_bold'),
458+
('t2star_t1', 'inputnode.t2star_t1'),
459+
('t2star_std', 'inputnode.t2star_std'),
452460
('confounds_metadata', 'inputnode.confounds_metadata'),
453461
('acompcor_masks', 'inputnode.acompcor_masks'),
454462
('tcompcor_mask', 'inputnode.tcompcor_mask'),
@@ -547,8 +555,31 @@ def init_func_preproc_wf(bold_file, has_fieldmap=False, existing_derivatives=Non
547555
name="bold_t2smap_wf",
548556
)
549557

558+
t2s_reporting_wf = init_t2s_reporting_wf()
559+
560+
ds_report_t2scomp = pe.Node(
561+
DerivativesDataSink(
562+
desc="t2scomp",
563+
datatype="figures",
564+
dismiss_entities=("echo",),
565+
),
566+
name="ds_report_t2scomp",
567+
run_without_submitting=True,
568+
)
569+
570+
ds_report_t2star_hist = pe.Node(
571+
DerivativesDataSink(
572+
desc="t2starhist",
573+
datatype="figures",
574+
dismiss_entities=("echo",),
575+
),
576+
name="ds_report_t2star_hist",
577+
run_without_submitting=True,
578+
)
579+
550580
bold_final = pe.Node(
551-
niu.IdentityInterface(fields=["bold", "boldref", "mask", "bold_echos"]), name="bold_final"
581+
niu.IdentityInterface(fields=["bold", "boldref", "mask", "bold_echos", "t2star"]),
582+
name="bold_final",
552583
)
553584

554585
# Mask input BOLD reference image
@@ -580,6 +611,9 @@ def init_func_preproc_wf(bold_file, has_fieldmap=False, existing_derivatives=Non
580611
('bold_ref', 'inputnode.raw_ref_image')]),
581612
(validate_bolds, bold_hmc_wf, [
582613
(("out_file", pop_file), 'inputnode.bold_file')]),
614+
(bold_hmc_wf, outputnode, [
615+
("outputnode.xforms", "hmc_xforms"),
616+
]),
583617
# Native-space BOLD files
584618
(final_boldref_wf, final_boldref_mask, [('outputnode.epi_ref_file', 'in_file')]),
585619
(final_boldref_wf, bold_final, [('outputnode.epi_ref_file', 'boldref')]),
@@ -589,6 +623,7 @@ def init_func_preproc_wf(bold_file, has_fieldmap=False, existing_derivatives=Non
589623
('boldref', 'bold_native_ref'),
590624
('mask', 'bold_mask_native'),
591625
('bold_echos', 'bold_echos_native'),
626+
('t2star', 't2star_bold'),
592627
]),
593628
# EPI-T1 registration workflow
594629
(inputnode, bold_reg_wf, [
@@ -668,15 +703,22 @@ def init_func_preproc_wf(bold_file, has_fieldmap=False, existing_derivatives=Non
668703
workflow.connect([
669704
# update name source for optimal combination
670705
(inputnode, func_derivatives_wf, [
671-
(('bold_file', combine_meepi_source), 'inputnode.source_file')]),
672-
(join_echos, bold_t2s_wf, [
673-
('bold_files', 'inputnode.bold_file')]),
674-
(bold_t2s_wf, split_opt_comb, [
675-
('outputnode.bold', 'in_file')]),
676-
(split_opt_comb, bold_t1_trans_wf, [
677-
("out_files", "inputnode.bold_split")]),
706+
(("bold_file", combine_meepi_source), "inputnode.source_file"),
707+
]),
708+
(join_echos, bold_t2s_wf, [("bold_files", "inputnode.bold_file")]),
678709
(join_echos, bold_final, [("bold_files", "bold_echos")]),
679-
(bold_t2s_wf, bold_final, [("outputnode.bold", "bold")]),
710+
(bold_t2s_wf, split_opt_comb, [("outputnode.bold", "in_file")]),
711+
(split_opt_comb, bold_t1_trans_wf, [("out_files", "inputnode.bold_split")]),
712+
(bold_t2s_wf, bold_final, [("outputnode.bold", "bold"),
713+
("outputnode.t2star_map", "t2star")]),
714+
(inputnode, t2s_reporting_wf, [("anat_dseg", "inputnode.label_file")]),
715+
(bold_reg_wf, t2s_reporting_wf, [
716+
("outputnode.itk_t1_to_bold", "inputnode.label_bold_xform")
717+
]),
718+
(bold_final, t2s_reporting_wf, [("t2star", "inputnode.t2star_file"),
719+
("boldref", "inputnode.boldref")]),
720+
(t2s_reporting_wf, ds_report_t2scomp, [('outputnode.t2s_comp_report', 'in_file')]),
721+
(t2s_reporting_wf, ds_report_t2star_hist, [("outputnode.t2star_hist", "in_file")]),
680722
])
681723
# fmt:on
682724

@@ -704,6 +746,23 @@ def init_func_preproc_wf(bold_file, has_fieldmap=False, existing_derivatives=Non
704746
])
705747
# fmt:on
706748

749+
if multiecho:
750+
t2star_to_t1w = pe.Node(
751+
ApplyTransforms(interpolation="LanczosWindowedSinc", float=True),
752+
name="t2star_to_t1w",
753+
mem_gb=0.1,
754+
)
755+
# fmt:off
756+
workflow.connect([
757+
(bold_reg_wf, t2star_to_t1w, [("outputnode.itk_bold_to_t1", "transforms")]),
758+
(bold_t1_trans_wf, t2star_to_t1w, [
759+
("outputnode.bold_mask_t1", "reference_image")
760+
]),
761+
(bold_final, t2star_to_t1w, [("t2star", "input_image")]),
762+
(t2star_to_t1w, outputnode, [("output_image", "t2star_t1")]),
763+
])
764+
# fmt:on
765+
707766
if spaces.get_spaces(nonstandard=False, dim=(3,)):
708767
# Apply transforms in 1 shot
709768
# Only use uncompressed output if AROMA is to be run
@@ -712,6 +771,7 @@ def init_func_preproc_wf(bold_file, has_fieldmap=False, existing_derivatives=Non
712771
mem_gb=mem_gb["resampled"],
713772
omp_nthreads=omp_nthreads,
714773
spaces=spaces,
774+
multiecho=multiecho,
715775
name="bold_std_trans_wf",
716776
use_compression=not config.execution.low_mem,
717777
)
@@ -727,6 +787,7 @@ def init_func_preproc_wf(bold_file, has_fieldmap=False, existing_derivatives=Non
727787
('anat_aparc', 'inputnode.bold_aparc')]),
728788
(bold_final, bold_std_trans_wf, [
729789
('mask', 'inputnode.bold_mask'),
790+
('t2star', 'inputnode.t2star'),
730791
]),
731792
(bold_reg_wf, bold_std_trans_wf, [
732793
('outputnode.itk_bold_to_t1', 'inputnode.itk_bold_to_t1')]),
@@ -761,8 +822,8 @@ def init_func_preproc_wf(bold_file, has_fieldmap=False, existing_derivatives=Non
761822
else:
762823
# fmt:off
763824
workflow.connect([
764-
(split_opt_comb, bold_std_trans_wf, [
765-
('out_files', 'inputnode.bold_split')])
825+
(split_opt_comb, bold_std_trans_wf, [('out_files', 'inputnode.bold_split')]),
826+
(bold_std_trans_wf, outputnode, [("outputnode.t2star_std", "t2star_std")]),
766827
])
767828
# fmt:on
768829

0 commit comments

Comments
 (0)