Raspberry Pi Timelapse Camera

My parents are building a house from scratch. They designed it, had it prefabbed and assembled on site, and now the rest is up to them. It was nice to make a time-lapse video of the construction, but not at the €300+ price point of most commercial solutions.

The Raspberry Pi Camera Module V2 is an 8MP camera with a perfectly good sensor and glass for this project. Together with a Raspberry Pi 3B+, 16GB micro SD-card, power supply, enclosure and UTP cable this is more than enough to take all the pictures we’ll need, at €100 total.

Assembling the Pi and Camera is only marginally harder than plugging in the power cord on a premade solution. You install the OS, Raspbian and run pip install picamera. Now you have everything you need to be taking pictures.

For parent-level user-friendliness I wanted the camera settings (shutter speed, ISO, etc.) to be accessible via a text file in my dad’s Dropbox.

  1. Create a Dropbox app – follow these steps.

  2. Install the Dropbox Python SDKpip install dropbox.

  3. Use TOML for the config file format.

# Place a copy of this config file in your Dropbox:
# '<Dropbox>/apps/<your-app-name>/config.txt'

# Resolution
# Format: WIDTHxHEIGHT
resolution = 1920x1080

# ISO
# Options: {100, 200, 320, 400, 500, 640, 800}
iso = 100

# Rotation
# Options: {0, 90, 180, 270}
rotation = 0

# Saturation.
# Integer in range [-100,100] (inclusive)
saturation = 0

# Shutter speed
# Options: 0 for automatic, or desired shutter speed in microseconds
shutter_speed = 0

# Contrast
# Integer in range [-100,100] (inclusive)
contrast = 0

# Brightness
# Integer in range [0, 100] (inclusive)
brightness = 50

I wrote a class that parses a given config file in this format when an instance is created. It also has a single method that snaps a picture and returns its bytes to clean up the main event loop.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from io import BytesIO
from time import sleep
from picamera import PiCamera

class Camera():
    def __init__(self, config):
        self.settings = {}
        for line in config.split('\n'):
            line = line.replace(' ', '')
            if line == '' or line[0] == '#' or line == '\n':
            continue
            key, val = line.split('=')
            if key == 'resolution':
                self.settings[key] = val
            else:
                self.settings[key] = int(val)

    def snap(self):
        cam = PiCamera()
        pic = BytesIO()

        cam.resolution = self.settings['resolution']
        cam.iso = self.settings['iso']
        cam.rotation = self.settings['rotation']
        cam.saturation = self.settings['saturation']
        cam.shutter_speed = self.settings['shutter_speed']
        cam.contrast = self.settings['contrast']
        cam.brightness = self.settings['brightness']

        # Recommended camera warm-up
        cam.start_preview()
        sleep(2)

        cam.capture(pic, 'jpeg')
        cam.close()

        return pic.getvalue()

Note

This config parser works fine for this exact situation, but you may want to look into using Python’s built-in module configparser, which implements a basic configuration language (ini) out of the box.

Now you only need to get the config file from Dropbox folder, take a picture with an instance of the class above and feed the result back into Dropbox. I named the images year-month-day-hour-minute.jpg so that they sort in chronological order when sorting alphabetically, and we can easily filter out pictures that we don’t need later.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import os
from datetime import datetime as dt
from dropbox import Dropbox
from camera import Camera

def main():
    dbx = Dropbox(os.environ.get('DROPBOX_ACCESS_TOKEN'))

    print('Initializing camera with config file from Dropbox.')
    metadata, response = dbx.files_download('/config.txt')
    cam = Camera(response.text)

    print('Taking picture.')
    pic = cam.snap()

    filename = '/%s.jpg' % (dt.now().strftime('%Y-%m-%d-%H-%M'))

    print('Uploading pic with filename', filename)
    dbx.files_upload(pic, filename)

    print('Done.')

if __name__ == "__main__":
    main()

Now we can take pictures according to our specification and find them in our Dropbox. Place your Dropbox API key in a .env file in the project folder to keep code and secrets separate. All that remains is adding a job to the Pi’s Cron table to run the script every 5 minutes (crontab -e):

*/5 * * * * source $HOME/timelapse/.env && /usr/bin/python3 $HOME/timelapse/main.py

For convenience I added a tiny Flask web server. It snaps a picture and returns it to the browser so my parents could set up the right frame and tweak the settings without having to wait 5 minutes for the latest picture to show up with every change. They can just load a web page and look at the pictures in (near) real-time. The development server that comes with Flask is fine for this purpose.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import os
from flask import Flask, make_response
from datetime import datetime as dt

from dropbox import Dropbox

from camera import Camera

app = Flask(__name__)

@app.route('/')
def main():
    dbx = Dropbox(os.environ.get('DROPBOX_ACCESS_TOKEN'))
    metadata, response = dbx.files_download('/config.txt')

    cam = Camera(response.text)
    pic = cam.snap()
    dtstring = dt.now().strftime('%Y-%m-%d-%H-%M')
    filename = f'/{dtstring}.jpg'

    response = make_response(pic)
    response.headers.set('Content-Type', 'image/jpeg')
    response.headers.set('Content-Disposition',
                        'inline',
                        filename=filename)

    return response

if __name__ == "__main__":
    app.run(host='0.0.0.0')

Lastly, the preview server should start up on system boot so it’s always live. I used systemd, the standard Raspbian init system manager. You specify what needs to happen how and when in a .service file – say, timelapse-preview-server.service –, plop it in /etc/systemd/system and tell systemd about it with systemctl enable timelapse-preview-server.service.

[Unit]
Description = The preview server for the timelapse script.
After = network.target

[Service]
User=pi
EnvironmentFile=/home/pi/timelapse/.env
ExecStart = /usr/bin/python3 /home/pi/green-pi/preview-server.py

[Install]
WantedBy = multi-user.target

And that’s that! A Raspberry Pi with a configurable camera that takes a photo every 5 minutes, uploads it to Dropbox and runs a preview server.