|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- |
| 3 | +# vi: set ft=python sts=4 ts=4 sw=4 et: |
| 4 | +''' |
| 5 | +Algorithms to compute statistics on :abbr:`fMRI (functional MRI)` |
| 6 | +
|
| 7 | + Change directory to provide relative paths for doctests |
| 8 | + >>> import os |
| 9 | + >>> filepath = os.path.dirname(os.path.realpath(__file__)) |
| 10 | + >>> datadir = os.path.realpath(os.path.join(filepath, '../testing/data')) |
| 11 | + >>> os.chdir(datadir) |
| 12 | +
|
| 13 | +''' |
| 14 | +from __future__ import (print_function, division, unicode_literals, |
| 15 | + absolute_import) |
| 16 | + |
| 17 | +import numpy as np |
| 18 | +import nibabel as nb |
| 19 | + |
| 20 | +from .. import logging |
| 21 | +from ..interfaces.base import (traits, TraitedSpec, BaseInterface, |
| 22 | + BaseInterfaceInputSpec, File, InputMultiPath) |
| 23 | +IFLOG = logging.getLogger('interface') |
| 24 | + |
| 25 | +class SignalExtractionInputSpec(BaseInterfaceInputSpec): |
| 26 | + in_file = File(exists=True, mandatory=True, desc='4-D fMRI nii file') |
| 27 | + label_files = InputMultiPath(File(exists=True), mandatory=True, |
| 28 | + desc='a 3-D label image, with 0 denoting ' |
| 29 | + 'background, or a list of 3-D probability ' |
| 30 | + 'maps (one per label) or the equivalent 4D ' |
| 31 | + 'file.') |
| 32 | + class_labels = traits.List(mandatory=True, |
| 33 | + desc='Human-readable labels for each segment ' |
| 34 | + 'in the label file, in order. The length of ' |
| 35 | + 'class_labels must be equal to the number of ' |
| 36 | + 'segments (background excluded). This list ' |
| 37 | + 'corresponds to the class labels in label_file ' |
| 38 | + 'in ascending order') |
| 39 | + out_file = File('signals.tsv', usedefault=True, exists=False, |
| 40 | + mandatory=False, desc='The name of the file to output to. ' |
| 41 | + 'signals.tsv by default') |
| 42 | + incl_shared_variance = traits.Bool(True, usedefault=True, mandatory=False, desc='By default ' |
| 43 | + '(True), returns simple time series calculated from each ' |
| 44 | + 'region independently (e.g., for noise regression). If ' |
| 45 | + 'False, returns unique signals for each region, discarding ' |
| 46 | + 'shared variance (e.g., for connectivity. Only has effect ' |
| 47 | + 'with 4D probability maps.') |
| 48 | + include_global = traits.Bool(False, usedefault=True, mandatory=False, |
| 49 | + desc='If True, include an extra column ' |
| 50 | + 'labeled "global", with values calculated from the entire brain ' |
| 51 | + '(instead of just regions).') |
| 52 | + detrend = traits.Bool(False, usedefault=True, mandatory=False, |
| 53 | + desc='If True, perform detrending using nilearn.') |
| 54 | + |
| 55 | +class SignalExtractionOutputSpec(TraitedSpec): |
| 56 | + out_file = File(exists=True, desc='tsv file containing the computed ' |
| 57 | + 'signals, with as many columns as there are labels and as ' |
| 58 | + 'many rows as there are timepoints in in_file, plus a ' |
| 59 | + 'header row with values from class_labels') |
| 60 | + |
| 61 | +class SignalExtraction(BaseInterface): |
| 62 | + ''' |
| 63 | + Extracts signals over tissue classes or brain regions |
| 64 | +
|
| 65 | + >>> seinterface = SignalExtraction() |
| 66 | + >>> seinterface.inputs.in_file = 'functional.nii' |
| 67 | + >>> seinterface.inputs.label_files = 'segmentation0.nii.gz' |
| 68 | + >>> seinterface.inputs.out_file = 'means.tsv' |
| 69 | + >>> segments = ['CSF', 'gray', 'white'] |
| 70 | + >>> seinterface.inputs.class_labels = segments |
| 71 | + >>> seinterface.inputs.detrend = True |
| 72 | + >>> seinterface.inputs.include_global = True |
| 73 | + ''' |
| 74 | + input_spec = SignalExtractionInputSpec |
| 75 | + output_spec = SignalExtractionOutputSpec |
| 76 | + |
| 77 | + def _run_interface(self, runtime): |
| 78 | + maskers = self._process_inputs() |
| 79 | + |
| 80 | + signals = [] |
| 81 | + for masker in maskers: |
| 82 | + signals.append(masker.fit_transform(self.inputs.in_file)) |
| 83 | + region_signals = np.hstack(signals) |
| 84 | + |
| 85 | + output = np.vstack((self.inputs.class_labels, region_signals.astype(str))) |
| 86 | + |
| 87 | + # save output |
| 88 | + np.savetxt(self.inputs.out_file, output, fmt=b'%s', delimiter='\t') |
| 89 | + return runtime |
| 90 | + |
| 91 | + def _process_inputs(self): |
| 92 | + ''' validate and process inputs into useful form. |
| 93 | + Returns a list of nilearn maskers and the list of corresponding label names.''' |
| 94 | + import nilearn.input_data as nl |
| 95 | + import nilearn.image as nli |
| 96 | + |
| 97 | + label_data = nli.concat_imgs(self.inputs.label_files) |
| 98 | + maskers = [] |
| 99 | + |
| 100 | + # determine form of label files, choose appropriate nilearn masker |
| 101 | + if np.amax(label_data.get_data()) > 1: # 3d label file |
| 102 | + n_labels = np.amax(label_data.get_data()) |
| 103 | + maskers.append(nl.NiftiLabelsMasker(label_data)) |
| 104 | + else: # 4d labels |
| 105 | + n_labels = label_data.get_data().shape[3] |
| 106 | + if self.inputs.incl_shared_variance: # 4d labels, independent computation |
| 107 | + for img in nli.iter_img(label_data): |
| 108 | + maskers.append(nl.NiftiMapsMasker(self._4d(img.get_data(), img.affine))) |
| 109 | + else: # 4d labels, one computation fitting all |
| 110 | + maskers.append(nl.NiftiMapsMasker(label_data)) |
| 111 | + |
| 112 | + # check label list size |
| 113 | + if len(self.inputs.class_labels) != n_labels: |
| 114 | + raise ValueError('The length of class_labels {} does not ' |
| 115 | + 'match the number of regions {} found in ' |
| 116 | + 'label_files {}'.format(self.inputs.class_labels, |
| 117 | + n_labels, |
| 118 | + self.inputs.label_files)) |
| 119 | + |
| 120 | + if self.inputs.include_global: |
| 121 | + global_label_data = label_data.get_data().sum(axis=3) # sum across all regions |
| 122 | + global_label_data = np.rint(global_label_data).astype(int).clip(0, 1) # binarize |
| 123 | + global_label_data = self._4d(global_label_data, label_data.affine) |
| 124 | + global_masker = nl.NiftiLabelsMasker(global_label_data, detrend=self.inputs.detrend) |
| 125 | + maskers.insert(0, global_masker) |
| 126 | + self.inputs.class_labels.insert(0, 'global') |
| 127 | + |
| 128 | + for masker in maskers: |
| 129 | + masker.set_params(detrend=self.inputs.detrend) |
| 130 | + |
| 131 | + return maskers |
| 132 | + |
| 133 | + def _4d(self, array, affine): |
| 134 | + ''' takes a 3-dimensional numpy array and an affine, |
| 135 | + returns the equivalent 4th dimensional nifti file ''' |
| 136 | + return nb.Nifti1Image(array[:, :, :, np.newaxis], affine) |
| 137 | + |
| 138 | + def _list_outputs(self): |
| 139 | + outputs = self._outputs().get() |
| 140 | + outputs['out_file'] = self.inputs.out_file |
| 141 | + return outputs |
0 commit comments