Skip to content

Raspberry Pi Workshop: Automated 4K Time-Lapse Camera Rig (libcamera, rpicam-still & FFmpeg)

Created: 2026-11-06 Last updated: 2026-11-06

Executive Summary: Legacy Raspberry Pi camera tutorials still reference raspistill and picamera—proprietary Broadcom MMAL tools that were permanently removed in 64-bit Debian Bookworm in favor of the open-source Linux kernel V4L2 + libcamera (rpicam-apps / Picamera2) stack. In this workshop, we engineer an unattended 4K Time-Lapse Camera Appliance: connecting a Raspberry Pi Camera Module 3 or HQ Camera over the high-speed MIPI CSI-2 bus, locking white-balance and gain to prevent frame-to-frame flicker, triggering captures via a precision systemd timer, and compiling smooth 60 fps H.264 MP4 movies using ffmpeg with temporal deflickering.

  • MIPI CSI-2 & libcamera Stack


    Drive Sony IMX708 (Camera Module 3) or IMX477 (HQ Camera) sensors directly via the modern Linux Media Controller and rpicam-still ISP pipeline.

  • Anti-Flicker Exposure Locking


    Prevent jarring color and brightness jumps between frames by locking Auto White Balance (--awb), Autofocus (--lens-position), and EV metering.

  • Precision systemd Timer


    Schedule deterministic frame acquisition (OnCalendar / AccuracySec=1s) with disk-space circuit breakers so your storage drive never fills to 100%.

  • ffmpeg 4K 60fps Rendering


    Compile thousands of sequential JPEGs into a universally compatible yuv420p H.264 MP4 video complete with deflicker and timestamp overlays.


1. Camera Architecture: MIPI CSI-2 & The Modern rpicam Pipeline

Unlike USB webcams that compress video on a tiny webcam chip and saturate the USB bus, a MIPI CSI-2 (Camera Serial Interface) ribbon cable streams raw 10-bit/12-bit Bayer sensor data (RAW10/RAW12) directly into the Raspberry Pi's hardware Image Signal Processor (ISP) at up to 4 Gbps.

%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
    SENS["📷 Sony IMX708 / IMX477<br/>(MIPI CSI-2 Ribbon)<br/>12 MP Raw Bayer Stream"]
    ISP["⚡ Broadcom / PiSP Hardware<br/>Debayer, Lens Shading,<br/>Denoise & AWB Tuning"]
    LIB["🛠️ libcamera + rpicam-still<br/>(V4L2 Media Controller)<br/>Saves High-Bitrate JPEG"]
    FFM["🎬 FFmpeg Render Pipeline<br/>deflicker + scale=3840:-2<br/>-> 4K 60fps MP4"]

    SENS ==>|"CSI-2 Lanes"| ISP
    ISP ==>|"DMA Buffer"| LIB
    LIB -->|"frame_00001.jpg"| FFM

Hardware Safety & Pi 5 Connector Note

  1. Always shut down (sudo poweroff) and unplug USB-C power before connecting or disconnecting the flat CSI ribbon cable—hot-plugging the CSI ribbon can short the camera's 3.3V rail.
  2. Raspberry Pi 5 uses smaller 22-pin (0.5 mm pitch) MIPI connectors labeled CAM/DISP 0 and CAM/DISP 1, whereas Raspberry Pi 4 uses a wider 15-pin (1.0 mm pitch) connector. Use the official 22-pin to 15-pin adapter cable when attaching standard camera modules to a Pi 5.

2. Verifying Sensor Detection & Installing rpicam-apps + ffmpeg

On Raspberry Pi OS (Debian 12 Bookworm), camera_auto_detect=1 is enabled in /boot/firmware/config.txt by default. Verify that the kernel V4L2 driver detects your camera sensor and install ffmpeg:

# File: /opt/timelapse/01_verify_camera_setup.sh
#!/usr/bin/env bash
set -euo pipefail

# 1. Install modern rpicam-apps (libcamera CLI), Picamera2, and FFmpeg
sudo apt update && sudo apt install -y rpicam-apps python3-picamera2 ffmpeg

# 2. Grant user access to video/DMA-heap device nodes
sudo usermod -aG video "$USER"

# 3. Enumerate connected MIPI CSI-2 sensors and supported native modes
rpicam-still --list-cameras

3. Time-Lapse Mathematics & Anti-Flicker Capture Script

To plan storage capacity and capture intervals, apply the fundamental time-lapse formula:

  • Frames Required = Target Video Length (seconds) × Playback Frame Rate (fps)
  • Capture Interval (seconds) = Real-World Event Duration (seconds) ÷ Frames Required
Target Subject Real-World Duration Capture Interval Frames Captured Final Video (@ 30 fps) Approx. Disk Space (4K JPEG)
Fast Storm Clouds / Sunset 1 hour (3,600 s) Every 3 seconds 1,200 frames 40 seconds ~3.0 GB
Day-to-Night Urban Skyline 12 hours (43,200 s) Every 15 seconds 2,880 frames 96 seconds ~7.2 GB
3D Print / Construction Site 24 hours (86,400 s) Every 30 seconds 2,880 frames 96 seconds ~7.2 GB
Plant Growth / Germinating Seeds 14 days (1,209,600 s) Every 10 minutes 2,016 frames 67 seconds ~5.0 GB

Production Capture Script (/opt/timelapse/capture_frame.sh)

