La détection de personnes ESP32-CAM place un petit réseau de neurones sur une carte qui coûte à peu près le prix d'un sandwich et la transforme en capteur de présence qui publie des alertes MQTT. Dans ce tutoriel, vous allez exécuter le modèle de détection de personnes de TensorFlow Lite Micro sur un ESP32-CAM AI-Thinker, publier un score de confiance et des alertes anti-rebond vers broker.connectmq.com, et découvrir les pièges de mémoire et de timing qui font échouer la plupart des premières tentatives.
Ce que la carte peut faire, et ce qu'elle ne peut pas faire
L'ESP32-CAM associe un ESP32 double cœur à une caméra OV2640 et, sur la plupart des cartes, à 4 Mo de PSRAM. C'est suffisant pour un modèle TinyML soigneusement conçu, mais pas pour un détecteur d'objets généraliste.
| Capacité | ESP32-CAM | Raspberry Pi avec caméra | NVIDIA Jetson |
|---|---|---|---|
| Type de modèle | Classificateur binaire (personne / pas de personne) | Petits détecteurs, classificateurs | Détecteurs complets comme YOLO |
| Taille d'entrée | 96 x 96 en niveaux de gris | 224 à 640 pixels | 640 pixels et plus |
| Boîtes englobantes | Non | Oui | Oui |
| Usage typique | Présence, déclencheur de réveil | Comptage, zones | Multi-classes, multi-caméras |
| Alimentation | Compatible batterie avec mise en veille | Secteur | Secteur |
Le modèle utilisé ici, issu des exemples TensorFlow Lite Micro, répond à une seule question par image : y a-t-il une personne sur cette image ? Il ne vous dit ni où, ni combien. Pour cela, publiez un événement vers un modèle plus puissant, ou consultez la détection d'objets YOLO via MQTT.
Matériel et logiciels
- ESP32-CAM AI-Thinker avec PSRAM, plus un adaptateur USB-série ou une carte de programmation ESP32-CAM-MB
- Une alimentation 5 V stable capable de fournir au moins 500 mA (les chutes de tension sont le problème numéro un)
- Arduino IDE 2 avec le paquet de cartes esp32 d'Espressif, carte « AI Thinker ESP32-CAM »
- Bibliothèques : PubSubClient et TensorFlowLite_ESP32 (gestionnaire de bibliothèques)
- Depuis l'exemple
person_detectionde TensorFlowLite_ESP32 : copiezperson_detect_model_data.hetperson_detect_model_data.cppdans le dossier de votre sketch
Si vous n'avez jamais connecté un ESP32 à MQTT, le tutoriel ESP32 MQTT couvre les bases, et ESP32-CAM avec MQTT explique comment publier des instantanés JPEG.
Fonctionnement du pipeline
- La caméra capture directement une image de 96 x 96 en niveaux de gris, donc aucun redimensionnement n'est nécessaire.
- Les pixels sont convertis de la plage non signée 0 à 255 vers la plage int8 signée attendue par le modèle quantifié.
- L'interpréteur exécute le modèle et renvoie deux scores : « pas de personne » et « personne ».
- Le sketch publie périodiquement le score de personne et ne déclenche une alerte que lorsque le score reste au-dessus d'un seuil pendant plusieurs images.
- Un délai de récupération évite qu'une seule personne restant dans le champ génère une alerte chaque seconde.
Le sketch complet
Remplacez les identifiants Wi-Fi et l'identifiant de topic. La zone publique de broker.connectmq.com, le serveur MQTT public gratuit, accepte les connexions anonymes avec n'importe quel client ID non vide, mais tout ce qui y est publié est lisible par d'autres : publiez donc uniquement des scores et des alertes, jamais d'images de personnes réelles.
#include <WiFi.h>
#include <PubSubClient.h>
#include "esp_camera.h"
#include "esp_heap_caps.h"
#include <TensorFlowLite_ESP32.h>
#include "tensorflow/lite/micro/micro_error_reporter.h"
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/micro/micro_mutable_op_resolver.h"
#include "tensorflow/lite/schema/schema_generated.h"
#include "person_detect_model_data.h"
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* PREFIX = "connectmq/demo/pd-e51a/home/frontdoor/cam1";
const int PERSON_THRESHOLD = 60; // percent
const int FRAMES_TO_RAISE = 3;
const unsigned long COOLDOWN_MS = 30000;
const unsigned long SCORE_EVERY_MS = 5000;
// AI-Thinker ESP32-CAM pin map
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
WiFiClient net;
PubSubClient mqtt(net);
char clientId[32];
constexpr int kArenaSize = 100 * 1024;
uint8_t* tensorArena = nullptr;
tflite::MicroErrorReporter errorReporter;
tflite::MicroInterpreter* interpreter = nullptr;
TfLiteTensor* input = nullptr;
int hits = 0;
unsigned long lastAlert = 0, lastScore = 0;
bool initCamera() {
camera_config_t c = {};
c.ledc_channel = LEDC_CHANNEL_0;
c.ledc_timer = LEDC_TIMER_0;
c.pin_d0 = Y2_GPIO_NUM; c.pin_d1 = Y3_GPIO_NUM; c.pin_d2 = Y4_GPIO_NUM;
c.pin_d3 = Y5_GPIO_NUM; c.pin_d4 = Y6_GPIO_NUM; c.pin_d5 = Y7_GPIO_NUM;
c.pin_d6 = Y8_GPIO_NUM; c.pin_d7 = Y9_GPIO_NUM;
c.pin_xclk = XCLK_GPIO_NUM; c.pin_pclk = PCLK_GPIO_NUM;
c.pin_vsync = VSYNC_GPIO_NUM; c.pin_href = HREF_GPIO_NUM;
c.pin_sccb_sda = SIOD_GPIO_NUM; c.pin_sccb_scl = SIOC_GPIO_NUM;
c.pin_pwdn = PWDN_GPIO_NUM; c.pin_reset = RESET_GPIO_NUM;
c.xclk_freq_hz = 20000000;
c.pixel_format = PIXFORMAT_GRAYSCALE;
c.frame_size = FRAMESIZE_96X96;
c.fb_count = 1;
c.fb_location = CAMERA_FB_IN_PSRAM;
c.grab_mode = CAMERA_GRAB_LATEST;
return esp_camera_init(&c) == ESP_OK;
}
bool initModel() {
tensorArena = (uint8_t*)heap_caps_malloc(kArenaSize, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
if (!tensorArena) return false;
const tflite::Model* model = tflite::GetModel(g_person_detect_model_data);
static tflite::MicroMutableOpResolver<5> resolver;
resolver.AddAveragePool2D();
resolver.AddConv2D();
resolver.AddDepthwiseConv2D();
resolver.AddReshape();
resolver.AddSoftmax();
static tflite::MicroInterpreter staticInterpreter(
model, resolver, tensorArena, kArenaSize, &errorReporter);
interpreter = &staticInterpreter;
if (interpreter->AllocateTensors() != kTfLiteOk) return false;
input = interpreter->input(0);
return true;
}
void connectMqtt() {
while (!mqtt.connected()) {
String status = String(PREFIX) + "/status";
if (mqtt.connect(clientId, status.c_str(), 1, true, "offline")) {
mqtt.publish(status.c_str(), "online", true);
} else {
Serial.printf("MQTT failed, state=%d\n", mqtt.state());
delay(3000);
}
}
}
void setup() {
Serial.begin(115200);
if (!initCamera()) { Serial.println("Camera init failed"); ESP.restart(); }
if (!initModel()) { Serial.println("Model init failed, increase arena"); ESP.restart(); }
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) delay(300);
snprintf(clientId, sizeof(clientId), "esp32cam-%06lx", (unsigned long)(ESP.getEfuseMac() & 0xFFFFFF));
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setKeepAlive(30);
connectMqtt();
}
void loop() {
if (!mqtt.connected()) connectMqtt();
mqtt.loop();
camera_fb_t* fb = esp_camera_fb_get();
if (!fb) return;
for (size_t i = 0; i < input->bytes && i < fb->len; i++) {
input->data.int8[i] = (int8_t)(fb->buf[i] - 128);
}
esp_camera_fb_return(fb);
unsigned long t0 = millis();
if (interpreter->Invoke() != kTfLiteOk) return;
unsigned long inferMs = millis() - t0;
TfLiteTensor* out = interpreter->output(0);
int personPct = (out->data.int8[1] + 128) * 100 / 255;
hits = personPct >= PERSON_THRESHOLD ? hits + 1 : 0;
unsigned long now = millis();
if (hits == FRAMES_TO_RAISE && now - lastAlert > COOLDOWN_MS) {
lastAlert = now;
char msg[96];
snprintf(msg, sizeof(msg), "{\"event\":\"person\",\"score\":%d,\"uptimeMs\":%lu}", personPct, now);
mqtt.publish((String(PREFIX) + "/alert").c_str(), msg);
Serial.println(msg);
}
if (now - lastScore > SCORE_EVERY_MS) {
lastScore = now;
char msg[96];
snprintf(msg, sizeof(msg), "{\"person\":%d,\"inferMs\":%lu,\"rssi\":%d}", personPct, inferMs, WiFi.RSSI());
mqtt.publish((String(PREFIX) + "/score").c_str(), msg);
}
}
Ouvrez le moniteur série à 115200 bauds et abonnez-vous depuis votre ordinateur :
mosquitto_sub -h broker.connectmq.com -p 1883 -i pd-watch-e51a -t "connectmq/demo/pd-e51a/#" -v
Placez-vous devant la caméra : une alerte devrait apparaître après quelques images ; le topic de score se met à jour toutes les cinq secondes.
Réglage de la détection de personnes ESP32-CAM
- Seuil. Commencez à 60 %. Abaissez-le si des personnes en bord d'image ne sont pas détectées, relevez-le si des ombres ou des affiches déclenchent des alertes.
- Images avant alerte. Trois images consécutives filtrent la plupart des anomalies ponctuelles, au prix d'une alerte légèrement plus tardive.
- Délai de récupération. Réglez-le selon la fréquence à laquelle vous souhaitez réellement être notifié. Une porte d'entrée peut nécessiter 30 secondes ; une zone réglementée peut exiger chaque passage.
- Éclairage. Le modèle a été entraîné sur des images variées mais peine en très faible luminosité. Un module caméra compatible infrarouge avec un éclairage IR aide la nuit.
- Champ de vision. Le modèle ne voit que 96 x 96 pixels : une personne doit donc occuper une part raisonnable de l'image. Montez la caméra de sorte que la zone d'intérêt soit proche.
Alimentation et emplacement
Faire tourner en continu une caméra et un réseau de neurones occupe l'ESP32 et maintient le Wi-Fi actif ; ce montage convient donc aux emplacements alimentés sur secteur. Pour un fonctionnement sur batterie, associez la carte à un capteur infrarouge passif (PIR) : laissez l'ESP32 en veille, réveillez-le quand le PIR se déclenche, exécutez la détection de personnes sur quelques images pour confirmer que le mouvement provient bien d'une personne et non d'un animal ou d'un rideau, publiez le résultat et rendormez-le. Le payload MQTT reste identique, si bien que les abonnés ne se soucient pas du mode de fonctionnement de la caméra.
L'emplacement compte autant que le modèle. Installez la caméra à une hauteur et sous un angle où les personnes apparaissent debout et suffisamment grandes dans l'image, évitez de la pointer vers des fenêtres qui provoquent un fort contre-jour, et protégez-la de la pluie sauf si le boîtier est prévu pour cela.
Exploiter les alertes
Dès que les alertes arrivent sur un topic, tout ce qui parle MQTT peut réagir : dans une maison connectée, Home Assistant peut allumer une lumière, Node-RED peut envoyer une notification sur téléphone, et un Raspberry Pi exécutant un détecteur plus puissant peut demander un instantané et compter correctement les personnes. Avec ConnectMQ au milieu, chacun de ces éléments n'est qu'un abonné de plus, et vous pouvez en ajouter ou en retirer sans reflasher la caméra. Jeedom et Domoticz savent aussi communiquer en MQTT.
Dépannage
- « Brownout detector was triggered » et redémarrages. Utilisez une meilleure alimentation 5 V et un câble court et épais ; la caméra et le Wi-Fi réunis provoquent des pics de courant.
- Camera init failed. Vérifiez que la carte sélectionnée est AI Thinker ESP32-CAM et que la PSRAM est activée ; vérifiez aussi que la nappe est bien enfoncée.
- Model init failed. L'arène de tenseurs est trop petite ou la RAM interne est fragmentée ; augmentez
kArenaSizepar pas de 10 Ko et initialisez le modèle avant le Wi-Fi. - Erreurs de compilation dans les en-têtes TensorFlow. Le constructeur de
MicroInterpreterdiffère selon les versions de la bibliothèque ; alignez les chemins d'inclusion et le constructeur sur l'exemple fourni avec la version installée. - État MQTT -2 ou reconnexions permanentes. Le Wi-Fi est faible ou le client ID est en double. Rapprochez la carte du point d'accès et assurez-vous qu'aucun autre appareil n'utilise le même client ID.
Sécuriser l'installation pour un usage réel
Une caméra qui surveille une vraie porte produit des données personnelles, même si seul un score quitte l'appareil. Pour tout ce qui dépasse un test, passez à un broker privé avec TLS : utilisez WiFiClientSecure au lieu de WiFiClient, connectez-vous au port 8883 et authentifiez-vous avec mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD", ...). Le serveur MQTT public gratuit est idéal pour le développement, tandis qu'un broker MQTT managé comme ConnectMQ attribue à chaque caméra ses propres identifiants et garde le trafic chiffré.
Comme le modèle est un classificateur de personnes, une extension naturelle consiste à l'utiliser comme déclencheur : lorsqu'il se déclenche, publiez un instantané sur un topic séparé ou réveillez un modèle plus performant ailleurs. Pour en savoir plus sur l'intégration de modèles dans des microcontrôleurs, lisez TinyML sur l'ESP32 et le guide plus général sur l'IA et l'IoT.
Démarrer avec ConnectMQ
Créez un broker privé avec l'offre gratuite Serverless sur app.connectmq.com, sans carte bancaire, puis passez le sketch en TLS avec les identifiants générés. Vous pouvez voir les alertes arriver dans l'explorateur de topics en direct du portail et ajouter une règle d'alerte à seuil sur le score person.
Questions fréquentes
Quelle est la vitesse de la détection de personnes sur un ESP32-CAM ?
Comptez au maximum quelques inférences par seconde, selon la version de la bibliothèque et l'activation ou non des noyaux optimisés. C'est largement suffisant pour détecter une présence, mais pas pour suivre un mouvement.
L'ESP32-CAM peut-il compter les personnes ou dessiner des boîtes englobantes ?
Pas avec ce modèle, qui se contente de classer une image entière en « personne » ou « pas de personne ». Pour le comptage et les boîtes, envoyez des événements vers un Raspberry Pi ou un Jetson exécutant un détecteur.
Ai-je besoin de PSRAM pour la détection de personnes ?
Dans ce sketch, le tampon d'image de la caméra est placé en PSRAM, et la plupart des cartes ESP32-CAM en sont équipées. L'arène de tenseurs, elle, est allouée dans la RAM interne, plus rapide.
Est-il sûr d'envoyer des images vers le serveur MQTT public gratuit ?
Non. La zone publique est lisible par tous : publiez-y uniquement des scores et des alertes, et conservez les images sur un broker privé avec TLS et identifiants.



