IoT und KI

ESP32-CAM Personenerkennung mit On-Device-KI und MQTT-Alarmen

Führen Sie ein winziges Personenerkennungsmodell direkt auf einem ESP32-CAM aus, veröffentlichen Sie einen Konfidenzwert und entprellte Alarme über MQTT und behalten Sie Bilder auf dem Gerät, sofern Sie sie nicht ausdrücklich benötigen.

Illustration zur ESP32-CAM Personenerkennung mit einer winzigen Kameraplatine, die eine Person erkennt und einen MQTT-Alarm an ein Smartphone-Dashboard sendet
Probieren Sie es auf Ihrem eigenen MQTT-Broker ausHolen Sie sich im Free-Tarif einen privaten, verwalteten MQTT-Broker und starten Sie mit fertigen Codebeispielen.
Kostenlos testen Codebeispiele Keine Kreditkarte erforderlich

Die ESP32-CAM Personenerkennung bringt ein kleines neuronales Netz auf eine Platine, die etwa so viel kostet wie ein belegtes Brötchen, und macht daraus einen Anwesenheitssensor, der MQTT-Alarme veröffentlicht. In diesem Tutorial führen Sie das Personenerkennungsmodell von TensorFlow Lite Micro auf einem AI-Thinker ESP32-CAM aus, veröffentlichen einen Konfidenzwert und entprellte Alarme an broker.connectmq.com und lernen die Speicher- und Timing-Fallen kennen, an denen die meisten ersten Versuche scheitern.

Was die Platine kann und was nicht

Das ESP32-CAM kombiniert einen Dual-Core-ESP32 mit einer OV2640-Kamera und auf den meisten Boards 4 MB PSRAM. Das reicht für ein sorgfältig entworfenes TinyML-Modell, aber nicht für einen allgemeinen Objektdetektor.

Fähigkeit ESP32-CAM Raspberry Pi mit Kamera NVIDIA Jetson
Modelltyp Binärer Klassifikator (Person / keine Person) Kleine Detektoren, Klassifikatoren Vollständige Detektoren wie YOLO
Eingabegröße 96 x 96 Graustufen 224 bis 640 Pixel 640 Pixel und mehr
Bounding Boxes Nein Ja Ja
Typischer Einsatz Anwesenheit, Weck-Trigger Zählen, Zonen Mehrere Klassen, mehrere Kameras
Stromversorgung Batteriefreundlich mit Sleep Netzstrom Netzstrom

Das hier verwendete Modell aus den Beispielen von TensorFlow Lite Micro beantwortet pro Frame genau eine Frage: Ist auf diesem Bild eine Person zu sehen? Es sagt Ihnen weder, wo, noch wie viele. Dafür veröffentlichen Sie ein Ereignis an ein größeres Modell oder lesen den Beitrag zur YOLO-Objekterkennung über MQTT.

Teile und Software

  • AI-Thinker ESP32-CAM mit PSRAM sowie ein USB-Seriell-Adapter oder eine ESP32-CAM-MB-Programmierplatine
  • Eine stabile 5-V-Versorgung mit mindestens 500 mA (Brownouts sind das Problem Nummer eins)
  • Arduino IDE 2 mit dem esp32-Boardpaket von Espressif, Board „AI Thinker ESP32-CAM“
  • Bibliotheken: PubSubClient und TensorFlowLite_ESP32 (Bibliotheksverwalter)
  • Aus dem Beispiel person_detection von TensorFlowLite_ESP32: Kopieren Sie person_detect_model_data.h und person_detect_model_data.cpp in Ihren Sketch-Ordner

Wenn Sie noch nie einen ESP32 mit MQTT verbunden haben, behandelt das ESP32-MQTT-Tutorial die Grundlagen, und ESP32-CAM mit MQTT zeigt, wie Sie JPEG-Schnappschüsse veröffentlichen.

So funktioniert die Pipeline

  1. Die Kamera nimmt direkt einen 96 x 96 großen Graustufen-Frame auf, sodass keine Skalierung nötig ist.
  2. Die Pixel werden vom vorzeichenlosen Bereich 0 bis 255 in den vorzeichenbehafteten int8-Bereich umgerechnet, den das quantisierte Modell erwartet.
  3. Der Interpreter führt das Modell aus und liefert zwei Werte: „keine Person“ und „Person“.
  4. Der Sketch veröffentlicht den Personenwert regelmäßig und löst nur dann einen Alarm aus, wenn der Wert über mehrere Frames hinweg über einem Schwellenwert bleibt.
  5. Eine Abklingzeit verhindert, dass eine einzelne Person im Bild jede Sekunde einen Alarm erzeugt.

