Commit 89056ab9 authored by Eric Larson's avatar Eric Larson Committed by GitHub

Merge pull request #154 from christianbrodbeck/imageio

[WIP] Imageio
parents 40621da6 f2190219
......@@ -28,7 +28,7 @@ install:
travis_retry sudo apt-get update -qq;
travis_retry sudo apt-get install mencoder;
fi;
- pip install -q coverage coveralls nose-timer nibabel flake8
- pip install -q coverage coveralls nose-timer nibabel flake8 imageio
- python setup.py build
- python setup.py install
- SRC_DIR=$(pwd)
......
......@@ -23,7 +23,7 @@ install:
# Install the dependencies of the project.
- "conda install --yes --quiet ipython==1.1.0 numpy scipy mayavi matplotlib nose imaging"
- "pip install -q nose-timer nibabel"
- "pip install -q nose-timer nibabel imageio"
- "python setup.py develop"
- "SET SUBJECTS_DIR=%CD%\\subjects"
......
......@@ -11,6 +11,11 @@ If you already have PySurfer installed, you can also use pip to update it::
pip install -U pysurfer
If you would like to save movies of time course data, also include the
optional dependency `imageio` with::
pip install -U pysurfer[save_movie]
If you'd like to install the development version, you have two options. You can
use pip::
......
......@@ -93,4 +93,5 @@ if __name__ == "__main__":
packages=['surfer', 'surfer.tests'],
scripts=['bin/pysurfer'],
install_requires=['nibabel >= 1.2'],
extras_require={'save_movie': ['imageio >= 1.5']},
)
import numpy as np
import os
import os.path as op
from os.path import join as pjoin
import re
import shutil
import subprocess
from nose.tools import assert_equal
from numpy.testing import assert_raises, assert_array_equal
from tempfile import mkdtemp, mktemp
from nose.tools import assert_equal
from mayavi import mlab
import nibabel as nib
import numpy as np
from numpy.testing import assert_raises, assert_array_equal
from surfer import Brain, io, utils
from surfer.utils import requires_ffmpeg, requires_fsaverage
from mayavi import mlab
from surfer.utils import requires_fsaverage, requires_imageio
subj_dir = utils._get_subjects_dir()
subject_id = 'fsaverage'
......@@ -214,11 +213,13 @@ def test_morphometry():
brain.close()
@requires_ffmpeg
@requires_imageio
@requires_fsaverage
def test_movie():
"""Test saving a movie of an MEG inverse solution
"""
import imageio
# create and setup the Brain instance
mlab.options.backend = 'auto'
brain = Brain(*std_args)
......@@ -234,18 +235,16 @@ def test_movie():
tempdir = mkdtemp()
try:
dst = os.path.join(tempdir, 'test.mov')
# test the number of frames in the movie
brain.save_movie(dst)
frames = imageio.mimread(dst)
assert_equal(len(frames), 2)
brain.save_movie(dst, time_dilation=10)
frames = imageio.mimread(dst)
assert_equal(len(frames), 7)
brain.save_movie(dst, tmin=0.081, tmax=0.102)
# test the number of frames in the movie
sp = subprocess.Popen(('ffmpeg', '-i', 'test.mov', '-vcodec', 'copy',
'-f', 'null', '/dev/null'), cwd=tempdir,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = sp.communicate()
m = re.search('frame=\s*(\d+)\s', stderr)
if not m:
raise RuntimeError(stderr)
n_frames = int(m.group(1))
assert_equal(n_frames, 3)
frames = imageio.mimread(dst)
assert_equal(len(frames), 2)
finally:
# clean up
shutil.rmtree(tempdir)
......
......@@ -5,7 +5,6 @@ import os
from os import path as op
import inspect
from functools import wraps
import subprocess
import numpy as np
import nibabel as nib
......@@ -624,101 +623,18 @@ def has_fsaverage(subjects_dir=None):
return False
return True
requires_fsaverage = np.testing.dec.skipif(not has_fsaverage(),
'Requires fsaverage subject data')
def has_ffmpeg():
"""Test whether the FFmpeg is available in a subprocess
Returns
-------
ffmpeg_exists : bool
True if FFmpeg can be successfully called, False otherwise.
"""
def has_imageio():
try:
subprocess.call(["ffmpeg"], stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
return True
except OSError:
import imageio # NOQA
except ImportError:
return False
else:
return True
def assert_ffmpeg_is_available():
"Raise a RuntimeError if FFmpeg is not in the PATH"
if not has_ffmpeg():
err = ("FFmpeg is not in the path and is needed for saving "
"movies. Install FFmpeg and try again. It can be "
"downlaoded from http://ffmpeg.org/download.html.")
raise RuntimeError(err)
requires_ffmpeg = np.testing.dec.skipif(not has_ffmpeg(), 'Requires FFmpeg')
def ffmpeg(dst, frame_path, framerate=24, codec='mpeg4', bitrate='1M'):
"""Run FFmpeg in a subprocess to convert an image sequence into a movie
Parameters
----------
dst : str
Destination path. If the extension is not ".mov" or ".avi", ".mov" is
added. If the file already exists it is overwritten.
frame_path : str
Path to the source frames (with a frame number field like '%04d').
framerate : float
Framerate of the movie (frames per second, default 24).
codec : str | None
Codec to use (default 'mpeg4'). If None, the codec argument is not
forwarded to ffmpeg, which preserves compatibility with very old
versions of ffmpeg
bitrate : str | float
Bitrate to use to encode movie. Can be specified as number (e.g.
64000) or string (e.g. '64k'). Default value is 1M
requires_fsaverage = np.testing.dec.skipif(not has_fsaverage(),
'Requires fsaverage subject data')
Notes
-----
Requires FFmpeg to be in the path. FFmpeg can be downlaoded from `here
<http://ffmpeg.org/download.html>`_. Stdout and stderr are written to the
logger. If the movie file is not created, a RuntimeError is raised.
"""
assert_ffmpeg_is_available()
# find target path
dst = os.path.expanduser(dst)
dst = os.path.abspath(dst)
root, ext = os.path.splitext(dst)
dirname = os.path.dirname(dst)
if ext not in ['.mov', '.avi']:
dst += '.mov'
if os.path.exists(dst):
os.remove(dst)
elif not os.path.exists(dirname):
os.mkdir(dirname)
frame_dir, frame_fmt = os.path.split(frame_path)
# make the movie
cmd = ['ffmpeg', '-i', frame_fmt, '-r', str(framerate),
'-b:v', str(bitrate)]
if codec is not None:
cmd += ['-c', codec]
cmd += [dst]
logger.info("Running FFmpeg with command: %s", ' '.join(cmd))
sp = subprocess.Popen(cmd, cwd=frame_dir, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
# log stdout and stderr
stdout, stderr = sp.communicate()
std_info = os.linesep.join(("FFmpeg stdout", '=' * 25, stdout))
logger.info(std_info)
if stderr.strip():
err_info = os.linesep.join(("FFmpeg stderr", '=' * 27, stderr))
# FFmpeg prints to stderr in the absence of an error
logger.info(err_info)
# check that movie file is created
if not os.path.exists(dst):
err = ("FFmpeg failed, no file created; see log for more more "
"information.")
raise RuntimeError(err)
requires_imageio = np.testing.dec.skipif(not has_imageio(),
"Requires imageio package")
from math import floor
import os
from os.path import join as pjoin
from tempfile import mkdtemp
from warnings import warn
import numpy as np
......@@ -21,7 +20,7 @@ from traits.api import (HasTraits, Range, Int, Float,
from . import utils, io
from .utils import (Surface, verbose, create_color_lut, _get_subjects_dir,
string_types, assert_ffmpeg_is_available, ffmpeg)
string_types)
import logging
......@@ -730,6 +729,35 @@ class Brain(object):
return min, max
def _iter_time(self, time_idx, interpolation):
"""Iterate through time points, then reset to current time
Parameters
----------
time_idx : array_like
Time point indexes through which to iterate.
interpolation : str
Interpolation method (``scipy.interpolate.interp1d`` parameter,
one of 'linear' | 'nearest' | 'zero' | 'slinear' | 'quadratic' |
'cubic'). Interpolation is only used for non-integer indexes.
Yields
------
idx : int | float
Current index.
Notes
-----
Used by movie and image sequence saving functions.
"""
current_time_idx = self.data_time_index
for idx in time_idx:
self.set_data_time_index(idx, interpolation)
yield idx
# Restore original time index
self.set_data_time_index(current_time_idx)
###########################################################################
# ADDING DATA PLOTS
def add_overlay(self, source, min=2, max="robust_max", sign="abs",
......@@ -2041,12 +2069,9 @@ class Brain(object):
images_written: list
all filenames written
"""
current_time_idx = self.data_time_index
images_written = list()
rel_pos = 0
for idx in time_idx:
self.set_data_time_index(idx, interpolation)
fname = fname_pattern % (idx if use_abs_idx else rel_pos)
for i, idx in enumerate(self._iter_time(time_idx, interpolation)):
fname = fname_pattern % (idx if use_abs_idx else i)
if montage == 'single':
self.save_single_image(fname, row, col)
elif montage == 'current':
......@@ -2055,10 +2080,6 @@ class Brain(object):
self.save_montage(fname, montage, 'h', border_size, colorbar,
row, col)
images_written.append(fname)
rel_pos += 1
# Restore original time index
self.set_data_time_index(current_time_idx)
return images_written
......@@ -2175,11 +2196,20 @@ class Brain(object):
Notes
-----
This method requires FFmpeg to be installed in the system PATH. FFmpeg
is free and can be obtained from `here
<http://ffmpeg.org/download.html>`_.
Requires imageio package, which can be installed together with
PySurfer with::
$ pip install -U pysurfer[save_movie]
"""
assert_ffmpeg_is_available()
try:
import imageio
except ImportError:
raise ImportError("Saving movies from PySurfer requires the "
"imageio library. To install imageio with pip, "
"run\n\n $ pip install imageio\n\nTo "
"install/update PySurfer and imageio together, "
"run\n\n $ pip install -U "
"pysurfer[save_movie]\n")
if tmin is None:
tmin = self._times[0]
......@@ -2206,12 +2236,10 @@ class Brain(object):
logger.debug("Save movie for time points/samples\n%s\n%s"
% (times, time_idx))
tempdir = mkdtemp()
frame_pattern = 'frame%%0%id.png' % (np.floor(np.log10(n_times)) + 1)
fname_pattern = os.path.join(tempdir, frame_pattern)
self.save_image_sequence(time_idx, fname_pattern, False, -1, -1,
'current', interpolation=interpolation)
ffmpeg(fname, fname_pattern, framerate, codec=codec, bitrate=bitrate)
images = (self.screenshot() for _ in
self._iter_time(time_idx, interpolation))
imageio.mimwrite(fname, images, fps=framerate, codec=codec,
bitrate=bitrate)
def animate(self, views, n_steps=180., fname=None, use_cache=False,
row=-1, col=-1):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment