Eine Raspberry Pi Kamera mit Bewegungserkennung per MQTT macht aus einem Kameramodul eine Ereignisquelle: Statt den ganzen Tag Video zu streamen, beobachtet der Pi einen niedrig aufgelösten Bildstrom, speichert bei einer Veränderung einen Snapshot und veröffentlicht ein kleines JSON-Event, das jeder abonnieren kann. In diesem Tutorial bauen Sie diesen Detektor mit Picamera2 und Python, stimmen ihn gegen Fehlalarme ab und liefern die Events über einen MQTT-Broker aus.
So funktioniert der Detektor
Picamera2 kann gleichzeitig zwei Streams von der Kamera liefern: einen main-Stream in voller Auflösung und einen winzigen lores-Stream. Der Detektor vergleicht aufeinanderfolgende Graustufen-Frames des lores-Streams, was selbst auf einem Pi Zero 2 W kaum CPU kostet, und greift nur dann auf den main-Stream zu, wenn bei erkannter Bewegung ein JPEG gespeichert wird.
Der Ablauf:
- Die Luminanzebene des 320x240-
lores-Frames abgreifen. - Die mittlere quadratische Abweichung zum vorherigen Frame berechnen.
- Überschreitet die Abweichung einen Schwellwert, ein Bewegungs-Event starten, einen Snapshot in voller Größe speichern und eine MQTT-Nachricht mit dessen Link veröffentlichen.
- Weitere Auslösungen während einer Abklingzeit ignorieren.
- Nach einer ruhigen Phase ein Event „Bewegung beendet“ veröffentlichen und den Retained-Zustand auf
idlezurücksetzen.
Bilder per MQTT zu senden ist möglich, aber ein Broker ist ein schlechter Dateiserver: Große Payloads sind langsam, und der öffentliche MQTT-Broker begrenzt Nachrichten auf 64 KB. Das robuste Muster ist, ein Event mit Link zu veröffentlichen und das Bild auf einem Webserver oder Objektspeicher unter Ihrer Kontrolle abzulegen.
Hardware und Software
| Komponente | Hinweise |
|---|---|
| Raspberry Pi 4, 5 oder Zero 2 W | Raspberry Pi OS Bookworm |
| Camera Module 2, 3 oder HQ Camera | Pi 5 und Zero benötigen das passende schmale Flachbandkabel |
| Picamera2 | Auf Raspberry Pi OS vorinstalliert |
paho-mqtt 2.x |
In einer virtuellen Umgebung installiert |
Picamera2 und NumPy stammen aus den Betriebssystempaketen, legen Sie die virtuelle Umgebung daher mit Zugriff darauf an:
sudo apt update
sudo apt install -y python3-picamera2 python3-numpy python3-venv
rpicam-hello --list-cameras
mkdir -p ~/motion-mqtt/snapshots && cd ~/motion-mqtt
python3 -m venv --system-site-packages .venv
source .venv/bin/activate
pip install "paho-mqtt>=2.0"
Zeigt rpicam-hello --list-cameras keine Kamera an, schalten Sie aus, stecken Sie das Flachbandkabel mit korrekt ausgerichteten Kontakten neu ein und versuchen Sie es erneut. Das Picamera2-Handbuch dokumentiert die unten verwendete Stream-Konfiguration.
Event-Topics
| Topic | Payload | Retained |
|---|---|---|
connectmq/demo/e71b04/frontdoor/motion |
JSON-Event: Start oder Ende, Score, Snapshot-URL | Nein |
connectmq/demo/e71b04/frontdoor/motion/state |
active oder idle |
Ja |
connectmq/demo/e71b04/frontdoor/status |
online oder offline (Last Will) |
Ja |
Ein typisches Start-Event sieht so aus:
{
"event": "motion_start",
"score": 18.4,
"snapshot": "http://192.168.1.50:8000/20261016-142233.jpg",
"ts": 1792160553
}
Das Skript für die Raspberry Pi Kamera Bewegungserkennung per MQTT
Speichern Sie es als motion_mqtt.py:
#!/usr/bin/env python3
"""Detect motion with Picamera2 and publish MQTT events with snapshot links."""
import json
import os
import socket
import time
import uuid
from datetime import datetime
from pathlib import Path
import numpy as np
import paho.mqtt.client as mqtt
from picamera2 import Picamera2
BROKER = os.getenv("MQTT_HOST", "broker.connectmq.com")
PORT = int(os.getenv("MQTT_PORT", "1883"))
USERNAME = os.getenv("MQTT_USERNAME")
PASSWORD = os.getenv("MQTT_PASSWORD")
CLIENT_ID = os.getenv("MQTT_CLIENT_ID", f"cam-{uuid.uuid4().hex[:8]}")
PREFIX = os.getenv("MQTT_PREFIX", "connectmq/demo/e71b04/frontdoor")
THRESHOLD = float(os.getenv("MOTION_THRESHOLD", "12"))
COOLDOWN = 10 # Sekunden zwischen Start-Events
QUIET_TIME = 15 # Sekunden ohne Bewegung bis "motion_end"
SNAP_DIR = Path(os.getenv("SNAP_DIR", str(Path.home() / "motion-mqtt" / "snapshots")))
HTTP_PORT = int(os.getenv("SNAP_HTTP_PORT", "8000"))
LORES = (320, 240)
def local_ip():
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.connect(("8.8.8.8", 80))
return s.getsockname()[0]
BASE_URL = os.getenv("SNAP_BASE_URL", f"http://{local_ip()}:{HTTP_PORT}")
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code.is_failure:
print(f"Connection refused: {reason_code}")
return
print(f"Connected as {CLIENT_ID}")
client.publish(f"{PREFIX}/status", "online", qos=1, retain=True)
client.publish(f"{PREFIX}/motion/state", "idle", qos=1, retain=True)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID)
client.on_connect = on_connect
client.will_set(f"{PREFIX}/status", "offline", qos=1, retain=True)
client.reconnect_delay_set(min_delay=1, max_delay=60)
if USERNAME:
client.username_pw_set(USERNAME, PASSWORD)
if PORT == 8883:
client.tls_set()
client.connect(BROKER, PORT, keepalive=30)
client.loop_start()
picam2 = Picamera2()
config = picam2.create_video_configuration(
main={"size": (1280, 720), "format": "RGB888"},
lores={"size": LORES, "format": "YUV420"},
)
picam2.configure(config)
picam2.start()
time.sleep(2) # Belichtung und Weißabgleich einpendeln lassen
SNAP_DIR.mkdir(parents=True, exist_ok=True)
w, h = LORES
previous = None
active = False
last_start = 0.0
last_motion = 0.0
def publish_event(event, score, snapshot=None):
payload = {"event": event, "score": round(score, 1), "ts": int(time.time())}
if snapshot:
payload["snapshot"] = snapshot
client.publish(f"{PREFIX}/motion", json.dumps(payload), qos=1)
print(payload)
try:
while True:
frame = picam2.capture_buffer("lores")
current = frame[: w * h].reshape(h, w).astype(np.int16)
if previous is not None:
score = float(np.square(current - previous).mean())
now = time.time()
if score > THRESHOLD:
last_motion = now
if not active and now - last_start > COOLDOWN:
name = datetime.now().strftime("%Y%m%d-%H%M%S") + ".jpg"
picam2.capture_file(str(SNAP_DIR / name))
publish_event("motion_start", score, f"{BASE_URL}/{name}")
client.publish(f"{PREFIX}/motion/state", "active", qos=1, retain=True)
active = True
last_start = now
elif active and now - last_motion > QUIET_TIME:
publish_event("motion_end", score)
client.publish(f"{PREFIX}/motion/state", "idle", qos=1, retain=True)
active = False
previous = current
time.sleep(0.1)
except KeyboardInterrupt:
pass
finally:
picam2.stop()
client.publish(f"{PREFIX}/status", "offline", qos=1, retain=True).wait_for_publish(2)
client.loop_stop()
client.disconnect()
Die Umwandlung mit astype(np.int16) ist wichtig: Die Subtraktion zweier vorzeichenloser 8-Bit-Arrays läuft über und erzeugt riesige Scheindifferenzen. Wenn Sie nur die ersten w * h Bytes des YUV420-Puffers nehmen, erhalten Sie die Graustufen-Luminanzebene – mehr braucht die Bewegungserkennung nicht.
Die Snapshots bereitstellen
Für eine schnelle lokale Lösung stellen Sie den Snapshot-Ordner in einem zweiten Terminal mit dem integrierten Webserver von Python bereit:
python3 -m http.server 8000 --directory ~/motion-mqtt/snapshots
Die Links in jedem Event öffnen sich jetzt auf jedem Gerät in Ihrem Netzwerk. Dieser Server hat keine Authentifizierung, also lassen Sie ihn ausschließlich im LAN. Für den Zugriff von außen laden Sie Snapshots mit einem eigenen Skript in einen Speicher unter Ihrer Kontrolle hoch und setzen SNAP_BASE_URL auf diesen Ort; das Format der MQTT-Events ändert sich nicht. Ergänzen Sie außerdem einen Cronjob oder systemd-Timer, der alte Bilder löscht, sonst läuft die SD-Karte voll.
Testen und Feinabstimmung
Verfolgen Sie die Events von einem beliebigen Rechner aus:
mosquitto_sub -h broker.connectmq.com -p 1883 -i viewer-e71b04 \
-t 'connectmq/demo/e71b04/frontdoor/#' -v
Bewegen Sie eine Hand vor dem Objektiv, und Sie sollten motion_start mit einem Snapshot-Link sehen, dann motion_end nach 15 ruhigen Sekunden. Die Feinabstimmung dreht sich vor allem um den Schwellwert:
| Symptom | Anpassung |
|---|---|
| Events durch Wolken, Flackern oder Änderungen der Belichtungsautomatik | MOTION_THRESHOLD erhöhen |
| Vorbeigehende Personen werden nicht erkannt | Schwellwert oder Schlafintervall senken |
| Ständige Start- und End-Events | QUIET_TIME erhöhen |
| Nachts viele Fehlalarme | IR-Licht ergänzen oder Alarme zeitgesteuert deaktivieren |
| Bäume oder eine Straße im Bild lösen aus | Das lores-Array vor dem Vergleich auf einen Interessenbereich zuschneiden |
Geben Sie den Score einige Minuten lang ohne Bewegung aus, um das Grundrauschen Ihrer Szene zu sehen, und setzen Sie den Schwellwert dann mit ausreichendem Abstand darüber.
Kameraerkennung vs. PIR-Sensor
Ein passiver Infrarotsensor (PIR) ist der andere gängige Weg, Bewegung zu erkennen, und es lohnt sich zu wissen, wann welcher passt. Ein PIR-Sensor reagiert auf Körperwärme, verbraucht kaum Strom und ignoriert Lichtänderungen, daher löst er seltener durch Schatten und Wolken falsch aus. Er kann Ihnen aber nicht sagen, was sich bewegt hat, und liefert kein Bild zum Anhängen. Die Differenzbildung mit der Kamera reagiert auf jede visuelle Veränderung, auch auf einen Vorhang im Wind, liefert dafür aber einen Snapshot und erlaubt es, die Erkennung auf einen genauen Bildbereich zu beschränken.
Beide lassen sich gut kombinieren. Schließen Sie einen PIR-Sensor an einen freien GPIO-Pin an und akzeptieren Sie eine Kamera-Auslösung nur, wenn die PIR-Leitung innerhalb derselben ein bis zwei Sekunden ebenfalls High ist. Die Kamera bestätigt und dokumentiert dann, was der PIR erkannt hat, und das MQTT-Event kann beide Signale enthalten, sodass Abonnenten entscheiden können, wie sehr sie ihm vertrauen. Halten Sie diese Bestätigungslogik auf dem Pi, damit das veröffentlichte Event einfach bleibt: ein motion_start, ein motion_end, ein Link.
Auf Bewegungs-Events reagieren
Da der Detektor nur Events veröffentlicht, liegt die Reaktion woanders und lässt sich ändern, ohne den Kameracode anzufassen. Ein Node-RED-Flow kann eine Push-Benachrichtigung senden; ein kleiner Python-Subscriber kann den Snapshot per E-Mail verschicken, wie in MQTT-E-Mail-Alarme mit Python gezeigt; und ein zweiter Pi kann mit der Steuerung aus Raspberry Pi Relais per MQTT einen Strahler einschalten, indem er das Topic motion/state abonniert. Auch Home Assistant kann den Retained-Zustand als Binärsensor nutzen. Wenn Sie einen günstigeren Kameraknoten für eine zweite Tür möchten, nutzt der Leitfaden zur ESP32-CAM über MQTT dieselbe Event-Idee auf einem Mikrocontroller.
Datenschutz und Produktivbetrieb
Bewegungs-Events verraten, wann jemand zu Hause ist, daher sollten sie in einer echten Installation nie über einen öffentlichen Broker laufen. Der öffentliche MQTT-Broker unter broker.connectmq.com ist für alle lesbar und nur für den ersten Test gedacht. Für eine echte Kamera nutzen Sie einen privaten Broker von ConnectMQ mit TLS und generierten Zugangsdaten:
export MQTT_PORT=8883
export MQTT_USERNAME=YOUR_USERNAME
export MQTT_PASSWORD=YOUR_PASSWORD
export MQTT_CLIENT_ID=YOUR_CLIENT_ID
export MQTT_PREFIX=home/frontdoor
python motion_mqtt.py
Mit einem verwalteten MQTT-Broker bauen Smartphone, Heimserver und Kamera alle ausgehende Verbindungen zum selben TLS-Endpunkt auf, sodass Sie keine Portweiterleitung benötigen. Betreiben Sie Detektor und Snapshot-Server als systemd-Dienste, damit sie nach einem Stromausfall wieder starten; das Raspberry Pi MQTT Tutorial enthält eine fertige Dienstvorlage.
Jetzt mit ConnectMQ starten
Geben Sie Ihrer Kamera einen sicheren Ort für ihre Alarme. Erstellen Sie einen kostenlosen Serverless-Broker bei ConnectMQ ohne Kreditkarte und verbinden Sie sich mit TLS auf Port 8883. Der Live-Topic-Explorer im Portal zeigt jedes Bewegungs-Event und jeden Retained-Zustand in Echtzeit, was die Abstimmung der Schwellwerte erheblich erleichtert.
Häufig gestellte Fragen
Kann ich das Snapshot-Bild selbst per MQTT senden?
Sie können JPEG-Bytes als Payload veröffentlichen, aber das ist ineffizient und stößt schnell an Größenlimits für Nachrichten. Einen Link zum Bild zu veröffentlichen und die Datei anderswo zu speichern, skaliert deutlich besser.
Wie viel CPU braucht die Bewegungserkennung mit Picamera2?
Sehr wenig, da der Vergleich auf einem 320x240-Graustufen-Stream läuft. Selbst ein Raspberry Pi Zero 2 W schafft das neben dem MQTT-Client.
Warum erhalte ich Bewegungs-Events, obwohl sich nichts bewegt?
Änderungen der Belichtungsautomatik, flackernde Leuchten und wandernde Schatten verändern Pixelwerte. Erhöhen Sie den Schwellwert, schneiden Sie auf einen Interessenbereich zu oder fixieren Sie die Belichtung über die Picamera2-Controls.
Funktioniert das mit dem Raspberry Pi Camera Module 3?
Ja. Picamera2 unterstützt Camera Module 2, Camera Module 3 und die HQ Camera; der Code ändert sich nicht.



