Dr Stuart Grey

Creating a 4K Time-Lapse Video using Open-Source, Cross-Platform Tools

Jul 28, 2015

As part of the Spark Festival London 2015 I will be demonstrating a 3D printer making a copy of itself. To get things started I had to build a 3D printer (RepRap Huxley Duo) myself and thought i’d document the process and create a timelapse video. Just to make my life a bit more interesting, I aimed to use only cross-platfrom, open-source software.

I set up my camera (Sony A5100) on a little tripod and had it take a photo every minute I was working on the printer. The following is the resulting timelapse. To see how I made the movie from the image files read on.

Step 1: Alignment

The photographs were taken in a number of different sessions as I was doing the build in the evenings and at the weekend. This meant that the photographs from one session did not align exactly with the next session as shown in the following gif (made from the first picture from each session). Look at the top left corner of the table to see it jump around. To create a .gif from these images I used ImageMagick (IM) like follows (we’ll be seeing a lot more of ImageMagick!)

convert -delay 20 -loop 0 -resize 600x400 *.jpg test.gif

Unaligned Images

I calculated the offset of each session’s images from the first by adding each image to a layer in GIMP, reducing the opacity, moving it until it looked right and recording the offset. The result was an improvement but not perfect.

To test the new alignment I moved the first images by their offset, this was done using IM like so:

convert 0137.jpg -page -1-30 -background none -flatten aligned/0137.jpg

This is the result:

Aligned Images

We can see that there are now small black areas in the bottom right of the image that we need to crop off. 60 pixesls off X and Y should do it:

for file in *.jpg; do convert -crop 5940x3940+0+0 $file s$file; done

So that works for the tests, now we apply the alignment and crop to each photo in the respective sessions, again using IM but all wrapped in a nice bit of bash..

for file in *.jpg;
do convert -page -1-30 -background none -flatten $file aligned/$file;
convert aligned/$file -crop 5940x3940+0+0 +repage aligned/$file; 
done

Step 2: Colour Adjustment

We can see in this first gif that the brightness really drops near the end of the session. The photographs were taken using the same settings as it got dark outside the photos becames really underexposed!

Unlevelled Images

We can fix this (to an extent) by bumping up the levels in the darker images. To do this I used the this fantastic IM script, Autolevel, thanks Fred!

for file in *.jpg; do ../autolevel -c rgb -m 0.3 $file aligned_levelled/$file; done

Levelled Images

Now to get all the levelled frames together in a folder and make a test movie using ffmpeg:

ffmpeg -r 25 -i %04d.jpg -vf scale=-1:1080 test.mp4

Step 3: Cropping for a nice smooth zoom

To make the time-lapse more visually interesting I wanted to have a slow zoom in to the more interesting area of the frame (the printer) over the course of the time-lapse. I did this by cropping the images by a slowly increasing amount. As long as I kept the vertical resolution over 2160 pixels I could still resize to my desired 4k resolution.

For this I used a bash script like so:

#!/bin/bash
x=5910;
y=3940;
a=0;
b=0;
count=0;
for file in *.jpg;
do 
convert -crop 5910x3940+0+0 $file cropped/$file;
convert -crop ${x}x${y}+$a+$b cropped/$file cropped/$file;
let count=count+1;
#echo $(($count % 2))
echo $count
if [ $(($count % 7)) -ne 0 ]
then
	let x=x-3;
	let y=y-2;
fi
if [ $(($count % 3)) -ne 0 ]
then
	let a=a+2;
	let b=b+1;
fi
done

Now I have to resize all of the images to match my desired resolution:

for file in *.jpg; do convert -resize 3240x2160 $file resized/$file; done

Step 4: Adding the elapsed time and ‘progress bar’

I wanted to add text displaying the elapsed time and the current ‘task’ such as ‘X Axis Assembly’ to the frames. I hacked this together in Python, its really not pretty and the if/else statement is truly horrible.

Text Overlay

The interesting bit is where I write the current time (basically the frame number as it was one frame per minute mod 60) and the current stage. I write the full ‘stage’ string at just under 50% alpha transparency and then write a number of letters of the same string at full opacity as the we progress through the task.

import PIL
from PIL import ImageFont
from PIL import Image
from PIL import ImageDraw

