opncrafter

Project: AI Security Camera Analyzer

Dec 30, 2025 • 20 min read

Traditional motion-detection security systems trigger on anything that moves: a tree branch, a passing car, a shadow. YOLOv8 detects specific objects — specifically, it can distinguish a person from a cat, a car from a bicycle, and do all of this at 30+ FPS on a Raspberry Pi 5. Combined with Telegram's Bot API for push notifications, you get a genuinely intelligent security system in about 100 lines of Python.

What We Are Building

  • Eye: USB webcam (local) or IP camera RTSP stream (remote)
  • Brain: YOLOv8n — the fastest YOLO variant, runs on CPU at 30+ FPS
  • Logic: Multi-zone detection, confidence thresholds, alert cooldown
  • Alert: Telegram Bot sends annotated photo with detection metadata

1. Setup

pip install ultralytics opencv-python requests

# Setting up Telegram Bot:
# 1. Open Telegram → search @BotFather → /newbot → follow prompts → copy API Token
# 2. Search @userinfobot to get your personal user ID (numeric, e.g., 123456789)
# 3. Start your bot in Telegram (send /start) — required before it can message you

# Test your bot token:
import requests
TOKEN = "YOUR_BOT_TOKEN"
resp = requests.get(f"https://api.telegram.org/bot{TOKEN}/getMe").json()
print(resp)  # Should show your bot's details

2. Full Security Agent Implementation

import cv2
import time
import requests
from datetime import datetime
from ultralytics import YOLO
from pathlib import Path

# ─── CONFIGURATION ──────────────────────────────────────────────────
TELEGRAM_TOKEN = "YOUR_BOT_TOKEN"
CHAT_ID = "YOUR_USER_ID"
CONFIDENCE_THRESHOLD = 0.55  # 55% confidence minimum (lower = more alerts, more false positives)
COOLDOWN_SECONDS = 30         # Minimum seconds between alerts (prevents spam)
MAX_ALERTS_PER_HOUR = 20      # Hard cap — protects Telegram API rate limits

# Camera source:
# 0 = first USB webcam (laptop built-in or USB)
# "rtsp://admin:password@192.168.1.100/stream" = IP camera RTSP URL
# "/path/to/video.mp4" = pre-recorded video file for testing
CAMERA_SOURCE = 0

# Detection zone: (x1_pct, y1_pct, x2_pct, y2_pct) as fraction of frame
# Default: full frame. Example: (0.3, 0.0, 0.7, 1.0) = center third only
# This prevents false alerts from the road/public sidewalk outside your property
DETECTION_ZONE = (0.0, 0.0, 1.0, 1.0)

# What objects to alert on (COCO class IDs):
# 0=person, 1=bicycle, 2=car, 3=motorcycle, 15=cat, 16=dog
ALERT_CLASSES = {0: "👤 Person", 2: "🚗 Car", 3: "🏍️ Motorcycle"}

# Output directory for annotated alert images
ALERT_DIR = Path("alerts")
ALERT_DIR.mkdir(exist_ok=True)
# ─────────────────────────────────────────────────────────────────────

model = YOLO("yolov8n.pt")  # ~6MB download on first run; nano = fastest, ~85% accuracy

def is_in_detection_zone(box_xyxy, frame_shape):
    """Check if detected bounding box overlaps with our monitoring zone."""
    h, w = frame_shape[:2]
    x1z, y1z = int(DETECTION_ZONE[0] * w), int(DETECTION_ZONE[1] * h)
    x2z, y2z = int(DETECTION_ZONE[2] * w), int(DETECTION_ZONE[3] * h)
    
    x1b, y1b, x2b, y2b = map(int, box_xyxy)
    
    # Check for overlap (not just containment — partial overlap triggers alert)
    return not (x2b < x1z or x1b > x2z or y2b < y1z or y1b > y2z)

def draw_annotations(frame, detections):
    """Draw bounding boxes and labels on the frame."""
    annotated = frame.copy()
    
    # Draw detection zone boundary
    h, w = frame.shape[:2]
    x1z, y1z = int(DETECTION_ZONE[0] * w), int(DETECTION_ZONE[1] * h)
    x2z, y2z = int(DETECTION_ZONE[2] * w), int(DETECTION_ZONE[3] * h)
    cv2.rectangle(annotated, (x1z, y1z), (x2z, y2z), (0, 255, 0), 1)  # Green zone border
    
    for det in detections:
        x1, y1, x2, y2 = det['box']
        label = f"{det['label']} {det['confidence']:.0%}"
        
        # Red bounding box for detected objects
        cv2.rectangle(annotated, (x1, y1), (x2, y2), (0, 0, 255), 2)
        
        # Label background + text
        (tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
        cv2.rectangle(annotated, (x1, y1 - th - 8), (x1 + tw, y1), (0, 0, 255), -1)
        cv2.putText(annotated, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
    
    # Timestamp
    ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    cv2.putText(annotated, ts, (10, frame.shape[0] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (200, 200, 200), 1)
    
    return annotated

def send_telegram_alert(frame, detections):
    """Send annotated alert image to Telegram."""
    annotated = draw_annotations(frame, detections)
    
    # Save with timestamp in filename
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    alert_path = ALERT_DIR / f"alert_{ts}.jpg"
    cv2.imwrite(str(alert_path), annotated, [cv2.IMWRITE_JPEG_QUALITY, 85])
    
    # Build alert message
    detection_summary = "
".join([
        f"• {d['label']} ({d['confidence']:.0%} confidence)"
        for d in detections
    ])
    caption = f"🚨 Security Alert — {datetime.now().strftime('%H:%M:%S')}

Detected:
{detection_summary}"
    
    url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendPhoto"
    with open(alert_path, "rb") as img:
        resp = requests.post(url, data={"chat_id": CHAT_ID, "caption": caption}, files={"photo": img}, timeout=10)
    
    if resp.status_code == 200:
        print(f"✅ Alert sent: {detection_summary.replace(chr(10), ', ')}")
    else:
        print(f"❌ Send failed: {resp.text}")

# ─── MAIN LOOP ───────────────────────────────────────────────────────
cap = cv2.VideoCapture(CAMERA_SOURCE)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)  # Minimal buffer — reduces frame stale lag