Der vollständige Sketch

Ersetzen Sie die WLAN-Zugangsdaten und die Topic-ID. Der öffentliche MQTT-Broker broker.connectmq.com akzeptiert anonyme Verbindungen mit jeder nicht leeren Client ID, doch alles, was dort veröffentlicht wird, ist für andere lesbar. Veröffentlichen Sie daher nur Werte und Alarme, niemals Bilder realer Personen.

#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);
  }
}

Öffnen Sie den seriellen Monitor mit 115200 Baud und abonnieren Sie die Topics von Ihrem Computer aus:

mosquitto_sub -h broker.connectmq.com -p 1883 -i pd-watch-e51a -t "connectmq/demo/pd-e51a/#" -v

Treten Sie vor die Kamera, und nach einigen Frames sollte ein Alarm erscheinen; das Score-Topic wird alle fünf Sekunden aktualisiert.

ESP32-CAM Personenerkennung feinabstimmen

  • Schwellenwert. Beginnen Sie bei 60 Prozent. Senken Sie ihn, wenn Personen am Bildrand übersehen werden, und erhöhen Sie ihn, wenn Schatten oder Poster Alarme auslösen.
  • Frames bis zum Alarm. Drei aufeinanderfolgende Frames filtern die meisten Ein-Frame-Störungen heraus, um den Preis eines etwas späteren Alarms.
  • Abklingzeit. Stellen Sie sie so ein, wie oft Sie tatsächlich benachrichtigt werden möchten. Ein Eingang braucht vielleicht 30 Sekunden, ein Sperrbereich womöglich jeden einzelnen Zutritt.
  • Beleuchtung. Das Modell wurde mit vielfältigen Bildern trainiert, hat aber bei sehr wenig Licht Probleme. Ein infrarotfähiges Kameramodul mit IR-Beleuchtung hilft nachts.
  • Sichtfeld. Das Modell sieht nur 96 x 96 Pixel, daher muss eine Person einen angemessenen Teil des Bildes ausfüllen. Montieren Sie die Kamera so, dass der relevante Bereich nah ist.

Stromversorgung und Platzierung

Wenn Kamera und neuronales Netz dauerhaft laufen, bleibt der ESP32 ausgelastet und das WLAN aktiv. Dieses Design eignet sich daher für Standorte mit Netzstrom. Für den Batteriebetrieb kombinieren Sie die Platine mit einem passiven Infrarotsensor (PIR): Lassen Sie den ESP32 schlafen, wecken Sie ihn, wenn der PIR auslöst, führen Sie die Personenerkennung auf einer Handvoll Frames aus, um zu bestätigen, dass die Bewegung von einer Person und nicht von einem Haustier oder einem Vorhang stammt, veröffentlichen Sie das Ergebnis und schicken Sie das Board wieder schlafen. Der MQTT-Payload bleibt gleich, sodass es den Abonnenten egal ist, in welchem Modus die Kamera läuft.

Die Platzierung ist ebenso wichtig wie das Modell. Montieren Sie die Kamera in einer Höhe und einem Winkel, in denen Personen aufrecht und ausreichend groß im Bild erscheinen, richten Sie sie nicht auf Fenster, die starkes Gegenlicht verursachen, und schützen Sie sie vor Regen, sofern das Gehäuse nicht dafür ausgelegt ist.

Auf den Alarmen aufbauen

Sobald Alarme auf einem Topic ankommen, kann alles reagieren, was MQTT spricht: Home Assistant kann ein Licht einschalten, Node-RED kann eine Push-Benachrichtigung senden, und ein Raspberry Pi mit einem größeren Detektor kann einen Schnappschuss anfordern und Personen korrekt zählen. Mit ConnectMQ in der Mitte ist jeder davon einfach ein weiterer Abonnent, und Sie können sie hinzufügen oder entfernen, ohne die Kamera neu zu flashen. In der Industrie lässt sich dasselbe Signal etwa nutzen, um an einer Maschine oder einem Roboterbereich die Anwesenheit von Personen als zusätzliche Information an ein Dashboard zu melden – als Ergänzung, niemals als Ersatz für zertifizierte Sicherheitstechnik.

