📝 Tutorials
· 6 min read

Build a Private Security Camera Analyzer with Local Vision AI (2026)


Most smart security cameras ship your footage to someone else’s server for analysis. Ring, Nest, Arlo — they all process your video in the cloud. That means a third party has access to continuous footage of your home, your family, and your daily routines.

There’s a better way. With Ollama and a vision model like LLaVA, you can analyze your security camera feeds entirely on your own hardware. No cloud uploads. No subscriptions. No one watching except you.

In this tutorial, we’ll build a Python script that captures frames from any RTSP camera feed, sends them to a local Ollama vision model, and alerts you when it detects specific events — a person at the door, a package delivery, or unusual activity.

Why Local Vision AI for Security Cameras?

Cloud-based camera analysis has real downsides:

  • Privacy: Your footage lives on someone else’s infrastructure. Data breaches, employee access, and law enforcement requests are all risks. See our GDPR and self-hosted AI guide for the full picture.
  • Subscriptions: Most smart camera features are locked behind monthly fees.
  • Latency: Round-trip to the cloud adds delay to alerts.
  • Internet dependency: No internet means no analysis.

Running a vision model locally eliminates all of these. Your footage never leaves your network.

What You’ll Need

  • A security camera with RTSP support (most IP cameras have this)
  • A machine running Ollama with the llava model pulled
  • Python 3.10+
  • A GPU with at least 8GB VRAM for reasonable speed (see our hardware guide for recommendations)

Install the dependencies:

pip install opencv-python requests

Pull the vision model:

ollama pull llava

If you’re new to Ollama or image analysis with local models, our local AI image describer tutorial covers the fundamentals.

The Complete Script

Here’s the full working script. It captures a frame from your RTSP feed at a configurable interval, sends it to LLaVA for analysis, and triggers alerts based on keywords in the model’s response.

#!/usr/bin/env python3
"""Local security camera analyzer using Ollama vision models."""

import base64
import json
import time
import logging
from datetime import datetime
from pathlib import Path

import cv2
import requests

# --- Configuration ---
RTSP_URL = "rtsp://<username>:<password>@<camera-ip>:554/stream1"
OLLAMA_URL = "http://localhost:11434/api/generate"
MODEL = "llava"
CAPTURE_INTERVAL = 30  # seconds between analyses
SNAPSHOT_DIR = Path("./snapshots")
LOG_FILE = "camera_alerts.log"

PROMPT = (
    "You are a security camera analyst. Describe what you see in this image "
    "in 2-3 sentences. Focus on: people, vehicles, packages, animals, and "
    "anything unusual. Start with the most important observation."
)

ALERT_KEYWORDS = [
    "person", "people", "someone", "man", "woman", "child",
    "package", "box", "delivery",
    "vehicle", "car", "truck",
    "animal", "dog", "cat",
    "fire", "smoke", "broken", "open door",
]

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler(LOG_FILE),
        logging.StreamHandler(),
    ],
)
log = logging.getLogger(__name__)


def capture_frame(rtsp_url: str) -> bytes | None:
    """Grab a single frame from the RTSP stream and return it as JPEG bytes."""
    cap = cv2.VideoCapture(rtsp_url)
    ret, frame = cap.read()
    cap.release()
    if not ret:
        log.error("Failed to capture frame from %s", rtsp_url)
        return None
    _, buf = cv2.imencode(".jpg", frame)
    return buf.tobytes()


def analyze_frame(image_bytes: bytes) -> str:
    """Send a frame to Ollama's vision model and return the description."""
    payload = {
        "model": MODEL,
        "prompt": PROMPT,
        "images": [base64.b64encode(image_bytes).decode("utf-8")],
        "stream": False,
    }
    resp = requests.post(OLLAMA_URL, json=payload, timeout=120)
    resp.raise_for_status()
    return resp.json()["response"]


def check_alerts(description: str) -> list[str]:
    """Return any alert keywords found in the description."""
    desc_lower = description.lower()
    return [kw for kw in ALERT_KEYWORDS if kw in desc_lower]


def save_snapshot(image_bytes: bytes, triggers: list[str]) -> Path:
    """Save a timestamped snapshot when alerts trigger."""
    SNAPSHOT_DIR.mkdir(exist_ok=True)
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    tag = triggers[0] if triggers else "general"
    path = SNAPSHOT_DIR / f"{ts}_{tag}.jpg"
    path.write_bytes(image_bytes)
    return path


def send_notification(description: str, triggers: list[str], snapshot_path: Path):
    """Send an alert. Replace this with your preferred notification method."""
    msg = f"🚨 ALERT: {', '.join(triggers).upper()}\n{description}\nSnapshot: {snapshot_path}"
    log.warning(msg)
    # Examples of what you could add here:
    # - requests.post("http://<home-assistant-ip>:8123/api/services/notify/mobile_app", ...)
    # - smtplib email
    # - Apprise for multi-platform notifications


