Python Script for RAW Timelapses with Ken Burns style panning
Since installing magic lantern on my Canon t3i, I've been very interested in starting to learn to create timelapses. I'm specifically impressed with timelapses with ken burns effect styled motion like this one from Philip Bloom: http://philipbloom.net/film/24-hours-of-neon/ The only downside is, I didn't have all the software tools to make it work. To solve this, I wrote my own python script to automate the timelapse process and provide a GUI to select unlimited keyframes for ken burns style motion. My first test can be seen below.
The script, builds upon the RAW deflickr script written by a1ex from the Magic Lantern project Current Dependencies: (can all be installed on most versions of GNU/Linux with apt-get) *Timelapse ffmpeg* python yasm x264 imagemagick ffmpeg *Deflicker* numpy scipy matplotlib dcraw ufraw imagemagick *GUI* PIL to start it use the command line format: python timelapse.py -i raw -o jpg -r Where timelapse.py is the script, raw is the input folder containing canon raw files, and jpg is the destination folder for converted jpg and movie. The -r switch designates that the files in raw will be developed, leave this out if you already have converted files in the jpg directory. The script is a little rough around the edges, but it works well enough for me. Comment if you have any improvements or questions.
from __future__ import division import os import glob import sys, re, time, datetime, subprocess, shlex, getopt, fnmatch from math import * from pylab import * import Tkinter import ttk import tkMessageBox from PIL import Image, ImageTk # RAW deflickering script # Copyright (2012) a1ex. License: GPL. def progress(x, interval=1): global _progress_first_time, _progress_last_time, _progress_message, _progress_interval try: p = float(x) init = False except: init = True if init: _progress_message = x _progress_last_time = time.time() _progress_first_time = time.time() _progress_interval = interval elif x: if time.time() - _progress_last_time > _progress_interval: print >> sys.stderr, "%s [%d%% done, ETA %s]..." % (_progress_message, int(100*p), datetime.timedelta(seconds = round((1-p)/p*(time.time()-_progress_first_time)))) _progress_last_time = time.time() def change_ext(file, newext): if newext and (not newext.startswith(".")): newext = "." + newext return os.path.splitext(file)[0] + newext def get_median(file): cmd1 = "dcraw -c -D -4 -o 0 '%s'" % file cmd2 = "convert - -type Grayscale -scale 500x500 -format %c histogram:info:-" #~ print cmd1, "|", cmd2 p1 = subprocess.Popen(shlex.split(cmd1), stdout=subprocess.PIPE) p2 = subprocess.Popen(shlex.split(cmd2), stdin=p1.stdout, stdout=subprocess.PIPE) lines = p2.communicate()[0].split("\n") X = [] for l in lines[1:]: p1 = l.find("(") if p1 > 0: p2 = l.find(",", p1) level = int(l[p1+1:p2]) count = int(l[:p1-2]) X += [level]*count m = median(X) return m def deflickerRAW(inputfolder, outputfolder): ion() progress("Analyzing RAW exposures..."); files = sorted(os.listdir(inputfolder)) i = 0; M = []; for k,f in enumerate(files): m = get_median(os.path.join(inputfolder, f)) M.append(m); E = [-log2(m/M[0]) for m in M] E = detrend(array(E)) cla(); stem(range(1,len(E)+1), E); xlabel('Image number') ylabel('Exposure correction (EV)') title(f) draw(); progress(k / len(files)) if not os.path.exists(outputfolder): os.makedirs(outputfolder) progress("Developing JPG images..."); i = 0; for k,f in enumerate(files): ec = 2 + E[k]; cmd = "ufraw-batch --out-type=jpg --overwrite --clip=film --saturation=2 --exposure=%s '%s' --output='%s/%s'" % (ec, os.path.join(inputfolder, f),outputfolder, change_ext(f, ".jpg")) os.system(cmd) progress(k / len(files)) #declare variables############ moving=False resize=False aspectx=16 aspecty=9 rectcenterx=0 rectcentery=0 rectsizex=0 rectsizey=0 imagesizexpre=0 imagesizeypre=0 imagesizex=0 imagesizey=0 #Events######################### #triggers on left click in canvas def xy(event): global rectcenterx, rectcentery, rectsizex, rectsizey, moving, resize #if moving or resize: moving=False resize=False #detect rectangle center grab for move if event.x>(rectcenterx-int(rectsizex/2)) and event.x<(rectcenterx+int(rectsizex/2)) and event.y<(int(rectcentery+rectsizey/2)) and event.y>(int(rectcentery-rectsizey/2)): moving=True #detect lower right rectangle corner grabs for resize if event.x>(rectcenterx+rectsizex-rectsizex/4) and event.x<(rectcenterx+rectsizex+rectsizex/4) and event.y<(rectcentery+rectsizey+rectsizey/4) and event.y>(rectcentery+rectsizey-rectsizey/4): resize=True #triggers on motion in canvas def canvasmotion(event, canvas, rectangle): global rectcenterx,rectcentery,rectsizex,rectsizey,moving,imagesizex,imagesizey,resize,aspectx,aspecty,checkboxstate if checkboxstate.get(): if moving: if ((event.x+rectsizex<imagesizex) and (event.x-rectsizex>0)): rectcenterx=event.x if ((event.y+rectsizey<imagesizey) and (event.y-rectsizey>0)): rectcentery=event.y if resize: if (event.x<=imagesizex) and (event.x-(2*(event.x-rectcenterx))>=0) and (rectcentery-((event.x-rectcenterx)*aspecty/aspectx))>0 and int((event.x-rectcenterx)*2*imagesizexpre/imagesizex)>=1920: rectsizex=event.x-rectcenterx rectsizey=(rectsizex*aspecty)/aspectx drawRect(canvas,rectangle) def drawRect(canvas, rectangle): global rectcenterx,rectcentery,rectsizex,rectsizey,moving,imagesizex,imagesizey,resize,aspectx,aspecty,keyframes,currentframe canvas.tag_raise(rectangle) canvas.coords(rectangle, rectcenterx-rectsizex, rectcentery-rectsizey, rectcenterx+rectsizex, rectcentery+rectsizey) keyframes[currentframe]=[rectsizex,rectsizey,rectcenterx,rectcentery] def changeFrame(FrameNumSpin, outputfolder, canvas, canvasimage, rectangle,c1): global rectcenterx,rectcentery,rectsizex,rectsizey,photo,currentframe,checkboxstate currentframe = int(FrameNumSpin.get()) files = sorted(glob.glob(outputfolder + "/IMG_*.jpg")) image = Image.open(files[currentframe-1]) image.thumbnail((350, 350), Image.ANTIALIAS) photo = ImageTk.PhotoImage(image) canvas.delete(canvasimage) canvasimage = canvas.create_image(0,0, image=photo, anchor=Tkinter.NW) c1.deselect() for keyframe in keyframes: if keyframe==currentframe: c1.select() rectsizex=keyframes[currentframe][0] rectsizey=keyframes[currentframe][1] rectcenterx=keyframes[currentframe][2] rectcentery=keyframes[currentframe][3] drawRect(canvas, rectangle) break def checkboxClicked(canvas,rectangle,c1): global rectcenterx,rectcentery,rectsizex,rectsizey,moving,imagesizex,imagesizey,resize,aspectx,aspecty,imagesizex,imagesizey,imagesizexpre,imagesizeypre, photo, keyframes,currentframe,checkboxstate if currentframe == 1: c1.select() else: if checkboxstate.get(): rectsizex=imagesizex/2 rectsizey=(rectsizex*9)/16 rectcenterx=imagesizex/2 rectcentery=imagesizey/2 #keyframes[currentframe]=[rectsizex,rectsizey,rectcenterx,rectcentery] drawRect(canvas,rectangle) else: del keyframes[currentframe] print "deleted keyframe" canvas.tag_lower(rectangle) ''' #graceful exit def ask_quit(root): if tkMessageBox.askokcancel("Quit", "Do you want to quit now?"): root.destroy() ''' def initGUI(inputfolder, outputfolder): # global rectcenterx,rectcentery,rectsizex,rectsizey,moving,imagesizex,imagesizey,resize,aspectx,aspecty,imagesizex,imagesizey,imagesizexpre,imagesizeypre, photo, keyframes,currentframe,checkboxstate #Create User Interface # root = Tkinter.Tk() #root.columnconfigure(0, weight=1) #root.rowconfigure(0, weight=1) canvas = Tkinter.Canvas(root) files = sorted(glob.glob(outputfolder + "/IMG_*.jpg")) #add image to canvas image = Image.open(files[0]) imagesizexpre = image.size[0] imagesizeypre = image.size[1] image.thumbnail((350, 350), Image.ANTIALIAS) imagesizex = image.size[0] imagesizey = image.size[1] #set initial rect size rectsizex=imagesizex/2 rectsizey=(rectsizex*9)/16 rectcenterx=imagesizex/2 rectcentery=imagesizey/2 currentframe=1 keyframes={1:[rectsizex,rectsizey,rectcenterx,rectcentery]} photo = ImageTk.PhotoImage(image) canvasimage = canvas.create_image(0,0, image=photo, anchor=Tkinter.NW) rectangle=canvas.create_rectangle(rectcenterx-rectsizex, rectcentery-rectsizey, rectcenterx+rectsizex, rectcentery+rectsizey, outline="#fb0") #generate header button row HeaderRow = Tkinter.Frame(root) b1 = Tkinter.Button(HeaderRow, text="One") b2 = Tkinter.Button(HeaderRow, text="Two") checkboxstate=Tkinter.IntVar() c1 = Tkinter.Checkbutton(HeaderRow, text="Keyframe", variable=checkboxstate, command=lambda: checkboxClicked(canvas,rectangle,c1)) headerLabel1 = Tkinter.Label(HeaderRow, text="frame #") headerLabel2 = Tkinter.Label(HeaderRow, text="of %d" % len(files)) FrameNumSpin = Tkinter.Spinbox(HeaderRow, from_=1, to_=len(files), command=lambda: changeFrame(FrameNumSpin, outputfolder,canvas,canvasimage,rectangle,c1)) b1.pack(side = Tkinter.LEFT) b2.pack(side = Tkinter.LEFT) c1.pack(side = Tkinter.LEFT) headerLabel1.pack(side = Tkinter.LEFT) FrameNumSpin.pack(side = Tkinter.LEFT) headerLabel2.pack(side = Tkinter.LEFT) HeaderRow.grid(column=0, row=0) c1.select() #create canvas canvas.grid(column=0, row=1, sticky=(Tkinter.N, Tkinter.W, Tkinter.E, Tkinter.S)) canvas.bind("", xy) canvas.bind("", lambda event: canvasmotion(event, canvas, rectangle)) #generate foot button row FooterRow = Tkinter.Frame(root) footerLabel = Tkinter.Label(FooterRow, text="Useful help tips") b3 = Tkinter.Button(FooterRow, text="Three") b4 = Tkinter.Button(FooterRow,text="Process!", command=lambda: processTimelapse(imagesizex,imagesizey,imagesizexpre,imagesizeypre,inputfolder,outputfolder)) footerLabel.pack(side = Tkinter.LEFT) b3.pack(side = Tkinter.LEFT) b4.pack(side = Tkinter.LEFT) FooterRow.grid(column=0, row=3) #root.protocol("WM_DELETE_WINDOW", ask_quit()) root.title ("Timelapse") #w,h = root.winfo_screenwidth(), root.winfo_screenheight() #root.geometry("%dx%d+0+0" % (w,h)) root.mainloop() def processTimelapse(imagesizex,imagesizey,imagesizexpre,imagesizeypre,inputfolder,outputfolder): global rectcenterx,rectcentery,rectsizex,rectsizey,aspectx,aspecty,keyframes #renumberjpeg(inputfolder,outputfolder) i=0 files = sorted(glob.glob(outputfolder + "/IMG_*.jpg")) if not os.path.exists(outputfolder+"/resized"): os.makedirs(outputfolder+"/resized") for onefile in files: if fnmatch.fnmatch(onefile, '*.jpg'): print "cropping %s" % onefile filename=onefile image = Image.open(filename) for keyframe in sorted(keyframes): if keyframe==i+1: print "equals" rectsizex=keyframes[i+1][0] rectsizey=keyframes[i+1][1] rectcenterx=keyframes[i+1][2] rectcentery=keyframes[i+1][3] interpolatelatch=0 rectsizexslice=0 rectsizeyslice=0 rectcenterxslice=0 rectcenteryslice=0 break elif keyframe>(i+1): print "greaterthan" if interpolatelatch==0: rectsizexslice=(keyframes[keyframe][0]-rectsizex)/(keyframe-(i)) rectsizeyslice=(keyframes[keyframe][1]-rectsizey)/(keyframe-(i)) rectcenterxslice=(keyframes[keyframe][2]-rectcenterx)/(keyframe-(i)) rectcenteryslice=(keyframes[keyframe][3]-rectcentery)/(keyframe-(i)) interpolatelatch=1 rectsizex=rectsizex+rectsizexslice rectsizey=rectsizey+rectsizeyslice rectcenterx=rectcenterx+rectcenterxslice rectcentery=rectcentery+rectcenteryslice break box = (int((rectcenterx-rectsizex)*imagesizexpre/imagesizex), int((rectcentery-rectsizey)*imagesizeypre/imagesizey), int((rectcenterx+rectsizex)*imagesizexpre/imagesizex), int((rectcentery+rectsizey)*imagesizeypre/imagesizey)) print box area = image.crop(box) area = area.resize((1920, 1080), Image.ANTIALIAS) area.save(outputfolder+"/resized/%03d.jpg" % i, 'jpeg') i=i+1 cmd = "avconv -i %s/resized/" % outputfolder cmd= cmd + "%" + "03d.jpg -r 24 -s hd1080 -vcodec libx264 -crf 16 %s/timelapse.mp4" % outputfolder os.system(cmd) def main(argv): # print command line arguments inputfolder = '' outputfolder = '' process = 0 try: opts, args = getopt.getopt(argv,"hi:o:r",["ifolder=","ofolder="]) except getopt.GetoptError: print 'timelapse.py -i -o -r ' sys.exit(2) for opt, arg in opts: if opt == '-h': print 'timelapse.py -i -o -r ' sys.exit() elif opt in ("-i", "--ifolder"): inputfolder = arg elif opt in ("-o", "--ofolder"): outputfolder = arg elif opt == "-r": process = 1 print 'Input folder is "', inputfolder print 'Output folder is "', outputfolder if process==1: deflickerRAW(inputfolder, outputfolder) initGUI(inputfolder, outputfolder) if __name__ == "__main__": main(sys.argv[1:])