Raspberry Pi

Alertes MQTT de détection de mouvement par caméra Raspberry Pi avec Picamera2

Construisez un détecteur de mouvement avec une caméra Raspberry Pi et Picamera2, enregistrez un cliché lorsque quelque chose bouge et publiez des événements de mouvement avec lien vers l'image en MQTT pour que tableaux de bord, téléphones et automatisations puissent réagir.

Module caméra Raspberry Pi surveillant une porte d'entrée et envoyant des alertes de mouvement avec liens vers les clichés via MQTT
Essayez-le sur votre propre broker MQTTObtenez un broker MQTT privé et managé avec l’offre Free et partez d’exemples de code prêts à l’emploi.
Essayer gratuitement Exemples de code Aucune carte bancaire requise

Un détecteur de mouvement MQTT à base de caméra Raspberry Pi transforme un module caméra en source d'événements : au lieu de diffuser de la vidéo toute la journée, le Pi observe un flux basse résolution, enregistre un cliché lorsque quelque chose change et publie un petit événement JSON auquel n'importe quel client peut s'abonner. Dans ce tutoriel, vous construirez ce détecteur avec Picamera2 et Python, le réglerez pour éviter les fausses alertes et transmettrez les événements via un broker MQTT.

Comment fonctionne le détecteur

Picamera2 peut exploiter simultanément deux flux de la caméra : un flux main en pleine résolution et un minuscule flux lores. Le détecteur compare des images lores successives en niveaux de gris, ce qui coûte très peu de processeur, même sur un Pi Zero 2 W, et n'utilise le flux main que pour enregistrer un JPEG lorsqu'un mouvement est détecté.

Le déroulement est le suivant :

  1. Récupérer le plan de luminance de l'image lores en 320x240.
  2. Calculer l'écart quadratique moyen par rapport à l'image précédente.
  3. Si l'écart dépasse un seuil, démarrer un événement de mouvement, enregistrer un cliché pleine taille et publier un message MQTT contenant son lien.
  4. Ignorer les déclenchements suivants pendant une période de temporisation.
  5. Après une période calme, publier un événement « fin de mouvement » et remettre l'état retenu à idle.

Envoyer des images via MQTT est possible, mais un broker est un piètre serveur de fichiers : les gros payloads sont lents, et le broker public de test limite les messages à 64 Ko. Le schéma robuste consiste à publier un événement avec un lien et à laisser l'image sur un serveur web ou un stockage objet que vous maîtrisez.

Matériel et logiciels

Élément Remarques
Raspberry Pi 4, 5 ou Zero 2 W Raspberry Pi OS Bookworm
Camera Module 2, 3 ou caméra HQ Le Pi 5 et le Zero nécessitent la nappe étroite correspondante
Picamera2 Préinstallé sur Raspberry Pi OS
paho-mqtt 2.x Installé dans un environnement virtuel

Picamera2 et NumPy proviennent des paquets du système : créez donc l'environnement virtuel avec accès à ces paquets :

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"

Si rpicam-hello --list-cameras n'affiche aucune caméra, éteignez le Pi, remettez la nappe en place avec les contacts dans le bon sens et réessayez. Le manuel Picamera2 documente la configuration des flux utilisée ci-dessous.

Topics des événements

Topic Payload Retenu
connectmq/demo/e71b04/frontdoor/motion Événement JSON : début ou fin, score, URL du cliché Non
connectmq/demo/e71b04/frontdoor/motion/state active ou idle Oui
connectmq/demo/e71b04/frontdoor/status online ou offline (Last Will) Oui

Un événement de début typique ressemble à ceci :

{
  "event": "motion_start",
  "score": 18.4,
  "snapshot": "http://192.168.1.50:8000/20261016-142233.jpg",
  "ts": 1792160553
}

Le script de détection de mouvement MQTT pour caméra Raspberry Pi

Enregistrez-le sous 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       # secondes entre deux événements de début
QUIET_TIME = 15     # secondes sans mouvement avant "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)  # laisser l'exposition et la balance des blancs se stabiliser

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()

La conversion astype(np.int16) est importante : soustraire deux tableaux d'entiers non signés sur 8 bits provoque un dépassement qui produit d'énormes écarts fictifs. Ne prendre que les w * h premiers octets du tampon YUV420 vous donne le plan de luminance en niveaux de gris, qui suffit à la détection de mouvement.

Servir les clichés

Pour une installation locale rapide, servez le dossier des clichés avec le serveur web intégré de Python dans un second terminal :

python3 -m http.server 8000 --directory ~/motion-mqtt/snapshots

Les liens de chaque événement s'ouvrent désormais sur n'importe quel appareil de votre réseau. Ce serveur n'a aucune authentification : gardez-le uniquement sur votre réseau local. Pour un accès depuis l'extérieur, envoyez les clichés depuis votre propre script vers un stockage que vous maîtrisez et définissez SNAP_BASE_URL sur cet emplacement ; le format des événements MQTT ne change pas. Ajoutez aussi une tâche cron ou un timer systemd pour supprimer les anciennes images, sinon la carte SD finira par se remplir.

Tester et régler

Observez les événements depuis n'importe quelle machine :

mosquitto_sub -h broker.connectmq.com -p 1883 -i viewer-e71b04 \
  -t 'connectmq/demo/e71b04/frontdoor/#' -v

Agitez la main devant l'objectif : vous devriez voir motion_start avec un lien vers le cliché, puis motion_end après 15 secondes de calme. Le réglage porte surtout sur le seuil :

