Saturday, January 30, 2010

Automating Conversion of Still Image Sequences to QuickTime Movies

For my current timelapse video project, I collect over 12,000 images almost every day, and need to automate the processing as much as possible.  In my notes on the first video, Mountain View Rainstorm, I explained why I chose to create a QuickTime movie from the still image sequence before video editing, and in this article I explore how to automate the QuickTime movie production from the stills.

I thought this article would be short and simple, but it raised many questions ... "You take the red pill - you stay in Wonderland and I show you how deep the rabbit-hole goes." -- The Matrix 1999.

Try Built-In Automation First

QuickTime Pro already provides a very convenient way to make a movie from a sequence of stills.  If you're only doing this operation occasionally, you don't need to automate it further.  Simply open QuickTime and use the "Open Image Sequence" command.  When it asks you to open a file: give it the first file in your sequence.  For example, if my files are named "MTV_0001" to "MTV_12345", I specify "MTV_0001".

The "Open A File" dialog is also where you specify the framerate of your movie.

Yes, your files must be numbered.  If you're trying to make a movie out of "red.jpg", "orange.jpg", "yellow.jpg" etc. it won't work.  Fix the names.

For most of us that's all we need.  Thanks for stopping by!

Scripting QuickTime Operations

A Google search for "quicktime automate" leads very quickly Luc-Eric Rousseau's 2006 XSIBlog post Automating QuickTime at the Command Line on Windows in which he provides a script to do almost exactly what I want.  Hooray!  But there were a few tricks involved in making it work the way I wanted -- truthfully it took me several days -- and in the process I learned more about QuickTime, its COM interface, and the "Windows Script Host" (WSH) facility.

Using qtMovieFromStills.js

My adaptation of Rousseau's script is called qtMovieFromStills.js.  Like his, it is written in JScript and runs from a Windows command shell using WSH.  Here's how to use the script:
  1. Copy qtMovieFromStills.js from the bottom of this post, paste it into an editor (e.g. Notepad) and save it somewhere convenient -- I keep a "scripts" folder inside my project folder.
  2. Open a command window and run it using WSH, i.e. with the cscript command.
  3. qtMovieFromStills has two required and one optional argument:

    1. Windows file path to the first image in the sequence.
      E.g. "L:\Images\2010-01-19\MTV_0001.jpg"
    2. Windows file path to the desired output file.
      E.g. "L:\Images\MTV_2010-01-19.mov"
    3. Optionally, specify the framerate in frames-per-second.  E.g. "60".  It defaults to 30.  A list of supported frame rates appears in the QuickTime "Open A File" dialog.
  4. So, your shell command looks something like this.
cscript qtMovieFromStills.js L:\Images\01-19\MTV_0001.jpg L:\Images\MV01-19.mov 60

Difficulties

The first problem I ran into turned out to be very simple: I was using relative file paths in the arguments passed to QuickTime and this does not work well.  Apparently the QuickTime components do not recognize your current working directory, nor did the script provide useful error messages.  You must provide full paths (Like "C:\bletch\srcfile.jpg") rather than relative paths to the source and dest files.

The second problem is that the QuickTime export operation will fail beyond a certain number of frames.  And once again there is no useful error message.  The exact number depends on your machine resources -- on my larger machine the export was successful with 9,000 frames but failed with 12,000.  On my laptop it fails for 7,000 frames.  The workaround is to split them into two (or more) folders and generate separate movies.

The original script had little error detection and reporting.  I've added a lot more, but it doesn't yet work as well as I would like.

Automating the Conversion Process

Entering the script command itself is not easier than using the QuickTime GUI.  Where it pays off is in running it repeatedly over a batch of folders or automatically as part of a more general workflow automation.

Here's a little bash shell script I use with Cygwin to process  a series of directories. Note how the Windows-style paths must be escaped to the bash shell.
frameRate=60
basedir="L:\\Graphics\\Images\\SX110\\MTV-TL01"

dirs="2009-12-09 2009-12-10 2009-12-11 2009-12-12 2009-12-14"

script="$basedir\\scripts\\qtMovieFromStills.js"
for d in $dirs; do
  date 
  echo $d
  src="$basedir\\$d\\MTV-TL01_0001.JPG"
  dst="$basedir\\MTV-TL01_$d.mov"
  cscript $script $src $dst $frameRate