last_alert_time = 0
alerts_this_hour = 0
hour_start = time.time()

print(f"Security agent active. Camera source: {CAMERA_SOURCE}")
print(f"Monitoring: {', '.join(ALERT_CLASSES.values())} | Confidence >{CONFIDENCE_THRESHOLD:.0%}")
print("Press 'q' to quit, 's' for screenshot")

while cap.isOpened():
    success, frame = cap.read()
    if not success:
        print("Frame read failed — reconnecting...")
        cap.set(cv2.CAP_PROP_POS_FRAMES, 0)  # Rewind if video file
        time.sleep(0.1)
        continue

    # Reset hourly alert counter
    if time.time() - hour_start > 3600:
        alerts_this_hour = 0
        hour_start = time.time()

    # Run YOLOv8 inference
    results = model(frame, verbose=False)
    in_zone_detections = []

    for r in results:
        for box in r.boxes:
            cls = int(box.cls[0])
            conf = float(box.conf[0])

            if cls not in ALERT_CLASSES or conf < CONFIDENCE_THRESHOLD:
                continue

            box_coords = list(map(int, box.xyxy[0]))
            if not is_in_detection_zone(box_coords, frame.shape):
                continue

            in_zone_detections.append({
                'label': ALERT_CLASSES[cls],
                'confidence': conf,
                'box': box_coords,
                'class': cls,
            })

    # Trigger alert if conditions met
    now = time.time()
    if (in_zone_detections
            and now - last_alert_time > COOLDOWN_SECONDS
            and alerts_this_hour < MAX_ALERTS_PER_HOUR):
        send_telegram_alert(frame, in_zone_detections)
        last_alert_time = now
        alerts_this_hour += 1

    # Live preview window
    display_frame = draw_annotations(frame, in_zone_detections) if in_zone_detections else frame
    cv2.imshow("Security Agent (q=quit, s=screenshot)", display_frame)

    key = cv2.waitKey(1) & 0xFF
    if key == ord('q'):
        break
    elif key == ord('s'):
        shot_path = ALERT_DIR / f"manual_{datetime.now().strftime('%Y%m%d_%H%M%S')}.jpg"
        cv2.imwrite(str(shot_path), frame)
        print(f"Screenshot saved: {shot_path}")

cap.release()
cv2.destroyAllWindows()

Frequently Asked Questions

How do I connect to an IP camera instead of a USB webcam?

Replace CAMERA_SOURCE = 0 with CAMERA_SOURCE = "rtsp://username:password@192.168.1.100:554/stream1". Find your camera's RTSP URL in its web admin interface (or search "[camera model] RTSP URL"). Network cameras typically stream H.264 video over RTSP on port 554. If you get connection errors: (1) ensure the camera and your computer are on the same network, (2) try the alternate stream URL (some cameras use /stream2 for lower-resolution feeds), (3) add cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('H','2','6','4')) to explicitly request H.264 decoding for network cameras. For cameras on a separate network, use a VPN or OpenCV's FFMPEG-based reader which handles more codec configurations.

What's the best way to run this 24/7 on a Raspberry Pi?

Use a systemd service file for automatic restart on failure: create /etc/systemd/system/security-agent.service with ExecStart=/usr/bin/python3 /home/pi/security_agent.py, Restart=always, and RestartSec=10. Enable headless mode by setting cv2.VideoCapture(0) and removing the cv2.imshow() call, since Raspberry Pi OS Lite has no display server. A Raspberry Pi 5 runs YOLOv8n at ~20 FPS on CPU alone — add a Hailo AI Hat for 30+ FPS at $70. Use a UPS (Uninterruptible Power Supply) to prevent SD card corruption from power cuts, a major failure mode for Pi-based appliances. Log alerts to a SQLite database alongside Telegram so you have a searchable archive even if Telegram history is lost.

"The best security system is one you actually trust — because you built it."

Learn more about YOLO →

Continue Reading

👨‍💻
Written by

Vivek

AI Engineer

Full-stack AI engineer with 4+ years building LLM-powered products, autonomous agents, and RAG pipelines. I've shipped AI features to production for startups and worked hands-on with GPT-4o, LangChain, LlamaIndex, and the Vercel AI SDK. I started OpnCrafter to share everything I wish I had when learning — no fluff, just working code and real-world context.

GPT-4oLangChainNext.jsVector DBsRAGVercel AI SDK