Why do amateur time-lapse videos suffer from distracting brightness and color pulsing ("flicker")? Because leaving Auto White Balance (--awb auto) and Continuous Autofocus (--autofocus-mode continuous) enabled causes the camera to shift color temperature and focus breathing every time a cloud passes!

Our capture script locks white balance (--awb daylight), locks manual focus at infinity (--autofocus-mode manual --lens-position 0.0), and checks available disk space before writing:

# File: /opt/timelapse/capture_frame.sh
#!/usr/bin/env bash
set -euo pipefail

OUTPUT_DIR="/var/lib/timelapse/frames/$(date -u +%Y-%m-%d)"
MIN_FREE_MB=2048

mkdir -p "$OUTPUT_DIR"

# 1. Disk Space Circuit Breaker: abort if less than 2 GB free on filesystem
FREE_MB=$(df -BM --output=avail "$OUTPUT_DIR" | tail -n1 | tr -dc '0-9')
if (( FREE_MB < MIN_FREE_MB )); then
    echo "[CRITICAL] Disk space below ${MIN_FREE_MB} MB (${FREE_MB} MB left). Skipping capture!" >&2
    exit 1
fi

TIMESTAMP=$(date -u +"%Y%m%d_%H%M%S")
OUT_FILE="${OUTPUT_DIR}/frame_${TIMESTAMP}.jpg"

# 2. Capture 4K (3840x2160) frame with locked Daylight White Balance & Infinity Focus
rpicam-still \
    --nopreview \
    --immediate \
    --width 3840 \
    --height 2160 \
    --quality 93 \
    --awb daylight \
    --metering centre \
    --autofocus-mode manual \
    --lens-position 0.0 \
    --denoise cdn_hq \
    --output "$OUT_FILE"

echo "[OK] Captured: $OUT_FILE"

Make the script executable: sudo chmod +x /opt/timelapse/capture_frame.sh


4. Automating Frame Capture with a Precision systemd Timer

Unlike cron (which has a minimum granularity of 1 minute), a systemd Timer supports second-level intervals (OnUnitActiveSec=15s with AccuracySec=1s) and guarantees that two capture processes never overlap if an exposure takes longer at night.

# File: /etc/systemd/system/timelapse-capture.service
[Unit]
Description=Capture Single 4K Time-Lapse Frame via rpicam-still

[Service]
Type=oneshot
User=nejat
SupplementaryGroups=video
ExecStart=/opt/timelapse/capture_frame.sh
# File: /etc/systemd/system/timelapse-capture.timer
[Unit]
Description=Precision 15-Second Timer for 4K Time-Lapse Rig

[Timer]
OnBootSec=30s
OnUnitActiveSec=15s
AccuracySec=1s
Persistent=true

[Install]
WantedBy=timers.target

Activate the timer:

# File: /opt/timelapse/02_enable_timer.sh
sudo mkdir -p /var/lib/timelapse/frames /var/lib/timelapse/renders
sudo chown -R "$USER:$USER" /var/lib/timelapse /opt/timelapse
sudo systemctl daemon-reload
sudo systemctl enable --now timelapse-capture.timer
systemctl list-timers timelapse-capture.timer

5. Rendering Flicker-Free 4K 60fps MP4 Video with ffmpeg

Once a day's sequence of JPEGs is complete, use ffmpeg with -pattern_type glob, the temporal deflicker filter (which smooths subtle exposure transitions across a rolling 7-frame window), and -pix_fmt yuv420p (mandatory for playback compatibility on QuickTime, iOS, and web browsers):

# File: /opt/timelapse/render_daily_timelapse.sh
#!/usr/bin/env bash
set -euo pipefail

TARGET_DATE="${1:-$(date -u +%Y-%m-%d)}"
FRAME_DIR="/var/lib/timelapse/frames/${TARGET_DATE}"
OUTPUT_MP4="/var/lib/timelapse/renders/timelapse_${TARGET_DATE}_4K60.mp4"

if [[ ! -d "$FRAME_DIR" ]]; then
    echo "Error: Frame directory $FRAME_DIR does not exist." >&2
    exit 1
fi

FRAME_COUNT=$(find "$FRAME_DIR" -name "frame_*.jpg" | wc -l)
echo "Rendering ${FRAME_COUNT} frames from ${FRAME_DIR} into ${OUTPUT_MP4}..."

ffmpeg -y \
    -framerate 60 \
    -pattern_type glob \
    -i "${FRAME_DIR}/frame_*.jpg" \
    -vf "deflicker=mode=pm:size=7,scale=3840:2160:flags=lanczos,format=yuv420p" \
    -c:v libx264 \
    -preset slow \
    -crf 18 \
    -movflags +faststart \
    "$OUTPUT_MP4"

echo "[OK] Rendered 4K60 MP4: $OUTPUT_MP4 ($(du -h "$OUTPUT_MP4" | cut -f1))"

Where to Go Next

Explore the rest of the Raspberry Pi hardware engineering series:

  • Raspberry Pi Hardware Architecture


    Review ARM64 SoC architecture, PCIe Gen 3 NVMe SSD booting, and vcgencmd power/thermal diagnostics.

    Raspberry Pi Introduction

  • Workshop: I2C Mini Weather Station


    Combine your sky time-lapse rig with a Bosch BME280 barometric pressure, temperature, and dew-point logger.

    Weather Station Lab

  • Workshop: IoT Sensor Web Server


    Serve live environmental telemetry and Prometheus metrics via a decoupled Gunicorn + Flask + SQLite WAL stack.

    IoT Web Server Lab