Fehlerbehebung

  1. „Brownout detector was triggered“ und Neustarts. Verwenden Sie ein besseres 5-V-Netzteil und ein kurzes, dickes Kabel; Kamera und WLAN erzeugen zusammen Stromspitzen.
  2. Camera init failed. Prüfen Sie, ob als Board AI Thinker ESP32-CAM ausgewählt und PSRAM aktiviert ist; kontrollieren Sie außerdem, ob das Flachbandkabel richtig sitzt.
  3. Model init failed. Die Tensor-Arena ist zu klein oder der interne RAM ist fragmentiert; erhöhen Sie kArenaSize in Schritten von 10 KB und initialisieren Sie das Modell vor dem WLAN.
  4. Kompilierfehler in TensorFlow-Headern. Die Bibliotheksversionen unterscheiden sich beim Konstruktor von MicroInterpreter; gleichen Sie Include-Pfade und Konstruktor an das Beispiel an, das mit Ihrer installierten Bibliotheksversion ausgeliefert wird.
  5. MQTT-State -2 oder ständige Neuverbindungen. Das WLAN ist schwach oder die Client ID ist doppelt vergeben. Bringen Sie die Platine näher an den Access Point und stellen Sie sicher, dass kein anderes Gerät dieselbe Client ID verwendet.

Absicherung für den echten Einsatz

Eine Kamera, die einen echten Eingang überwacht, erzeugt personenbezogene Daten, selbst wenn nur ein Wert das Gerät verlässt; in Deutschland, Österreich und der Schweiz gelten dafür die DSGVO beziehungsweise das revidierte Schweizer Datenschutzgesetz, und im Betrieb ist oft zusätzlich die Mitbestimmung zu beachten. Wechseln Sie für alles über einen Test hinaus zu einem privaten Broker mit TLS: Verwenden Sie WiFiClientSecure statt WiFiClient, verbinden Sie sich mit Port 8883 und authentifizieren Sie sich mit mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD", ...). Der öffentliche MQTT-Broker ist ideal für Entwicklung und Tests, aber nicht für produktive Daten gedacht, während ein verwalteter MQTT-Broker wie ConnectMQ jeder Kamera eigene Zugangsdaten gibt und den Datenverkehr verschlüsselt.

Da das Modell ein Personenklassifikator ist, liegt es nahe, es als Auslöser zu nutzen: Wenn es anschlägt, veröffentlichen Sie einen Schnappschuss auf einem separaten Topic oder wecken ein leistungsfähigeres Modell an anderer Stelle. Mehr dazu, wie Sie Modelle auf Mikrocontroller bringen, lesen Sie in TinyML auf dem ESP32 und im umfassenderen Leitfaden zu KI und IoT.

Starten Sie mit ConnectMQ

Erstellen Sie unter app.connectmq.com einen privaten Broker im kostenlosen Serverless-Plan, ohne Kreditkarte, und stellen Sie den Sketch anschließend auf TLS und die generierten Zugangsdaten um. Im Live-Topic-Explorer des Portals sehen Sie die Alarme eintreffen und können eine Schwellenwert-Alarmregel für den person-Wert hinzufügen.

Häufig gestellte Fragen

Wie schnell ist die Personenerkennung auf einem ESP32-CAM?

Rechnen Sie mit höchstens einigen Inferenzen pro Sekunde, abhängig von der Bibliotheksversion und davon, ob optimierte Kernels aktiviert sind. Das reicht für die Anwesenheitserkennung, aber nicht für die Verfolgung von Bewegungen.

Kann das ESP32-CAM Personen zählen oder Bounding Boxes zeichnen?

Mit diesem Modell nicht, denn es klassifiziert nur einen ganzen Frame als Person oder keine Person. Zum Zählen und für Boxen senden Sie Ereignisse an einen Raspberry Pi oder Jetson, auf dem ein Detektor läuft.

Brauche ich PSRAM für die Personenerkennung?

In diesem Sketch liegt der Frame-Buffer der Kamera im PSRAM, und die meisten ESP32-CAM-Boards bringen ihn mit. Die Tensor-Arena selbst wird im schnelleren internen RAM angelegt.

Ist es sicher, Bilder an den öffentlichen MQTT-Broker zu senden?

Nein. Der öffentliche MQTT-Broker ist für jeden lesbar. Veröffentlichen Sie dort daher nur Werte und Alarme und behalten Sie Bilder auf einem privaten Broker mit TLS und Zugangsdaten.

Verwandte Artikel

Alle Artikel →

Ihr privater MQTT-Broker ist nur wenige Klicks entfernt

Konto anlegen, Broker-Zugangsdaten erhalten und das erste Gerät verbinden — ohne Server, die installiert, gepatcht oder skaliert werden müssen.