Skip to content

Commit 198fced

Browse files
authored
Merge pull request #28 from oesteban/enh/new-report-capable-interface
ENH: Improved fieldmap reportlets
2 parents c4f834b + 7bec3a9 commit 198fced

File tree

5 files changed

+152
-9
lines changed

5 files changed

+152
-9
lines changed

sdcflows/interfaces/fmap.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
# -*- coding: utf-8 -*-
21
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
32
# vi: set ft=python sts=4 ts=4 sw=4 et:
43
"""
5-
Interfaces to deal with the various types of fieldmap sources
4+
Interfaces to deal with the various types of fieldmap sources.
65
76
.. testsetup::
87

sdcflows/interfaces/reportlets.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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 speciality reportlets."""
4+
from nilearn.image import threshold_img, load_img
5+
from niworkflows import NIWORKFLOWS_LOG
6+
from niworkflows.viz.utils import cuts_from_bbox, compose_view
7+
from nipype.interfaces.base import File, isdefined
8+
from nipype.interfaces.mixins import reporting
9+
10+
from ..viz.utils import plot_registration, coolwarm_transparent
11+
12+
13+
class FieldmapReportletInputSpec(reporting.ReportCapableInputSpec):
14+
reference = File(exists=True, mandatory=True, desc='input reference')
15+
fieldmap = File(exists=True, mandatory=True, desc='input fieldmap')
16+
mask = File(exists=True, desc='brain mask')
17+
out_report = File('report.svg', usedefault=True,
18+
desc='filename for the visual report')
19+
20+
21+
class FieldmapReportlet(reporting.ReportCapableInterface):
22+
"""An abstract mixin to registration nipype interfaces."""
23+
24+
_n_cuts = 7
25+
input_spec = FieldmapReportletInputSpec
26+
output_spec = reporting.ReportCapableOutputSpec
27+
28+
def __init__(self, **kwargs):
29+
"""Instantiate FieldmapReportlet."""
30+
self._n_cuts = kwargs.pop('n_cuts', self._n_cuts)
31+
super(FieldmapReportlet, self).__init__(generate_report=True, **kwargs)
32+
33+
def _run_interface(self, runtime):
34+
return runtime
35+
36+
def _generate_report(self):
37+
"""Generate a reportlet."""
38+
NIWORKFLOWS_LOG.info('Generating visual report')
39+
40+
refnii = load_img(self.inputs.reference)
41+
fmapnii = load_img(self.inputs.fieldmap)
42+
contour_nii = load_img(self.inputs.mask) if isdefined(self.inputs.mask) else None
43+
mask_nii = threshold_img(refnii, 1e-3)
44+
cuts = cuts_from_bbox(contour_nii or mask_nii, cuts=self._n_cuts)
45+
fmapdata = fmapnii.get_fdata()
46+
vmax = max(fmapdata.max(), abs(fmapdata.min()))
47+
48+
# Call composer
49+
compose_view(
50+
plot_registration(refnii, 'fixed-image',
51+
estimate_brightness=True,
52+
cuts=cuts,
53+
label='reference',
54+
contour=contour_nii,
55+
compress=False),
56+
plot_registration(fmapnii, 'moving-image',
57+
estimate_brightness=True,
58+
cuts=cuts,
59+
label='fieldmap (Hz)',
60+
contour=contour_nii,
61+
compress=False,
62+
plot_params={'cmap': coolwarm_transparent(),
63+
'vmax': vmax,
64+
'vmin': -vmax}),
65+
out_file=self._out_report
66+
)

sdcflows/viz/__init__.py

Whitespace-only changes.

