# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
# vi: set ft=python sts=4 ts=4 sw=4 et:
#
# Copyright 2021 The NiPreps Developers <nipreps@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# We support and encourage derived works from this project, please read
# about our expectations at
#
# https://www.nipreps.org/community/licensing/
#
"""Interfaces to generate reportlets."""
import time
from pathlib import Path
from nipype.interfaces import freesurfer as fs
from nipype.interfaces.base import (
BaseInterfaceInputSpec,
Directory,
File,
InputMultiObject,
SimpleInterface,
Str,
TraitedSpec,
isdefined,
)
from nipype.interfaces.io import FSSourceInputSpec as _FSSourceInputSpec
from nipype.interfaces.mixins import reporting
from niworkflows.interfaces.reportlets.base import _SVGReportCapableInputSpec
SUBJECT_TEMPLATE = """\
\t<ul class="elem-desc">
\t\t<li>Subject ID: {subject_id}</li>
\t\t<li>Structural images: {n_t1s:d} T1-weighted {t2w}</li>
\t\t<li>Standard spaces: {output_spaces}</li>
\t\t<li>FreeSurfer reconstruction: {freesurfer_status}</li>
\t</ul>
"""
ABOUT_TEMPLATE = """\t<ul>
\t\t<li>sMRIPrep version: {version}</li>
\t\t<li>sMRIPrep command: <code>{command}</code></li>
\t\t<li>Date preprocessed: {date}</li>
\t</ul>
</div>
"""
class _SummaryOutputSpec(TraitedSpec):
out_report = File(exists=True, desc='HTML segment containing summary')
[docs]
class SummaryInterface(SimpleInterface):
"""Base Nipype interface for html summaries."""
output_spec = _SummaryOutputSpec
def _run_interface(self, runtime):
segment = self._generate_segment()
path = Path(runtime.cwd) / 'report.html'
path.write_text(segment)
self._results['out_report'] = str(path)
return runtime
def _generate_segment(self):
raise NotImplementedError
class _SubjectSummaryInputSpec(BaseInterfaceInputSpec):
t1w = InputMultiObject(File(exists=True), desc='T1w structural images')
t2w = InputMultiObject(File(exists=True), desc='T2w structural images')
subjects_dir = Directory(desc='FreeSurfer subjects directory')
subject_id = Str(desc='Subject ID')
output_spaces = InputMultiObject(Str, desc='list of standard spaces')
class _SubjectSummaryOutputSpec(_SummaryOutputSpec):
# This exists to ensure that the summary is run prior to the first ReconAll
# call, allowing a determination whether there is a pre-existing directory
subject_id = Str(desc='FreeSurfer subject ID')
[docs]
class SubjectSummary(SummaryInterface):
"""Subject html summary reportlet."""
input_spec = _SubjectSummaryInputSpec
output_spec = _SubjectSummaryOutputSpec
def _run_interface(self, runtime):
if isdefined(self.inputs.subject_id):
self._results['subject_id'] = self.inputs.subject_id
return super()._run_interface(runtime)
def _generate_segment(self):
if not isdefined(self.inputs.subjects_dir):
freesurfer_status = 'Not run'
else:
recon = fs.ReconAll(
subjects_dir=self.inputs.subjects_dir,
subject_id=self.inputs.subject_id,
T1_files=self.inputs.t1w,
flags='-noskullstrip',
)
if recon.cmdline.startswith('echo'):
freesurfer_status = 'Pre-existing directory'
else:
freesurfer_status = 'Run by sMRIPrep'
t2w_seg = ''
if self.inputs.t2w:
t2w_seg = f'(+ {len(self.inputs.t2w):d} T2-weighted)'
output_spaces = self.inputs.output_spaces
if not isdefined(output_spaces):
output_spaces = '<none given>'
else:
output_spaces = ', '.join(output_spaces)
return SUBJECT_TEMPLATE.format(
subject_id=self.inputs.subject_id,
n_t1s=len(self.inputs.t1w),
t2w=t2w_seg,
output_spaces=output_spaces,
freesurfer_status=freesurfer_status,
)
class _AboutSummaryInputSpec(BaseInterfaceInputSpec):
version = Str(desc='sMRIPrep version')
command = Str(desc='sMRIPrep command')
# Date not included - update timestamp only if version or command changes
[docs]
class AboutSummary(SummaryInterface):
"""About section reportlet."""
input_spec = _AboutSummaryInputSpec
def _generate_segment(self):
return ABOUT_TEMPLATE.format(
version=self.inputs.version,
command=self.inputs.command,
date=time.strftime('%Y-%m-%d %H:%M:%S %z'),
)
class _FSSurfaceReportInputSpec(_SVGReportCapableInputSpec, _FSSourceInputSpec):
pass
class _FSSurfaceReportOutputSpec(reporting.ReportCapableOutputSpec):
pass
[docs]
class FSSurfaceReport(SimpleInterface):
"""Replaces ``ReconAllRPT``, without need of calling recon-all."""
input_spec = _FSSurfaceReportInputSpec
output_spec = _FSSurfaceReportOutputSpec
def _run_interface(self, runtime):
from nibabel import load
from niworkflows.viz.utils import (
compose_view,
cuts_from_bbox,
plot_registration,
)
rootdir = Path(self.inputs.subjects_dir) / self.inputs.subject_id
_anat_file = str(rootdir / 'mri' / 'brain.mgz')
_contour_file = str(rootdir / 'mri' / 'ribbon.mgz')
anat = load(_anat_file)
contour_nii = load(_contour_file)
n_cuts = 7
cuts = cuts_from_bbox(contour_nii, cuts=n_cuts)
self._results['out_report'] = str(Path(runtime.cwd) / self.inputs.out_report)
# Call composer
compose_view(
plot_registration(
anat,
'fixed-image',
estimate_brightness=True,
cuts=cuts,
contour=contour_nii,
compress=self.inputs.compress_report,
),
[],
out_file=self._results['out_report'],
)
return runtime