for n in range(1,1042,1):
	filestring = str(n).zfill(4) 
	if (n <= 137):
		taskstring = "Frame Assembly"
		offset = 0
		duration = 137
	elif (n <= 204):
		taskstring = "Y Axis Assembly"
		offset = 137
		duration = 67
	elif (n <= 268):
		taskstring = "X Axis Assembly"
		offset = 204
		duration = 64
	elif (n <= 341):
		taskstring = "Z Axis Assembly"
		offset = 268
		duration = 73
	elif (n <= 360):
		taskstring = "Heat Bed Assembly"
		offset = 341
		duration = 19
	elif (n <= 403):
		taskstring = "Extruder Assembly"
		offset = 360
		duration = 43
	elif (n <= 485):
		taskstring = "Hot End Assembly"
		offset = 403
		duration = 82
	elif (n <= 599):
		taskstring = "Electronics"
		offset = 485
		duration = 114
	elif (n <= 756):
		taskstring = "Wiring"
		offset = 599
		duration = 157
	elif (n <= 844):
		taskstring = "Commissioning"
		offset = 756
		duration = 88
	elif (n <= 980):
		taskstring = "Calibration"
		offset = 844
		duration = 136
	elif (n <= 1041):
		taskstring = "Printing"
		offset = 980
		duration = 120

	font = ImageFont.truetype("HNLT.ttf",120)
	base =Image.open('../images/' + filestring + '.jpg').convert('RGBA')

	txt = Image.new('RGBA', base.size, (86, 120, 148,0))
	draw = ImageDraw.Draw(txt)

	h, m = divmod(n, 60)
	draw.text((50, 210), "%02d:%02d" % (h, m) ,(255,255,255,255),font=font)

	draw.text(( 50, 50 ), taskstring , (255,255,255,100) ,font=font)

	length  = len(taskstring.replace(" ", ""))

	string_pos = int((n-offset)/(duration/length))

	draw.text(( 50, 50 ), 
		taskstring[:( string_pos + taskstring[:string_pos+1].count(' ') )],
		(255,255,255,255) ,font=font)

	out = Image.alpha_composite(base, txt)

	out.save('./frames/' + filestring + '.png')

	print n

I’m really happy with how this works. I think it gives a good feeling of the speed of doing each task in quite a concise way (I especially like the speed of fitting the heat bed assembly).

Step 5: Intro and Outro

I liked the effect so I thought I would use it to create an intro and an outro using the same effect. As I already had the python code this was surprisingly easy.

Intro Frame

import PIL
from PIL import ImageFont
from PIL import Image
from PIL import ImageDraw

for n in range(1,131,1):
	filestring = str(n).zfill(4)
	
	font = ImageFont.truetype("HNLT.ttf",120)
	base =Image.open('../cropped_resized/0001.jpg').convert('RGBA')

	txt = Image.new('RGBA', base.size, (86, 120, 148,0))
	draw = ImageDraw.Draw(txt)

	taskstring_1 = "Building a 3D Printer"
	taskstring_2 = "Dr Stuart Grey"
	taskstring_3 = "University College London"
	taskstring_4 = "Spark Festival London 2015"

	draw.text(( 50, 50 ), taskstring_1, (255,255,255,100) ,font=font)
	draw.text(( 50, 220 ), taskstring_2, (255,255,255,100) ,font=font)
	draw.text(( 50, 390 ), taskstring_3, (255,255,255,100) ,font=font)
	draw.text(( 50, 560 ), taskstring_4, (255,255,255,100) ,font=font)

	offset_1 = 0
	duration_1 = 42
	length_1  = len(taskstring_1.replace(" ", ""))
	string_pos_1 = int((n-offset_1)/(duration_1/length_1))
	draw.text(( 50, 50 ), 
	taskstring_1[:( string_pos_1 + taskstring_1[:string_pos_1+1].count(' ') )],
	(255,255,255,255) ,font=font)
	
	offset_2 = 25
	if (n>= offset_2):
		duration_2 = 28
		length_2  = len(taskstring_2.replace(" ", ""))
		string_pos_2 = int((n-offset_2)/(duration_2/length_2))
		draw.text(( 50, 220 ), 
		taskstring_2[:(string_pos_2+taskstring_2[:string_pos_2+1].count(' '))],
		(255,255,255,255) ,font=font)

	offset_3 = 50
	if (n>= offset_3):
		duration_3 = 50
		length_3  = len(taskstring_3.replace(" ", ""))
		string_pos_3 = int((n-offset_3)/(duration_3/length_3))
		draw.text(( 50, 390 ), 
		taskstring_3[:(string_pos_3+taskstring_3[:string_pos_3+1].count(' '))],
		(255,255,255,255) ,font=font)
	
	offset_4 = 75
	if (n>= offset_4):
		duration_4 = 52
		length_4  = len(taskstring_4.replace(" ", ""))
		string_pos_4 = int((n-offset_4)/(duration_4/length_4))
		draw.text(( 50, 560 ), 
		taskstring_4[:(string_pos_4+taskstring_4[:string_pos_4+1].count(' '))],
		(255,255,255,255) ,font=font)

	out = Image.alpha_composite(base, txt)

	out.save('./introframes/' + filestring + '.png')

	print n

I just used a slight variation of this script for the outro using a different base image (the last frame) and reversing the order.

To create a 4K video from a set of frames (into, main or outro) we just navigate to the folder and use the following command:

ffmpeg -r 25 -i %04d.png -vf scale=-1:2160 main4k.mp4

Step 6: Putting it all together

To join the videos we use still use ffmpeg. We create a list of the files to be joined and then put them in a text file called ‘mylist4k.txt’ or whatever you would like to call it:

