Skip to content

Commit 89056ab

Browse files
authored
Merge pull request #154 from christianbrodbeck/imageio
[WIP] Imageio
2 parents 40621da + f219021 commit 89056ab

File tree

7 files changed

+83
-134
lines changed

7 files changed

+83
-134
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ install:
2828
travis_retry sudo apt-get update -qq;
2929
travis_retry sudo apt-get install mencoder;
3030
fi;
31-
- pip install -q coverage coveralls nose-timer nibabel flake8
31+
- pip install -q coverage coveralls nose-timer nibabel flake8 imageio
3232
- python setup.py build
3333
- python setup.py install
3434
- SRC_DIR=$(pwd)

appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ install:
2323

2424
# Install the dependencies of the project.
2525
- "conda install --yes --quiet ipython==1.1.0 numpy scipy mayavi matplotlib nose imaging"
26-
- "pip install -q nose-timer nibabel"
26+
- "pip install -q nose-timer nibabel imageio"
2727
- "python setup.py develop"
2828
- "SET SUBJECTS_DIR=%CD%\\subjects"
2929

doc/install.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ If you already have PySurfer installed, you can also use pip to update it::
1111

1212
pip install -U pysurfer
1313

14+
If you would like to save movies of time course data, also include the
15+
optional dependency `imageio` with::
16+
17+
pip install -U pysurfer[save_movie]
18+
1419
If you'd like to install the development version, you have two options. You can
1520
use pip::
1621

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,5 @@ def check_dependencies():
9393
packages=['surfer', 'surfer.tests'],
9494
scripts=['bin/pysurfer'],
9595
install_requires=['nibabel >= 1.2'],
96+
extras_require={'save_movie': ['imageio >= 1.5']},
9697
)

surfer/tests/test_viz.py

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1-
import numpy as np
21
import os
32
import os.path as op
43
from os.path import join as pjoin
5-
import re
64
import shutil
7-
import subprocess
8-
from nose.tools import assert_equal
9-
from numpy.testing import assert_raises, assert_array_equal
105
from tempfile import mkdtemp, mktemp
6+
7+
from nose.tools import assert_equal
8+
from mayavi import mlab
119
import nibabel as nib
10+
import numpy as np
11+
from numpy.testing import assert_raises, assert_array_equal
1212

1313
from surfer import Brain, io, utils
14-
from surfer.utils import requires_ffmpeg, requires_fsaverage
15-
from mayavi import mlab
14+
from surfer.utils import requires_fsaverage, requires_imageio
1615

1716
subj_dir = utils._get_subjects_dir()
1817
subject_id = 'fsaverage'
@@ -214,11 +213,13 @@ def test_morphometry():
214213
brain.close()
215214

216215

