33
33
34
34
#Needs:
35
35
# - Need comments, docstrings
36
- # - Need to look at codecs
37
- # - Is there a common way to add metadata?
38
- # - Should refactor the way we get frames to save to simplify saving from multiple figures
36
+ # - Is there a common way to add metadata? Yes. Could probably just pass
37
+ # in a dict and assemble the string from there.
38
+ # - Examples showing:
39
+ # - Passing in a MovieWriter instance
40
+ # - Using a movie writer's context manager to make a movie using only Agg,
41
+ # no GUI toolkit.
42
+
39
43
40
44
# A registry for available MovieWriter classes
41
45
class MovieWriterRegistry (object ):
@@ -55,6 +59,7 @@ def wrapper(writerClass):
55
59
return wrapper
56
60
57
61
def list (self ):
62
+ ''' Get a list of available MovieWriters.'''
58
63
return self .avail .keys ()
59
64
60
65
def __getitem__ (self , name ):
@@ -65,7 +70,47 @@ def __getitem__(self, name):
65
70
writers = MovieWriterRegistry ()
66
71
67
72
class MovieWriter (object ):
73
+ '''
74
+ Base class for writing movies. Fundamentally, what a MovieWriter does
75
+ is provide is a way to grab frames by calling grab_frame(). setup()
76
+ is called to start the process and finish() is called afterwards.
77
+ This class is set up to provide for writing movie frame data to a pipe.
78
+ saving() is provided as a context manager to facilitate this process as::
79
+
80
+ `` with moviewriter.saving('myfile.mp4'):
81
+ # Iterate over frames
82
+ moviewriter.grab_frame()``
83
+
84
+ The use of the context manager ensures that setup and cleanup are
85
+ performed as necessary.
86
+
87
+ Attributes
88
+ ----------
89
+ frame_format: string
90
+ The format used in writing frame data, defaults to 'rgba'
91
+ '''
68
92
def __init__ (self , fps = 5 , codec = None , bitrate = None , extra_args = None ):
93
+ '''
94
+ Construct a new MovieWriter object.
95
+
96
+ Parameters
97
+ ----------
98
+ fps: int
99
+ Framerate for movie.
100
+ codec: string or None, optional
101
+ The codec to use. If None (the default) the setting in the
102
+ rcParam `animation.codec` is used.
103
+ bitrate: int or None, optional
104
+ The bitrate for the saved movie file, which is one way to control
105
+ the output file size and quality. The default value is None,
106
+ which uses the value stored in the rcParam `animation.bitrate`.
107
+ A value of -1 implies that the bitrate should be determined
108
+ automatically by the underlying utility.
109
+ extra_args: list of strings or None
110
+ A list of extra string arguments to be passed to the underlying
111
+ movie utiltiy. The default is None, which passes the additional
112
+ argurments in the 'animation.extra_args' rcParam.
113
+ '''
69
114
self .fps = fps
70
115
self .frame_format = 'rgba'
71
116
@@ -86,17 +131,41 @@ def __init__(self, fps=5, codec=None, bitrate=None, extra_args=None):
86
131
87
132
@property
88
133
def frame_size (self ):
134
+ 'A tuple (width,height) in pixels of a movie frame.'
89
135
width_inches , height_inches = self .fig .get_size_inches ()
90
136
return width_inches * self .dpi , height_inches * self .dpi
91
137
92
138
def setup (self , fig , outfile , dpi , * args ):
139
+ '''
140
+ Perform setup for writing the movie file.
141
+
142
+ Parameters
143
+ ----------
144
+
145
+ fig: `matplotlib.Figure` instance
146
+ The figure object that contains the information for frames
147
+ outfile: string
148
+ The filename of the resulting movie file
149
+ dpi: int
150
+ The DPI (or resolution) for the file. This controls the size
151
+ in pixels of the resulting movie file.
152
+ '''
93
153
self .outfile = outfile
94
154
self .fig = fig
95
155
self .dpi = dpi
156
+
157
+ # Run here so that grab_frame() can write the data to a pipe. This
158
+ # eliminates the need for temp files.
96
159
self ._run ()
97
160
98
161
@contextlib .contextmanager
99
162
def saving (self , * args ):
163
+ '''
164
+ Context manager to facilitate writing the movie file.
165
+
166
+ *args are any parameters that should be passed to setup()
167
+ '''
168
+ # This particular sequence is what contextlib.contextmanager wants
100
169
self .setup (* args )
101
170
yield
102
171
self .finish ()
@@ -105,18 +174,24 @@ def _run(self):
105
174
# Uses subprocess to call the program for assembling frames into a
106
175
# movie file. *args* returns the sequence of command line arguments
107
176
# from a few configuration options.
108
- command = self .args ()
177
+ command = self ._args ()
109
178
verbose .report ('MovieWriter.run: running command: %s' % ' ' .join (command ))
110
179
self ._proc = subprocess .Popen (command , shell = False ,
111
180
stdout = subprocess .PIPE , stderr = subprocess .PIPE ,
112
181
stdin = subprocess .PIPE )
113
182
114
183
def finish (self ):
184
+ 'Finish any processing for writing the movie.'
115
185
self .cleanup ()
116
186
117
187
def grab_frame (self ):
188
+ '''
189
+ Grab the image information from the figure and save as a movie frame.
190
+ '''
118
191
verbose .report ('MovieWriter.grab_frame: Grabbing frame.' , level = 'debug' )
119
192
try :
193
+ # Tell the figure to save its data to the sink, using the
194
+ # frame format and dpi.
120
195
self .fig .savefig (self ._frame_sink (), format = self .frame_format ,
121
196
dpi = self .dpi )
122
197
except RuntimeError :
@@ -126,12 +201,15 @@ def grab_frame(self):
126
201
raise
127
202
128
203
def _frame_sink (self ):
204
+ 'Returns the place to which frames should be written.'
129
205
return self ._proc .stdin
130
206
131
- def args (self ):
207
+ def _args (self ):
208
+ 'Assemble list of utility-specific command-line arguments.'
132
209
return NotImplementedError ("args needs to be implemented by subclass." )
133
210
134
211
def cleanup (self ):
212
+ 'Clean-up and collect the process used to write the movie file.'
135
213
out ,err = self ._proc .communicate ()
136
214
verbose .report ('MovieWriter -- Command stdout:\n %s' % out ,
137
215
level = 'debug' )
@@ -140,10 +218,19 @@ def cleanup(self):
140
218
141
219
@classmethod
142
220
def bin_path (cls ):
221
+ '''
222
+ Returns the binary path to the commandline tool used by a specific
223
+ subclass. This is a class method so that the tool can be looked for
224
+ before making a particular MovieWriter subclass available.
225
+ '''
143
226
return rcParams [cls .exec_key ]
144
227
145
228
@classmethod
146
229
def isAvailable (cls ):
230
+ '''
231
+ Check to see if a MovieWriter subclass is actually available by
232
+ running the commandline tool.
233
+ '''
147
234
try :
148
235
subprocess .Popen (cls .bin_path (), shell = False ,
149
236
stdout = subprocess .PIPE , stderr = subprocess .PIPE )
@@ -153,23 +240,47 @@ def isAvailable(cls):
153
240
154
241
155
242
class FileMovieWriter (MovieWriter ):
243
+ '`MovieWriter` subclass that handles writing to a file.'
156
244
def __init__ (self , * args ):
157
245
MovieWriter .__init__ (self , * args )
158
246
self .frame_format = rcParams ['animation.frame_format' ]
159
247
160
248
def setup (self , fig , outfile , dpi , frame_prefix = '_tmp' , clear_temp = True ):
161
- print fig , outfile , dpi , frame_prefix , clear_temp
249
+ '''
250
+ Perform setup for writing the movie file.
251
+
252
+ Parameters
253
+ ----------
254
+
255
+ fig: `matplotlib.Figure` instance
256
+ The figure object that contains the information for frames
257
+ outfile: string
258
+ The filename of the resulting movie file
259
+ dpi: int
260
+ The DPI (or resolution) for the file. This controls the size
261
+ in pixels of the resulting movie file.
262
+ frame_prefix: string, optional
263
+ The filename prefix to use for the temporary files. Defaults
264
+ to '_tmp'
265
+ clear_temp: bool
266
+ Specifies whether the temporary files should be deleted after
267
+ the movie is written. (Useful for debugging.) Defaults to True.
268
+ '''
162
269
self .fig = fig
163
270
self .outfile = outfile
164
271
self .dpi = dpi
165
272
self .clear_temp = clear_temp
166
273
self .temp_prefix = frame_prefix
167
- self ._frame_counter = 0
274
+ self ._frame_counter = 0 # used for generating sequential file names
168
275
self ._temp_names = list ()
169
276
self .fname_format_str = '%s%%04d.%s'
170
277
171
278
@property
172
279
def frame_format (self ):
280
+ '''
281
+ Format (png, jpeg, etc.) to use for saving the frames, which can be
282
+ decided by the individual subclasses.
283
+ '''
173
284
return self ._frame_format
174
285
175
286
@frame_format .setter
@@ -180,27 +291,36 @@ def frame_format(self, frame_format):
180
291
self ._frame_format = self .supported_formats [0 ]
181
292
182
293
def _base_temp_name (self ):
294
+ # Generates a template name (without number) given the frame format
295
+ # for extension and the prefix.
183
296
return self .fname_format_str % (self .temp_prefix , self .frame_format )
184
297
185
298
def _frame_sink (self ):
299
+ # Creates a filename for saving using the basename and the current
300
+ # counter.
186
301
fname = self ._base_temp_name () % self ._frame_counter
302
+
303
+ # Save the filename so we can delete it later if necessary
187
304
self ._temp_names .append (fname )
188
305
verbose .report (
189
306
'FileMovieWriter.frame_sink: saving frame %d to fname=%s' % (self ._frame_counter , fname ),
190
307
level = 'debug' )
191
- self ._frame_counter += 1
308
+ self ._frame_counter += 1 # Ensures each created name is 'unique'
192
309
193
310
# This file returned here will be closed once it's used by savefig()
194
311
# because it will no longer be referenced and will be gc-ed.
195
312
return open (fname , 'wb' )
196
313
197
314
def finish (self ):
198
- #Delete temporary files
315
+ # Call run here now that all frame grabbing is done. All temp files
316
+ # are available to be assembled.
199
317
self ._run ()
200
- MovieWriter .finish (self )
318
+ MovieWriter .finish (self ) # Will call clean-up
201
319
202
320
def cleanup (self ):
203
321
MovieWriter .cleanup (self )
322
+
323
+ #Delete temporary files
204
324
if self .clear_temp :
205
325
import os
206
326
verbose .report (
@@ -210,6 +330,8 @@ def cleanup(self):
210
330
os .remove (fname )
211
331
212
332
333
+ # Base class of ffmpeg information. Has the config keys and the common set
334
+ # of arguments that controls the *output* side of things.
213
335
class FFMpegBase :
214
336
exec_key = 'animation.ffmpeg_path'
215
337
args_key = 'animation.ffmpeg_args'
@@ -226,26 +348,30 @@ def output_args(self):
226
348
return args + ['-y' , self .outfile ]
227
349
228
350
351
+ # Combine FFMpeg options with pipe-based writing
229
352
@writers .register ('ffmpeg' )
230
353
class FFMpegWriter (MovieWriter , FFMpegBase ):
231
- def args (self ):
354
+ def _args (self ):
232
355
# Returns the command line parameters for subprocess to use
233
- # ffmpeg to create a movie
356
+ # ffmpeg to create a movie using a pipe
234
357
return [self .bin_path (), '-f' , 'rawvideo' , '-vcodec' , 'rawvideo' ,
235
358
'-s' , '%dx%d' % self .frame_size , '-pix_fmt' , self .frame_format ,
236
359
'-r' , str (self .fps ), '-i' , 'pipe:' ] + self .output_args
237
360
238
361
362
+ #Combine FFMpeg options with temp file-based writing
239
363
@writers .register ('ffmpeg_file' )
240
364
class FFMpegFileWriter (FileMovieWriter , FFMpegBase ):
241
365
supported_formats = ['png' , 'jpeg' , 'ppm' , 'tiff' , 'sgi' , 'bmp' , 'pbm' , 'raw' , 'rgba' ]
242
- def args (self ):
366
+ def _args (self ):
243
367
# Returns the command line parameters for subprocess to use
244
- # ffmpeg to create a movie
368
+ # ffmpeg to create a movie using a collection of temp images
245
369
return [self .bin_path (), '-r' , str (self .fps ), '-i' ,
246
370
self ._base_temp_name ()] + self .output_args
247
371
248
372
373
+ # Base class of mencoder information. Contains configuration key information
374
+ # as well as arguments for controlling *output*
249
375
class MencoderBase :
250
376
exec_key = 'animation.mencoder_path'
251
377
args_key = 'animation.mencoder_args'
@@ -260,20 +386,22 @@ def output_args(self):
260
386
return args
261
387
262
388
389
+ # Combine Mencoder options with pipe-based writing
263
390
@writers .register ('mencoder' )
264
391
class MencoderWriter (MovieWriter , MencoderBase ):
265
- def args (self ):
392
+ def _args (self ):
266
393
# Returns the command line parameters for subprocess to use
267
394
# mencoder to create a movie
268
395
return [self .bin_path (), '-' , '-demuxer' , 'rawvideo' , '-rawvideo' ,
269
396
('w=%i:h=%i:' % self .frame_size +
270
397
'fps=%i:format=%s' % (self .fps , self .frame_format ))] + self .output_args
271
398
272
399
400
+ # Combine Mencoder options with temp file-based writing
273
401
@writers .register ('mencoder_file' )
274
402
class MencoderFileWriter (FileMovieWriter , MencoderBase ):
275
403
supported_formats = ['png' , 'jpeg' , 'tga' , 'sgi' ]
276
- def args (self ):
404
+ def _args (self ):
277
405
# Returns the command line parameters for subprocess to use
278
406
# mencoder to create a movie
279
407
return [self .bin_path (),
0 commit comments