Pi Zero Camera Module Time-Lapse with Picamera2 and ffmpeg
A time-lapse compresses hours of slow change, such as a plant growing, clouds moving or concrete curing on a construction site, into a short video. The Pi Zero is well suited to this job: it is small enough to tape to a window, cheap enough to leave outdoors, and it has a dedicated camera connector so no USB webcam is needed.
In this tutorial you will connect a Raspberry Pi camera module, test it with the rpicam command-line tools on Bookworm, write a Python time-lapse script with Picamera2, calculate how many frames and how much storage you need, and assemble the images into an MP4 with ffmpeg.
Hardware and the Zero Camera Cable
| Item | Notes |
|---|---|
| Pi Zero W or Zero 2 W | Original Zero also works, but has no network to fetch images |
| Camera Module 3, Camera Module 2 or HQ Camera | All supported by the libcamera stack on Bookworm |
| Zero camera cable (22-pin to 15-pin) | The cable supplied with most camera modules is 15-pin at both ends and will not fit the Zero |
| Good 5 V supply | Camera capture raises current draw |
| microSD card with spare space | Each frame is a full JPEG |
- Shut down the Pi and remove power. Never connect the camera with the board powered.
- Gently pull the dark plastic latch on the Zero's camera connector outwards by about a millimetre.
- Insert the narrow end of the cable with the metal contacts facing the board, then push the latch back in.
- Connect the wide end to the camera module in the same way, contacts facing the camera's circuit board.
- Power up. Bookworm's
config.txtcontainscamera_auto_detect=1, so supported official cameras are found automatically.
Testing with rpicam-still
On Bookworm the camera applications are called rpicam-*; older guides use the names libcamera-* or the legacy raspistill, which no longer exists. Test the camera before writing any code.
# List detected cameras; you should see the sensor name (e.g. imx708 for Camera Module 3)
rpicam-hello --list-cameras
# Capture one still at 1920x1080 after a 2 s settling time, without a preview window
rpicam-still -n -t 2000 --width 1920 --height 1080 -o test.jpg
# Install the Python library and ffmpeg (Lite images may not include them)
sudo apt install -y python3-picamera2 ffmpeg --no-install-recommends
# Copy the test image to your laptop to view it (run this on the laptop)
# scp student@pizero.local:test.jpg .
If --list-cameras reports no cameras, the cable is almost always the problem: inserted the wrong way round, not fully seated, or the wrong type.
Planning Frames, Video Length and Storage
Three numbers define a time-lapse: how long you record, the interval between frames, and the playback frame rate. Work them out before starting so the card does not fill up halfway.
frames = recording time (s) / interval (s) video length = frames / playback fps storage needed = frames x average JPEG size Example: 6 hours at one frame every 10 s, played at 25 fps frames = 21600 / 10 = 2160 video length = 2160 / 25 = 86.4 s
JPEG size depends on resolution and scene detail. Measure one of your own test images with ls -lh test.jpg, multiply by the frame count, and keep a healthy margin. Check free space with df -h /.
Choosing the interval depends on how fast the subject changes. Clouds and traffic look smooth at one frame every 2 to 5 seconds; a sunset works well at around 5 to 10 seconds; a growing plant or a construction site needs only one frame every few minutes, and in that case you may also want to stop capturing at night. Writing the interval and duration as constants at the top of the script, as below, makes it easy to change the plan without touching the capture logic.
The Time-Lapse Script
Picamera2 keeps the camera running between captures, so exposure and white balance adapt smoothly and each frame is captured quickly. Save this as timelapse.py and run it with python3 timelapse.py.
#!/usr/bin/env python3
"""Time-lapse capture for Raspberry Pi Zero with Picamera2."""
import os
import shutil
import time
from datetime import datetime
from picamera2 import Picamera2
INTERVAL_S = 10 # seconds between frames
DURATION_S = 6 * 3600 # total recording time
RESOLUTION = (1920, 1080) # matches a 1080p video; keeps memory use low on the Zero
MIN_FREE_MB = 200 # stop before the SD card is completely full
# One folder per run, e.g. ~/timelapse/2025-01-15_0930
out_dir = os.path.expanduser(datetime.now().strftime('~/timelapse/%Y-%m-%d_%H%M'))
os.makedirs(out_dir, exist_ok=True)
picam2 = Picamera2()
config = picam2.create_still_configuration(main={'size': RESOLUTION})
picam2.configure(config)
picam2.start()
time.sleep(2) # let auto exposure and white balance settle
total_frames = DURATION_S // INTERVAL_S
print(f'Capturing {total_frames} frames into {out_dir}')
next_shot = time.monotonic()
try:
for n in range(total_frames):
free_mb = shutil.disk_usage(out_dir).free / 1_000_000
if free_mb < MIN_FREE_MB:
print('Low disk space, stopping')
break
# Zero-padded names (frame_00001.jpg) keep the frames in order for ffmpeg
path = os.path.join(out_dir, f'frame_{n:05d}.jpg')
picam2.capture_file(path)
print(f'{n + 1}/{total_frames} saved {path}')
# Schedule from a fixed start so small delays do not accumulate
next_shot += INTERVAL_S
time.sleep(max(0, next_shot - time.monotonic()))
except KeyboardInterrupt:
print('Stopped by user')
finally:
picam2.stop()
picam2.close()
Using time.monotonic() and a running next_shot target keeps the interval accurate. A naive sleep(10) after each capture would drift, because the capture itself takes time. The free-space check protects the operating system, which can misbehave when the root filesystem is completely full.
Turning Frames into a Video
cd ~/timelapse/2025-01-15_0930 # use your run's folder name
# -framerate sets playback speed; yuv420p makes the file play on phones and browsers
ffmpeg -framerate 25 -i frame_%05d.jpg -c:v libx264 -pix_fmt yuv420p timelapse.mp4
Software H.264 encoding of thousands of 1080p frames is slow on a Zero W. For long runs, copy the folder to your laptop with scp -r and run the same ffmpeg command there.
Common Problems
- Flickering brightness between frames: auto exposure reacts to passing clouds. For indoor scenes with steady light, set fixed controls with
picam2.set_controls({'AeEnable': False, 'ExposureTime': 10000, 'AnalogueGain': 1.0}), choosing values that suit your scene. - Out of memory at full resolution: 512 MB is tight for full-sensor stills. Stay at 1920x1080 or similar.
- Script stops when SSH disconnects: run it under
tmuxor as a systemd service. - Fogged or blurred outdoor images: condensation inside the enclosure. Add a vent or a silica gel packet.
- Wrong order in the video: frame names were not zero-padded.
Summary
With the correct Zero camera cable, rpicam-still for testing, a Picamera2 loop with drift-free timing and one ffmpeg command, a Pi Zero becomes a reliable time-lapse camera. Planning frames and storage in advance stops long recordings from failing halfway. The next camera project in this series builds on Picamera2 to detect motion and record video clips.