dimanche 15 mars 2020

Time-lapse avec l'ESP32-CAM

La technique du time-lapse consiste à produire une vidéo ultra-accélérée au moyen de photographies prises sur une longue période de temps.

J'ai apporté quelques modifications à mes sketches précédents afin de produire des vidéos time-lapse au moyen de mon ESP32-CAM (programmée avec l'IDE Arduino, comme d'habitude).

Comme dans mes projets précédents, l'image captée par la caméra est présentée en temps réel sur une page web. Mais cette page comporte, en plus, un champ texte permettant à l'utilisateur d'écrire le nombre de secondes désiré entre deux images consécutives ainsi qu'un bouton permettant de démarrer et d'interrompre l'enregistrement des images.


Les images sont enregistrées en format jpeg sur une carte micro-SD insérée dans l'ESP32-CAM. On peut ensuite combiner ces images pour en faire une vidéo (pour réaliser la vidéo ci-dessous, j'ai utilisé Movie Maker).





Sketch

Mon point de départ à été mon sketch qui enregistre des photos sur une carte microSD.

Les principales modifications sont:
  • dans la routine web_handler(), aux lignes 127 à 134: ajout d'un champ de texte pour choisir le nombre de secondes entre 2 images successives.
  • dans la routine clic_handler(), aux lignes 189 à 225: récupération du délai entré par l'utilisateur, et activation (ou désactivation) de l'enregistrement des photos.
  • dans la routine loop(), aux lignes 360 à 365: vérification si c'est le moment de prendre une photo, compte tenu du délai choisi par l'utilisateur.
