A Raspberry Pi combined with OpenCV makes a surprisingly capable CCTV and motion detection system. Unlike commercial security cameras that lock you into subscription cloud plans, a Pi-based system gives you full privacy control, local storage, and the flexibility to trigger any kind of alert — email, Telegram, SMS, or even a siren — when motion is detected.
In this tutorial, we’ll build a complete Raspberry Pi CCTV system that uses OpenCV’s background subtraction algorithms to detect motion, records video clips when activity is detected, and sends instant alerts to your phone via Telegram. No cloud subscriptions required.
Hardware You Need
Before writing a single line of code, gather your hardware:
- Raspberry Pi: Pi 4B, Pi 5, or Pi Zero 2 W (Pi 5 highly recommended for smoother video processing)
- Camera: Raspberry Pi Camera Module (CSI interface) or USB camera — CSI cameras are preferred for lower CPU overhead
- Storage: MicroSD card (32GB+) or USB drive for recording footage
- Power supply: Official Pi power supply or 5V/3A USB-C adapter
- Housing: Weatherproof enclosure if mounting outdoors
Installing OpenCV on Raspberry Pi
OpenCV installation on Raspberry Pi has gotten much simpler. We’ll use the pre-built wheels via pip, which avoids a 2-4 hour compilation process.
Step 1: Update and install dependencies
sudo apt update && sudo apt upgrade -y
sudo apt install -y python3-pip python3-venv libopencv-dev python3-opencv
sudo apt install -y libatlas-base-dev libhdf5-dev libhdf5-serial-dev
sudo apt install -y libjasper-dev libqtgui4 libqt4-test 2>/dev/null || true
Step 2: Create a virtual environment
python3 -m venv ~/cctv-env
source ~/cctv-env/bin/activate
pip install opencv-python-headless numpy Pillow requests python-telegram-bot
Step 3: Enable the camera interface
sudo raspi-config
# Navigate to: Interface Options → Camera → Enable
# Then reboot: sudo reboot
For Pi 5 with newer Pi OS (Bookworm), the camera is enabled by default via libcamera. No raspi-config step needed.
Step 4: Verify camera access
# For USB camera:
python3 -c "import cv2; cap=cv2.VideoCapture(0); print('Camera OK' if cap.isOpened() else 'FAILED')"
# For Pi Camera Module:
libcamera-hello --timeout 2000
Motion Detection Algorithm: Background Subtraction
OpenCV offers multiple motion detection approaches. We’ll use MOG2 (Mixture of Gaussians 2), which is OpenCV’s most reliable background subtraction algorithm for CCTV use:
- Automatically models the background by observing multiple frames
- Adapts to gradual lighting changes (day/night, clouds)
- Distinguishes moving foreground objects from static background
- Built into OpenCV — no additional libraries needed
Here’s the core motion detection logic:
import cv2
import numpy as np
# Initialize background subtractor
# history=500: frames to model background
# varThreshold=16: sensitivity (lower = more sensitive)
# detectShadows=True: ignore shadows
bg_subtractor = cv2.createBackgroundSubtractorMOG2(
history=500, varThreshold=16, detectShadows=True
)
def detect_motion(frame, min_area=500):
"""
Returns True and list of bounding boxes if motion detected.
min_area: minimum contour area in pixels (filters noise)
"""
# Apply background subtraction
fg_mask = bg_subtractor.apply(frame)
# Remove shadows (gray pixels, value 127)
_, fg_mask = cv2.threshold(fg_mask, 200, 255, cv2.THRESH_BINARY)
# Morphological operations to remove noise
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
fg_mask = cv2.morphologyEx(fg_mask, cv2.MORPH_OPEN, kernel)
fg_mask = cv2.dilate(fg_mask, kernel, iterations=2)
# Find contours of moving objects
contours, _ = cv2.findContours(
fg_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
motion_detected = False
boxes = []
for contour in contours:
if cv2.contourArea(contour) > min_area:
motion_detected = True
x, y, w, h = cv2.boundingRect(contour)
boxes.append((x, y, w, h))
return motion_detected, boxes, fg_mask
Complete CCTV Script with Telegram Alerts
Here is the full script that combines motion detection, video recording, and Telegram alerting:
#!/usr/bin/env python3
"""
Raspberry Pi CCTV with OpenCV Motion Detection
Sends Telegram alerts with snapshot on motion detected.
"""
import cv2
import numpy as np
import time
import requests
import os
from datetime import datetime
# --- CONFIGURATION ---
CAMERA_INDEX = 0 # 0 for USB cam, change for Pi camera
FRAME_WIDTH = 640
FRAME_HEIGHT = 480
FPS = 20
MIN_MOTION_AREA = 800 # px² — increase to reduce false triggers
COOLDOWN_SECONDS = 30 # min seconds between alerts
SAVE_DIR = "/home/pi/cctv-recordings"
# Telegram bot settings
TELEGRAM_TOKEN = "YOUR_BOT_TOKEN"
TELEGRAM_CHAT_ID = "YOUR_CHAT_ID"
def send_telegram_photo(image_path, caption):
url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendPhoto"
with open(image_path, 'rb') as f:
requests.post(url, data={"chat_id": TELEGRAM_CHAT_ID, "caption": caption},
files={"photo": f}, timeout=10)
def main():
os.makedirs(SAVE_DIR, exist_ok=True)
cap = cv2.VideoCapture(CAMERA_INDEX)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, FRAME_WIDTH)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, FRAME_HEIGHT)
cap.set(cv2.CAP_PROP_FPS, FPS)
bg_sub = cv2.createBackgroundSubtractorMOG2(
history=500, varThreshold=16, detectShadows=True
)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
last_alert = 0
print("CCTV started. Monitoring for motion...")
while True:
ret, frame = cap.read()
if not ret:
time.sleep(1)
continue
fg = bg_sub.apply(frame)
_, fg = cv2.threshold(fg, 200, 255, cv2.THRESH_BINARY)
fg = cv2.morphologyEx(fg, cv2.MORPH_OPEN, kernel)
fg = cv2.dilate(fg, kernel, iterations=2)
contours, _ = cv2.findContours(
fg, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
motion = any(cv2.contourArea(c) > MIN_MOTION_AREA for c in contours)
now = time.time()
if motion and (now - last_alert) > COOLDOWN_SECONDS:
last_alert = now
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
snap_path = f"{SAVE_DIR}/motion_{ts}.jpg"
cv2.imwrite(snap_path, frame)
print(f"Motion detected! Saved: {snap_path}")
try:
send_telegram_photo(snap_path, f"Motion detected at {ts}")
except Exception as e:
print(f"Telegram error: {e}")
time.sleep(1.0 / FPS)
if __name__ == "__main__":
main()
Before running, set up your Telegram bot: message @BotFather on Telegram to create a new bot, get your token, then message your bot once and check https://api.telegram.org/bot{TOKEN}/getUpdates to find your chat_id.
Running as a Systemd Service
To make the CCTV system start automatically on boot, create a systemd service:
sudo tee /etc/systemd/system/cctv-monitor.service << 'EOF'
[Unit]
Description=Raspberry Pi CCTV OpenCV Monitor
After=network.target
[Service]
User=pi
WorkingDirectory=/home/pi
ExecStart=/home/pi/cctv-env/bin/python3 /home/pi/cctv-monitor.py
Restart=always
RestartSec=10
Environment=DISPLAY=:0
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable cctv-monitor
sudo systemctl start cctv-monitor
sudo systemctl status cctv-monitor
Storage Management: Auto-Delete Old Recordings
CCTV recordings will fill your SD card quickly. Add a cron job to clean up recordings older than 7 days:
crontab -e
# Add:
0 2 * * * find /home/pi/cctv-recordings -name "*.jpg" -mtime +7 -delete
0 2 * * * find /home/pi/cctv-recordings -name "*.mp4" -mtime +7 -delete
For longer retention, consider mounting a USB drive or external SSD. With Raspberry Pi 5’s USB 3.0 port, an external SSD provides much faster write speeds than an SD card — important when continuously recording video.
Advanced Features: Zones and Sensitivity
One of the advantages of OpenCV over commercial cameras is fine-grained control over detection zones and sensitivity:
Region of Interest (ROI): Only process a specific portion of the frame to avoid false triggers from trees, passing cars, etc.:
# Define ROI: only monitor the doorway area
ROI_X, ROI_Y, ROI_W, ROI_H = 200, 100, 300, 250
roi_frame = frame[ROI_Y:ROI_Y+ROI_H, ROI_X:ROI_X+ROI_W]
fg = bg_sub.apply(roi_frame) # Apply subtraction only to ROI
Time-based arming: Only monitor at night or during work hours:
from datetime import datetime
def is_armed():
now = datetime.now()
# Monitor between 10pm and 7am
return now.hour >= 22 or now.hour < 7
Frequently Asked Questions
What is the best camera for a Raspberry Pi CCTV system?
For best results, use the official Raspberry Pi Camera Module v2 or v3 via the CSI interface — it uses hardware encoding and consumes less CPU than USB cameras. For PTZ functionality or higher resolution, the Arducam IMX477 12MP PTZ camera is excellent. For a budget setup, any USB UVC-compatible camera works with OpenCV via cv2.VideoCapture(0).
Can Raspberry Pi run CCTV 24/7?
Yes, Raspberry Pi is designed for continuous operation. However, use quality SD cards (endurance-rated) or boot from SSD/USB to avoid SD card wear from continuous writes. Enable log rotation and limit recording to motion events only to reduce write cycles. Monitor CPU temperature — add a heatsink and small fan if it exceeds 70°C regularly.
How do I reduce false motion detections?
Increase MIN_MOTION_AREA to filter small objects like insects. Use ROI masking to exclude areas with frequent movement like trees or roads. Increase the history parameter in MOG2 to let the background model adapt longer before triggering. You can also add a frame differencing threshold — only trigger if motion persists for 3+ consecutive frames.
Can I view the live stream remotely?
Yes. The simplest approach is to run an MJPEG stream server using Flask or MotionEye. MotionEye is a popular web interface for Raspberry Pi cameras that handles streaming, motion detection, and recording through a browser UI. Install it via pip: pip install motioneye. Access the stream via your Pi’s IP address on port 8765 over your home network (or VPN for remote access).
Does this work with Raspberry Pi Zero 2 W?
Yes, but with limitations. The Pi Zero 2 W can run OpenCV motion detection at 320×240 resolution smoothly, but struggles at 640×480 or above. Use lower resolution and reduce FPS to 10-15 for stable operation. The Zero 2 W’s 512MB RAM is also a constraint when using heavy OpenCV features. For a capable headless motion detection node, it works well at lower resolutions.
Conclusion
A Raspberry Pi CCTV system with OpenCV motion detection is more capable, more private, and more customizable than any off-the-shelf security camera. With background subtraction, you get reliable detection that adapts to lighting changes, and by routing alerts through Telegram, you get instant smartphone notifications without any cloud subscription fees.
The system described in this tutorial is a solid foundation — you can extend it with face recognition (OpenCV’s Haar cascades or a deep learning model), license plate reading, multiple camera support, or a full NVR (Network Video Recorder) using MotionEye or Frigate.
Ready to build your security camera? Browse our full range of Raspberry Pi cameras and accessories at Zbotic — including camera modules, PTZ cameras, and everything you need to get started.
Add comment