file 'intro4k.mp4'
file 'main4k.mp4'
file 'outro4k.mp4'

We then use the ‘concat’ flag with ffmpeg and pass it our list.

ffmpeg -f concat -i mylist4k.txt -c copy output4k.mp4

Step 7: Adding Audio

Now the last piece was to add some music, I bought a jaunty tune from AudioJungle and downloaded it as an mp3. Then to add the audio to the full video we just use ffmpeg as follows:

ffmpeg -i output4k.mp4 -i preview2.mp3 -map 0 -map 1 -codec copy -shortest audio4k.mp4

Thats it! Job done! Here is the end result one more time, make sure to select 2160p on the quality settings!


Other Articles

Engineering the Future for Girls 2018 Jun 18, 2018

More VR Space Debris for the Scottish Space School 2018 Jun 12, 2018

Strathclyde Teaching Excellence Awards 2018 May 11, 2018

Fast solar radiation pressure modelling with ray tracing and multiple reflections May 1, 2018

Reflecting Space Symposium Feb 2, 2018

Young Weir-Wise: Discovering Engineering with S2 Girls Jan 29, 2018

Autonomous orbit determination for formations of cubesats beyond LEO Jan 1, 2018

Angles-only navigation of a formation in the proximity of a binary system Jan 1, 2018

The Naked Scientists - Question of the Week Dec 12, 2017

Uncertainty treatment in the GOCE re-entry Nov 1, 2017

2,000,000 YouTube Views! Aug 5, 2017

Glasgow Science Festival: My Favourite Doomsday Talk Jun 14, 2017

Scottish Space School VR Space Debris Workshops Jun 14, 2017

Space Debris Workshops for P6/7 students at SmartSTEMS Jun 7, 2017

Autonomous orbit determination and navigation for formations of CubeSats beyond LEO Jun 1, 2017

Autonomous navigation of a formation of spacecraft in the proximity of a binary asteroid Jun 1, 2017

Sunlight illumination models for spacecraft surface charging May 1, 2017

Deformation of Space Debris due to Solar and Earth Radiation Fluxes - 7th European Conference on Space Debris 2017 Presentation Apr 17, 2017

Deformation of space debris and subsequent changes in orbit during eclipse due to solar and earth radiation fluxes Apr 1, 2017

Contributing to the 3rd Istanbul Design Biennial 22 October – 20 November 2016 Oct 9, 2016

Contributing to Natures Weirdest Events - Series 5, Episode 1, BBC2 Sep 29, 2016

Video: Watch this Space - Episode 2 Jun 25, 2016

Video: Watch this Space - Episode 1 Jun 11, 2016

Space Debris Wallpapers - May 2016 Jun 6, 2016

Talking Space Debris with BBC Radio Bristol Jun 1, 2016

Modelling spacecraft illumination and the effect of specular, diffuse and multiple reflection on photoelectron emission Apr 1, 2016

Space debris talk at the Royal Institution Mar 11, 2016

Launch of the UCL Aerospace Engineering Minor Jan 20, 2016

When space debris goes viral... Jan 1, 2016

Video: Space Debris 1957-2015 Dec 22, 2015

Royal Institution Advent Calendar: A History of Space Debris Dec 7, 2015

Future London 2015 Nov 5, 2015

Spark Festival 2015 Aug 31, 2015

St Pauls Way Science Summer School 2015 Aug 26, 2015

Creating a 4K Time-Lapse Video using Open-Source, Cross-Platform Tools Jul 28, 2015

Video: Time-lapse of Building a 3D Printer Jul 26, 2015

2015 Student Choice Teaching Awards Nomination May 7, 2015

Aviation Xtended interview on my space debris research and new aerospace course at UCL Mar 6, 2015

NT12–230 Discrimination via Extremely High Accuracy Trajectory Modelling Mar 1, 2015

My appearance on episode 1 of "Keeping it Civil", the new podcast from CEGE at UCL Feb 11, 2015

Slides from "Describing the World in 3D: Matrices and Vectors" Nov 25, 2014

AIAA/AAS Astrodynamics Specialist Conference 2014 Presentation: Developments in High Fidelity Surface Force Models and their Relative Effects on Orbit Prediction Aug 4, 2014

Non-conservative torque and attitude modelling for enhanced space situational awareness Aug 1, 2014

Geomagnetic Lorentz force modeling for orbit prediction- methods and initial results Aug 1, 2014

Developments in high fidelity surface force models and their relative effects on orbit prediction Aug 1, 2014

2014 "How to Change the World" Course Jul 10, 2014

Spacecraft Charging Technology Conference 2014 Presentation: High Fidelity Modelling of the Photoelectric Effect on Spacecraft Jun 28, 2014

My PhD Thesis - "Distributed Agents for Autonomous Spacecraft" May 10, 2014

2014 Student Choice Teaching Awards Nomination Apr 30, 2014

Scoping study on ballistic missile and countermeasures trajectory evolution modeling and tracking Aug 1, 2012