Raspberry Pi Timelapser

Raspberry Pi HQ Camera Module Timelapser. Script generiert jeden Abend ein Video. Jahresrückblick in 12 Sekunden.

Den Pflanzen beim Wachsen zuschauen. Wenns nur nicht so lange dauern würde, bis sich was tut.

Zum Glück gibts Timelapses. Alle 10 Minuten ein Foto machen und dann mit 24 Bildern die Sekunde abspielen. Ein Tag hat dann 6 Sekunden.

Oder nach einem Jahr: von jedem Tag, das Foto heraussuchen, das um 12:00 gemacht wurde und daraus ein Video generieren. Leider kann ich die Lichtverhältnisse nicht beeinflussen, deswegen flackert es so. An machen Tagen scheint halt die Sonne, an machen ist es bewölkt…

Die Idee

  • Eine Kamera bauen, die sich programmieren lässt. Am besten Raspberry Pi basierend.
  • Gute Fotoqualität und Kamera mit austauschbaren Objektiven -> HQ Kamera Modul.
  • Ein Gehäuse, das unterdacht die Elektronik schützt und die Module sicher zusammenhält.
  • Ein Script, das in gewissen Zeitabständen ein Foto macht und dieses auf meinen Server lädt.
  • Ein anderes Script, das zu Mitternacht aus den Fotos des Tages ein Video generiert.
  • So wenig Kabelsalat wie möglich.
  • Möglichkeiten zur Erweiterung der Funktionen 😀

Hardware

Raspberry Pi 4 Model B

  • Quad Core CPU
  • 4GB Ram
  • 128GB MicroSD
  • 2x USB3.0 Ports
  • 2-lane MIPI CSI camera port


Alles wichtige Infos für dieses Projekt!

Raspberry PI HQ Camera

  • Sony IMX477R Stacked Sensor
  • 12.3 MP
  • C/CS Mount Objektive
  • 1/4″ Stativgewinde

Waveshare POE Hat (C)

  • Power over Ethernet spart Stromkabel.
  • Lüfter für heiße Tage.

Arducam 3.2mm CS Mount Objektiv

  • Weitwinkel – 120 Grad Blickwinkel
  • 12mm Full Frame Equivalent. Crazy Wide.

Gehäuse

Alle Komponenten in Fusion 360 laden und dann das Gehäuse drumherum bauen. Ich wollte eine Möglichst schlanke Lösung, wo die Ports des Raspberry Pi nach hinten raus gehen für eventuelle Erweiterungen.

Herzstück des ganzen Cases. Das Kamera Modul wird zuerst angeschraubt, dann der Raspberry pi.

Somit schaut das Stativgewinde nach unten. Durch die 4 Schrauben wird das Gesamte Case an der Alu Konstruktion des Kamera Moduls gehalten.

Wenn das Objektiv drauf sitzt, ist der Schwerpunkt auch gut ausgeglichen. Die Ports des Pi schauen nach hinten raus.

POE Hat drauf stecken und fertig ist die Elektronik. Die Gesamte Kamera kann jetzt nur mit einem Ethernetkabel betrieben werden.

Braucht natürlich einen POE Injector auf der anderen Seite. Der Standard Router oder Switch kann das natürlich nicht.

Deckel drauf und gut ist. Extra Stylische Belüftungsschlitze designt für noch mehr CPU Speed!

Optik passend zu meinem Hochbeet.

Magic Arm aus dem Foto Zubehör und an den Balkon übers Hochbeet geklemmt.

Die Kiste hängt schon seit 4 Jahren. Das Plastik ist durch die Hitze schon verformt. Mittlerweile macht der Pi auch schon mehr als nur Fotos. Zum Beispiel Temperatur, Luftfeuchtigkeit und Luftdruck messen. Und weil das nicht so zuverlässig funktioniert hat, hört er bei den Wetterstationen der Nachbarn mit.

Nach 4 Jahren hat der 350GB an Fotos und Videos gemacht. Platz ist auf meinem Server genug dafür.

Files als STL zum Download

Software

Zumindest die Teile davon, die für die Timelapse Funktionalität von Relevanz sind.

Da der Raspberry Pi zu gewissen Uhrzeiten Tätigkeiten ausführen muss, verwende ich Crontab dafür. Bei den meisten Linux Distros dabei.

