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 details2. 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
Vivek
AI EngineerFull-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.