|
| 1 | +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- |
| 2 | +# vi: set ft=python sts=4 ts=4 sw=4 et: |
| 3 | +"""Interfaces to generate reportlets.""" |
| 4 | + |
| 5 | +import os |
| 6 | +import time |
| 7 | + |
| 8 | +from collections import Counter |
| 9 | +from nipype.interfaces.base import ( |
| 10 | + traits, TraitedSpec, BaseInterfaceInputSpec, |
| 11 | + File, Directory, InputMultiObject, Str, isdefined, |
| 12 | + SimpleInterface) |
| 13 | +from nipype.interfaces import freesurfer as fs |
| 14 | +from niworkflows.utils.bids import BIDS_NAME |
| 15 | + |
| 16 | + |
| 17 | +SUBJECT_TEMPLATE = """\ |
| 18 | +\t<ul class="elem-desc"> |
| 19 | +\t\t<li>Subject ID: {subject_id}</li> |
| 20 | +\t\t<li>Structural images: {n_t1s:d} T1-weighted {t2w}</li> |
| 21 | +\t\t<li>Diffusion Weighted Images: {n_dwi:d}</li> |
| 22 | +{tasks} |
| 23 | +\t\t<li>Standard output spaces: {std_spaces}</li> |
| 24 | +\t\t<li>Non-standard output spaces: {nstd_spaces}</li> |
| 25 | +\t\t<li>FreeSurfer reconstruction: {freesurfer_status}</li> |
| 26 | +\t</ul> |
| 27 | +""" |
| 28 | + |
| 29 | +DWI_TEMPLATE = """\t\t<h3 class="elem-title">Summary</h3> |
| 30 | +\t\t<ul class="elem-desc"> |
| 31 | +\t\t\t<li>Susceptibility distortion correction: {sdc}</li> |
| 32 | +\t\t\t<li>Registration: {registration}</li> |
| 33 | +\t\t</ul> |
| 34 | +""" |
| 35 | + |
| 36 | +ABOUT_TEMPLATE = """\t<ul> |
| 37 | +\t\t<li>dMRIPrep version: {version}</li> |
| 38 | +\t\t<li>dMRIPrep command: <code>{command}</code></li> |
| 39 | +\t\t<li>Date preprocessed: {date}</li> |
| 40 | +\t</ul> |
| 41 | +</div> |
| 42 | +""" |
| 43 | + |
| 44 | + |
| 45 | +class SummaryOutputSpec(TraitedSpec): |
| 46 | + out_report = File(exists=True, desc='HTML segment containing summary') |
| 47 | + |
| 48 | + |
| 49 | +class SummaryInterface(SimpleInterface): |
| 50 | + output_spec = SummaryOutputSpec |
| 51 | + |
| 52 | + def _run_interface(self, runtime): |
| 53 | + segment = self._generate_segment() |
| 54 | + fname = os.path.join(runtime.cwd, 'report.html') |
| 55 | + with open(fname, 'w') as fobj: |
| 56 | + fobj.write(segment) |
| 57 | + self._results['out_report'] = fname |
| 58 | + return runtime |
| 59 | + |
| 60 | + def _generate_segment(self): |
| 61 | + raise NotImplementedError |
| 62 | + |
| 63 | + |
| 64 | +class SubjectSummaryInputSpec(BaseInterfaceInputSpec): |
| 65 | + t1w = InputMultiObject(File(exists=True), desc='T1w structural images') |
| 66 | + t2w = InputMultiObject(File(exists=True), desc='T2w structural images') |
| 67 | + subjects_dir = Directory(desc='FreeSurfer subjects directory') |
| 68 | + subject_id = Str(desc='Subject ID') |
| 69 | + dwi = InputMultiObject(traits.Either( |
| 70 | + File(exists=True), traits.List(File(exists=True))), |
| 71 | + desc='DWI files') |
| 72 | + std_spaces = traits.List(Str, desc='list of standard spaces') |
| 73 | + nstd_spaces = traits.List(Str, desc='list of non-standard spaces') |
| 74 | + |
| 75 | + |
| 76 | +class SubjectSummaryOutputSpec(SummaryOutputSpec): |
| 77 | + # This exists to ensure that the summary is run prior to the first ReconAll |
| 78 | + # call, allowing a determination whether there is a pre-existing directory |
| 79 | + subject_id = Str(desc='FreeSurfer subject ID') |
| 80 | + |
| 81 | + |
| 82 | +class SubjectSummary(SummaryInterface): |
| 83 | + input_spec = SubjectSummaryInputSpec |
| 84 | + output_spec = SubjectSummaryOutputSpec |
| 85 | + |
| 86 | + def _run_interface(self, runtime): |
| 87 | + if isdefined(self.inputs.subject_id): |
| 88 | + self._results['subject_id'] = self.inputs.subject_id |
| 89 | + return super(SubjectSummary, self)._run_interface(runtime) |
| 90 | + |
| 91 | + def _generate_segment(self): |
| 92 | + if not isdefined(self.inputs.subjects_dir): |
| 93 | + freesurfer_status = 'Not run' |
| 94 | + else: |
| 95 | + recon = fs.ReconAll(subjects_dir=self.inputs.subjects_dir, |
| 96 | + subject_id=self.inputs.subject_id, |
| 97 | + T1_files=self.inputs.t1w, |
| 98 | + flags='-noskullstrip') |
| 99 | + if recon.cmdline.startswith('echo'): |
| 100 | + freesurfer_status = 'Pre-existing directory' |
| 101 | + else: |
| 102 | + freesurfer_status = 'Run by dMRIPrep' |
| 103 | + |
| 104 | + t2w_seg = '' |
| 105 | + if self.inputs.t2w: |
| 106 | + t2w_seg = '(+ {:d} T2-weighted)'.format(len(self.inputs.t2w)) |
| 107 | + |
| 108 | + # Add list of tasks with number of runs |
| 109 | + dwi_files = self.inputs.dwi if isdefined(self.inputs.dwi) else [] |
| 110 | + dwi_files = [s[0] if isinstance(s, list) else s for s in dwi_files] |
| 111 | + |
| 112 | + counts = Counter(BIDS_NAME.search(series).groupdict()['task_id'][5:] |
| 113 | + for series in dwi_files) |
| 114 | + |
| 115 | + tasks = '' |
| 116 | + if counts: |
| 117 | + header = '\t\t<ul class="elem-desc">' |
| 118 | + footer = '\t\t</ul>' |
| 119 | + lines = ['\t\t\t<li>Task: {task_id} ({n_runs:d} run{s})</li>'.format( |
| 120 | + task_id=task_id, n_runs=n_runs, s='' if n_runs == 1 else 's') |
| 121 | + for task_id, n_runs in sorted(counts.items())] |
| 122 | + tasks = '\n'.join([header] + lines + [footer]) |
| 123 | + |
| 124 | + return SUBJECT_TEMPLATE.format( |
| 125 | + subject_id=self.inputs.subject_id, |
| 126 | + n_t1s=len(self.inputs.t1w), |
| 127 | + t2w=t2w_seg, |
| 128 | + n_dwi=len(dwi_files), |
| 129 | + tasks=tasks, |
| 130 | + std_spaces=', '.join(self.inputs.std_spaces), |
| 131 | + nstd_spaces=', '.join(self.inputs.nstd_spaces), |
| 132 | + freesurfer_status=freesurfer_status) |
| 133 | + |
| 134 | + |
| 135 | +class AboutSummaryInputSpec(BaseInterfaceInputSpec): |
| 136 | + version = Str(desc='dMRIPrep version') |
| 137 | + command = Str(desc='dMRIPrep command') |
| 138 | + # Date not included - update timestamp only if version or command changes |
| 139 | + |
| 140 | + |
| 141 | +class AboutSummary(SummaryInterface): |
| 142 | + input_spec = AboutSummaryInputSpec |
| 143 | + |
| 144 | + def _generate_segment(self): |
| 145 | + return ABOUT_TEMPLATE.format(version=self.inputs.version, |
| 146 | + command=self.inputs.command, |
| 147 | + date=time.strftime("%Y-%m-%d %H:%M:%S %z")) |
| 148 | + |
| 149 | + |
| 150 | +class DiffusionSummaryInputSpec(BaseInterfaceInputSpec): |
| 151 | + distortion_correction = traits.Str(desc='Susceptibility distortion correction method', |
| 152 | + mandatory=True) |
| 153 | + pe_direction = traits.Enum(None, 'i', 'i-', 'j', 'j-', mandatory=True, |
| 154 | + desc='Phase-encoding direction detected') |
| 155 | + registration = traits.Enum('FSL', 'FreeSurfer', mandatory=True, |
| 156 | + desc='Diffusion/anatomical registration method') |
| 157 | + fallback = traits.Bool(desc='Boundary-based registration rejected') |
| 158 | + registration_dof = traits.Enum(6, 9, 12, desc='Registration degrees of freedom', |
| 159 | + mandatory=True) |
| 160 | + |
| 161 | + |
| 162 | +class DiffusionSummary(SummaryInterface): |
| 163 | + input_spec = DiffusionSummaryInputSpec |
| 164 | + |
| 165 | + def _generate_segment(self): |
| 166 | + dof = self.inputs.registration_dof |
| 167 | + reg = { |
| 168 | + 'FSL': [ |
| 169 | + 'FSL <code>flirt</code> with boundary-based registration' |
| 170 | + ' (BBR) metric - %d dof' % dof, |
| 171 | + 'FSL <code>flirt</code> rigid registration - 6 dof'], |
| 172 | + 'FreeSurfer': [ |
| 173 | + 'FreeSurfer <code>bbregister</code> ' |
| 174 | + '(boundary-based registration, BBR) - %d dof' % dof, |
| 175 | + 'FreeSurfer <code>mri_coreg</code> - %d dof' % dof], |
| 176 | + }[self.inputs.registration][self.inputs.fallback] |
| 177 | + if self.inputs.pe_direction is None: |
| 178 | + pedir = 'MISSING - Assuming Anterior-Posterior' |
| 179 | + else: |
| 180 | + pedir = {'i': 'Left-Right', 'j': 'Anterior-Posterior'}[self.inputs.pe_direction[0]] |
| 181 | + |
| 182 | + return DWI_TEMPLATE.format( |
| 183 | + pedir=pedir, sdc=self.inputs.distortion_correction, registration=reg) |
0 commit comments