terça-feira, junho 02, 2020

ESP32-CAM: Tirando Foto ao Detectar Movimento - Primeiras Experiências

No post passado, vimos como tirar uma foto e convertê-la de jpg para bmp. Neste post vou compartilhar minhas primeiras experiências para detectar movimento.


Note to self: Testar em locais mais iluminados!

A minha ideia básica de detecção de movimento foi:
  • Tirar uma foto
  • Convertê-la para tons de cinza
  • Comparar com a anterior
  • Considerar que houve movimento se tiver diferença significativa
A conversão em si para tons de cinza não é complicado. A fórmula normalmente usada (que leva em conta a sensibilidade da vista) é:

  cinza = 0.2989*R + 0.5870*G + 0.1140*B

Para a minha aplicação (que é comprar fotos), provavelmente eu não precisaria usar estes pesos. Acabei usando, mas de forma aproximada.

O primeiro problema foi a alocação das áreas de memória para guardar as duas imagens em tons de cinza. Declarando como variáveis estáticas com tamanho VGA acusou falta de memória.

Pensando um pouco, decidi que não precisava da imagem com resolução VGA (640x480) para detectar movimento. Decidi usar QVGA (320x240). Mesmo assim, deu erro ao tentar alocar memória dinamicamente. Fui dar uma olhada nos fontes da rotina de conversão para BMP e vi que ela usava uma função diferente para alocar memória:

  heap_caps_malloc(tamanho, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);

Resumindo uma história complicada, alguns modelos do ESP32 (como o usado na ESP32-CAM) possuem 4M de Ram externa adicional. Apesar desta memória estar ligada via SPI, ela é mapeada no endereçamento do processador e pode ser acessada via ponteiros. Os detalhes estão aqui.

Conseguindo guardar as duas imagens, a próxima questão é como comparar. Fiz algo relativamente simples: comparei pixel a pixel, ignorei diferenças pequenas e no final examino a porcentagem de pixels diferentes. A ideia de ignorar diferenças pequenas é para ficar menos sensíveis a diferenças de luminosidade.

Para completar este teste, resolvi tirar uma foto com resolução UXGA quando movimento é detectado. Mudar a configuração exigiu finalizar a câmera primeiro, ainda dá um erro quando reativa, mas parece ser inócuo.

O código ficou assim:
#include "esp_camera.h"
#include "FS.h"
#include "SD_MMC.h"

// Cabeçalho de arquivo bmp (incluindo a assinatura)
typedef struct {
    uint16_t signature;
    uint32_t filesize;
    uint32_t reserved;
    uint32_t fileoffset_to_pixelarray;
    uint32_t dibheadersize;
    int32_t width;
    int32_t height;
    uint16_t planes;
    uint16_t bitsperpixel;
    uint32_t compression;
    uint32_t imagesize;
    uint32_t ypixelpermeter;
    uint32_t xpixelpermeter;
    uint32_t numcolorspallette;
    uint32_t mostimpcolor;
} bmp_header_t;

// Conexões da ESP32-CAM
#define CAM_PIN_PWDN    32
#define CAM_PIN_RESET   -1 // usar software reset
#define CAM_PIN_XCLK    0
#define CAM_PIN_SIOD    26
#define CAM_PIN_SIOC    27

#define CAM_PIN_D7      35
#define CAM_PIN_D6      34
#define CAM_PIN_D5      39
#define CAM_PIN_D4      36
#define CAM_PIN_D3      21
#define CAM_PIN_D2      19
#define CAM_PIN_D1      18
#define CAM_PIN_D0       5

#define CAM_PIN_VSYNC   25
#define CAM_PIN_HREF    23
#define CAM_PIN_PCLK    22

