Skip to content

Commit 3ce2d67

Browse files
committed
Merge pull request #89 from christianbrodbeck/movie2
Brain.save_movie() II
2 parents 8ca4192 + e6f2055 commit 3ce2d67

File tree

4 files changed

+380
-33
lines changed

4 files changed

+380
-33
lines changed

examples/save_movie.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""
2+
Create movie from MEG inverse solution
3+
=======================================
4+
5+
Data were computed using mne-python (http://martinos.org/mne)
6+
7+
"""
8+
print __doc__
9+
10+
import os
11+
import numpy as np
12+
13+
from surfer import Brain
14+
from surfer.io import read_stc
15+
16+
"""
17+
create Brain object for visualization
18+
"""
19+
brain = Brain('fsaverage', 'split', 'inflated',
20+
config_opts=dict(width=800, height=400))
21+
22+
"""
23+
read and display MNE dSPM inverse solution
24+
"""
25+
stc_fname = os.path.join('example_data', 'meg_source_estimate-%s.stc')
26+
for hemi in ['lh', 'rh']:
27+
stc = read_stc(stc_fname % hemi)
28+
data = stc['data']
29+
times = np.arange(data.shape[1]) * stc['tstep'] + stc['tmin']
30+
brain.add_data(data, colormap='hot', vertices=stc['vertices'],
31+
smoothing_steps=10, time=times, time_label='%0.3f s',
32+
hemi=hemi)
33+
34+
"""
35+
scale colormap
36+
"""
37+
brain.scale_data_colormap(fmin=13, fmid=18, fmax=22, transparent=True)
38+
39+
"""
40+
Save a movie. Use a large value for time_dilation because the sample stc only
41+
covers 30 ms.
42+
"""
43+
brain.save_movie('example_current.mov', time_dilation=30)
44+
45+
brain.close()

surfer/tests/test_viz.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22
import os
33
import os.path as op
44
from os.path import join as pjoin
5+
import re
6+
import shutil
7+
import subprocess
8+
from nose.tools import assert_equal
59
from numpy.testing import assert_raises, assert_array_equal
6-
from tempfile import mktemp
10+
from tempfile import mkdtemp, mktemp
711
import nibabel as nib
812

913
from surfer import Brain, io, utils
10-
from surfer.utils import requires_fsaverage
14+
from surfer.utils import requires_ffmpeg, requires_fsaverage
1115
from mayavi import mlab
1216

1317
subj_dir = utils._get_subjects_dir()
@@ -206,6 +210,44 @@ def test_morphometry():
206210
brain.close()
207211

208212

213+
@requires_ffmpeg
214+
@requires_fsaverage
215+
def test_movie():
216+
"""Test saving a movie of an MEG inverse solution
217+
"""
218+
# create and setup the Brain instance
219+
mlab.options.backend = 'auto'
220+
brain = Brain(*std_args)
221+
stc_fname = os.path.join(data_dir, 'meg_source_estimate-lh.stc')
222+
stc = io.read_stc(stc_fname)
223+
data = stc['data']
224+
time = np.arange(data.shape[1]) * stc['tstep'] + stc['tmin']
225+
brain.add_data(data, colormap='hot', vertices=stc['vertices'],
226+
smoothing_steps=10, time=time, time_label='time=%0.2f ms')
227+
brain.scale_data_colormap(fmin=13, fmid=18, fmax=22, transparent=True)
228+
229+
# save movies with different options
230+
tempdir = mkdtemp()
231+
try:
232+
dst = os.path.join(tempdir, 'test.mov')
233+
brain.save_movie(dst)
234+
brain.save_movie(dst, tmin=0.081, tmax=0.102)
235+
# test the number of frames in the movie
236+
sp = subprocess.Popen(('ffmpeg', '-i', 'test.mov', '-vcodec', 'copy',
237+
'-f', 'null', '/dev/null'), cwd=tempdir,
238+
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
239+
stdout, stderr = sp.communicate()
240+
m = re.search('frame=\s*(\d+)\s', stderr)
241+
if not m:
242+
raise RuntimeError(stderr)
243+
n_frames = int(m.group(1))
244+
assert_equal(n_frames, 3)
245+
finally:
246+
# clean up
247+
shutil.rmtree(tempdir)
248+
brain.close()
249+
250+
209251
@requires_fsaverage
210252
def test_overlay():
211253
"""Test plotting of overlay

surfer/utils.py

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from os import path as op
66
import inspect
77
from functools import wraps
8+
import subprocess
89

910
import numpy as np
1011
import nibabel as nib
@@ -241,7 +242,6 @@ def set_log_level(verbose=None, return_old_level=False):
241242
if verbose not in logging_types:
242243
raise ValueError('verbose must be of a valid type')
243244
verbose = logging_types[verbose]
244-
logger = logging.getLogger('surfer')
245245
old_verbose = logger.level
246246
logger.setLevel(verbose)
247247
return (old_verbose if return_old_level else None)
@@ -273,7 +273,6 @@ def set_log_file(fname=None, output_format='%(message)s', overwrite=None):
273273
but additionally raises a warning to notify the user that log
274274
entries will be appended.
275275
"""
276-
logger = logging.getLogger('surfer')
277276
handlers = logger.handlers
278277
for h in handlers:
279278
if isinstance(h, logging.FileHandler):
@@ -634,3 +633,89 @@ def has_fsaverage(subjects_dir=None):
634633

635634
requires_fsaverage = np.testing.dec.skipif(not has_fsaverage(),
636635
'Requires fsaverage subject data')
636+
637+
638+
def has_ffmpeg():
639+
"""Test whether the FFmpeg is available in a subprocess
640+
641+
Returns
642+
-------
643+
ffmpeg_exists : bool
644+
True if FFmpeg can be successfully called, False otherwise.
645+
"""
646+
try:
647+
subprocess.call(["ffmpeg"], stdout=subprocess.PIPE,
648+
stderr=subprocess.PIPE)
649+
return True
650+
except OSError:
651+
return False
652+
653+
654+
def assert_ffmpeg_is_available():
655+
"Raise a RuntimeError if FFmpeg is not in the PATH"
656+
if not has_ffmpeg():
657+
err = ("FFmpeg is not in the path and is needed for saving "
658+
"movies. Install FFmpeg and try again. It can be "
659+
"downlaoded from http://ffmpeg.org/download.html.")
660+
raise RuntimeError(err)
661+
662+
requires_ffmpeg = np.testing.dec.skipif(not has_ffmpeg(), 'Requires FFmpeg')
663+
664+
665+
def ffmpeg(dst, frame_path, framerate=24, codec='mpeg4'):
666+
"""Run FFmpeg in a subprocess to convert an image sequence into a movie
667+
668+
Parameters
669+
----------
670+
dst : str
671+
Destination path. If the extension is not ".mov" or ".avi", ".mov" is
672+
added. If the file already exists it is overwritten.
673+
frame_path : str
674+
Path to the source frames (with a frame number field like '%04d').
675+
framerate : float
676+
Framerate of the movie (frames per second, default 24).
677+
codec : str
678+
Codec to use (default 'mpeg4').
679+
680+
Notes
681+
-----
682+
Requires FFmpeg to be in the path. FFmpeg can be downlaoded from `here
683+
<http://ffmpeg.org/download.html>`_. Stdout and stderr are written to the
684+
logger. If the movie file is not created, a RuntimeError is raised.
685+
"""
686+
assert_ffmpeg_is_available()
687+
688+
# find target path
689+
dst = os.path.expanduser(dst)
690+
dst = os.path.abspath(dst)
691+
root, ext = os.path.splitext(dst)
692+
dirname = os.path.dirname(dst)
693+
if ext not in ['.mov', '.avi']:
694+
dst += '.mov'
695+
696+
if os.path.exists(dst):
697+
os.remove(dst)
698+
elif not os.path.exists(dirname):
699+
os.mkdir(dirname)
700+
701+
frame_dir, frame_fmt = os.path.split(frame_path)
702+
703+
# make the movie
704+
cmd = ['ffmpeg', '-i', frame_fmt, '-r', str(framerate), '-c', codec, dst]
705+
logger.info("Running FFmpeg with command: %s", ' '.join(cmd))
706+
sp = subprocess.Popen(cmd, cwd=frame_dir, stdout=subprocess.PIPE,
707+
stderr=subprocess.PIPE)
708+
709+
# log stdout and stderr
710+
stdout, stderr = sp.communicate()
711+
std_info = os.linesep.join(("FFmpeg stdout", '=' * 25, stdout))
712+
logger.info(std_info)
713+
if stderr.strip():
714+
err_info = os.linesep.join(("FFmpeg stderr", '=' * 27, stderr))
715+
logger.error(err_info)
716+
717+
# check that movie file is created
718+
if not os.path.exists(dst):
719+
err = ("FFmpeg failed, no file created; see log for more more "
720+
"information.")
721+
raise RuntimeError(err)

0 commit comments

Comments
 (0)