16

I try to generate a movie using the matplotlib movie writer. If I do that, I always get a white margin around the video. Has anyone an idea how to remove that margin?

Adjusted example from http://matplotlib.org/examples/animation/moviewriter.html

# This example uses a MovieWriter directly to grab individual frames and
# write them to a file. This avoids any event loop integration, but has
# the advantage of working with even the Agg backend. This is not recommended
# for use in an interactive setting.
# -*- noplot -*-

import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.animation as manimation

FFMpegWriter = manimation.writers['ffmpeg']
metadata = dict(title='Movie Test', artist='Matplotlib',
        comment='Movie support!')
writer = FFMpegWriter(fps=15, metadata=metadata, extra_args=['-vcodec', 'libx264'])

fig = plt.figure()
ax = plt.subplot(111)
plt.axis('off')
fig.subplots_adjust(left=None, bottom=None, right=None, wspace=None, hspace=None)
ax.set_frame_on(False)
ax.set_xticks([])
ax.set_yticks([])
plt.axis('off')

with writer.saving(fig, "writer_test.mp4", 100):
    for i in range(100):
        mat = np.random.random((100,100))
        ax.imshow(mat,interpolation='nearest')
        writer.grab_frame()
P.R.
  • 3,785
  • 1
  • 27
  • 47

4 Answers4

22

Passing None as an arguement to subplots_adjust does not do what you think it does (doc). It means 'use the deault value'. To do what you want use the following instead:

fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=None, hspace=None)

You can also make your code much more efficent if you re-use your ImageAxes object

mat = np.random.random((100,100))
im = ax.imshow(mat,interpolation='nearest')
with writer.saving(fig, "writer_test.mp4", 100):
    for i in range(100):
        mat = np.random.random((100,100))
        im.set_data(mat)
        writer.grab_frame()

By default imshow fixes the aspect ratio to be equal, that is so your pixels are square. You either need to re-size your figure to be the same aspect ratio as your images:

fig.set_size_inches(w, h, forward=True)

or tell imshow to use an arbitrary aspect ratio

im = ax.imshow(..., aspect='auto')
tacaswell
  • 84,579
  • 22
  • 210
  • 199
  • 2
    Using your `subplots_adjust` only removes the margin on the top and bottom. I still have a white margin on the right and left of the video. Also somehow I could not do `im.set_cdata(mat)`. I got the error: `AttributeError: 'AxesImage' object has no attribute 'set_cdata'` – P.R. Apr 09 '13 at 11:04
  • awesome, that solved my problem. There are no margins left, and using `set_data` makes the whole process much faster indeed! – P.R. Apr 09 '13 at 15:23
  • your additional comment "You can also make your code much more efficient if you re-use your ImageAxes object" really helped me. Due to this change program used less memory during animation. – The Imp May 12 '20 at 09:50
3

I searched all day for this and ended up using this solution from @matehat when creating each image.

import matplotlib.pyplot as plt
import matplotlib.animation as animation

To make a figure without the frame :

fig = plt.figure(frameon=False)
fig.set_size_inches(w,h)

To make the content fill the whole figure

ax = plt.Axes(fig, [0., 0., 1., 1.])
ax.set_axis_off()
fig.add_axes(ax)

Draw the first frame, assuming your movie is stored in 'imageStack':

movieImage = ax.imshow(imageStack[0], aspect='auto')

I then wrote an animation function:

def animate(i):
    movieImage.set_array(imageStack[i])
    return movieImage

anim = animation.FuncAnimation(fig,animate,frames=len(imageStack),interval=100)
anim.save('myMovie.mp4',fps=20,extra_args=['-vcodec','libx264']

It worked beautifully!

Here is the link to the whitespace removal solution:

1: remove whitespace from image

Bevans18
  • 31
  • 2
  • I'd recommend to point out exactly the part that helped you and/or answers the user's question, and then reference the link :) – oetoni Feb 21 '20 at 23:23
1

In a recent build of matplotlib, it looks like you can pass arguments to the writer:

