Skip to content

Commit 9bc5e41

Browse files
committed
Merge pull request #1603 from dengemann/report_svg
WIP: add svg backend to report.py
2 parents 9405ced + e62c76b commit 9bc5e41

4 files changed

Lines changed: 77 additions & 42 deletions

File tree

doc/source/mne_report_tutorial.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,12 @@ if available)::
4343
To properly render `trans` and `covariance` files, add the measurement information::
4444

4545
mne report --path MNE-sample-data/ --info MNE-sample-data/MEG/sample/sample_audvis-ave.fif \
46-
--subject sample --subjects_dir MNE-sample-data/subjects --verbose
46+
--subject sample --subjects-dir MNE-sample-data/subjects --verbose
4747

4848
To generate the report in parallel::
4949

5050
mne report --path MNE-sample-data/ --info MNE-sample-data/MEG/sample/sample_audvis-ave.fif \
51-
--subject sample --subjects_dir MNE-sample-data/subjects --verbose --jobs 6
51+
--subject sample --subjects-dir MNE-sample-data/subjects --verbose --jobs 6
5252

5353
The Python interface
5454
--------------------

doc/source/whats_new.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Changelog
2323

2424
- Average evoked topographies across time points by `Denis Engemann`_
2525

26+
- Add option to Report class to save images as vector graphics (SVG) by `Denis Engemann`_
27+
2628
BUG
2729
~~~
2830

mne/report.py

Lines changed: 71 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,45 @@
4545
# PLOTTING FUNCTIONS
4646

4747

48-
def _fig_to_img(function=None, fig=None, **kwargs):
48+
def _fig_to_img(function=None, fig=None, image_format='png',
49+
scale=None, **kwargs):
4950
"""Wrapper function to plot figure and create a binary image"""
5051
import matplotlib.pyplot as plt
5152
if function is not None:
5253
plt.close('all')
5354
fig = function(**kwargs)
5455
output = BytesIO()
55-
fig.savefig(output, format='png', bbox_inches='tight', dpi=fig.get_dpi())
56+
if scale is not None:
57+
_scale_mpl_figure(fig, scale)
58+
fig.savefig(output, format=image_format, bbox_inches='tight',
59+
dpi=fig.get_dpi())
5660
plt.close(fig)
57-
return base64.b64encode(output.getvalue()).decode('ascii')
61+
output = output.getvalue()
62+
return (output if image_format == 'svg' else
63+
base64.b64encode(output).decode('ascii'))
64+
65+
66+
def _scale_mpl_figure(fig, scale):
67+
"""Magic scaling helper
68+
69+
Keeps font-size and artist sizes constant
70+
0.5 : current font - 4pt
71+
2.0 : current font + 4pt
72+
73+
XXX it's unclear why this works, but good to go for most cases
74+
"""
75+
fig.set_size_inches(fig.get_size_inches() * scale)
76+
fig.set_dpi(fig.get_dpi() * scale)
77+
import matplotlib as mpl
78+
if scale >= 1:
79+
sfactor = scale ** 2
80+
elif scale < 1:
81+
sfactor = -((1. / scale) ** 2)
82+
for text in fig.findobj(mpl.text.Text):
83+
fs = text.get_fontsize()
84+
text.set_fontsize(fs + sfactor)
85+
86+
fig.canvas.draw()
5887

5988

6089
def _figs_to_mrislices(sl, n_jobs, **kwargs):
@@ -75,7 +104,6 @@ def _iterate_trans_views(function, **kwargs):
75104
from scipy.misc import imread
76105
import matplotlib.pyplot as plt
77106
import mayavi
78-
79107
fig = function(**kwargs)
80108

81109
assert isinstance(fig, mayavi.core.scene.Scene)
@@ -283,7 +311,8 @@ def _iterate_coronal_slices(array, limits=None):
283311
yield ind, np.flipud(np.rot90(array[:, :, ind]))
284312

285313

