Skip to content

Commit

Permalink
Merge pull request #154 from christianbrodbeck/imageio
Browse files Browse the repository at this point in the history
[WIP] Imageio
  • Loading branch information
larsoner committed Jun 27, 2016
2 parents 40621da + f219021 commit 89056ab
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 134 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
5 changes: 5 additions & 0 deletions doc/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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::

Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,5 @@ def check_dependencies():
packages=['surfer', 'surfer.tests'],
scripts=['bin/pysurfer'],
install_requires=['nibabel >= 1.2'],
extras_require={'save_movie': ['imageio >= 1.5']},
)
35 changes: 17 additions & 18 deletions surfer/tests/test_viz.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
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'
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
102 changes: 9 additions & 93 deletions surfer/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from os import path as op
import inspect
from functools import wraps
import subprocess

import numpy as np
import nibabel as nib
Expand Down Expand Up @@ -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")
70 changes: 49 additions & 21 deletions surfer/viz.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
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
Expand All @@ -21,7 +20,7 @@

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
Expand Down Expand Up @@ -730,6 +729,35 @@ def _get_display_range(self, scalar_data, min, max, sign):

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",
Expand Down Expand Up @@ -2041,12 +2069,9 @@ def save_image_sequence(self, time_idx, fname_pattern, use_abs_idx=True,
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':
Expand All @@ -2055,10 +2080,6 @@ def save_image_sequence(self, time_idx, fname_pattern, use_abs_idx=True,
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

Expand Down Expand Up @@ -2175,11 +2196,20 @@ def save_movie(self, fname, time_dilation=4., tmin=None, tmax=None,
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]
Expand All @@ -2206,12 +2236,10 @@ def save_movie(self, fname, time_dilation=4., tmin=None, tmax=None,

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):
Expand Down

0 comments on commit 89056ab

Please sign in to comment.