clawpack / visclaw

Clawpack visualization tools
http://www.clawpack.org
BSD 3-Clause "New" or "Revised" License
29 stars 47 forks source link

Add option to write out .mp4 file of animations #302

Closed kbarnhart closed 6 months ago

kbarnhart commented 7 months ago

In my pre-5.0 workflows, I've found it very useful to make .mp4s with ffmpeg. (I know that this somewhat duplicates the workflow in the visclaw.make_anim routines 😬 ).

This PR adds the ffmpeg functionality, and constructs the ffmpeg command as

f"ffmpeg {plotdata.ffmpeg_global_options} {plotdata.ffmpeg_input_options} -i frame%4dfig{figno}.png {plotdata.ffmpeg_output_options} {plotdata.ffmpeg_name}moviefig{figno}.mp4"

to mimic the ffmpeg syntax of:

ffmpeg [global_options] {[input_file_options] -i input_url} ... {[output_file_options] output_url} ...

https://ffmpeg.org/ffmpeg.html

I've set the input and output options to a set that (after a moderate amount of fiddling) I found worked for my applications.

Happy to improve it based on feedback.

ketch commented 7 months ago

I often find that using ffmpeg gives me ugly movies (e.g., text in the plots looks bad due to compression artifacts), whereas matplotlib's animation.save() gives movies with no compression artifacts (the text and plot lines are perfectly crisp). I imagine that it's a question of using the right options for ffmpeg (and will also depend on the image format you're converting from, which isn't an issue for animation.save()).

I'm not objecting to this PR, just saying that I personally would never use it as I get bad results from this approach.

rjleveque commented 7 months ago

@kbarnhart: I agree it's good to have something like this, but I think it might be made simpler using what's in animation_tools.py and/or animation.save. Also it would be good if there were another line in the PlotIndex.html file generated with a pointer to the new animation. I can try to play around with this a bit, but might not get too it right away.

@ketch: It seems like animation.save() has to use some MovieWriter, and I see there is e.g. FFMpegWriter and a ImageMagickWriter. Can you be more specific about what you use to get nice plots?

For higher quality animations (and much smaller file sizes) I usually use a separate script that creates the plot and then just modifies the data plotted each frame (the more proper way to use matplotlib.animation rather than re-displaying a bunch of png files as is done in plotpages.py or animation_tools.py. I think this is what @ketch is alluding to also. It's really the re-plotting of png images that gives crappy resolution, not the use of ffmpeg, I think.

There is one example of how to do this for GeoClaw output in the example $CLAW/geoclaw/examples/tsunami/chile2010_fgmax-fgout, see the make_fgout_animation.py script.

I've been meaning to clean up some more examples of this and post them....

kbarnhart commented 7 months ago

Thanks for the input. All I really want/need is the ability to write something like

plotdata.make_mp4=True

and end up with some standalone .mp4 files that have a similar resolution to the .pngs. I can play around with whether there is an easy way to do that with the current animation tools and report back.

ketch commented 7 months ago

Yes, I should have been more precise. For line plots, I've found that nowadays the following gives excellent results with no compression artifacts:

anim = animation.FuncAnimation(fig, plot_function, frames=num_plots+1, repeat=False)
anim.save('movie.mp4')

I think this is also using ffmpeg under the hood, though I haven't checked. It's all a question of what image format is used, what video encoding is used, etc. I used to mess with that directly but now matplotlib seems to do it best automatically.

rjleveque commented 7 months ago

@ketch: The issue with what you suggest is that you have to supply a plot_function that updates the plot from one frame to the next, and presumably you do this for line plots by simply resetting the data used to specify the line from one frame to the next. This works great for simple plots, also for pcolor plots as in the example I pointed to above, which is for a sequence of fgout frames that are each a single array with the same dimensions for every frame, making it easy to replace the data appearing in the plot from one frame to the next.

Sadly it would be much harder if not impossible to do the same thing for pcolor plots showing AMR results, in which each frame shows dozens or hundreds of individual patches and the number and dimensions change from one frame to the next. Hence for complex plots of this nature about the best we can do is animate the sequence of png's generated for each frame, I think. (If someone knows how to do this better, I'm all ears.). Nonetheless, in spite of the poor resolution, it's very useful to make these animations for quickly viewing animations after doing make plots. For higher quality it seems necessary to do something like specifying an fgout fixed grid, which was one of the main motivations for introducing that capability.

kbarnhart commented 6 months ago

I've resuscitated this effort and think I almost have it and this is ready for some additional feedback.

  1. I ditched the ffmpeg call and switched to a 'mp4_movie' boolean in data.py. If this is true, then the animation_tools call for JS movies also includes a mp4 movie.
  2. If mp4_movie is True, I grab the fig size/dpi if set by the user and pass it into the animation writer. This ensures that the resolution of a mp4 and the frames are the same.
  3. I added a movie_name_prefix to data.py
  4. I added an html link for this movie.

I found one issue and have a proposed solution. If the fig size and dpi are set as large, then the embedded JS movie doesn't fit in a browser window nicely. I addressed this by adding an html_movie_dpi attribute to plotdata (default of 100). As long as someone doesn't give fig sizes that are huge, I think this will work.

I consider using the existing html_movie_width attribute (which does not appear to be used). I'd prefer to just specify the width... however, if figsize was not set by the user, I don't know the figsize from within the plotclaw_driver() and thus don't know how to set the dpi to get a width. It didn't make sense to me to reproduce something like L120-L124 of animation_tools.py, so I just settled on using a lower DPI.

rjleveque commented 6 months ago

@kbarnhart: Thanks for continuing to work on this. I'll take a look and think about the figsize issue.