def grab_frame(self, **savefig_kwargs):
        '''
        Grab the image information from the figure and save as a movie frame.
        All keyword arguments in savefig_kwargs are passed on to the 'savefig'
        command that saves the figure.
        '''
        verbose.report('MovieWriter.grab_frame: Grabbing frame.',
                       level='debug')
        try:
            # Tell the figure to save its data to the sink, using the
            # frame format and dpi.
            self.fig.savefig(self._frame_sink(), format=self.frame_format,
                dpi=self.dpi, **savefig_kwargs)
        except RuntimeError:
            out, err = self._proc.communicate()
            verbose.report('MovieWriter -- Error running proc:\n%s\n%s' % (out,
                err), level='helpful')
            raise

If this was the case, you could pass bbox_inches="tight" and pad_inches=0 to grab_frame -> savefig and this should remove most of the border. The most up to date version on Ubuntu however, still has this code:

def grab_frame(self):
    '''
    Grab the image information from the figure and save as a movie frame.
    '''
    verbose.report('MovieWriter.grab_frame: Grabbing frame.',
                   level='debug')
    try:
        # Tell the figure to save its data to the sink, using the
        # frame format and dpi.
        self.fig.savefig(self._frame_sink(), format=self.frame_format,
            dpi=self.dpi)
    except RuntimeError:
        out, err = self._proc.communicate()
        verbose.report('MovieWriter -- Error running proc:\n%s\n%s' % (out,
            err), level='helpful')
        raise

So it looks like the functionality is being put in. Grab this version and give it a shot!

Hooked
  • 84,485
  • 43
  • 192
  • 261
  • I have had really bad experience with `bbox_inches='tight'` as the pixel size of the grabbed frames and the pixelsize of the writer were configured with don't match and get a movie of garbage. If you want to test this, set the `bbox_inches` value via rcparam. – tacaswell Apr 08 '13 at 15:44
  • @tcaswell Interesting, that sounds like a bug and should be reported. I was unable to test with the version on this computer so I tried to divine an answer from the source. – Hooked Apr 08 '13 at 15:54
  • It is not a bug, it is just a bad interaction between mp4s requiring a fixed frame size in pixels and mpl generating varying size images when it uses the tight bounding box. Controlling this better would (I think) require breaking down some of the nice abstraction barriers mpl has between the user and rendering details. – tacaswell Apr 08 '13 at 15:59
  • I just updated to matplotlib 1.2.1 via pip. Its not included there yet. I am a bit anxious to update to git HEAD, as I need the plotting to be stable. I will wait and try at the next version. – P.R. Apr 09 '13 at 11:15
  • @P.R. The alternative is to forgo the matplotlib animation writer and make the movie yourself. It really isn't too hard (esp if you're using a Unix distribution), you simply have to generate the movie as images sequentially and run something like `ffmpeg` over them. At that level you'll have complete control. – Hooked Apr 09 '13 at 13:33
  • yeah, I will do that now. I just thought, maybe the matplotlib approach is a bit more efficient. But as I see it, under the hood it is doing the same thing anyway. – P.R. Apr 09 '13 at 13:42
0

If you "just" want to save a matshow/imshow rendering of a matrix without axis annotation then newest developer version of scikit-video (skvideo) may also be relevant, - if you have avconv installed. An example in the distribution shows a dynamic image constructed from numpy function: https://github.com/aizvorski/scikit-video/blob/master/skvideo/examples/test_writer.py

Here is my modification of the example:

# Based on https://github.com/aizvorski/scikit-video/blob/master/skvideo/examples/test_writer.py
from __future__ import print_function

from skvideo.io import VideoWriter
import numpy as np

w, h = 640, 480

checkerboard = np.tile(np.kron(np.array([[0, 1], [1, 0]]), np.ones((30, 30))), (30, 30))
checkerboard = checkerboard[:h, :w]

filename = 'checkerboard.mp4'
wr = VideoWriter(filename, frameSize=(w, h), fps=8)

wr.open()
for frame_num in range(300):
    checkerboard = 1 - checkerboard
    image = np.tile(checkerboard[:, :, np.newaxis] * 255, (1, 1, 3))
    wr.write(image)
    print("frame %d" % (frame_num))

wr.release()
print("done")
Finn Årup Nielsen
  • 6,130
  • 1
  • 33
  • 43