done

Internals of qtMovieFromStills.js

If you just want to use the script, you can stop reading here.

At this point, most of the script is error checking and reporting, and unraveling the QuickTime object hierarchy. The essentials are in only two lines.
qtControl.CreateNewMovieFromImages(sourcePath, frameRate, true);
This function does essentially what the QuickTime GUI does: makes a movie out of an image sequence.
qtExporter.BeginExport();
The Exporter takes QuickTime's internal representation of your movie and renders it to a file.  This takes the most time.

The one other piece that's worth some explanation is the way the script saves your export codec settings for reuse. They are saved as an opaque blob of binary data in the XML file:
C:\qtMovieFromStillsCodecInfo.xml
Whenever you want to change the settings, just delete the file.  Details on how this works are discussed by Cromie in a post to the quicktime-api mail list, as well as by Rousseau in his blog post.

References

Source Code: qtMovieFromStills.js

Copy the code and paste it into an editor. Save it as qtMovieFromStills.js.
// Windows Script Host JScript
// Create a QuickTime movie from a sequence of still images.
// Run from the command line on Windows using WSH:
//   cscript qtMovieFromStills.js sourcepath destpath [framerate]
//
// Authors:
//   Chip Chapin <cchapin@gmail.com> has extended a script by 
//   by Luc-Eric Rousseau (XSIBlog 2006, http://www.xsi-blog.com/archives/103)
//   which was in turn based on sample code by John Cromie that
//   accompanied his book "QuickTime for .NET and COM Developers"
//   (Elsevier 2006, http://www.skylark.ie/qt4.net/samplecode.asp).
//
//   My changes provide
//     -- Additional error checking/reporting of various kinds.
//     -- The script can be run in a loop without crashing.
//     -- Verbose progress reporting.

// Last Update: 2010-01-30

// Be sure to use FULLY-QUALIFIED Windows file paths, even from Cygwin.
// Relative paths don't work, and there is no error report from QT.
// TODO: can we get a progress bar during the initial rendering stage?

/*
 * Check existence of the parent folder of a file path.
 * @param {fso} fso
 * @param {string} fname
 * @return {boolean}
 */
function CheckFileFolder(fso, fname) {
  s = fso.GetParentFolderName(fname);
  return(fso.FolderExists(s));
}


// Get script arguments
if (WScript.Arguments.Length >= 2) {
  sourcePath = WScript.Arguments(0);
  destPath = WScript.Arguments(1);
  if (WScript.Arguments.Length >= 3) {
    frameRate = WScript.Arguments(2);
  } else {
    frameRate = 30;
  }
  WScript.Echo("FrameRate: " + frameRate);
} else {
  WScript.Echo("not enough parameters");
  WScript.Quit();
}

var fso =  WScript.CreateObject("Scripting.FileSystemObject");
if (!fso.FileExists(sourcePath)) {
  WScript.Echo("Missing source file '" + sourcePath +"'");
  WScript.Quit();
}
if (!CheckFileFolder(fso, destPath)) {
  WScript.Echo("Bad destination file path '" + destPath +"'");
  WScript.Quit();
}

// Launch QuickTime Player Application
var qtPlayerApp = WScript.CreateObject("QuickTimePlayerLib.QuickTimePlayerApp");
WScript.Sleep(8000);  // Give it time to launch.

if (!qtPlayerApp) {
  WScript.Echo("ERROR Failed to launch QuickTime Player App.");
  WScript.Quit();
}

// Get the QuickTime player and its associated controller.
var qtPlayer = qtPlayerApp.Players(1);
if (qtPlayer == null) {
  WScript.Echo("ERROR Failed to get QuickTime Player.");
  WScript.Quit();
}
WScript.Echo("Got player '" + qtPlayer.Caption + "'");
var qtControl = qtPlayer.QTControl;