crontab -e
# Edit this file to introduce tasks
# m h  dom mon dow   command
#Minutes [0-59]  
#|   Hours [0-23]  
#|   |   Days [1-31]  
#|   |   |   Months [1-12]  
#|   |   |   |   Days of the Week [Numeric, 0-6]  
#|   |   |   |   |  
#*   *   *   *   * home/path/to/command/the_command.sh 

59 23 * * * sudo /home/pi/timelapse.sh 2>&1 | logger

# take a pic every xx min
*/10 *   * * * sudo ./pic.sh 2>&1 | logger

#publish stats over mqtt and to database
*/10 *   * * * /usr/bin/python3 publisher.py | logger

# change owner of pics and vid to pi
10 00 * * * sudo chown -cR pi:pi /home/pi/Pictures/
10 00 * * * sudo chown -cR pi:pi /home/pi/Videos/

# rsync to nas
15 00 * * * sudo sshpass -p "password" sudo rsync -rat /home/pi/Pictures nasuser@nas_ip::SHARE/Wetterstation/ | logger
15 00 * * * sudo sshpass -p "password" sudo rsync -rat /home/pi/Videos nasuser@nas_ip::SHARE/Wetterstation/ | logger

Klingt schwierig, aber hier die Erklärung Zeile für Zeile.

Ein Foto alle 10 Minuten

# take a pic every xx min
*/10 *   * * * sudo ./pic.sh 2>&1 | logger

Mit dieser Zeile wird alle 10 Minuten das script pic.sh aufgerufen.

pic.sh

DATE=$(date +"%Y-%m-%d_%H%M")
FOLDER=$(date +"%Y-%m-%d")
YEAR=$(date +"%Y")

#echo "taking pic to Pictures/$YEAR/$FOLDER/$DATE.jpg" | /usr/bin/logger
DIR=Pictures/$YEAR/$FOLDER
FILE=$DATE.jpg
OUTPATH=$DIR/$FILE
#echo "directory: $DIR"
#echo "file: $FILE"
echo "saving pic to: $OUTPATH"
mkdir -p $DIR
#raspistill -w 1920 -h 1280 -q 75 -ex verylong -o $OUTPATH
raspistill -w 2028 -h 1520 -drc high -ex verylong -o $OUTPATH
#raspistill -md 2 -sh 15 -ev -3 -drc high -awb auto -ex verylong -o $OUTPATH
#raspistill -mode 2 -awb sun -sh 15 -ev -3 -drc high -ex verylong -o $OUTPATH

Der Filename des Fotos wird aus der Aktuellen Zeit und dem Datum generiert. Der Ordner wo das Foto gespeichert wird auch.

Mit raspistill -w 2028 -h 1520 -drc high -ex verylong -o $OUTPATH wird das Foto gemacht. Doku hier.

Das Bild wird zuerst lokal auf der SD Karte gespeichert. Das Synchronisieren auf das NAS passiert wann anders.

Folder Structure schaut dann so aus:

Stats alle 10 Minuten

Ebenfalls alle 10 Minuten wird ein Python Script aufgerufen, das ein paar Stats über den Raspberry Pi über MQTT verschickt.

#publish stats over mqtt and to database
*/10 *   * * * /usr/bin/python3 publisher.py | logger

import time
import paho.mqtt.client as mqtt
import requests
import mysql.connector as mariadb
from mysql.connector import Error

import re
import subprocess

MQTT_SERVER = "nas_ip
MQTT_TOPIC = "homesens/weatherpi/"
MQTT_PORT = 1883

DB_HOST = "db_ip"
DB_PORT = 3306
DB_NAME = "db_name"
DB_DATABASE = "db_database"
DB_USER = "db_user"
DB_PASS = "db_password"

def on_publish(client, userdata, result):
    #print("published!")
    pass

def mqtt_publish(subtopic, value):
    global mqttc
    mqttc.publish(MQTT_TOPIC + subtopic, value)
    #print(f"MQTT published [{MQTT_TOPIC + subtopic}, {value}] done.")

def db_upload(cpu_temp, disk_total, disk_available, disk_used, lastpic):
    '''
    datetime[datetime], location[varchar24], value[smallint]
    '''
    global connection, cursor
    execstring = f"INSERT INTO `{DB_DATABASE}` " \
                 f"(`datetime`, `cpu_temp`, `disk_total`, `disk_available`, `disk_used`, `lastpic`) " \
                 f"VALUES " \
                 f"(CURRENT_TIMESTAMP(), '{cpu_temp}', '{disk_total}', '{disk_available}', '{disk_used}', '{lastpic}');"
    connection.commit()
    cursor.reset()
    print("DB upload done. Exiting.")

