IoT e IA

Detecção de Pessoas com ESP32-CAM, IA no Dispositivo e Alertas MQTT

Rode um pequeno modelo de detecção de pessoas diretamente em uma ESP32-CAM, publique um índice de confiança e alertas com debounce via MQTT e mantenha as imagens no dispositivo, a menos que realmente precise delas.

Ilustração de detecção de pessoas com ESP32-CAM, com uma pequena placa de câmera reconhecendo uma pessoa e enviando um alerta MQTT para um dashboard no celular
Rode isso no seu próprio broker MQTTTenha um broker MQTT privado e gerenciado no plano Free e comece com exemplos de código prontos.
Teste grátis Exemplos de código Sem cartão de crédito

A detecção de pessoas com ESP32-CAM coloca uma pequena rede neural em uma placa que custa mais ou menos o preço de um sanduíche e a transforma em um sensor de presença que publica alertas MQTT. Neste tutorial você vai rodar o modelo de detecção de pessoas do TensorFlow Lite Micro em uma ESP32-CAM AI-Thinker, publicar um índice de confiança e alertas com debounce no servidor MQTT público broker.connectmq.com e conhecer as armadilhas de memória e de temporização que derrubam a maioria das primeiras tentativas.

O que a placa consegue e o que não consegue fazer

A ESP32-CAM combina um ESP32 dual-core com uma câmera OV2640 e, na maioria das placas, 4 MB de PSRAM. Isso é suficiente para um modelo TinyML cuidadosamente projetado, mas não para um detector de objetos de uso geral.

Capacidade ESP32-CAM Raspberry Pi com câmera NVIDIA Jetson
Tipo de modelo Classificador binário (pessoa / sem pessoa) Detectores pequenos, classificadores Detectores completos como YOLO
Tamanho da entrada 96 x 96 em escala de cinza 224 a 640 pixels 640 pixels ou mais
Bounding boxes Não Sim Sim
Uso típico Presença, gatilho de despertar Contagem, zonas Multiclasse, múltiplas câmeras
Energia Compatível com bateria usando sleep Rede elétrica Rede elétrica

O modelo usado aqui, dos exemplos do TensorFlow Lite Micro, responde a uma única pergunta por frame: há uma pessoa nesta imagem? Ele não diz onde nem quantas. Para isso, publique um evento para um modelo maior ou veja detecção de objetos com YOLO via MQTT.

Componentes e software

  • ESP32-CAM AI-Thinker com PSRAM, além de um adaptador USB-serial ou da placa programadora ESP32-CAM-MB
  • Uma fonte de 5 V estável capaz de fornecer pelo menos 500 mA (quedas de tensão são o problema número um)
  • Arduino IDE 2 com o pacote de placas esp32 da Espressif, placa "AI Thinker ESP32-CAM"
  • Bibliotecas: PubSubClient e TensorFlowLite_ESP32 (Gerenciador de Bibliotecas)
  • Do exemplo person_detection da TensorFlowLite_ESP32: copie person_detect_model_data.h e person_detect_model_data.cpp para a pasta do seu sketch

Se você nunca conectou um ESP32 ao MQTT, o tutorial de ESP32 com MQTT cobre o básico, e ESP32-CAM com MQTT explica como publicar snapshots JPEG.

Como o pipeline funciona

  1. A câmera captura diretamente um frame de 96 x 96 em escala de cinza, então não é preciso redimensionar.
  2. Os pixels são convertidos da faixa sem sinal de 0 a 255 para a faixa int8 com sinal que o modelo quantizado espera.
  3. O interpretador executa o modelo e retorna dois índices: "sem pessoa" e "pessoa".
  4. O sketch publica o índice de pessoa periodicamente e só dispara um alerta quando o índice permanece acima de um limiar por vários frames.
  5. Um tempo de espera (cooldown) impede que uma única pessoa parada diante da câmera gere um alerta a cada segundo.

O sketch completo

Substitua as credenciais de Wi-Fi e o ID do topic. A área pública do broker.connectmq.com aceita conexões anônimas com qualquer client ID não vazio, mas tudo o que é publicado ali pode ser lido por outras pessoas; portanto, publique apenas índices e alertas, nunca imagens de pessoas reais.

#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;   // percentual
const int FRAMES_TO_RAISE = 3;
const unsigned long COOLDOWN_MS = 30000;
const unsigned long SCORE_EVERY_MS = 5000;

// Mapa de pinos da ESP32-CAM 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);
  }
}

Abra o Monitor Serial a 115200 baud e assine os topics a partir do seu computador:

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

Fique na frente da câmera e um alerta deve aparecer depois de alguns frames; o topic de índice é atualizado a cada cinco segundos.

Ajustando a detecção de pessoas na ESP32-CAM

  • Limiar. Comece em 60 por cento. Diminua se pessoas na borda do quadro não forem detectadas; aumente se sombras ou cartazes dispararem alertas.
  • Frames para disparar. Três frames consecutivos filtram a maioria das falhas de um único frame, ao custo de um alerta um pouco mais tardio.
  • Cooldown. Ajuste para a frequência com que você realmente quer ser notificado. Uma porta de entrada pode precisar de 30 segundos; uma área restrita pode exigir alerta a cada entrada.
  • Iluminação. O modelo foi treinado com imagens variadas, mas tem dificuldade com pouquíssima luz. Um módulo de câmera compatível com infravermelho e iluminação IR ajuda à noite.
  • Campo de visão. O modelo enxerga apenas 96 x 96 pixels, então a pessoa precisa ocupar uma parte razoável do quadro. Monte a câmera de modo que a área de interesse fique próxima.

