La detección de personas con ESP32-CAM coloca una pequeña red neuronal en una placa que cuesta más o menos lo mismo que un bocadillo y la convierte en un sensor de presencia que publica alertas MQTT. En este tutorial ejecutarás el modelo de detección de personas de TensorFlow Lite Micro en un ESP32-CAM de AI-Thinker, publicarás una puntuación de confianza y alertas con antirrebote en broker.connectmq.com y conocerás las trampas de memoria y de temporización que hacen fracasar la mayoría de los primeros intentos.
Qué puede y qué no puede hacer la placa
El ESP32-CAM combina un ESP32 de doble núcleo con una cámara OV2640 y, en la mayoría de las placas, 4 MB de PSRAM. Es suficiente para un modelo TinyML cuidadosamente diseñado, pero no para un detector de objetos de uso general.
| Capacidad | ESP32-CAM | Raspberry Pi con cámara | NVIDIA Jetson |
|---|---|---|---|
| Tipo de modelo | Clasificador binario (persona / sin persona) | Detectores pequeños, clasificadores | Detectores completos como YOLO |
| Tamaño de entrada | 96 x 96 en escala de grises | 224 a 640 píxeles | 640 píxeles o más |
| Cuadros delimitadores | No | Sí | Sí |
| Uso típico | Presencia, disparador de activación | Conteo, zonas | Multiclase, multicámara |
| Alimentación | Apto para batería con modo de suspensión | Red eléctrica | Red eléctrica |
El modelo que se usa aquí, procedente de los ejemplos de TensorFlow Lite Micro, responde a una sola pregunta por fotograma: ¿hay una persona en esta imagen? No te dice dónde ni cuántas. Para eso, publica un evento hacia un modelo más grande o consulta detección de objetos con YOLO sobre MQTT.
Componentes y software
- ESP32-CAM de AI-Thinker con PSRAM, más un adaptador USB-serie o una placa programadora ESP32-CAM-MB
- Una fuente de 5 V estable capaz de entregar al menos 500 mA (las caídas de tensión son el problema número uno)
- Arduino IDE 2 con el paquete de placas esp32 de Espressif, placa "AI Thinker ESP32-CAM"
- Librerías: PubSubClient y TensorFlowLite_ESP32 (Gestor de librerías)
- Del ejemplo
person_detectionde TensorFlowLite_ESP32: copiaperson_detect_model_data.hyperson_detect_model_data.cppen la carpeta de tu sketch
Si nunca has conectado un ESP32 a MQTT, el tutorial de ESP32 con MQTT cubre lo básico y ESP32-CAM con MQTT explica cómo publicar instantáneas JPEG.
Cómo funciona el pipeline
- La cámara captura directamente un fotograma de 96 x 96 en escala de grises, por lo que no hace falta redimensionar.
- Los píxeles se convierten del rango sin signo de 0 a 255 al rango int8 con signo que espera el modelo cuantizado.
- El intérprete ejecuta el modelo y devuelve dos puntuaciones: "sin persona" y "persona".
- El sketch publica periódicamente la puntuación de persona y solo lanza una alerta cuando la puntuación se mantiene por encima de un umbral durante varios fotogramas.
- Un periodo de enfriamiento evita que una sola persona que permanece en el campo de visión genere una alerta cada segundo.
El sketch completo
Sustituye las credenciales Wi-Fi y el ID del topic. El servidor MQTT público gratuito broker.connectmq.com, pensado para pruebas y prototipos, acepta conexiones anónimas con cualquier client ID no vacío, pero todo lo que se publica allí puede leerlo cualquiera, así que publica solo puntuaciones y alertas, nunca imágenes de personas reales.
#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; // porcentaje
const int FRAMES_TO_RAISE = 3;
const unsigned long COOLDOWN_MS = 30000;
const unsigned long SCORE_EVERY_MS = 5000;
// Mapa de pines del ESP32-CAM de AI-Thinker
#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);
}
}
Abre el Monitor Serie a 115200 baudios y suscríbete desde tu ordenador:
mosquitto_sub -h broker.connectmq.com -p 1883 -i pd-watch-e51a -t "connectmq/demo/pd-e51a/#" -v
Ponte delante de la cámara y, tras unos cuantos fotogramas, debería aparecer una alerta; el topic de puntuación se actualiza cada cinco segundos.
Ajuste de la detección de personas con ESP32-CAM
- Umbral. Empieza en el 60 por ciento. Bájalo si se pierden personas en el borde del encuadre y súbelo si sombras o pósteres disparan alertas.
- Fotogramas para disparar. Tres fotogramas consecutivos filtran la mayoría de los fallos de un solo fotograma, a costa de una alerta algo más tardía.
- Enfriamiento. Ajústalo a la frecuencia con la que realmente quieres recibir avisos. Una puerta de entrada puede necesitar 30 segundos; una zona restringida puede requerir cada acceso.
- Iluminación. El modelo se entrenó con imágenes variadas, pero le cuesta con muy poca luz. Un módulo de cámara compatible con infrarrojos y con iluminación IR ayuda de noche.
- Campo de visión. El modelo solo ve 96 x 96 píxeles, así que una persona debe ocupar una parte razonable del fotograma. Monta la cámara de modo que la zona de interés quede cerca.
Alimentación y ubicación
Mantener una cámara y una red neuronal funcionando sin parar mantiene ocupado al ESP32 y el Wi-Fi activo, por lo que este diseño encaja en ubicaciones con alimentación de red. Para funcionar con batería, combina la placa con un sensor de infrarrojos pasivo (PIR): deja que el ESP32 duerma, despiértalo cuando el PIR se active, ejecuta la detección de personas en un puñado de fotogramas para confirmar que el movimiento fue una persona y no una mascota o una cortina, publica el resultado y vuelve a dormir. El payload MQTT sigue siendo el mismo, así que a los suscriptores les da igual en qué modo funcione la cámara.
La ubicación importa tanto como el modelo. Monta la cámara a una altura y con un ángulo en los que las personas aparezcan erguidas y razonablemente grandes en el encuadre, evita apuntarla hacia ventanas que provoquen un contraluz fuerte y mantenla resguardada de la lluvia salvo que la carcasa esté certificada para ello.
Construir sobre las alertas
Una vez que las alertas llegan a un topic, cualquier cosa que hable MQTT puede reaccionar: Home Assistant puede encender una luz, Node-RED puede enviar una notificación al móvil y una Raspberry Pi con un detector más grande puede pedir una instantánea y contar personas como es debido. Con ConnectMQ en medio, cada uno de ellos es simplemente otro suscriptor, y puedes añadirlos o quitarlos sin volver a flashear la cámara.
Solución de problemas
- "Brownout detector was triggered" y reinicios. Usa una fuente de 5 V mejor y un cable corto y grueso; la cámara y el Wi-Fi juntos provocan picos de consumo.
- Camera init failed. Comprueba que la placa seleccionada es AI Thinker ESP32-CAM y que la PSRAM está habilitada; verifica también que el cable plano esté bien encajado.
- Model init failed. La tensor arena es demasiado pequeña o la RAM interna está fragmentada; aumenta
kArenaSizeen pasos de 10 KB e inicializa el modelo antes del Wi-Fi. - Errores de compilación en las cabeceras de TensorFlow. Las versiones de la librería difieren en el constructor de
MicroInterpreter; ajusta las rutas de include y el constructor al ejemplo que incluye la versión de la librería que tienes instalada. - Estado MQTT -2 o reconexiones constantes. El Wi-Fi es débil o el client ID está duplicado. Acerca la placa al punto de acceso y asegúrate de que ningún otro dispositivo use el mismo client ID.
Protegerlo para un uso real
Una cámara que vigila una puerta real produce datos personales, aunque solo salga del dispositivo una puntuación. Para cualquier cosa que vaya más allá de una prueba, pasa a un broker privado con TLS: usa WiFiClientSecure en lugar de WiFiClient, conéctate al puerto 8883 y autentícate con mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD", ...). El servidor público es ideal para el desarrollo, pero no para datos de producción, mientras que un broker MQTT gestionado como ConnectMQ da a cada cámara sus propias credenciales y mantiene el tráfico cifrado.
Como el modelo es un clasificador de personas, una extensión natural es usarlo como disparador: cuando se activa, publica una instantánea en un topic separado o despierta un modelo más capaz en otro lugar. Para saber más sobre cómo encajar modelos en microcontroladores, lee TinyML en el ESP32 y la guía más amplia de IA e IoT.
Empieza con ConnectMQ
Crea un broker privado con el plan Serverless gratuito en app.connectmq.com, sin tarjeta de crédito, y después cambia el sketch a TLS con las credenciales generadas. Puedes ver cómo llegan las alertas en el explorador de topics en directo del portal y añadir una regla de alerta por umbral sobre la puntuación person.
Preguntas frecuentes
¿Qué velocidad tiene la detección de personas en un ESP32-CAM?
Cuenta con unas pocas inferencias por segundo como máximo, según la versión de la librería y si los kernels optimizados están habilitados. Es más que suficiente para detectar presencia, pero no para seguir movimientos.
¿Puede el ESP32-CAM contar personas o dibujar cuadros delimitadores?
No con este modelo, que solo clasifica el fotograma completo como persona o sin persona. Para contar y obtener cuadros, envía eventos a una Raspberry Pi o una Jetson que ejecute un detector.
¿Necesito PSRAM para la detección de personas?
En este sketch el búfer de fotogramas de la cámara se coloca en la PSRAM, y la mayoría de las placas ESP32-CAM la incluyen. La tensor arena en sí se reserva en la RAM interna, que es más rápida.
¿Es seguro enviar imágenes al servidor MQTT público gratuito?
No. Cualquiera puede leer el área pública, así que publica allí solo puntuaciones y alertas y guarda cualquier imagen en un broker privado con TLS y credenciales.



