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!