217-
@requires_ffmpeg
216+
@requires_imageio
218217
@requires_fsaverage
219218
def test_movie():
220219
"""Test saving a movie of an MEG inverse solution
221220
"""
221+
import imageio
222+
222223
# create and setup the Brain instance
223224
mlab.options.backend = 'auto'
224225
brain = Brain(*std_args)
@@ -234,18 +235,16 @@ def test_movie():
234235
tempdir = mkdtemp()
235236
try:
236237
dst = os.path.join(tempdir, 'test.mov')
238+
# test the number of frames in the movie
237239
brain.save_movie(dst)
240+
frames = imageio.mimread(dst)
241+
assert_equal(len(frames), 2)
242+
brain.save_movie(dst, time_dilation=10)
243+
frames = imageio.mimread(dst)
244+
assert_equal(len(frames), 7)
238245
brain.save_movie(dst, tmin=0.081, tmax=0.102)
239-
# test the number of frames in the movie
240-
sp = subprocess.Popen(('ffmpeg', '-i', 'test.mov', '-vcodec', 'copy',
241-
'-f', 'null', '/dev/null'), cwd=tempdir,
242-
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
243-
stdout, stderr = sp.communicate()
244-
m = re.search('frame=\s*(\d+)\s', stderr)
245-
if not m:
246-
raise RuntimeError(stderr)
247-
n_frames = int(m.group(1))
248-
assert_equal(n_frames, 3)
246+
frames = imageio.mimread(dst)
247+
assert_equal(len(frames), 2)
249248
finally:
250249
# clean up
251250
shutil.rmtree(tempdir)

surfer/utils.py

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

109
import numpy as np
1110
import nibabel as nib
@@ -624,101 +623,18 @@ def has_fsaverage(subjects_dir=None):
624623
return False
625624
return True
626625

627-
requires_fsaverage = np.testing.dec.skipif(not has_fsaverage(),
628-
'Requires fsaverage subject data')
629-
630626

631-
def has_ffmpeg():
632-
"""Test whether the FFmpeg is available in a subprocess
633-
634-
Returns
635-
-------
636-
ffmpeg_exists : bool
637-
True if FFmpeg can be successfully called, False otherwise.
638-
"""
627+
def has_imageio():
639628
try:
640-
subprocess.call(["ffmpeg"], stdout=subprocess.PIPE,
641-
stderr=subprocess.PIPE)
642-
return True
643-
except OSError:
629+
import imageio # NOQA
630+
except ImportError:
644631
return False
632+
else:
633+
return True
645634

646635

647-
def assert_ffmpeg_is_available():
648-
"Raise a RuntimeError if FFmpeg is not in the PATH"
649-
if not has_ffmpeg():
650-
err = ("FFmpeg is not in the path and is needed for saving "
651-
"movies. Install FFmpeg and try again. It can be "
652-
"downlaoded from http://ffmpeg.org/download.html.")
653-
raise RuntimeError(err)
654-
655-
requires_ffmpeg = np.testing.dec.skipif(not has_ffmpeg(), 'Requires FFmpeg')
656-
657-
658-
def ffmpeg(dst, frame_path, framerate=24, codec='mpeg4', bitrate='1M'):
659-
"""Run FFmpeg in a subprocess to convert an image sequence into a movie
660-
661-
Parameters
662-
----------
663-
dst : str
664-
Destination path. If the extension is not ".mov" or ".avi", ".mov" is
665-
added. If the file already exists it is overwritten.
666-
frame_path : str
667-
Path to the source frames (with a frame number field like '%04d').
668-
framerate : float
669-
Framerate of the movie (frames per second, default 24).
670-
codec : str | None
671-
Codec to use (default 'mpeg4'). If None, the codec argument is not
672-
forwarded to ffmpeg, which preserves compatibility with very old
673-
versions of ffmpeg
674-
bitrate : str | float
675-
Bitrate to use to encode movie. Can be specified as number (e.g.
676-
64000) or string (e.g. '64k'). Default value is 1M
636+
requires_fsaverage = np.testing.dec.skipif(not has_fsaverage(),
637+
'Requires fsaverage subject data')
677638

678-
Notes
679-
-----
680-
Requires FFmpeg to be in the path. FFmpeg can be downlaoded from `here
681-
<http://ffmpeg.org/download.html>`_. Stdout and stderr are written to the
682-
logger. If the movie file is not created, a RuntimeError is raised.
683-
"""
684-
assert_ffmpeg_is_available()
685-
686-
# find target path
687-
dst = os.path.expanduser(dst)
688-
dst = os.path.abspath(dst)
689-
root, ext = os.path.splitext(dst)
690-
dirname = os.path.dirname(dst)
691-
if ext not in ['.mov', '.avi']:
692-
dst += '.mov'
693-
694-
if os.path.exists(dst):
695-
os.remove(dst)
696-
elif not os.path.exists(dirname):
697-
os.mkdir(dirname)
698-
699-
frame_dir, frame_fmt = os.path.split(frame_path)
700-
701-
# make the movie
702-
cmd = ['ffmpeg', '-i', frame_fmt, '-r', str(framerate),
703-
'-b:v', str(bitrate)]
704-
if codec is not None:
705-
cmd += ['-c', codec]
706-
cmd += [dst]
707-
logger.info("Running FFmpeg with command: %s", ' '.join(cmd))
708-
sp = subprocess.Popen(cmd, cwd=frame_dir, stdout=subprocess.PIPE,
709-
stderr=subprocess.PIPE)
710-
711-
# log stdout and stderr
712-
stdout, stderr = sp.communicate()
713-
std_info = os.linesep.join(("FFmpeg stdout", '=' * 25, stdout))
714-
logger.info(std_info)
715-
if stderr.strip():
716-
err_info = os.linesep.join(("FFmpeg stderr", '=' * 27, stderr))
717-
# FFmpeg prints to stderr in the absence of an error
718-
logger.info(err_info)
719-
720-
# check that movie file is created
721-
if not os.path.exists(dst):
722-
err = ("FFmpeg failed, no file created; see log for more more "
723-
"information.")
724-
raise RuntimeError(err)
639+
requires_imageio = np.testing.dec.skipif(not has_imageio(),
640+
"Requires imageio package")

surfer/viz.py

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from math import floor
22
import os
33
from os.path import join as pjoin
4-
from tempfile import mkdtemp
54
from warnings import warn
65

76
import numpy as np
@@ -21,7 +20,7 @@
2120

2221
from . import utils, io
2322
from .utils import (Surface, verbose, create_color_lut, _get_subjects_dir,
24-
string_types, assert_ffmpeg_is_available, ffmpeg)
23+
string_types)
2524