-
/*
Timelapse avec ESP32-CAM. Les photos s'enregistrent sur une carte SD.
On démarre la prise de vue et on contrôle le délai entre 2 images
consécutives grâce à une page web.
Plus d'infos:
https://electroniqueamateur.blogspot.com/2020/03/time-lapse-avec-lesp32-cam.html
*/
// inclusion des bibliothèques utile
#include "esp_camera.h"
#include <WiFi.h>
#include "esp_http_server.h"
#include "FS.h" // manipulation de fichiers
#include "SD_MMC.h" // carte SD
// paramètres de votre réseau WiFi
const char* ssid = "**********";
const char* password = "**********";
#define PART_BOUNDARY "123456789000000000000987654321"
static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";
httpd_handle_t stream_httpd = NULL;
httpd_handle_t camera_httpd = NULL;
int numero_port; // numéro du port du stream server
bool carte_presente = 0; // présence d'une carte micro SD
int numero_fichier = 10000; // numéro initial de la photo
unsigned long photoPrecedente = 0; // prise de la dernière photo
long intervalle = 10000; // nombre de millisecondes entre 2 photos.
bool timeLapseActif = 0; // devient vrai pendant l'enregistrement de la séquence
// ********************************************************
// stream_handler: routine qui gère le streaming vidéo
static esp_err_t stream_handler(httpd_req_t *req) {
camera_fb_t * fb = NULL;
esp_err_t res = ESP_OK;
size_t _jpg_buf_len = 0;
uint8_t * _jpg_buf = NULL;
char * part_buf[64];
static int64_t last_frame = 0;
if (!last_frame) {
last_frame = esp_timer_get_time();
}
res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
if (res != ESP_OK) {
return res;
}
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
while (true) {
fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Echec de la capture de camera");
res = ESP_FAIL;
} else {
if (fb->width > 400) {
if (fb->format != PIXFORMAT_JPEG) {
bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len);
esp_camera_fb_return(fb);
fb = NULL;
if (!jpeg_converted) {
Serial.println("Echec de la compression JPEG");
res = ESP_FAIL;
}
} else {
_jpg_buf_len = fb->len;
_jpg_buf = fb->buf;
}
}
}
if (res == ESP_OK) {
size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, _jpg_buf_len);
res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
}
if (res == ESP_OK) {
res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
}
if (res == ESP_OK) {
res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
}
if (fb) {
esp_camera_fb_return(fb);
fb = NULL;
_jpg_buf = NULL;
} else if (_jpg_buf) {
free(_jpg_buf);
_jpg_buf = NULL;
}
if (res != ESP_OK) {
break;
}
}
last_frame = 0;
return res;
}
// ********************************************************
// web_handler: affichage de la page web
static esp_err_t web_handler(httpd_req_t *req) {
httpd_resp_set_type(req, "text/html");
httpd_resp_set_hdr(req, "Content-Encoding", "identity");
sensor_t * s = esp_camera_sensor_get();
char pageWeb[450] = "";
strcat(pageWeb, "<!doctype html> <html> <head> <title id='title'>ESP32-CAM</title> </head> <body> <img id='stream' src='http://");
// l'adresse du stream server (exemple: 192.168.0.145:81):
char adresse[20] = "";
sprintf (adresse, "%d.%d.%d.%d:%d", WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3], numero_port);
strcat(pageWeb, adresse);
strcat(pageWeb, "/stream'> <br> <br>");
if (carte_presente) {
// on ajoute le bouton
if (timeLapseActif) {
strcat(pageWeb, "<form action='/clic' method='GET'> <label for='intervalle'> Intervalle (en secondes):</label> <br> <input type='text' id='intervalle' name='intervalle' value = '10'><br><br><INPUT type='submit' value='Arr&ecirc;ter'></form> ");
}
else {
strcat(pageWeb, "<form action='/clic' method='GET'> <label for='intervalle'> Intervalle (en secondes):</label> <br> <input type='text' id='intervalle' name='intervalle' value = '10'><br><br><INPUT type='submit' value='D&eacute;marrer'></form> ");
}
}
else {
// on indique l'absence de la carte
strcat(pageWeb, "<p> Pas de carte microSD.</p>");
}
strcat(pageWeb, "</body> </html>");
int taillePage = strlen(pageWeb);
return httpd_resp_send(req, (const char *)pageWeb, taillePage);
}
// ********************************************************
/* enregistrer_photo: prise de la photo et création du fichier jpeg */
void enregistrer_photo (void)
{
char adresse[20] = ""; // chemin d'accès du fichier .jpeg
camera_fb_t * fb = NULL; // frame buffer
// prise de la photo
fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Echec de la prise de photo.");
return;
}
numero_fichier = numero_fichier + 1;
// enregitrement du fichier sur la carte SD
sprintf (adresse, "/photo%d.jpg", numero_fichier);
fs::FS &fs = SD_MMC;
File file = fs.open(adresse, FILE_WRITE);
if (!file) {
Serial.println("Echec lors de la creation du fichier.");
}
else {
file.write(fb->buf, fb->len); // payload (image), payload length
Serial.printf("Fichier enregistre: %s\n", adresse);
}
file.close();
esp_camera_fb_return(fb);
}
// ********************************************************
/* clic_handler: Gestion du clic sur le bouton */
static esp_err_t clic_handler(httpd_req_t *req) {
// récupérons le contenu du champ texte (temps entre 2 poses successives)
char* buf;
size_t buf_len;
char valeur[32] = {0,};
buf_len = httpd_req_get_url_query_len(req) + 1;
if (buf_len > 1) {
buf = (char*)malloc(buf_len);
if (!buf) {
httpd_resp_send_500(req);
return ESP_FAIL;
}
if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
if (httpd_query_key_value(buf, "intervalle", valeur, sizeof(valeur)) == ESP_OK ) {
} else {
free(buf);
httpd_resp_send_404(req);
return ESP_FAIL;
}
} else {
free(buf);
httpd_resp_send_404(req);
return ESP_FAIL;
}
free(buf);
} else {
httpd_resp_send_404(req);
return ESP_FAIL;
}
intervalle = atoi(valeur) * 1000;
timeLapseActif = !(timeLapseActif);
return (web_handler(req));
}
// ********************************************************
// startCameraServer: démarrage du web server et du stream server
void startCameraServer() {
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
httpd_uri_t index_uri = {
.uri = "/",
.method = HTTP_GET,
.handler = web_handler,
.user_ctx = NULL
};
httpd_uri_t stream_uri = {
.uri = "/stream",
.method = HTTP_GET,
.handler = stream_handler,
.user_ctx = NULL
};
httpd_uri_t clic_uri = {
.uri = "/clic",
.method = HTTP_GET,
.handler = clic_handler,
.user_ctx = NULL
};
Serial.printf("Demarrage du web server sur le port: '%d'\n", config.server_port);
if (httpd_start(&camera_httpd, &config) == ESP_OK) {
httpd_register_uri_handler(camera_httpd, &index_uri);
httpd_register_uri_handler(camera_httpd, &clic_uri);
}
config.server_port += 1;
config.ctrl_port += 1;
Serial.printf("Demarrage du stream server sur le port: '%d'\n", config.server_port);
if (httpd_start(&stream_httpd, &config) == ESP_OK) {
httpd_register_uri_handler(stream_httpd, &stream_uri);
}
numero_port = config.server_port;
}
// ********************************************************
// initialisation de la caméra, connexion au réseau WiFi.
void setup() {
Serial.begin(115200);
Serial.println();
Serial.println("====");
// définition des broches pour le modèle AI Thinker - ESP32-CAM
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = 5;
config.pin_d1 = 18;
config.pin_d2 = 19;
config.pin_d3 = 21;
config.pin_d4 = 36;
config.pin_d5 = 39;
config.pin_d6 = 34;
config.pin_d7 = 35;
config.pin_xclk = 0;
config.pin_pclk = 22;
config.pin_vsync = 25;
config.pin_href = 23;
config.pin_sscb_sda = 26;
config.pin_sscb_scl = 27;
config.pin_pwdn = 32;
config.pin_reset = -1;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG; //YUV422|GRAYSCALE|RGB565|JPEG
config.frame_size = FRAMESIZE_VGA; // QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA
config.jpeg_quality = 10; // 0-63 ; plus bas = meilleure qualité
config.fb_count = 2; // nombre de frame buffers
// initialisation de la carte micro SD
if (SD_MMC.begin()) {
uint8_t cardType = SD_MMC.cardType();
if (cardType != CARD_NONE) {
carte_presente = 1;
}
}
if (!carte_presente) {
Serial.println("Pas de carte microSD dans l'ESP32-CAM");
}
// initialisation de la caméra
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Echec de l'initialisation de la camera, erreur 0x%x", err);
return;
}
sensor_t * s = esp_camera_sensor_get();
Serial.println("");
Serial.print("Connexion au reseau WiFi: ");
Serial.println(ssid);
delay(100);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(250);
}
Serial.println("WiFi connecte");
Serial.println("");
delay(100);
startCameraServer();
Serial.print("La camera est prete. Utilisez 'http://");
Serial.print(WiFi.localIP());
Serial.println("' pour vous connecter.");
}
// ********************************************************
void loop() {
// enregistrement d'une photo, si c'est le bon moment
unsigned long maintenant = millis();
if (maintenant - photoPrecedente >= intervalle) {
photoPrecedente = maintenant;
if (timeLapseActif) {
enregistrer_photo();
}
}
}
-