Symptôme Ajustement
Événements dus aux nuages, au scintillement ou aux changements d'exposition automatique Augmenter MOTION_THRESHOLD
Les passants ne sont pas détectés Baisser le seuil ou l'intervalle de pause
Événements de début et de fin incessants Augmenter QUIET_TIME
Alertes nocturnes trop nombreuses Ajouter un éclairage IR ou désactiver les alertes selon un horaire
Des arbres ou une route dans le champ déclenchent des alertes Recadrer le tableau lores sur une zone d'intérêt avant la comparaison

Affichez le score pendant quelques minutes sans rien qui bouge pour connaître le bruit de fond de votre scène, puis placez le seuil confortablement au-dessus.

Détection par caméra ou capteur PIR

Un capteur infrarouge passif (PIR) est l'autre moyen courant de détecter un mouvement, et il est utile de savoir quand choisir l'un ou l'autre. Un capteur PIR réagit à la chaleur corporelle, consomme presque rien et ignore les variations de lumière : il produit donc moins de fausses alertes dues aux ombres et aux nuages. En revanche, il ne peut pas dire ce qui a bougé et n'a aucune image à joindre. La différence d'images par caméra réagit à tout changement visuel, y compris un rideau qui bouge au vent, mais elle vous fournit un cliché et permet de limiter la détection à une zone précise de l'image.

Les deux se combinent bien. Raccordez un capteur PIR à une broche GPIO libre et n'acceptez un déclenchement de la caméra que si la ligne PIR est aussi à l'état haut dans la même seconde ou les deux secondes qui suivent. La caméra confirme et documente alors ce que le PIR a détecté, et l'événement MQTT peut inclure les deux signaux pour que les abonnés décident du degré de confiance à lui accorder. Gardez cette logique de confirmation sur le Pi afin que l'événement publié reste simple : un motion_start, un motion_end, un lien.

Réagir aux événements de mouvement

Comme le détecteur se contente de publier des événements, la réaction se trouve ailleurs et peut évoluer sans toucher au code de la caméra. Un flow Node-RED peut envoyer une notification push ; un petit abonné Python peut envoyer le cliché par e-mail, comme le montre l'article sur les alertes e-mail MQTT en Python ; et un second Pi peut allumer un projecteur avec le contrôleur relais MQTT sur Raspberry Pi en s'abonnant au topic motion/state. Dans une maison connectée, Home Assistant peut aussi consommer l'état retenu comme capteur binaire, et Jeedom et Domoticz savent eux aussi communiquer en MQTT. Si vous voulez un nœud caméra moins coûteux pour une seconde porte, le guide ESP32-CAM en MQTT applique le même principe d'événements sur un microcontrôleur.

Confidentialité et mise en production

Les événements de mouvement révèlent quand des personnes sont à la maison : dans une vraie installation, ils ne doivent jamais transiter par un broker public. Le serveur MQTT public gratuit broker.connectmq.com est lisible par tout le monde et n'est destiné qu'au premier test. Pour une vraie caméra, utilisez un broker privé ConnectMQ avec TLS et des identifiants générés :

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

Avec un broker MQTT managé, votre téléphone, un serveur domestique et la caméra se connectent tous en sortant vers le même point d'accès TLS : aucune redirection de ports n'est nécessaire. Exécutez le détecteur et le serveur de clichés comme services systemd afin qu'ils redémarrent après une coupure de courant ; le tutoriel MQTT sur Raspberry Pi inclut un modèle de service prêt à l'emploi.

Démarrer avec ConnectMQ

Offrez à votre caméra un endroit sûr où envoyer ses alertes. Créez un broker Serverless gratuit sur ConnectMQ, sans carte bancaire, et connectez-vous en TLS sur le port 8883. L'explorateur de topics en direct du portail affiche chaque événement de mouvement et chaque état retenu au moment où ils se produisent, ce qui facilite grandement le réglage des seuils.

Questions fréquentes

Peut-on envoyer l'image du cliché elle-même via MQTT ?

Vous pouvez publier les octets JPEG comme payload, mais c'est peu efficace et vous atteindrez vite les limites de taille des messages. Publier un lien vers l'image et stocker le fichier ailleurs passe beaucoup mieux à l'échelle.

Combien de processeur la détection de mouvement Picamera2 consomme-t-elle ?

Très peu, car la comparaison s'effectue sur un flux en niveaux de gris de 320x240. Même un Raspberry Pi Zero 2 W peut la faire tourner à côté du client MQTT.

Pourquoi reçois-je des événements de mouvement alors que rien ne bouge ?

Les changements d'exposition automatique, les éclairages qui scintillent et les ombres mouvantes modifient la valeur des pixels. Augmentez le seuil, recadrez sur une zone d'intérêt ou verrouillez l'exposition avec les contrôles de Picamera2.

Cela fonctionne-t-il avec le Raspberry Pi Camera Module 3 ?

Oui. Picamera2 prend en charge le Camera Module 2, le Camera Module 3 et la caméra HQ ; le code ne change pas.

Articles liés

Journal d'accès RFID MQTT sur Raspberry Pi avec le lecteur RC522

Câblez un lecteur RFID RC522 sur un Raspberry Pi, lisez les badges en Python, vérifiez-les dans une liste blanche locale et publiez chaque tentative d'accès sous forme d'événement MQTT exploitable par vos outils de journalisation, tableaux de bord et alertes.

8 min de lecture

Installer Mosquitto sur Raspberry Pi comme broker MQTT local sécurisé

Installez Mosquitto sur un Raspberry Pi, sécurisez-le avec des mots de passe et une configuration de listener raisonnable, testez-le avec mosquitto_pub et mosquitto_sub, et découvrez quand un broker local doit faire un pont vers un broker cloud.

9 min de lecture

Tous les articles →

Votre broker MQTT privé, à quelques clics

Créez un compte, récupérez les identifiants de votre broker et connectez votre premier appareil — aucun serveur à installer, mettre à jour ou dimensionner.