Entry 3122
Image Sequence to QuickTime
Submitted by Chip Chapin
on Feb. 1, 2010 at 7:46 a.m.
Language: Python. Code size: 13.0 KB.
#! /usr/bin/env python """ Convert one or more still image sequences into QuickTime movies. Author: Chip Chapin <cchapin@gmail.com> For More Info: http://cchapin.blogspot.com You must have the PyWin32 COM interface installed, see http://sourceforge.net/projects/pywin32/ Acknowledgements: The QuickTime interface was originally written as a JScript+WSH script, which I based on a 2006 blog post by Luc-Eric Rousseau (XSIBlog http://www.xsi-blog.com/archives/103). Rousseau's script was in turn based on sample code by John Cromie, author of the book "QuickTime for .NET and COM Developers" (Elsevier 2006, http://www.skylark.ie/qt4.net/samplecode.asp). Latest Update: 2010-01-31 TODO: Some sort of progress indicator during the initial rendering stage. TODO: Better solution to the QT_PLAYER_DELAY hack. """ import datetime import math import os import re import shutil import sys import tempfile import time import win32com.client CODEC_INFO_FILENAME = "C:\\qtMovieFromStillsCodecInfo.xml" DEFAULT_CHUNK_FILES=7000 DEFAULT_FRAME_RATE = "60" # HACK: Delay (sec) while the QuickTime player initializes. QT_PLAYER_DELAY = 7.0 def abort(message=None): """Print error message and exit.""" if message: print "ERROR: ", message sys.exit(1) def usage(message=None): """Print usage message and exit.""" if message: print message sys.exit(2) def check_file_folder(fpath): """Returns true if the directory of fpath exists and is writable.""" fdir = os.path.dirname(fpath) try: f = tempfile.TemporaryFile("w", dir=fdir) except OSError: return False f.close() return True def file_exists(fpath): """Returns true if file fpath exists and is readable.""" try: f = open(fpath, "r") except IOError: return False f.close() return True def uniqueify(fpath): """Check to see if the file exists. If so, uniqueify the filename.""" upath = fpath count = 0 while file_exists(upath): count = count + 1 (root, ext) = os.path.splitext(fpath) upath = "%s-%02d%s" % (root, count, ext) if count: print "WARN: You already have a file '%s'" % fpath print " Saving as '%s' instead." % upath return upath def create_new_movie_from_images(sourcePath, frameRate, qtControl): """Create the movie from the still image sequence.""" print "Creating new movie from still sequence '" + sourcePath + "'..." try: qtControl.CreateNewMovieFromImages(sourcePath, frameRate, True) # rate is in frames per second except: print "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. print "QuickTime error " + qtControl.ErrorCode qte = qtControl.QuickTime.Error print " " + qte.ErrorCode + ", " + qte.Description print " " + qte.SourceReference raise qtMovie = qtControl.Movie; if not qtMovie: abort("No movie created (" + qtControl.ErrorCode + ")") 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). abort("Movie has duration 0.") # Duration is the number of frames * 10. print "Created new movie, duration %d" % duration def get_quicktime_exporter(qtControl): """Set up the QuickTime movie exporter.""" qt = qtControl.QuickTime if (qt.Exporters.Count == 0): # Only add an exporter if needed. qt.Exporters.Add() print "Adding new Exporter." else: print "Using existing Exporter." qtExporter = qt.Exporters(1) if not qtExporter: abort("Unable to get Exporter.") qtExporter.TypeName = "QuickTime Movie" qtExporter.SetDataSource(qtControl.Movie) if file_exists(CODEC_INFO_FILENAME): print "Reading codec config from '" + CODEC_INFO_FILENAME + "'" CodecFileInfo = open(CODEC_INFO_FILENAME, "r") xmlCodecInfoText = CodecFileInfo.read() # Cause the exporter to be reconfigured. # http://developer.apple.com/technotes/tn2006/tn2120.html tempSettings = qtExporter.Settings tempSettings.XML = xmlCodecInfoText qtExporter.Settings = tempSettings else: # Use the Settings dialog box, then save the results. qtExporter.ShowSettingsDialog() xmlCodecInfoText = qtExporter.Settings.XML try: CodecFileInfo = open(CODEC_INFO_FILENAME, "w") CodecFileInfo.write(xmlCodecInfoText) CodecFileInfo.close() except IOError: print ("Warning: failed to save codec info to '" + CODEC_INFO_FILENAME + "'") print "continuing ..." return qtExporter def export_movie(qtExporter, destPath): """Export the movie.""" print "Exporting ..." try: qtExporter.DestinationFileName = destPath qtExporter.ShowProgressDialog = True # Uncomment this line if you want the export dialog box to appear. # qtExporter.ShowExportDialog(); qtExporter.BeginExport() # This can take a l-o-n-g time. print "Exported to '%s'" % destPath except: print "ERROR exporting '%s'" % destPath #print "ERROR " + (e.number>>16 & 0x1FFF) + # "-" + (e.number & 0xffff) + # " exporting '" + destPath + "'" #print e.description #WScript.Echo(JSON.stringify(e, null, 2)); #qte = qt.Error; #print "QuickTime Error %d, %s" % (qte.ErrorCode, qte.Description) #WScript.Echo(JSON.stringify(qte, null, 2)); raise def qt_movie_from_stills(sourcePath, destPath, frameRate): """Create and export a QuickTime movie from a sequence of images.""" # Launch QuickTime Player Application qtPlayerApp = win32com.client.Dispatch( "QuickTimePlayerLib.QuickTimePlayerApp") time.sleep(QT_PLAYER_DELAY); # Give it time to launch. if not qtPlayerApp: abort("Failed to launch QuickTime Player App.") # Get the QuickTime player and its associated controller. # NOTE: The script will abort here if the player hasn't had time # to initialize. It should work if you run it again, or you can increase # QT_PLAYER_DELAY. qtPlayer = qtPlayerApp.Players(1); if not qtPlayer: abort("Failed to get QuickTime Player."); print "Got player '" + qtPlayer.Caption + "'" qtControl = qtPlayer.QTControl create_new_movie_from_images(sourcePath, frameRate, qtControl) qtExporter = get_quicktime_exporter(qtControl) export_movie(qtExporter, destPath) # Closing the player causes failures for subsequent invocations. # qtPlayer.Close(); def seq_file_list(a_seq_file): """Return the list of file names in the sequence.""" # Extract the leading invariant part of the sequence filename. # For example, if the name is Fooo-0001.jpg, the invariant is "Fooo-". # Use non-greedy match so the numbers are kept out. (work_folder, sequence_proto) = os.path.split(a_seq_file) sm = re.match("(.*?)[0-9]+\.", sequence_proto) if not sm: usage(("'%s' doesn't look like the start of a sequence." % sequence_proto) + "\nSequences have sequentially numbered file names like Foo-001.jpg, Foo-002.jpg...\n" + " sequenceExample -- Path to any file in the sequence.\n" + " maxSize -- Maximum number of sequence files to put in a folder."); # Construct a regexp for matching sequence file names. sq_name = sm.group(1) sq_re = re.compile("^" + sq_name + "[0-9]+\."); # Make a list of files that match the sq_re pattern. "fnmatch" patterns # are not powerful enough to be reliable so we don't use glob. sq_files = [] dir = os.listdir(work_folder) dir.sort() for f in dir: if sq_re.match(f): sq_files.append(f) return (sq_files, sq_name) def split_file_list(a_seq_file, sq_files, sq_name, max_size): """Split the sequence files into successive split folders. Returns the list of split folder names. """ work_folder = os.path.dirname(a_seq_file) sq_count = len(sq_files) num_splits = math.ceil(sq_count / max_size) print ("Splitting file sequence '%s' into %d parts." % (sq_name, num_splits)) parent_folder = os.path.dirname(work_folder) file_count = 0 next_file_in_split = 1 current_split = 0 splits = [] for fname in sq_files: if (file_count == 0 or next_file_in_split > max_size): current_split = current_split + 1 next_file_in_split = 1 split_folder = os.path.join(parent_folder, ("%s-%d" % (work_folder, current_split))) print "New split folder '%s' (%d)" % (split_folder, file_count) try: os.mkdir(split_folder, 0755) except EnvironmentError as (errno, strerror): print "ERROR: Failed to create split directory '%s'" % split_folder print "[Error %d] %s" % (errno, strerror) abort() splits.append((split_folder, fname)) # Move the file from work_folder to split_folder shutil.move(os.path.join(work_folder, fname), os.path.join(split_folder, fname)) file_count = file_count + 1 next_file_in_split = next_file_in_split + 1 print ("Split %d files in '%s' into %d folders." %(file_count, work_folder, current_split)) return splits def make_seq_chunks(a_seq_file, max_size): """Split a file sequence into folders of no more than max_size files each. Returns a list of (folder, starting_file) duples. Sequences have file names that end with a sequence number, like (Foo-001.jpg, Foo-002.jpg, ...). """ (sq_files, sq_name) = seq_file_list(a_seq_file) sq_count = len(sq_files) print "Sequence '%s' contains %d members." % (a_seq_file, sq_count) if sq_count <= max_size: print "No need to split." if (sq_count == 0): return [] else: return [os.path.split(a_seq_file)] else: return split_file_list(a_seq_file, sq_files, sq_name, max_size) def seq_name_from_filename(fname): """Extract the 'sequence name' from a representative filename.""" sm = re.match("(.*?)[-_]?[0-9]+\.", os.path.basename(fname)) if not sm: print ("'%s' doesn't look like the start of a sequence." % fname) return None return sm.group(1) def convert_sequences(base_dir, seqs, frame_rate=DEFAULT_FRAME_RATE, chunk_size=DEFAULT_CHUNK_FILES): """Generates QuickTime movies for a set of image sequences. base_dir: Full path to the common parent directory. seqs: List of duples describing image sequences. Each duple contains the FOLDER NAME of the sequence and the STARTING IMAGE file name: (folder, file) Names are unqualified, i.e. relative to their parent directories. frameRate: Numeric frame rate in frames per second. chunk_size: Maximum number of frames to be processed in a single movie. """ for (sq_dir, sq_file) in seqs: src = "%s\\%s\\%s" % (base_dir, sq_dir, sq_file) if not file_exists(src): print "WARN: '%s' is not readable. Skipping." % src continue # Possibly split the image sequence into chunks. splits = make_seq_chunks(src, chunk_size) # Iterate over the chunks. for (split_folder, split_file) in splits: sq_name = seq_name_from_filename(sq_file) if (sq_name): dst = ("%s\\%s_%s.mov" % (base_dir, sq_name, os.path.basename(split_folder))) dst = uniqueify(dst) print time.strftime("%Y-%m-%d %X Converting:") print (" Folder %s\n Start %s\n Dest %s" % (split_folder, split_file, dst)) qt_movie_from_stills(src, dst, frame_rate) print time.strftime("Complete at %Y-%m-%d %X") def main(argv): """Main Program""" usage("This is the seq_to_qt module.\n" + "For batch conversion, edit 'seq_batch_convert.py'. It calls the functions here.") if __name__ == "__main__": main(sys.argv)
This snippet took 0.05 seconds to highlight.
Back to the Entry List or Home.