diff --git a/.circleci/config.yml b/.circleci/config.yml index 6400602c..de31f1e0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -251,6 +251,24 @@ jobs: key: THP002-anat-v00-{{ .Branch }}-{{ .Revision }}-{{ epoch }} paths: - /tmp/THP002/work + - run: + name: Run full diffusion workflow on THP002 + no_output_timeout: 2h + command: | + mkdir -p /tmp/THP002/work /tmp/THP002/derivatives + sudo setfacl -d -m group:$(id -gn):rwx /tmp/THP002/derivatives && \ + sudo setfacl -m group:$(id -gn):rwx /tmp/THP002/derivatives + sudo setfacl -d -m group:$(id -gn):rwx /tmp/THP002/work && \ + sudo setfacl -m group:$(id -gn):rwx /tmp/THP002/work + docker run -e FS_LICENSE=$FS_LICENSE --rm \ + -v /tmp/data/THP002:/data \ + -v /tmp/THP002/derivatives:/out \ + -v /tmp/fslicense/license.txt:/tmp/fslicense/license.txt:ro \ + -v /tmp/config/nipype.cfg:/home/dmriprep/.nipype/nipype.cfg \ + -v /tmp/THP002/work:/work \ + --user $(id -u):$(id -g) \ + nipreps/dmriprep:latest /data /out participant -vv --sloppy \ + --notrack --skip-bids-validation -w /work --omp-nthreads 2 --nprocs 2 - store_artifacts: path: /tmp/THP002/derivatives/dmriprep - run: diff --git a/dmriprep/config/__init__.py b/dmriprep/config/__init__.py index 8f7e8bdc..01273ce1 100644 --- a/dmriprep/config/__init__.py +++ b/dmriprep/config/__init__.py @@ -2,4 +2,5 @@ # vi: set ft=python sts=4 ts=4 sw=4 et: """Settings.""" +DEFAULT_MEMORY_MIN_GB = 0.01 NONSTANDARD_REFERENCES = ['anat', 'T1w', 'dwi', 'fsnative'] diff --git a/dmriprep/config/reports-spec.yml b/dmriprep/config/reports-spec.yml index 176a0b4d..cc0cc62c 100644 --- a/dmriprep/config/reports-spec.yml +++ b/dmriprep/config/reports-spec.yml @@ -26,6 +26,16 @@ sections: caption: Surfaces (white and pial) reconstructed with FreeSurfer (recon-all) overlaid on the participant's T1w template. subtitle: Surface reconstruction +- name: Diffusion + ordering: session,acquisition,run + reportlets: + - bids: {datatype: dwi, desc: validation, suffix: dwi} + - bids: {datatype: dwi, desc: brain, suffix: mask} + caption: Average b=0 that serves for reference in early preprocessing steps. + descriptions: The reference b=0 is obtained as the voxel-wise median across + all b=0 found in the dataset, after accounting for signal drift. + The red contour shows the brain mask calculated using this reference b=0. + subtitle: Reference b=0 and brain mask - name: About reportlets: - bids: {datatype: anat, desc: about, suffix: T1w} diff --git a/dmriprep/data/tests/dwi_mask.nii.gz b/dmriprep/data/tests/dwi_mask.nii.gz new file mode 100644 index 00000000..e69de29b diff --git a/dmriprep/interfaces/images.py b/dmriprep/interfaces/images.py new file mode 100644 index 00000000..a59fedfb --- /dev/null +++ b/dmriprep/interfaces/images.py @@ -0,0 +1,145 @@ +"""Image tools interfaces.""" +import numpy as np +import nibabel as nb +from nipype.utils.filemanip import fname_presuffix +from nipype import logging +from nipype.interfaces.base import ( + traits, TraitedSpec, BaseInterfaceInputSpec, SimpleInterface, File +) + +LOGGER = logging.getLogger('nipype.interface') + + +class _ExtractB0InputSpec(BaseInterfaceInputSpec): + in_file = File(exists=True, mandatory=True, desc='dwi file') + b0_ixs = traits.List(traits.Int, mandatory=True, + desc='Index of b0s') + + +class _ExtractB0OutputSpec(TraitedSpec): + out_file = File(exists=True, desc='b0 file') + + +class ExtractB0(SimpleInterface): + """ + Extract all b=0 volumes from a dwi series. + + Example + ------- + >>> os.chdir(tmpdir) + >>> extract_b0 = ExtractB0() + >>> extract_b0.inputs.in_file = str(data_dir / 'dwi.nii.gz') + >>> extract_b0.inputs.b0_ixs = [0, 1, 2] + >>> res = extract_b0.run() # doctest: +SKIP + + """ + + input_spec = _ExtractB0InputSpec + output_spec = _ExtractB0OutputSpec + + def _run_interface(self, runtime): + self._results['out_file'] = extract_b0( + self.inputs.in_file, + self.inputs.b0_ixs, + newpath=runtime.cwd) + return runtime + + +def extract_b0(in_file, b0_ixs, newpath=None): + """Extract the *b0* volumes from a DWI dataset.""" + out_file = fname_presuffix( + in_file, suffix='_b0', newpath=newpath) + + img = nb.load(in_file) + data = img.get_fdata(dtype='float32') + + b0 = data[..., b0_ixs] + + hdr = img.header.copy() + hdr.set_data_shape(b0.shape) + hdr.set_xyzt_units('mm') + hdr.set_data_dtype(np.float32) + nb.Nifti1Image(b0, img.affine, hdr).to_filename(out_file) + return out_file + + +class _RescaleB0InputSpec(BaseInterfaceInputSpec): + in_file = File(exists=True, mandatory=True, desc='b0s file') + mask_file = File(exists=True, mandatory=True, desc='mask file') + + +class _RescaleB0OutputSpec(TraitedSpec): + out_ref = File(exists=True, desc='One average b0 file') + out_b0s = File(exists=True, desc='series of rescaled b0 volumes') + + +class RescaleB0(SimpleInterface): + """ + Rescale the b0 volumes to deal with average signal decay over time. + + Example + ------- + >>> os.chdir(tmpdir) + >>> rescale_b0 = RescaleB0() + >>> rescale_b0.inputs.in_file = str(data_dir / 'dwi.nii.gz') + >>> rescale_b0.inputs.mask_file = str(data_dir / 'dwi_mask.nii.gz') + >>> res = rescale_b0.run() # doctest: +SKIP + + """ + + input_spec = _RescaleB0InputSpec + output_spec = _RescaleB0OutputSpec + + def _run_interface(self, runtime): + self._results['out_b0s'] = rescale_b0( + self.inputs.in_file, + self.inputs.mask_file, + newpath=runtime.cwd + ) + self._results['out_ref'] = median( + self._results['out_b0s'], + newpath=runtime.cwd + ) + return runtime + + +def rescale_b0(in_file, mask_file, newpath=None): + """Rescale the input volumes using the median signal intensity.""" + out_file = fname_presuffix( + in_file, suffix='_rescaled_b0', newpath=newpath) + + img = nb.load(in_file) + if img.dataobj.ndim == 3: + return in_file + + data = img.get_fdata(dtype='float32') + mask_img = nb.load(mask_file) + mask_data = mask_img.get_fdata(dtype='float32') + + median_signal = np.median(data[mask_data > 0, ...], axis=0) + rescaled_data = 1000 * data / median_signal + hdr = img.header.copy() + nb.Nifti1Image(rescaled_data, img.affine, hdr).to_filename(out_file) + return out_file + + +def median(in_file, newpath=None): + """Average a 4D dataset across the last dimension using median.""" + out_file = fname_presuffix( + in_file, suffix='_b0ref', newpath=newpath) + + img = nb.load(in_file) + if img.dataobj.ndim == 3: + return in_file + if img.shape[-1] == 1: + nb.squeeze_image(img).to_filename(out_file) + return out_file + + median_data = np.median(img.get_fdata(dtype='float32'), + axis=-1) + + hdr = img.header.copy() + hdr.set_xyzt_units('mm') + hdr.set_data_dtype(np.float32) + nb.Nifti1Image(median_data, img.affine, hdr).to_filename(out_file) + return out_file diff --git a/dmriprep/interfaces/reports.py b/dmriprep/interfaces/reports.py index c61b64bb..0e14c2e0 100644 --- a/dmriprep/interfaces/reports.py +++ b/dmriprep/interfaces/reports.py @@ -23,13 +23,6 @@ \t """ -DWI_TEMPLATE = """\t\t

Summary

-\t\t -""" - ABOUT_TEMPLATE = """\t