286-
def _iterate_mri_slices(name, ind, global_id, slides_klass, data, cmap):
314+
def _iterate_mri_slices(name, ind, global_id, slides_klass, data, cmap,
315+
image_format='png'):
287316
"""Auxiliary function for parallel processing of mri slices.
288317
"""
289318
img_klass = 'slideimg-%s' % name
@@ -294,8 +323,7 @@ def _iterate_mri_slices(name, ind, global_id, slides_klass, data, cmap):
294323
img = _build_image(data, cmap=cmap)
295324
first = True if ind == 0 else False
296325
html = _build_html_image(img, slice_id, div_klass,
297-
img_klass, caption,
298-
first)
326+
img_klass, caption, first)
299327
return ind, html
300328

301329

@@ -497,6 +525,8 @@ def _build_html_slider(slices_range, slides_klass, slider_id):
497525
{{default interactive = False}}
498526
{{default width = 50}}
499527
{{default id = False}}
528+
{{default image_format = 'png'}}
529+
{{default scale = None}}
500530
501531
<li class="{{div_klass}}" {{if id}}id="{{id}}"{{endif}}
502532
{{if not show}}style="display: none"{{endif}}>
@@ -506,8 +536,19 @@ def _build_html_slider(slices_range, slides_klass, slider_id):
506536
{{endif}}
507537
<div class="thumbnail">
508538
{{if not interactive}}
509-
<img alt="" style="width:{{width}}%;"
510-
src="data:image/png;base64,{{img}}">
539+
{{if image_format == 'png'}}
540+
{{if scale is not None}}
541+
<img alt="" style="width:{{width}}%;"
542+
src="data:image/png;base64,{{img}}">
543+
{{else}}
544+
<img alt=""
545+
src="data:image/png;base64,{{img}}">
546+
{{endif}}
547+
{{elif image_format == 'svg'}}
548+
<div style="text-align:center;">
549+
{{img}}
550+
</div>
551+
{{endif}}
511552
{{else}}
512553
<center>{{interactive}}</center>
513554
{{endif}}
@@ -657,7 +698,7 @@ def _validate_input(self, items, captions, section):
657698
return items, captions
658699

659700
def _add_figs_to_section(self, figs, captions, section='custom',
660-
scale=None):
701+
image_format='png', scale=None):
661702
"""Auxiliary method for `add_section` and `add_figs_to_section`.
662703
"""
663704
try:
@@ -668,7 +709,6 @@ def _add_figs_to_section(self, figs, captions, section='custom',
668709
warnings.warn('Could not import mayavi. Trying to render '
669710
'`mayavi.core.scene.Scene` figure instances'
670711
' will throw an error.')
671-
import matplotlib as mpl
672712
figs, captions = self._validate_input(figs, captions, section)
673713
for fig, caption in zip(figs, captions):
674714
caption = 'custom plot' if caption == '' else caption
@@ -682,27 +722,16 @@ def _add_figs_to_section(self, figs, captions, section='custom',
682722
temp_fname = op.join(tempdir, 'test')
683723
fig.scene.save_png(temp_fname)
684724
mayavi.mlab.close(fig)
685-
with open(temp_fname, 'rb') as fid:
686-
img = base64.b64encode(fid.read()).decode('ascii')
687-
else:
688-
img = _fig_to_img(fig=fig)
689-
if scale is None:
690-
if isinstance(fig, mpl.figure.Figure):
691-
my_scale = fig.get_figwidth() # "magic' scaling factor
692-
else:
693-
my_scale = fig.scene.get_size()
694-
my_scale *= 5
695-
elif callable(scale):
696-
my_scale = scale(fig)
697725
else:
698-
my_scale = scale
699-
726+
img = _fig_to_img(fig=fig, scale=scale,
727+
image_format=image_format)
700728
html = image_template.substitute(img=img, id=global_id,
701729
div_klass=div_klass,
702730
img_klass=img_klass,
703731
caption=caption,
704732
show=True,
705-
width=my_scale)
733+
image_format=image_format,
734+
width=scale)
706735
self.fnames.append('%s-#-%s-#-custom' % (caption, sectionvar))
707736
self._sectionlabels.append(sectionvar)
708737
self.html.append(html)
@@ -728,7 +757,7 @@ def add_section(self, figs, captions, section='custom'):
728757
section=section)
729758