// Configuração da camera
static camera_config_t camera_config = {
    .pin_pwdn  = CAM_PIN_PWDN,
    .pin_reset = CAM_PIN_RESET,
    .pin_xclk = CAM_PIN_XCLK,
    .pin_sscb_sda = CAM_PIN_SIOD,
    .pin_sscb_scl = CAM_PIN_SIOC,

    .pin_d7 = CAM_PIN_D7,
    .pin_d6 = CAM_PIN_D6,
    .pin_d5 = CAM_PIN_D5,
    .pin_d4 = CAM_PIN_D4,
    .pin_d3 = CAM_PIN_D3,
    .pin_d2 = CAM_PIN_D2,
    .pin_d1 = CAM_PIN_D1,
    .pin_d0 = CAM_PIN_D0,
    .pin_vsync = CAM_PIN_VSYNC,
    .pin_href = CAM_PIN_HREF,
    .pin_pclk = CAM_PIN_PCLK,

    .xclk_freq_hz = 20000000,
    .ledc_timer = LEDC_TIMER_0,
    .ledc_channel = LEDC_CHANNEL_0,

    .pixel_format = PIXFORMAT_JPEG,
    .frame_size = FRAMESIZE_QVGA,

    .jpeg_quality = 12, //0-63 lower number means higher quality
    .fb_count = 1 //if more than one, i2s runs in continuous mode. Use only with JPEG
};

const int LARGURA = 320;
const int ALTURA = 240;
const int DIF_MIN = 10;
const int PERC_MOV = 5;

// Buffers para a imagem em b&p (tons de cinza)
byte *imgbp;
int imgAtual = 0;
bool primeira = true;

void setup() {
  
  Serial.begin(115200);

  // Iniciar a camera
  esp_err_t err = esp_camera_init(&camera_config);
  if (err != ESP_OK) {
    Serial.print("Erro ");
    Serial.print (err);
    Serial.println (" ao iniciar a camera!");
    // Não pode prosseguir
    for (;;) {
      delay (1000);
    }
  }

  // Iniciar o cartão SD
  if(!SD_MMC.begin() || (SD_MMC.cardType() == CARD_NONE)){
    Serial.println("Falha no cartão SD");
    // Não pode prosseguir
    for (;;) {
      delay (1000);
    }
  }
  fs::FS &fs = SD_MMC; 
  fs.mkdir("/fotos");

  imgbp = (byte *) heap_caps_malloc(2*ALTURA*LARGURA, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
  if (imgbp == NULL) {
    Serial.println ("Faltou memória!");
    // Não pode prosseguir
    for (;;) {
      delay (1000);
    }
  }
  Serial.println ("Pronto");
}

void loop() {
  delay(1000);
  Serial.print ("Tirando foto...");
  camera_fb_t * fb = esp_camera_fb_get();
  convBP(fb);
  esp_camera_fb_return(fb);
  if (primeira) {
    Serial.println (" Primeira");
    primeira = false;
  } else {
    Serial.print (" Processando...");
    if (diferente()) {
      Serial.println ("Detectou movimento!");
      tiraFotoHi();
      delay(30000);
      primeira = true;
    }
  }
}

// Tira foto em maior resolução e salva
void tiraFotoHi() {
  esp_camera_deinit();
  delay(500);
  camera_config.frame_size = FRAMESIZE_UXGA;
  esp_err_t err = esp_camera_init(&camera_config);
  if (err != ESP_OK) {
    Serial.println("Erro ao configurar camera para foto");
  } else {
    camera_fb_t * fb = esp_camera_fb_get();
    if (fb != NULL) {
      save (fb->buf, fb->len, ".jpg");   
      esp_camera_fb_return(fb);
    }
    esp_camera_deinit();
    delay(500);
    camera_config.frame_size = FRAMESIZE_QVGA;
    esp_err_t err = esp_camera_init(&camera_config);
    if (err != ESP_OK) {
      Serial.println("Erro ao voltar camera para deteccao");
    }
  }
}

// Converte foto para tons de cinza
void convBP(camera_fb_t * fb) {
  if (fb == NULL) {
    return;
  }
  uint8_t *buf = NULL;
  size_t buf_len = 0;
  if (frame2bmp(fb, &buf, &buf_len)) {
    byte *pixels = buf + sizeof(bmp_header_t);
    byte *cinza = imgbp;
    if (imgAtual == 1) {
      cinza += ALTURA*LARGURA;
    }
    for (int i = 0; i < ALTURA*LARGURA; i++) {
      // cinza = 0.2989*R + 0.5870*G + 0.1140*B
      // vamos aproximar
      *cinza++ = (3*pixels[0] + 6*pixels[1] + 1*pixels[2])/10;
      pixels += 3;
    }
    free(buf);
    imgAtual ^= 1;
  }
}

// Compara as imagens b&p
bool diferente() {
  int cont = 0;
  int dif;
  byte *img0 = imgbp;
  byte *img1 = imgbp + ALTURA*LARGURA;
  for (int i = 0; i < ALTURA*LARGURA; i++) {
    if (*img0 > *img1) {
      dif = *img0 - *img1;
    } else {
      dif = *img1 - *img0;
    }
    if (dif > DIF_MIN) {
      cont ++;
    }
    img0++;
    img1++;
  }
  Serial.print (" Diferenca: ");
  Serial.println (cont);
  return cont > ((ALTURA*LARGURA*PERC_MOV)/100);
}

// Salva uma foto
void save (byte *data, int len, char *type) {
  static int foto = 1;
  
  // Nome da foto no cartão
  String nome = "/fotos/foto" + String(foto) + type;

  fs::FS &fs = SD_MMC; 
  Serial.printf("Salvando %s\n", nome.c_str());
  
  File file = fs.open(nome.c_str(), FILE_WRITE);
  if(!file){
    Serial.println("Erro ao criar o arquivo");
  } 
  else {
    file.write(data, len);
    Serial.println("Arquivo salvo");
    foto++;
  }
  file.close();
}
Uma observação: a ESP32-CAM tem um LED forte (para uso como Flash) que está conectado a um dos pinos usados para comunicação com o cartão SD, O resultado é que o LED pisca quando é feito uma acesso ao SD e fica aceso fraco quando o SD não está sendo acessado.Preciso estudar mais para ver se é possível se livrar deste efeito.

No próximo post eu conto o resultado dos meus testes.

9 comentários:

Tales disse...

Bom dia.

Estou tentando implementar o código, porém quando é rodado o esp_camera_deinit() e o esp_camera_init() o programa começa a retornar dois erros.

E (17009) i2c: i2c driver install error
E (18136) gpio: gpio_install_isr_service(410): GPIO isr service already installed

Já atualizei as libs e o problema persiste.

Utilizo um esp32cam com a câmera CAMERA_MODEL_AI_THINKER

Enfrentou esse problema?
Teria alguma ideia de como resolver?



Daniel Quadros disse...

Tales, este é o erro que eu mencionei no texto, aparentemente não afeta o desempenho.

Unknown disse...

Olá. estou tendo problemas com minhas fotos. quando uso uma resolução ou qualidade mais alta, elas aparecem com uma tarja cinza em baixo. Pode ser o tamanho da foto?
não sei como resolver isso.

Daniel Quadros disse...

É difícil dizer o que está causando a tarja cinza, porque isso não me aconteceu, mas pode ser sim um problema de tamanho x memória disponível. Eu acabei encostando essa placa porque me pareceu bastante instável e o resultado deixou a desejar.

ALEKS disse...

e se usar dois cartos sd e madar comparar com a outra

ALEKS disse...

salva nosi cartoes edepois manda comparar

Daniel Quadros disse...

ALEKS, a placa só suporta um cartão. Daria para salvar em dois arquivos e depois comparar, mas a ideia era fazer na memória para ser mais rápido.

Bruno Tavares disse...

Olá,
sou aluno de Eng. controle e Automação.
achei interessante seu artigo ‘’ ESP32-CAM: tirando foto ao Detectar Movimento – primeiras experiências.’’
Estou trabalhando de uma forma de vez de manda as fotos para o Cartão SD, mandar para nuvem, FIREBASE.

Poderia me ajudar nisso ?

ficarei grato se puder fazer um artigo sobre.

Daniel Quadros disse...

Bruno, por enquanto eu dei uma encostada na ESP32-CAM e não tenho experiência com o Firebase, portanto acho que um artigo sobre isso não vai rolar nos próximos meses. Mas, em uma busca rápida, eu achei esta biblioteca: https://github.com/mobizt/Firebase-ESP-Client, um dos exemplos é upload de arquivo, o que poderia ser usado para armazenar fotos. Boa sorte no seu projeto!