3333
3434#Needs:
3535# - 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+
3943
4044# A registry for available MovieWriter classes
4145class MovieWriterRegistry (object ):
@@ -55,6 +59,7 @@ def wrapper(writerClass):
5559 return wrapper
5660
5761 def list (self ):
62+ ''' Get a list of available MovieWriters.'''
5863 return self .avail .keys ()
5964
6065 def __getitem__ (self , name ):
@@ -65,7 +70,47 @@ def __getitem__(self, name):
6570writers = MovieWriterRegistry ()
6671
6772class 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+ '''
6892 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+ '''
69114 self .fps = fps
70115 self .frame_format = 'rgba'
71116
@@ -86,17 +131,41 @@ def __init__(self, fps=5, codec=None, bitrate=None, extra_args=None):
86131
87132 @property
88133 def frame_size (self ):
134+ 'A tuple (width,height) in pixels of a movie frame.'
89135 width_inches , height_inches = self .fig .get_size_inches ()
90136 return width_inches * self .dpi , height_inches * self .dpi
91137
92138 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+ '''
93153 self .outfile = outfile
94154 self .fig = fig
95155 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.
96159 self ._run ()
97160
98161 @contextlib .contextmanager
99162 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
100169 self .setup (* args )
101170 yield
102171 self .finish ()
@@ -105,18 +174,24 @@ def _run(self):
105174 # Uses subprocess to call the program for assembling frames into a
106175 # movie file. *args* returns the sequence of command line arguments
107176 # from a few configuration options.
108- command = self .args ()
177+ command = self ._args ()
109178 verbose .report ('MovieWriter.run: running command: %s' % ' ' .join (command ))
110179 self ._proc = subprocess .Popen (command , shell = False ,
111180 stdout = subprocess .PIPE , stderr = subprocess .PIPE ,
112181 stdin = subprocess .PIPE )
113182
114183 def finish (self ):
184+ 'Finish any processing for writing the movie.'
115185 self .cleanup ()
116186
117187 def grab_frame (self ):
188+ '''
189+ Grab the image information from the figure and save as a movie frame.
190+ '''
118191 verbose .report ('MovieWriter.grab_frame: Grabbing frame.' , level = 'debug' )
119192 try :
193+ # Tell the figure to save its data to the sink, using the
194+ # frame format and dpi.
120195 self .fig .savefig (self ._frame_sink (), format = self .frame_format ,
121196 dpi = self .dpi )
122197 except RuntimeError :
@@ -126,12 +201,15 @@ def grab_frame(self):
126201 raise
127202
128203 def _frame_sink (self ):
204+ 'Returns the place to which frames should be written.'
129205 return self ._proc .stdin
130206
131- def args (self ):
207+ def _args (self ):
208+ 'Assemble list of utility-specific command-line arguments.'
132209 return NotImplementedError ("args needs to be implemented by subclass." )
133210
134211 def cleanup (self ):
212+ 'Clean-up and collect the process used to write the movie file.'
135213 out ,err = self ._proc .communicate ()
136214 verbose .report ('MovieWriter -- Command stdout:\n %s' % out ,
137215 level = 'debug' )
@@ -140,10 +218,19 @@ def cleanup(self):
140218
141219 @classmethod
142220 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+ '''
143226 return rcParams [cls .exec_key ]
144227
145228 @classmethod
146229 def isAvailable (cls ):
230+ '''
231+ Check to see if a MovieWriter subclass is actually available by
232+ running the commandline tool.
233+ '''
147234 try :
148235 subprocess .Popen (cls .bin_path (), shell = False ,
149236 stdout = subprocess .PIPE , stderr = subprocess .PIPE )
@@ -153,23 +240,47 @@ def isAvailable(cls):
153240
154241
155242class FileMovieWriter (MovieWriter ):
243+ '`MovieWriter` subclass that handles writing to a file.'
156244 def __init__ (self , * args ):
157245 MovieWriter .__init__ (self , * args )
158246 self .frame_format = rcParams ['animation.frame_format' ]
159247
160248 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+ '''
162269 self .fig = fig
163270 self .outfile = outfile
164271 self .dpi = dpi
165272 self .clear_temp = clear_temp
166273 self .temp_prefix = frame_prefix
167- self ._frame_counter = 0
274+ self ._frame_counter = 0 # used for generating sequential file names
168275 self ._temp_names = list ()
169276 self .fname_format_str = '%s%%04d.%s'
170277
171278 @property
172279 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+ '''
173284 return self ._frame_format
174285
175286 @frame_format .setter
@@ -180,27 +291,36 @@ def frame_format(self, frame_format):
180291 self ._frame_format = self .supported_formats [0 ]
181292
182293 def _base_temp_name (self ):
294+ # Generates a template name (without number) given the frame format
295+ # for extension and the prefix.
183296 return self .fname_format_str % (self .temp_prefix , self .frame_format )
184297
185298 def _frame_sink (self ):
299+ # Creates a filename for saving using the basename and the current
300+ # counter.
186301 fname = self ._base_temp_name () % self ._frame_counter
302+
303+ # Save the filename so we can delete it later if necessary
187304 self ._temp_names .append (fname )
188305 verbose .report (
189306 'FileMovieWriter.frame_sink: saving frame %d to fname=%s' % (self ._frame_counter , fname ),
190307 level = 'debug' )
191- self ._frame_counter += 1
308+ self ._frame_counter += 1 # Ensures each created name is 'unique'
192309
193310 # This file returned here will be closed once it's used by savefig()
194311 # because it will no longer be referenced and will be gc-ed.
195312 return open (fname , 'wb' )
196313
197314 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.
199317 self ._run ()
200- MovieWriter .finish (self )
318+ MovieWriter .finish (self ) # Will call clean-up
201319
202320 def cleanup (self ):
203321 MovieWriter .cleanup (self )
322+
323+ #Delete temporary files
204324 if self .clear_temp :
205325 import os
206326 verbose .report (
@@ -210,6 +330,8 @@ def cleanup(self):
210330 os .remove (fname )
211331
212332
333+ # Base class of ffmpeg information. Has the config keys and the common set
334+ # of arguments that controls the *output* side of things.
213335class FFMpegBase :
214336 exec_key = 'animation.ffmpeg_path'
215337 args_key = 'animation.ffmpeg_args'
@@ -226,26 +348,30 @@ def output_args(self):
226348 return args + ['-y' , self .outfile ]
227349
228350
351+ # Combine FFMpeg options with pipe-based writing
229352@writers .register ('ffmpeg' )
230353class FFMpegWriter (MovieWriter , FFMpegBase ):
231- def args (self ):
354+ def _args (self ):
232355 # Returns the command line parameters for subprocess to use
233- # ffmpeg to create a movie
356+ # ffmpeg to create a movie using a pipe
234357 return [self .bin_path (), '-f' , 'rawvideo' , '-vcodec' , 'rawvideo' ,
235358 '-s' , '%dx%d' % self .frame_size , '-pix_fmt' , self .frame_format ,
236359 '-r' , str (self .fps ), '-i' , 'pipe:' ] + self .output_args
237360
238361
362+ #Combine FFMpeg options with temp file-based writing
239363@writers .register ('ffmpeg_file' )
240364class FFMpegFileWriter (FileMovieWriter , FFMpegBase ):
241365 supported_formats = ['png' , 'jpeg' , 'ppm' , 'tiff' , 'sgi' , 'bmp' , 'pbm' , 'raw' , 'rgba' ]
242- def args (self ):
366+ def _args (self ):
243367 # 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
245369 return [self .bin_path (), '-r' , str (self .fps ), '-i' ,
246370 self ._base_temp_name ()] + self .output_args
247371
248372
373+ # Base class of mencoder information. Contains configuration key information
374+ # as well as arguments for controlling *output*
249375class MencoderBase :
250376 exec_key = 'animation.mencoder_path'
251377 args_key = 'animation.mencoder_args'
@@ -260,20 +386,22 @@ def output_args(self):
260386 return args
261387
262388
389+ # Combine Mencoder options with pipe-based writing
263390@writers .register ('mencoder' )
264391class MencoderWriter (MovieWriter , MencoderBase ):
265- def args (self ):
392+ def _args (self ):
266393 # Returns the command line parameters for subprocess to use
267394 # mencoder to create a movie
268395 return [self .bin_path (), '-' , '-demuxer' , 'rawvideo' , '-rawvideo' ,
269396 ('w=%i:h=%i:' % self .frame_size +
270397 'fps=%i:format=%s' % (self .fps , self .frame_format ))] + self .output_args
271398
272399
400+ # Combine Mencoder options with temp file-based writing
273401@writers .register ('mencoder_file' )
274402class MencoderFileWriter (FileMovieWriter , MencoderBase ):
275403 supported_formats = ['png' , 'jpeg' , 'tga' , 'sgi' ]
276- def args (self ):
404+ def _args (self ):
277405 # Returns the command line parameters for subprocess to use
278406 # mencoder to create a movie
279407 return [self .bin_path (),
0 commit comments