Energia e posicionamento

Rodar uma câmera e uma rede neural continuamente mantém o ESP32 ocupado e o Wi-Fi ativo, por isso este projeto é mais adequado a locais com energia da rede elétrica. Para operação com bateria, combine a placa com um sensor infravermelho passivo (PIR): deixe o ESP32 dormir, acorde-o quando o PIR disparar, rode a detecção de pessoas em alguns frames para confirmar que o movimento foi de uma pessoa e não de um animal de estimação ou de uma cortina, publique o resultado e volte a dormir. O payload MQTT continua o mesmo, então os assinantes não se importam com o modo em que a câmera está operando.

O posicionamento importa tanto quanto o modelo. Instale a câmera em uma altura e um ângulo em que as pessoas apareçam em pé e razoavelmente grandes no quadro, evite apontá-la para janelas que causem contraluz forte e mantenha-a longe da chuva, a menos que o gabinete seja certificado para isso.

Construindo em cima dos alertas

Assim que os alertas chegam a um topic, qualquer coisa que fale MQTT pode reagir: o Home Assistant pode acender uma luz, o Node-RED pode enviar uma notificação ao celular e um Raspberry Pi rodando um detector maior pode solicitar um snapshot e contar as pessoas de forma adequada. Com o ConnectMQ no meio, cada um deles é simplesmente mais um assinante, e você pode adicioná-los ou removê-los sem regravar o firmware da câmera.

Solução de problemas

  1. "Brownout detector was triggered" e reinicializações. Use uma fonte de 5 V melhor e um cabo curto e grosso; a câmera e o Wi-Fi juntos geram picos de corrente.
  2. Camera init failed. Verifique se a placa selecionada é AI Thinker ESP32-CAM e se a PSRAM está habilitada; confira também se o cabo flat está bem encaixado.
  3. Model init failed. A tensor arena é pequena demais ou a RAM interna está fragmentada; aumente kArenaSize em passos de 10 KB e inicialize o modelo antes do Wi-Fi.
  4. Erros de compilação nos headers do TensorFlow. As versões da biblioteca diferem no construtor do MicroInterpreter; ajuste os caminhos de include e o construtor de acordo com o exemplo que acompanha a versão instalada da biblioteca.
  5. Estado MQTT -2 ou reconexões constantes. O Wi-Fi está fraco ou o client ID está duplicado. Aproxime a placa do ponto de acesso e garanta que nenhum outro dispositivo use o mesmo client ID.

Protegendo para uso real

Uma câmera que monitora uma porta real produz dados pessoais (no Brasil, sujeitos à LGPD), mesmo que apenas um índice saia do dispositivo. Para qualquer coisa além de um teste, passe para um broker privado com TLS: use WiFiClientSecure em vez de WiFiClient, conecte-se à porta 8883 e autentique com mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD", ...). O servidor MQTT público é ideal para desenvolvimento e protótipos, não para dados de produção, enquanto um broker MQTT gerenciado como o ConnectMQ dá a cada câmera suas próprias credenciais e mantém o tráfego criptografado.

Como o modelo é um classificador de pessoas, uma extensão natural é usá-lo como gatilho: quando ele disparar, publique um snapshot em um topic separado ou acorde um modelo mais capaz em outro lugar. Para saber mais sobre como encaixar modelos em microcontroladores, leia TinyML no ESP32 e o guia mais amplo de IA e IoT.

Comece a usar o ConnectMQ

Crie um broker privado no plano gratuito Serverless em app.connectmq.com, sem cartão de crédito, e depois mude o sketch para TLS e para as credenciais geradas. Você pode acompanhar a chegada dos alertas no explorador de topics em tempo real do portal e adicionar uma regra de alerta por limiar no índice person.

Perguntas frequentes

Qual a velocidade da detecção de pessoas em uma ESP32-CAM?

Espere no máximo algumas inferências por segundo, dependendo da versão da biblioteca e de os kernels otimizados estarem habilitados ou não. Isso é mais do que suficiente para detecção de presença, mas não para rastrear movimento.

A ESP32-CAM consegue contar pessoas ou desenhar bounding boxes?

Não com este modelo, que apenas classifica o frame inteiro como pessoa ou sem pessoa. Para contagem e bounding boxes, envie eventos para um Raspberry Pi ou Jetson rodando um detector.

Preciso de PSRAM para a detecção de pessoas?

Neste sketch, o frame buffer da câmera fica na PSRAM, e a maioria das placas ESP32-CAM a inclui. A tensor arena em si é alocada na RAM interna, que é mais rápida.

É seguro enviar imagens para o servidor MQTT público?

Não. A área pública pode ser lida por qualquer pessoa, então publique ali apenas índices e alertas e mantenha quaisquer imagens em um broker privado com TLS e credenciais.

Artigos relacionados

Todos os artigos →

Seu broker MQTT privado está a poucos cliques de distância

Crie uma conta, receba as credenciais do seu broker e conecte seu primeiro dispositivo — sem servidores para instalar, atualizar ou escalar.