def check_CPU_temp():
    temp = None
    err, msg = subprocess.getstatusoutput('vcgencmd measure_temp')
    if not err:
        m = re.search(r'-?\d\.?\d*', msg)   # a solution with a  regex
        try:
            temp = float(m.group())
        except ValueError: # catch only error needed
            pass
    return temp

def getFreeSpace():
    err, msg = subprocess.getstatusoutput('df -m | grep /dev/root')
    if not err:
        #print(msg)
        out = list(filter(None, msg.split(" ")))
        return out

def getLastPicName():
    err, msg = subprocess.getstatusoutput('grep "saving pic" /var/log/messages | tail -1')
    if not err:
        out = list(filter(None, msg.split("/")))
        #print(out[-1])
        return out[-1]

# ################# PROGRAM START #################

print("WeatherPi publisher started!")

# CONNECT DATABASE
cursor = None
connection = None
try:
    connection = mariadb.connect(host=DB_HOST, user=DB_USER, port=DB_PORT, password=DB_PASS, charset="utf8")
    if connection.is_connected():
        cursor = connection.cursor()
        print(f"DB connected: {DB_USER}@{DB_HOST}:{DB_PORT} {DB_NAME}->{DB_DATABASE}")
        cursor.execute(f"use {DB_NAME};")
    else:
        print("No connection MySQL")
        exit()
except Error as e:
    print("Error while connecting to MySQL", e)

# CONNECT MQTT
mqttc = mqtt.Client("weatherpi")
mqttc.on_publish = on_publish
mqttc.connect(MQTT_SERVER, MQTT_PORT)
print(f"MQTT connected to {MQTT_SERVER}:{MQTT_PORT}")

# PUBLISH DATA
cpu_temp = check_CPU_temp()
mqtt_publish("cpu_temp", cpu_temp)

# 0: Filesystem
# 1: 1M-blocks
# 2: Used
# 3: Available
# 4: Use%
# 5: Mounted on
out = getFreeSpace()
mqtt_publish("disk_total", out[1])
mqtt_publish("disk_available", out[3])
mqtt_publish("disk_used", out[2])

lastpicname = getLastPicName()
mqtt_publish("lastpic", lastpicname)

db_upload(cpu_temp, out[1], out[3], out[2], lastpicname)

# CLEAN UP
if connection is not None:
    connection.close()
time.sleep(2)

Das Script schreibt über MQTT gewisse Stats über die Hardware heraus, die dann live gesehen werden können. Außerdem schreibt es diese Stats auch in die Datenbank, damit zu Debugzwecken und Monitoring Graphen gezeichnet werden können.

Hier Beispielsweise ein Grafana Dashboard der letzten 6 Monate.

Timelapse um Mitternacht

59 23 * * * sudo /home/pi/timelapse.sh 2>&1 | logger

Crontab übersetzt: jeden Tag um 23:59 ruf das Script “timelapse.sh” auf.

timelapse.sh

DATE=$(date +"%Y-%m-%d")
YEAR=$(date +"%Y")

INDIR=/home/pi/Pictures/$YEAR/$DATE
OUTDIR=/home/pi/Videos/$YEAR

FRAMERATEOUT=24
FRAMERATEIN=24

echo "Taking Pics from $INDIR/*"
echo "and saving Timelapse in $OUTDIR/$DATE.mp4"

mkdir -p $OUTDIR
ffmpeg -r $FRAMERATEIN -loglevel warning -pattern_type glob -i "$INDIR/*.jpg" -c:v libx264 -vf fps=$FRAMERATEOUT -pix_fmt yuv420p $OUTDIR/$DATE.mp4
echo "Timelapse done."

DATE Sting wird generiert. YEAR String wird generiert.

INDIR ist das Directory wo die Bilder des heutigen Tages liegen.

OUTDIR ist das Directory wo das Video abgespeichert wird.

Wenns noch kein OUTDIR gibt, dann wird eins angelegt.

Das Video wird mit ffmpeg erstellt.

Backup aufs NAS

