39
$\begingroup$

The good old GIF animation format allows us to set the duration of each individual frame in the animation separately. This is especially useful if some frames in the animation sequence are to remain static for an extended period of time, because such delays don't increase the file size of the GIF. Here is an example:

frames = Table[
   Graphics[
    Text[Style["Slow Down", FontFamily -> "Futura", FontColor -> Blue,
       FontSize -> 48], {0, y}], Background -> Cyan, 
    PlotRange -> {{-1, 1}, {.1, 1.5}}], {y, 1.2, .2, -.1}];
Export["slowDownMovie.gif", frames, 
 "DisplayDurations" -> Append[Range[Length[frames] - 1]/20, 2], 
 "AnimationRepetitions" -> Infinity]

slow down

Even though the last frames are very long in duration, that has no effect on the file size.

Of course one thing GIF doesn't have is a playback control, which allows you to pause and rewind a movie. To combine these two features, it would be nice if there were a movie export format in which one can set individual frame delays (variable frame rate), instead of a global frame rate. The problem with a global frame rate is that you have to duplicate frames if you want them to stay on the screen longer.

I know that the QuickTime movie format officially supports embedding image sequences and giving each image a separate display duration. This is an old feature of the QuickTime container structure that doesn't appear to be used much. Many other formats seem to support it too.

Another motivation: Many people nowadays make movies of slide presentations, which is often quite nice. But sometimes it's also wasteful, if you don't need audio and have just a sequence of stills. The above image-sequence approach (with playhead and rewind) would be a more efficient way of running such presentations with pre-set timing.

So my question is: are any of the available movie export formats in Mathematica able to accept an option analogous to the "DisplayDurations" option that GIF has?

I know how to make such image sequences for QuickTime on the Mac, but only with the help of external function calls using the QTKit (built-in) library. I couldn't find any documented options for this in the export help. If it turns out that there is no platform-independent way of doing this from Mathematica, I'd also be interested in OS-dependent solutions, and I'll post my own approach later.

$\endgroup$
6
  • 1
    $\begingroup$ The second argument of ListAnimate controling the display rate does not have to be a scalar. For example, ListAnimate[Table[Graphics[{Hue[RandomReal[]], Disk[{0, 0}, n]}, PlotRange -> 15], {n, 10}],{1, 5, 1, 10, 1, 1, 1, 10, 1, 1}] works. Similarly, the option AnimationRate can be set as a list (although this throws a an error message that needs to suppressed with Quiet) as in ListAnimate[Range[10], AnimationRate -> {.1, .5, .1, .5}]. $\endgroup$
    – kglr
    Commented Apr 25, 2012 at 5:54
  • $\begingroup$ @kguler Cool, I never tried that. But when I export that ListAnimate output to .mov format, it's blank. Export works if I delete the list of fps. $\endgroup$
    – Jens
    Commented Apr 25, 2012 at 6:02
  • $\begingroup$ Right; I could not make it work with Export either. $\endgroup$
    – kglr
    Commented Apr 25, 2012 at 6:09
  • $\begingroup$ I edited the title a couple of times trying to make it more descriptive. I'm not sure I succeeded. I'm also looking for information to answer your question. $\endgroup$
    – Mr.Wizard
    Commented Apr 25, 2012 at 8:55
  • $\begingroup$ This suggests that most modern formats support variable frame rate in some fashion, but I do not yet know if Mathematica does. $\endgroup$
    – Mr.Wizard
    Commented Apr 25, 2012 at 8:58

1 Answer 1

16
$\begingroup$

This is a solution specifically using the QTKit library in Mac OS X. I'm calling it through the built-in Python interface, using a script that could also be run as a Python module or in standalone mode.

Since the goal here is to make a Mathematica function, I wrapped the call in a Run command inside a function exportMov. This required stuffing the Python code into a string that shows up as a single huge line in the code below (Python is sensitive to the placement of newlines and indentation, so the spaces and \n characters in the string are all intentional). I've used the same technique for calling a Python script on the fly in this answer before.

Edit: The Python code is posted in a more readable and re-usable form on my website. End Edit.

exportMov[name_String, frames_List, delays_: {.03}] := Module[
  {
   outFile = ExpandFileName[name],
   fileNames,
   script ="printf \"from Foundation import NSNumber\nfrom AppKit import NSImage\nfrom QTKit import *\n\nclass QuickTimeError(Exception):\n    @classmethod\n    def from_nserror(cls, nserror):\n        return cls(nserror.userInfo()['NSLocalizedDescription'])\n\ndef createQT(infiles, sequence, durations, outfile):\n    attrs = {QTAddImageCodecType: 'avc1', QTAddImageCodecQuality: NSNumber.numberWithLong_(codecHighQuality)}\n    mov, err = QTMovie.alloc().initToWritableFile_error_(outfile, None)\n    if mov is None:\n        raise QuickTimeError.from_nserror(err)\n    n = len(durations)-1\n    i = 0\n    for index in sequence:\n        img = NSImage.alloc().initWithContentsOfFile_(infiles[sequence[index]])\n        t = durations[i]\n        if i<n:\n            i = i+1\n        else:\n            i = 0\n        time = QTMakeTime(t, 600)\n        mov.addImage_forDuration_withAttributes_(img, time, attrs)\n    mov.updateMovieFile()\n\nif __name__ == '__main__':\n    import os,sys,csv,string\n\n    buildfile = 'buildFile'\n    framedata = csv.reader(open(buildfile, 'rb'), delimiter=' ')\n    imagefiles = []\n    durations = []\n    for row in framedata:\n        if os.path.exists(row[0]):\n            imagefiles.append(row[0])\n            if len(row)>1:\n                durations.append(float(row[1]))\n            else:\n                durations.append(30)\n        else:\n            print row[0]+': File not found'\n    if len(imagefiles)==0:\n        print 'No images found. Movie not created'\n    else:\n        print 'Creating movie from frames'\n        outfileName = os.path.abspath('out.mov')\n        createQT(imagefiles, range(len(durations)), durations, outfileName) \n\" | /usr/bin/python",
   tempDir = 
    FileNameJoin[{$TemporaryDirectory, 
      "MathematicaOutput" <> StringJoin[Map[ToString, DateList[]]]}]
   },
  CreateDirectory[tempDir];
  SetDirectory[tempDir];
  Export["frame00000001.png", frames, "VideoFrames"];
  fileNames = FileNames[];
  Export["buildFile", 
   MapThread[(# <> " " <> ToString[#2]) &, {fileNames, 
     600 PadRight[delays, Length[fileNames], delays]}], "Text"];
  (* Process files *)
  Run[script];
  CopyFile["out.mov", outFile];
  (* Cleanup: *)
  Map[DeleteFile, FileNames[]];
  ResetDirectory[];
  DeleteDirectory[tempDir]
  ]

The example movie for which I defined the frames in the question above can now be exported as follows:

exportMov["slowDownMovie.mov", frames, 
 Append[Range[Length[frames] - 1]/20, 2]]

The frames are first exported as single images into a temporary directory, then assembled into a movie with the name given as the first argument.

The third argument to exportMov is the list of frame durations, and that is of course the whole point of this function. In the above example, I used the same list of durations as for the GIF animation. The list of durations is optional (when omitted, a constant default duration is used). If the list of durations is shorter than the list of frames, then the given durations are repeated cyclically until all frames are used up.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.