def run():
    """Main loop: capture, analyze, alert."""
    log.info("Starting camera analyzer — model=%s interval=%ds", MODEL, CAPTURE_INTERVAL)

    while True:
        frame = capture_frame(RTSP_URL)
        if frame is None:
            time.sleep(CAPTURE_INTERVAL)
            continue

        try:
            description = analyze_frame(frame)
        except requests.RequestException as e:
            log.error("Ollama request failed: %s", e)
            time.sleep(CAPTURE_INTERVAL)
            continue

        log.info("Analysis: %s", description)
        triggers = check_alerts(description)

        if triggers:
            snap = save_snapshot(frame, triggers)
            send_notification(description, triggers, snap)

        time.sleep(CAPTURE_INTERVAL)


if __name__ == "__main__":
    run()

Save this as camera_analyzer.py and run it:

python camera_analyzer.py

How It Works

The script follows a simple loop:

  1. Capture: OpenCV connects to your RTSP stream and grabs a single JPEG frame.
  2. Analyze: The frame is base64-encoded and sent to Ollama’s /api/generate endpoint with a security-focused prompt. LLaVA examines the image and returns a natural language description.
  3. Detect: The description is scanned for alert keywords — people, packages, vehicles, animals, or anything unusual.
  4. Alert: When a keyword matches, the frame is saved as a timestamped snapshot and a notification fires.

The 30-second default interval balances responsiveness with resource usage. On a machine with a decent GPU, each analysis takes 2-5 seconds. Adjust CAPTURE_INTERVAL based on your hardware and needs.

Customizing the Prompt

The prompt is the most powerful lever you have. LLaVA responds differently depending on what you ask it to focus on. Some examples:

Front door focus:

PROMPT = "Describe any people, packages, or vehicles visible near the front door. Note if anyone appears to be lingering."

Driveway/parking:

PROMPT = "Describe any vehicles in the driveway. Note license plates if readable, and whether car doors are open or closed."

Backyard/perimeter:

PROMPT = "Describe any movement or activity in this outdoor scene. Note animals, people, or anything out of place."

Tailoring the prompt to each camera’s field of view dramatically improves detection accuracy.

Adding Notifications

The send_notification function is a stub you can wire up to anything. A few practical options:

Home Assistant — If you’re running Home Assistant with Ollama, you can call its REST API to trigger automations, send mobile push notifications, or flash smart lights.

Email via SMTP:

import smtplib
from email.message import EmailMessage

def send_email_alert(subject: str, body: str):
    msg = EmailMessage()
    msg["Subject"] = subject
    msg["From"] = "<sender-email>"
    msg["To"] = "<recipient-email>"
    msg.set_content(body)
    with smtplib.SMTP("localhost", 587) as s:
        s.send_message(msg)

Apprise (multi-platform — Telegram, Slack, Pushover, etc.):

pip install apprise
import apprise

def send_apprise_alert(body: str):
    ap = apprise.Apprise()
    ap.add("tgram://<bot-token>/<chat-id>")
    ap.notify(body=body)

Multi-Camera Setup

To monitor multiple cameras, run one instance per feed. A simple approach with a config list:

CAMERAS = [
    {"name": "front_door", "url": "rtsp://...@192.168.1.10:554/stream1"},
    {"name": "backyard", "url": "rtsp://...@192.168.1.11:554/stream1"},
]

Then spawn a thread per camera, or run separate processes. For more than 3-4 cameras, you’ll want a machine with a beefier GPU — check our hardware recommendations.

Performance Tips

  • Use llava:7b for faster inference. The 13B variant is more accurate but roughly twice as slow.
  • Reduce resolution before sending to the model. LLaVA doesn’t need 4K — 720p or even 480p is usually sufficient for scene description.
  • Increase the interval during low-activity hours (e.g., 2-5 minutes overnight).
  • Queue frames instead of blocking. If analysis takes longer than your interval, you’ll want a producer-consumer pattern so you don’t miss captures.

Privacy and Security Considerations

The entire point of this setup is privacy, so don’t undermine it:

  • Run Ollama bound to localhost only (the default). Don’t expose it to the network unless you’ve added authentication.
  • Store snapshots on encrypted storage. A simple LUKS-encrypted drive works.
  • Set up log rotation so camera_alerts.log doesn’t grow unbounded.
  • If you’re in the EU, review our self-hosted AI and GDPR guide — even local processing of camera footage has legal considerations if it captures public spaces or other people.

What’s Next

This script gives you a solid foundation for private, local security camera analysis. From here you could:

  • Build a web dashboard to browse snapshots and alert history
  • Add motion detection as a pre-filter (OpenCV’s BackgroundSubtractorMOG2) so you only run the vision model when something moves
  • Integrate with Home Assistant for full smart home automation
  • Train a custom model for your specific environment

No cloud. No subscriptions. No one watching but you.