2625

2726
import logging
@@ -730,6 +729,35 @@ def _get_display_range(self, scalar_data, min, max, sign):
730729

731730
return min, max
732731

732+
def _iter_time(self, time_idx, interpolation):
733+
"""Iterate through time points, then reset to current time
734+
735+
Parameters
736+
----------
737+
time_idx : array_like
738+
Time point indexes through which to iterate.
739+
interpolation : str
740+
Interpolation method (``scipy.interpolate.interp1d`` parameter,
741+
one of 'linear' | 'nearest' | 'zero' | 'slinear' | 'quadratic' |
742+
'cubic'). Interpolation is only used for non-integer indexes.
743+
744+
Yields
745+
------
746+
idx : int | float
747+
Current index.
748+
749+
Notes
750+
-----
751+
Used by movie and image sequence saving functions.
752+
"""
753+
current_time_idx = self.data_time_index
754+
for idx in time_idx:
755+
self.set_data_time_index(idx, interpolation)
756+
yield idx
757+
758+
# Restore original time index
759+
self.set_data_time_index(current_time_idx)
760+
733761
###########################################################################
734762
# ADDING DATA PLOTS
735763
def add_overlay(self, source, min=2, max="robust_max", sign="abs",
@@ -2041,12 +2069,9 @@ def save_image_sequence(self, time_idx, fname_pattern, use_abs_idx=True,
20412069
images_written: list
20422070
all filenames written
20432071
"""
2044-
current_time_idx = self.data_time_index
20452072
images_written = list()
2046-
rel_pos = 0
2047-
for idx in time_idx:
2048-
self.set_data_time_index(idx, interpolation)
2049-
fname = fname_pattern % (idx if use_abs_idx else rel_pos)
2073+
for i, idx in enumerate(self._iter_time(time_idx, interpolation)):
2074+
fname = fname_pattern % (idx if use_abs_idx else i)
20502075
if montage == 'single':
20512076
self.save_single_image(fname, row, col)
20522077
elif montage == 'current':
@@ -2055,10 +2080,6 @@ def save_image_sequence(self, time_idx, fname_pattern, use_abs_idx=True,
20552080
self.save_montage(fname, montage, 'h', border_size, colorbar,
20562081
row, col)
20572082
images_written.append(fname)
2058-
rel_pos += 1
2059-
2060-
# Restore original time index
2061-
self.set_data_time_index(current_time_idx)
20622083

20632084
return images_written
20642085

@@ -2175,11 +2196,20 @@ def save_movie(self, fname, time_dilation=4., tmin=None, tmax=None,
21752196
21762197
Notes
21772198
-----
2178-
This method requires FFmpeg to be installed in the system PATH. FFmpeg
2179-
is free and can be obtained from `here
2180-
<http://ffmpeg.org/download.html>`_.
2199+
Requires imageio package, which can be installed together with
2200+
PySurfer with::
2201+
2202+
$ pip install -U pysurfer[save_movie]
21812203
"""
2182-
assert_ffmpeg_is_available()
2204+
try:
2205+
import imageio
2206+
except ImportError:
2207+
raise ImportError("Saving movies from PySurfer requires the "
2208+
"imageio library. To install imageio with pip, "
2209+
"run\n\n $ pip install imageio\n\nTo "
2210+
"install/update PySurfer and imageio together, "
2211+
"run\n\n $ pip install -U "
2212+
"pysurfer[save_movie]\n")
21832213

21842214
if tmin is None:
21852215
tmin = self._times[0]
@@ -2206,12 +2236,10 @@ def save_movie(self, fname, time_dilation=4., tmin=None, tmax=None,
22062236

22072237
logger.debug("Save movie for time points/samples\n%s\n%s"
22082238
% (times, time_idx))
2209-
tempdir = mkdtemp()
2210-
frame_pattern = 'frame%%0%id.png' % (np.floor(np.log10(n_times)) + 1)
2211-
fname_pattern = os.path.join(tempdir, frame_pattern)
2212-
self.save_image_sequence(time_idx, fname_pattern, False, -1, -1,
2213-
'current', interpolation=interpolation)
2214-
ffmpeg(fname, fname_pattern, framerate, codec=codec, bitrate=bitrate)
2239+
images = (self.screenshot() for _ in
2240+
self._iter_time(time_idx, interpolation))
2241+
imageio.mimwrite(fname, images, fps=framerate, codec=codec,
2242+
bitrate=bitrate)
22152243

22162244
def animate(self, views, n_steps=180., fname=None, use_cache=False,
22172245
row=-1, col=-1):

0 commit comments

Comments
 (0)