À lire également

Dans le passé, j'avais fait du time-lapse avec un vieil appareil photo numérique commandé par un Arduino (on peut aussi utiliser la camera du Raspberry Pi, mais je n'ai pas écrit d'article sur ce sujet spécifique).

De plus, j'ai publié plusieurs articles concernant l'ESP32-CAM: première utilisation,  un web server minimaliste , enregistrer des photos sur la carte microSDles LEDs de l'ESP32-CAM, mouvement panoramique de l'ESP32-CAM avec un servomoteur.

Yves Pelletier (TwitterFacebook)

7 commentaires:

  1. Bonjour,
    Merci pour votre partage et pour sa clarté.
    Est-il possible de détecter un changement dans l'image et de l'enregistrer avec un intervalle prédéfini ?
    Merci.

    RépondreSupprimer
  2. Bonjour,

    Impeccable ton programme, c'est tout à fait ce que je cherchais.
    Par contre je vois le stream sur le serveur web quand il n'y a pas de carte SD insérée, et plus lorsqu'il y a une carte SD. J'ai en haut de la page le petit icone vert qui m'indique que la source n'existe pas, cependant j'ai bien la prise de photo automatique, est-ce un fonctionnement normal ? J’imagine que non, avoir le timelaps et le retour vidéo me parait plus "sympa", une piste à me donner pour résoudre ce problème?

    Merci pour le coup de main.
    Et bonne continuation, j'ai trouvé plein d'article passionnant sur ce site.

    RépondreSupprimer
  3. Bonjour,
    J'ai le même problème (pas de stream lorsque carte SD mise).
    Il y a t'il une solution ?
    Merci d'avance pour la réponse.

    RépondreSupprimer
  4. une solution a vos problème depuis ou toujours en attente?

    RépondreSupprimer
  5. Bonjour,
    Je rencontre le même souci : si carte SD présente, alors rien ne s'affiche ...Quelqu'un a-t-il la solution ?
    Merci pour vos réponses.

    RépondreSupprimer
    Réponses
    1. Je me reponds : il faut utiliser la version 1.0.2 de ESP32 Espressif Systems (j'avais la 1.0.6 !)
      Ca marche tres bien.
      Encore merci pour tous ces tutos.

      Supprimer
    2. Bonjour Christian, peut-tu développer la procédure à suivre ?

      Supprimer