# change owner of pics and vid to pi
10 00 * * * sudo chown -cR pi:pi /home/pi/Pictures/
10 00 * * * sudo chown -cR pi:pi /home/pi/Videos/

# rsync to nas
15 00 * * * sudo sshpass -p "password" sudo rsync -rat /home/pi/Pictures nasuser@nas_ip::SHARE/Wetterstation/ | logger
15 00 * * * sudo sshpass -p "password" sudo rsync -rat /home/pi/Videos nasuser@nas_ip::SHARE/Wetterstation/ | logger

Jeden tag um 00:15 wird der Speicher vom Raspberry Pi mit dem meines Servers synchronisiert. Auch wenn der Pi mal voll sein sollte und ich alle Fotos und Videos lösche, bleiben sie am NAS trotzdem bestehen. Sogar über den Sync hinaus. Dafür das Dashboard. Monitoring.

rsync Doku.

Bevor ich das zuverlässig machen kann, Muss ich aber noch den owner der Fotos und Videos auf den standard User ändern. Die werden nämlich mit root gemacht, also kann ich die als pi nicht syncen.

Timelapse des ganzen Jahres

Wenn ich ganz motiviert bin und ein Timelapse des ganzen Jahres, oder genauer gesagt über eine gewisse Zeitspanne machen will, mach ich das lokal. Der Pi hat ja doch nur begrenzt Rechenleistung.

Ein kleines Python Script dazu. Ordnerstruktur beachten!

import os
import subprocess
import shutil
from datetime import datetime

base_folder = "/Users/michaelhedl/Desktop/Wetterstation/Pictures/2024/"
selectedtime = "1200"

temp_dir = "/Users/michaelhedl/Desktop/Wetterstation/temp/"
out_dir = "/Users/michaelhedl/Desktop/Wetterstation/out/"

if not os.path.exists(base_folder):
    print("Base folder does not exist. Exiting.")
    exit()
    
print("Creating timelapse video from " + base_folder + " with selected time " + selectedtime)

if os.path.exists(temp_dir):
    shutil.rmtree(temp_dir)
    print("Deleted old temp folder.")

os.makedirs(temp_dir)
print("Temporary files will be stored in " + temp_dir)

print("Copying files to temp folder...")
count = 0
for root, dirs, files in os.walk(base_folder):
    for file in sorted(files):
        if file.endswith(selectedtime + ".jpg"):
            sourcefilename = os.path.join(root, file)
            destfilename = temp_dir + file
            copiedfile = shutil.copy2(sourcefilename, destfilename)
            print("copied " + sourcefilename + " to " + destfilename)
            count = count+1

print("Copied " + str(count) + " files to temp folder.")
if count == 0:
    print("No files found. Exiting.")
    exit()

startstring = (sorted(os.listdir(temp_dir))[1])[:-4]  # get rid of .jpg
endstring = (sorted(os.listdir(temp_dir))[-1])[:-4]
video_filename = startstring + "--" + endstring

framerate_in = 30
framerate_out = 30
date = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
os.makedirs(out_dir, exist_ok=True)

ffmpeg_command = [
    'ffmpeg',
    '-r', str(framerate_in),              # Input framerate
    '-loglevel', 'info',                  # Only show warnings
    '-pattern_type', 'glob',              # Use glob to match files
    '-i', f'{temp_dir}/*.jpg',            # Input images directory
    '-c:v', 'libx264',                    # Video codec
    '-vf', f'fps={framerate_out}',        # Output framerate
    '-pix_fmt', 'yuv420p',                # Pixel format
    f'{out_dir}/{video_filename}.mp4'     # Output video file
]

print("Combining photos to video. This may take a while...")
ffmpeg = subprocess.run(ffmpeg_command, check=True, capture_output=True)
# print(ffmpeg.stdout.decode('utf-8').strip())
print("Video created. See " + video_filename + ".mp4")

print("Cleaning up temp files...")
shutil.rmtree(temp_dir)

Kurzbeschreibung des Scripts:

  • Wo liegen meine Files, Welches Uhrzeit interessiert mich?
  • Wenn Temp Folder existiert, lösch ihn. Dann mach nen neuen.
  • Kopier alle Files die ins Video kommen in den Temp Folder.
  • Filename = Datetime erstes Foto + Datetime letztes Foto.
  • ffmpeg configurieren
  • ffmpeg über subprocess aufrufen und Video erstellen.
  • Temp Folder löschen