// Now create the movie.
WScript.Echo("Creating new movie from stills at '" + sourcePath + "'...");
try {
  qtControl.CreateNewMovieFromImages(sourcePath,
                                     frameRate,
                                     true ); // rate is in frames per second
}
catch (e) {
  WScript.Echo("ERROR creating movie: " + e.number +
               " (" + (e.number>>16 & 0x1FFF) +
               "-" + (e.number & 0xffff) + ")");
  WScript.Echo(e.description);
  // TODO: Find more reliable error reporting.
  // The following doesn't work if the QTControl object is gone.
  WScript.Echo("QuickTime error " + qtControl.ErrorCode);
  var qte = qtControl.QuickTime.Error;
  WScript.Echo("  " + qte.ErrorCode + ", " + qte.Description);
  WScript.Echo("  " + qte.SourceReference);
  WScript.Quit();
}

var qtMovie = qtControl.Movie;
if (!qtMovie) {
  WScript.Echo("ERROR: No movie created (" + qtControl.ErrorCode + ")");
  WScript.Quit();
}
var duration = qtMovie.Duration;
if (duration == 0) {
  // This test isn't as helpful as I thought it would be.  I thought it
  // would catch the case where QT does not have a valid input file, but in
  // that case it seems to create a two-second empty movie 
  // (ie. duration = 20*framerate).
  WScript.Echo("ERROR: Movie has duration 0.");
  WScript.Quit();
}
// Duration appears to be the number of frames * 10.
WScript.Echo("Created new movie, duration " + duration);

// Set up the exporter.
var qt = qtPlayer.QTControl.QuickTime;
if (qt.Exporters.Count == 0) {
  // Only add an exporter if needed.
  qt.Exporters.Add();
  WScript.Echo("Adding new Exporter.");
} else {
  WScript.Echo("Using existing Exporter.");
}
var qtExporter = qt.Exporters(1);
if (!qtExporter) {
  WScript.Echo("ERROR: Unable to get Exporter.");
  WScript.Quit();
}
qtExporter.TypeName = "QuickTime Movie";
qtExporter.SetDataSource( qtMovie );

// Configure the exporter.
var CodecInfoFileName = "C:\\qtMovieFromStillsCodecInfo.xml";
var CodecFileInfo;
if (fso.FileExists(CodecInfoFileName)) {
  WScript.Echo("Reading codec config from '" + CodecInfoFileName + "'");
  CodecFileInfo =  fso.OpenTextFile( CodecInfoFileName );
}
if (CodecFileInfo) {
  var xmlCodecInfoText = CodecFileInfo.ReadAll();
  // cause the exporter to be reconfigured
  // http://developer.apple.com/technotes/tn2006/tn2120.html
  var tempSettings = qtExporter.Settings;
  tempSettings.XML = xmlCodecInfoText;
  qtExporter.Settings = tempSettings;
} else {
  qtExporter.ShowSettingsDialog();
  var xmlCodecInfoText = qtExporter.Settings.XML;
  CodecFileInfo = fso.CreateTextFile( CodecInfoFileName );
  if (CodecFileInfo) {
    CodecFileInfo.WriteLine(xmlCodecInfoText);
    CodecFileInfo.Close();
  } else {
    WScript.Echo("Warning: failed to save codec info to '"
                 + CodecInfoFileName + "'");
    WScript.Echo("continuing ...");
  }
}

// Export the movie.
WScript.Echo("Exporting ...");
try {
  qtExporter.DestinationFileName = destPath;
  qtExporter.ShowProgressDialog = true;
  // Uncomment this line if you want the export dialog box to appear.
  // qtExporter.ShowExportDialog();
  qtExporter.BeginExport();
  WScript.Echo("Exported to '" + destPath + "'");
}
catch (e) {
  WScript.Echo("ERROR " + (e.number>>16 & 0x1FFF) +
               "-" + (e.number & 0xffff) + 
               " exporting '" + destPath + "'");
  WScript.Echo(e.description);
  // JSON object only available in WSH 5.8+
  // WScript.Echo(JSON.stringify(e, null, 2));
  var qte = qt.Error;
  WScript.Echo("QuickTime Error " + qte.ErrorCode + ", " + qte.Description);
  //WScript.Echo(JSON.stringify(qte, null, 2));
}

// Close the player?
// Note: closing the player results in failures for subsequent invocations.
//qtPlayer.Close();

// End.

No comments: