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:
  1. #include "esp_camera.h"  
  2. #include "FS.h"  
  3. #include "SD_MMC.h"  
  4.   
  5. // Cabeçalho de arquivo bmp (incluindo a assinatura)  
  6. typedef struct {  
  7.     uint16_t signature;  
  8.     uint32_t filesize;  
  9.     uint32_t reserved;  
  10.     uint32_t fileoffset_to_pixelarray;  
  11.     uint32_t dibheadersize;  
  12.     int32_t width;  
  13.     int32_t height;  
  14.     uint16_t planes;  
  15.     uint16_t bitsperpixel;  
  16.     uint32_t compression;  
  17.     uint32_t imagesize;  
  18.     uint32_t ypixelpermeter;  
  19.     uint32_t xpixelpermeter;  
  20.     uint32_t numcolorspallette;  
  21.     uint32_t mostimpcolor;  
  22. } bmp_header_t;  
  23.   
  24. // Conexões da ESP32-CAM  
  25. #define CAM_PIN_PWDN    32  
  26. #define CAM_PIN_RESET   -1 // usar software reset  
  27. #define CAM_PIN_XCLK    0  
  28. #define CAM_PIN_SIOD    26  
  29. #define CAM_PIN_SIOC    27  
  30.   
  31. #define CAM_PIN_D7      35  
  32. #define CAM_PIN_D6      34  
  33. #define CAM_PIN_D5      39  
  34. #define CAM_PIN_D4      36  
  35. #define CAM_PIN_D3      21  
  36. #define CAM_PIN_D2      19  
  37. #define CAM_PIN_D1      18  
  38. #define CAM_PIN_D0       5  
  39.   
  40. #define CAM_PIN_VSYNC   25  
  41. #define CAM_PIN_HREF    23  
  42. #define CAM_PIN_PCLK    22  
  43.   
  44. // Configuração da camera  
  45. static camera_config_t camera_config = {  
  46.     .pin_pwdn  = CAM_PIN_PWDN,  
  47.     .pin_reset = CAM_PIN_RESET,  
  48.     .pin_xclk = CAM_PIN_XCLK,  
  49.     .pin_sscb_sda = CAM_PIN_SIOD,  
  50.     .pin_sscb_scl = CAM_PIN_SIOC,  
  51.   
  52.     .pin_d7 = CAM_PIN_D7,  
  53.     .pin_d6 = CAM_PIN_D6,  
  54.     .pin_d5 = CAM_PIN_D5,  
  55.     .pin_d4 = CAM_PIN_D4,  
  56.     .pin_d3 = CAM_PIN_D3,  
  57.     .pin_d2 = CAM_PIN_D2,  
  58.     .pin_d1 = CAM_PIN_D1,  
  59.     .pin_d0 = CAM_PIN_D0,  
  60.     .pin_vsync = CAM_PIN_VSYNC,  
  61.     .pin_href = CAM_PIN_HREF,  
  62.     .pin_pclk = CAM_PIN_PCLK,  
  63.   
  64.     .xclk_freq_hz = 20000000,  
  65.     .ledc_timer = LEDC_TIMER_0,  
  66.     .ledc_channel = LEDC_CHANNEL_0,  
  67.   
  68.     .pixel_format = PIXFORMAT_JPEG,  
  69.     .frame_size = FRAMESIZE_QVGA,  
  70.   
  71.     .jpeg_quality = 12, //0-63 lower number means higher quality  
  72.     .fb_count = 1 //if more than one, i2s runs in continuous mode. Use only with JPEG  
  73. };  
  74.   
  75. const int LARGURA = 320;  
  76. const int ALTURA = 240;  
  77. const int DIF_MIN = 10;  
  78. const int PERC_MOV = 5;  
  79.   
  80. // Buffers para a imagem em b&p (tons de cinza)  
  81. byte *imgbp;  
  82. int imgAtual = 0;  
  83. bool primeira = true;  
  84.   
  85. void setup() {  
  86.     
  87.   Serial.begin(115200);  
  88.   
  89.   // Iniciar a camera  
  90.   esp_err_t err = esp_camera_init(&camera_config);  
  91.   if (err != ESP_OK) {  
  92.     Serial.print("Erro ");  
  93.     Serial.print (err);  
  94.     Serial.println (" ao iniciar a camera!");  
  95.     // Não pode prosseguir  
  96.     for (;;) {  
  97.       delay (1000);  
  98.     }  
  99.   }  
  100.   
  101.   // Iniciar o cartão SD  
  102.   if(!SD_MMC.begin() || (SD_MMC.cardType() == CARD_NONE)){  
  103.     Serial.println("Falha no cartão SD");  
  104.     // Não pode prosseguir  
  105.     for (;;) {  
  106.       delay (1000);  
  107.     }  
  108.   }  
  109.   fs::FS &fs = SD_MMC;   
  110.   fs.mkdir("/fotos");  
  111.   
  112.   imgbp = (byte *) heap_caps_malloc(2*ALTURA*LARGURA, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);  
  113.   if (imgbp == NULL) {  
  114.     Serial.println ("Faltou memória!");  
  115.     // Não pode prosseguir  
  116.     for (;;) {  
  117.       delay (1000);  
  118.     }  
  119.   }  
  120.   Serial.println ("Pronto");  
  121. }  
  122.   
  123. void loop() {  
  124.   delay(1000);  
  125.   Serial.print ("Tirando foto...");  
  126.   camera_fb_t * fb = esp_camera_fb_get();  
  127.   convBP(fb);  
  128.   esp_camera_fb_return(fb);  
  129.   if (primeira) {  
  130.     Serial.println (" Primeira");  
  131.     primeira = false;  
  132.   } else {  
  133.     Serial.print (" Processando...");  
  134.     if (diferente()) {  
  135.       Serial.println ("Detectou movimento!");  
  136.       tiraFotoHi();  
  137.       delay(30000);  
  138.       primeira = true;  
  139.     }  
  140.   }  
  141. }  
  142.   
  143. // Tira foto em maior resolução e salva  
  144. void tiraFotoHi() {  
  145.   esp_camera_deinit();  
  146.   delay(500);  
  147.   camera_config.frame_size = FRAMESIZE_UXGA;  
  148.   esp_err_t err = esp_camera_init(&camera_config);  
  149.   if (err != ESP_OK) {  
  150.     Serial.println("Erro ao configurar camera para foto");  
  151.   } else {  
  152.     camera_fb_t * fb = esp_camera_fb_get();  
  153.     if (fb != NULL) {  
  154.       save (fb->buf, fb->len, ".jpg");     
  155.       esp_camera_fb_return(fb);  
  156.     }  
  157.     esp_camera_deinit();  
  158.     delay(500);  
  159.     camera_config.frame_size = FRAMESIZE_QVGA;  
  160.     esp_err_t err = esp_camera_init(&camera_config);  
  161.     if (err != ESP_OK) {  
  162.       Serial.println("Erro ao voltar camera para deteccao");  
  163.     }  
  164.   }  
  165. }  
  166.   
  167. // Converte foto para tons de cinza  
  168. void convBP(camera_fb_t * fb) {  
  169.   if (fb == NULL) {  
  170.     return;  
  171.   }  
  172.   uint8_t *buf = NULL;  
  173.   size_t buf_len = 0;  
  174.   if (frame2bmp(fb, &buf, &buf_len)) {  
  175.     byte *pixels = buf + sizeof(bmp_header_t);  
  176.     byte *cinza = imgbp;  
  177.     if (imgAtual == 1) {  
  178.       cinza += ALTURA*LARGURA;  
  179.     }  
  180.     for (int i = 0; i < ALTURA*LARGURA; i++) {  
  181.       // cinza = 0.2989*R + 0.5870*G + 0.1140*B  
  182.       // vamos aproximar  
  183.       *cinza++ = (3*pixels[0] + 6*pixels[1] + 1*pixels[2])/10;  
  184.       pixels += 3;  
  185.     }  
  186.     free(buf);  
  187.     imgAtual ^= 1;  
  188.   }  
  189. }  
  190.   
  191. // Compara as imagens b&p  
  192. bool diferente() {  
  193.   int cont = 0;  
  194.   int dif;  
  195.   byte *img0 = imgbp;  
  196.   byte *img1 = imgbp + ALTURA*LARGURA;  
  197.   for (int i = 0; i < ALTURA*LARGURA; i++) {  
  198.     if (*img0 > *img1) {  
  199.       dif = *img0 - *img1;  
  200.     } else {  
  201.       dif = *img1 - *img0;  
  202.     }  
  203.     if (dif > DIF_MIN) {  
  204.       cont ++;  
  205.     }  
  206.     img0++;  
  207.     img1++;  
  208.   }  
  209.   Serial.print (" Diferenca: ");  
  210.   Serial.println (cont);  
  211.   return cont > ((ALTURA*LARGURA*PERC_MOV)/100);  
  212. }  
  213.   
  214. // Salva uma foto  
  215. void save (byte *data, int len, char *type) {  
  216.   static int foto = 1;  
  217.     
  218.   // Nome da foto no cartão  
  219.   String nome = "/fotos/foto" + String(foto) + type;  
  220.   
  221.   fs::FS &fs = SD_MMC;   
  222.   Serial.printf("Salvando %s\n", nome.c_str());  
  223.     
  224.   File file = fs.open(nome.c_str(), FILE_WRITE);  
  225.   if(!file){  
  226.     Serial.println("Erro ao criar o arquivo");  
  227.   }   
  228.   else {  
  229.     file.write(data, len);  
  230.     Serial.println("Arquivo salvo");  
  231.     foto++;  
  232.   }  
  233.   file.close();  
  234. }  
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!