sdcflows/viz/utils.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
"""Visualization tooling."""
4+
5+
6+
def plot_registration(anat_nii, div_id, plot_params=None,
7+
order=('z', 'x', 'y'), cuts=None,
8+
estimate_brightness=False, label=None, contour=None,
9+
compress='auto'):
10+
"""
11+
Plot the foreground and background views.
12+
13+
Default order is: axial, coronal, sagittal
14+
"""
15+
from uuid import uuid4
16+
17+
from lxml import etree
18+
from nilearn.plotting import plot_anat
19+
from svgutils.transform import SVGFigure
20+
from niworkflows.viz.utils import robust_set_limits, extract_svg, SVGNS
21+
22+
plot_params = plot_params or {}
23+
24+
# Use default MNI cuts if none defined
25+
if cuts is None:
26+
raise NotImplementedError # TODO
27+
28+
out_files = []
29+
if estimate_brightness:
30+
plot_params = robust_set_limits(anat_nii.get_data().reshape(-1),
31+
plot_params)
32+
33+
# Plot each cut axis
34+
for i, mode in enumerate(list(order)):
35+
plot_params['display_mode'] = mode
36+
plot_params['cut_coords'] = cuts[mode]
37+
if i == 0:
38+
plot_params['title'] = label
39+
else:
40+
plot_params['title'] = None
41+
42+
# Generate nilearn figure
43+
display = plot_anat(anat_nii, **plot_params)
44+
if contour is not None:
45+
display.add_contours(contour, colors='g', levels=[0.5],
46+
linewidths=0.5)
47+
48+
svg = extract_svg(display, compress=compress)
49+
display.close()
50+
51+
# Find and replace the figure_1 id.
52+
xml_data = etree.fromstring(svg)
53+
find_text = etree.ETXPath("//{%s}g[@id='figure_1']" % SVGNS)
54+
find_text(xml_data)[0].set('id', '%s-%s-%s' % (div_id, mode, uuid4()))
55+
56+
svg_fig = SVGFigure()
57+
svg_fig.root = xml_data
58+
out_files.append(svg_fig)
59+
60+
return out_files
61+
62+
63+
def coolwarm_transparent():
64+
"""Modify the coolwarm color scale to have full transparency around the middle."""
65+
import numpy as np
66+
import matplotlib.pylab as pl
67+
from matplotlib.colors import ListedColormap
68+
69+
# Choose colormap
70+
cmap = pl.cm.coolwarm
71+
72+
# Get the colormap colors
73+
my_cmap = cmap(np.arange(cmap.N))
74+
75+
# Set alpha
76+
alpha = np.ones(cmap.N)
77+
alpha[128:160] = np.linspace(0, 1, len(alpha[128:160]))
78+
alpha[96:128] = np.linspace(1, 0, len(alpha[96:128]))
79+
my_cmap[:, -1] = alpha
80+
return ListedColormap(my_cmap)

sdcflows/workflows/tests/test_phdiff.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,8 @@ def test_workflow(bids_layouts, tmpdir, output_path, dataset):
3030
phdiff_wf.inputs.inputnode.metadata = phdiff_file.get_metadata()
3131

3232
if output_path:
33-
from niworkflows.interfaces.registration import SimpleBeforeAfterRPT
34-
rep = pe.Node(SimpleBeforeAfterRPT(), 'simple_report')
35-
rep.interface._fixed_image_label = 'magnitude'
36-
rep.interface._moving_image_label = 'fieldmap'
33+
from ...interfaces.reportlets import FieldmapReportlet
34+
rep = pe.Node(FieldmapReportlet(), 'simple_report')
3735

3836
dsink = pe.Node(DerivativesDataSink(
3937
base_directory=str(output_path), keep_dtype=True), name='dsink')
@@ -42,9 +40,9 @@ def test_workflow(bids_layouts, tmpdir, output_path, dataset):
4240

4341
wf.connect([
4442
(phdiff_wf, rep, [
45-
('outputnode.fmap', 'after'),
46-
('outputnode.fmap_ref', 'before'),
47-
('outputnode.fmap_mask', 'wm_seg')]),
43+
('outputnode.fmap', 'fieldmap'),
44+
('outputnode.fmap_ref', 'reference'),
45+
('outputnode.fmap_mask', 'mask')]),
4846
(rep, dsink, [('out_report', 'in_file')]),
4947
])
5048
else:

0 commit comments

Comments
 (0)