730759
def add_figs_to_section(self, figs, captions, section='custom',
731-
scale=None):
760+
scale=None, image_format='png'):
732761
"""Append custom user-defined figures.
733762
734763
Parameters
@@ -739,18 +768,23 @@ def add_figs_to_section(self, figs, captions, section='custom',
739768
or np.ndarray (images read in using scipy.imread).
740769
captions : list of str
741770
A list of captions to the figures.
742-
scale : float | None | callable
743-
Scale the images maintaining the aspect ratio.
744-
If None, equals fig.get_figwidth() * 5. If callable, function
745-
should take a figure object as input parameter.
746771
section : str
747772
Name of the section. If section already exists, the figures
748773
will be appended to the end of the section
774+
scale : float | None | callable
775+
Scale the images maintaining the aspect ratio.
776+
If None, no scaling is applied.
777+
If float, scale will determine the relative width in percent.
778+
If function, should take a figure object as input parameter.
779+
Defaults to None.
780+
image_format : {'png', 'svg'}
781+
The image format to be used for the report. Defaults to 'png'.
749782
"""
750783
return self._add_figs_to_section(figs=figs, captions=captions,
751-
section=section, scale=scale)
784+
section=section, scale=scale,
785+
image_format=image_format)
752786

753-
def add_images_to_section(self, fnames, captions, scale=1.0,
787+
def add_images_to_section(self, fnames, captions, scale=None,
754788
section='custom'):
755789
"""Append custom user-defined images.
756790
@@ -760,9 +794,9 @@ def add_images_to_section(self, fnames, captions, scale=1.0,
760794
A list of filenames from which images are read.
761795
captions : list of str
762796
A list of captions to the images.
763-
scale : float
797+
scale : float | None
764798
Scale the images maintaining the aspect ratio.
765-
Defaults to 1.
799+
Defaults to None. If None, no scaling will be applied.
766800
section : str
767801
Name of the section. If section already exists, the images
768802
will be appended to the end of the section.
@@ -771,8 +805,6 @@ def add_images_to_section(self, fnames, captions, scale=1.0,
771805
# imports PIL anyway. It's not possible to redirect image output
772806
# to binary string using scipy.misc.
773807
from PIL import Image
774-
775-
scale *= 100
776808
fnames, captions = self._validate_input(fnames, captions, section)
777809

778810
for fname, caption in zip(fnames, captions):
@@ -1245,7 +1277,7 @@ def _render_evoked(self, evoked_fname, figsize=None):
12451277
if len(pick_types(ev.info, meg='mag', eeg=False)) > 0:
12461278
has_types.append('mag')
12471279
for ch_type in has_types:
1248-
kwargs = dict(ch_type=ch_type, show=False)
1280+
kwargs.update(ch_type=ch_type)
12491281
img = _fig_to_img(ev.plot_topomap, **kwargs)
12501282
caption = u'Topomap (ch_type = %s)' % ch_type
12511283
html.append(image_template.substitute(img=img,
@@ -1315,7 +1347,7 @@ def _render_cov(self, cov_fname, info_fname):
13151347
return html
13161348

13171349
def _render_trans(self, trans_fname, path, info, subject,
1318-
subjects_dir):
1350+
subjects_dir, image_format='png'):
13191351
"""Render trans.
13201352
"""
13211353
kwargs = dict(info=info, trans_fname=trans_fname, subject=subject,

mne/tests/test_report.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ def test_render_add_sections():
111111
# Check add_figs_to_section functionality
112112
fig = plt.plot([1, 2], [1, 2])[0].figure
113113
report.add_figs_to_section(figs=fig, # test non-list input
114-
captions=['evoked response'])
114+
captions=['evoked response'], scale=1.2,
115+
image_format='svg')
115116
assert_raises(ValueError, report.add_figs_to_section, figs=[fig, fig],
116117
captions='H')
117118

0 commit comments

Comments
 (0)