Timelapse photography compresses hours or days into seconds, revealing patterns we can’t normally see—clouds racing, plants growing, cities pulsing with life. In this tutorial, we’ll build a simple timelapse camera using the M5Stack TimerCamera X and the Camera-Tool desktop app.
To finish, we’ll use a Python script to stitch the captured images into a smooth video.
🛠️ What You’ll Need- M5Stack TimerCamera X (ESP32 + OV3660 sensor, built-in RTC, battery)
- USB-C cable (data capable)
- Tripod or mount (optional but recommended)
- Computer (Windows for Camera-Tool)
Software:
- Camera-Tool
- Python 3.x with
opencv-python
You must check out PCBWAY for ordering PCBs online for cheap!
You get 10 good-quality PCBs manufactured and shipped to your doorstep for cheap. You will also get a discount on shipping on your first order. Upload your Gerber files onto PCBWAY to get them manufactured with good quality and quick turnaround time. PCBWay now could provide a complete product solution, from design to enclosure production. Check out their online Gerber viewer function. With reward points, you can get free stuff from their gift shop. Also, check out this useful blog on PCBWay Plugin for KiCad from here. Using this plugin, you can directly order PCBs in just one click after completing your design in KiCad.
⚙️ TimeCamX Hardware Features & Why It’s BetterThe M5Stack TimerCamera X is more than just a camera module — it’s a self‑contained, low‑power imaging system designed for real‑world deployments.
Here’s what makes it stand out:
🔑 Key Hardware Features- ESP32‑D0WDQ6 Dual‑Core MCU Provides reliable processing power for image capture, scheduling, and IoT connectivity.
- OV3660 3‑Megapixel Camera Delivers crisp still images and timelapse sequences, with wide‑angle support.
- Built‑in RTC (Real‑Time Clock) Enables precise scheduled captures, even when the device is in deep sleep.
- Deep Sleep Power Management Ultra‑low power consumption for long‑term outdoor or remote deployments.
- Lithium Battery Support + Charging Circuit Runs standalone with a LiPo battery, recharges via USB‑C.
- Compact Enclosure‑Ready Design Flat, modular form factor that fits into 3D‑printed or off‑the‑shelf cases.
- Connect TimerCamera X via USB-C.
- Download and extract the CameraTool software from the M5Stack site
- CameraTool.exe fromCameraTool.exe the files.
- Open Camera-Tool.
- We can use either Serial interface or Wi-Fi to connect the camera. To use the Wi-Fi. Click on the settings near to the port.
- And enter the credentials.
- Then click on Ok and click burn.
- Wait until the Burn finish.
- Then we can change the connection mode to Wi-Fi.
- Now you can able to see the feed in the tool.
- Preview the live feed and adjust focus/exposure from panel
- Set interval (e.g., 10s for clouds, 1min for plants).
- Choose resolution (e.g., 1600×1200).
- Select a save folder (e.g.,
captures/).
- Change the folder as where you want to save the images.
- Mount the camera securely.
- Start the timelapse in Camera-Tool.
- Let it run for your desired duration.
- Verify that images are being saved with timestamped filenames.
Here’s a simple Python script you can include in your blog:
import cv2
import os
import re
from datetime import datetime
import time
# 📁 Configuration
image_folder = r'C:\Users\prade\OneDrive\Desktop\TimeLapse From M5' # updated path
video_name = 'timelapse_annotated.avi'
fps = 30
font = cv2.FONT_HERSHEY_SIMPLEX
font_scale = 1
font_color = (255, 255, 255)
thickness = 2
position = (30, 50) # Top-left corner
def parse_timestamp_from_filename(filename):
"""
Parse timestamps like:
2025_10_25_13_32_53_355.jpg
2025-10-25-13-32-53-355.jpg
Returns a (datetime, formatted_string) tuple or (None, filename) on failure.
"""
base = os.path.splitext(filename)[0]
pattern = re.compile(
r'(?P<year>\d{4})[_-](?P<month>\d{1,2})[_-](?P<day>\d{1,2})[_-]'
r'(?P<hour>\d{1,2})[_-](?P<minute>\d{1,2})[_-](?P<second>\d{1,2})'
r'(?:[_-](?P<msec>\d{1,3}))?$'
)
m = pattern.search(base)
if not m:
return None, base
groups = m.groupdict()
year = int(groups['year'])
month = int(groups['month'])
day = int(groups['day'])
hour = int(groups['hour'])
minute = int(groups['minute'])
second = int(groups['second'])
msec = int(groups['msec']) if groups.get('msec') else 0
try:
dt = datetime(year, month, day, hour, minute, second, msec * 1000)
formatted = dt.strftime('%Y-%m-%d %H:%M:%S')
if msec:
formatted = f"{formatted}.{msec:03d}"
return dt, formatted
except ValueError:
return None, base
def print_progress(count, total, start, bar_len=40):
"""
Simple terminal progress bar showing percentage, counts, elapsed and ETA.
"""
elapsed = time.time() - start
pct = count / total if total else 1.0
filled = int(bar_len * pct)
if filled >= bar_len:
bar = '=' * bar_len
else:
bar = '=' * filled + '>' + '.' * (bar_len - filled - 1)
eta = (elapsed / count * (total - count)) if count and count < total else 0.0
print(f"\rProgress: [{bar}] {count}/{total} ({pct*100:5.1f}%) Elapsed: {elapsed:.1f}s ETA: {eta:.1f}s", end='', flush=True)
# 📸 Collect and sort image files
images = [img for img in os.listdir(image_folder) if img.lower().endswith(('.jpg', '.jpeg', '.png'))]
if not images:
print("No images found in", image_folder)
raise SystemExit(1)
# Parse timestamps and sort by datetime when possible
images_with_ts = []
for img in images:
dt, label = parse_timestamp_from_filename(img)
images_with_ts.append((dt, img, label))
# Sort: parsed datetimes first (by dt), then by filename for those without dt
images_with_ts.sort(key=lambda x: (x[0] is None, x[0] or datetime.min, x[1]))
sorted_images = [t[1] for t in images_with_ts]
# 🖼️ Load first image to get dimensions
first_frame = cv2.imread(os.path.join(image_folder, sorted_images[0]))
if first_frame is None:
print("Failed to read first image:", sorted_images[0])
raise SystemExit(1)
height, width, _ = first_frame.shape
# 🎞️ Setup video writer
fourcc = cv2.VideoWriter_fourcc(*'XVID')
video = cv2.VideoWriter(video_name, fourcc, fps, (width, height))
# 🔁 Frame processing
total_frames = len(sorted_images)
start_time = time.time()
for idx, img in enumerate(sorted_images, start=1):
frame = cv2.imread(os.path.join(image_folder, img))
if frame is None:
print(f"\nWarning: failed to read {img} — skipping")
continue
# 🧠 Extract timestamp from filename (again) for label
_, timestamp_label = parse_timestamp_from_filename(img)
# 📝 Put timestamp on frame
cv2.putText(frame, timestamp_label, position, font, font_scale, font_color, thickness, cv2.LINE_AA)
# ➕ Add frame to video
video.write(frame)
# update progress
print_progress(idx, total_frames, start_time)
video.release()
print() # newline after progress bar
print("✅ Annotated video generated:", video_name)Why this is great:- Cross-platform: Works on Windows, macOS, Linux.
- Customizable: Change FPS, resolution, or add overlays.
- Beginner-friendly: No command-line
ffmpegneeded.
- Try different intervals for different subjects.
- Add overlays (date/time) in Python with
cv2.putText. - Share your timelapse on YouTube, Instagram, or embed in your blog.
- Outdoor enclosure: 3D-print or repurpose a weatherproof box.
- Solar power: Add a small panel for long-term deployment.
- Cloud sync: Push images to Google Drive, Dropbox, or an FTP server.
- Advanced firmware: Use Arduino/ESP-IDF for standalone capture without PC.
With just the TimerCamera X, Camera-Tool, and a short Python script, you’ve built a DIY timelapse camera that’s simple enough for beginners but expandable for advanced makers.








Comments