Réaliser un time-lapse avec un module ESP32S3 Sense et FFmpeg

5
(1)

Dans ce tutoriel nous allons réaliser un time-lapse (vidéo accélérée) en utilisant le module Seeed Studio XIAO ESP32S3 Sense et le logiciel FFmpeg.

Installation et configuration de l’IDE Arduino pour la carte XIAO_ESP32S3

Installez l’IDE Arduino et ajouter le gestionnaire des cartes ESP32 (https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json) dans les préférences de l’IDE via le menu Fichier->Préférences :

Installer le gestionnaire de cartes esp32 par Espressif via le menu Outils->Carte->Gestionnaire de carte :

Sélectionner la carte XIAO_ESP32S3 et le port sur laquelle est connectée à votre ordinateur via le menu Outils->Carte et Outils->Port :

Le module ESP32S3 possède 8 MB de mémoire RAM supplémentaire (PSRAM), il faut l’activer en sélectionnant “OPI PSRAM” dans le menu Outils->PSRAM :

Test de l’ESP32 Sense

Pour valider le fonctionnement de la caméra nous allons utiliser l’exemple CameraWebServer fourni par Espressif, l’ouvrir via le menu Fichier->Exemples->ESP32->Camera :

Modifier le code de l’exemple afin de sélectionner la caméra CAMERA_MODEL_XIAO_ESP32S3 :

// ===================
// Select camera model
// ===================
//#define CAMERA_MODEL_WROVER_KIT // Has PSRAM
//#define CAMERA_MODEL_ESP_EYE  // Has PSRAM
//#define CAMERA_MODEL_ESP32S3_EYE // Has PSRAM
//#define CAMERA_MODEL_M5STACK_PSRAM // Has PSRAM
//#define CAMERA_MODEL_M5STACK_V2_PSRAM // M5Camera version B Has PSRAM
//#define CAMERA_MODEL_M5STACK_WIDE // Has PSRAM
//#define CAMERA_MODEL_M5STACK_ESP32CAM // No PSRAM
//#define CAMERA_MODEL_M5STACK_UNITCAM // No PSRAM
//#define CAMERA_MODEL_M5STACK_CAMS3_UNIT  // Has PSRAM
//#define CAMERA_MODEL_AI_THINKER // Has PSRAM
//#define CAMERA_MODEL_TTGO_T_JOURNAL // No PSRAM
#define CAMERA_MODEL_XIAO_ESP32S3 // Has PSRAM
// ** Espressif Internal Boards **
//#define CAMERA_MODEL_ESP32_CAM_BOARD
//#define CAMERA_MODEL_ESP32S2_CAM_BOARD
//#define CAMERA_MODEL_ESP32S3_CAM_LCD
//#define CAMERA_MODEL_DFRobot_FireBeetle2_ESP32S3 // Has PSRAM
//#define CAMERA_MODEL_DFRobot_Romeo_ESP32S3 // Has PSRAM

Entrer les paramètres de votre réseau WiFi (SSID et mot de passe) :

// ===========================
// Enter your WiFi credentials
// ===========================
const char *ssid = "ssdi-de-votre-wifi";
const char *password = "mot-de-passe-de-votre-wifi";

Compiler et téléverser le programme dans l’ESP32 via le menu Croquis->Téléverser et ouvrir le moniteur série via le menu Outils->Moniteur Série. Vous devriez voir s’afficher l’adresse IP de votre ESP32. Par exemple, mon ESP32 a obtenu l’adresse IP 192.168.1.9, comme on le voit dans les traces du moniteur série :

16:53:14.814 -> ......
16:53:17.320 -> WiFi connected
16:53:17.320 -> Camera Ready! Use 'http://192.168.1.9' to connect

Il vous suffit alors d’entrer “http://192.168.1.9” dans votre navigateur favori (Firefox bien sûr 😉 ) pour accéder au serveur web embarqué dans l’ESP32 et contrôler la caméra :

Il est également possible de lire le flux vidéo depuis un lecteur comme VLC. Il suffit d’ouvrir le flux réseau http://192.168.1.9:81/stream via le menu Média->Ouvrir un flux réseau… Bien entendu il faut remplacer “192.168.1.9” par l’adresse IP de votre module indiquée dans le moniteur série à l’étape précédente.

Température interne de l’ESP32

Lorsque le module ESP32S2 capture et diffuse un flux vidéo en continu, l’ESP32 a tendance à chauffer assez fortement. Une mesure du capteur interne de température du microcontrôleur indique une température de 111°C lorsque le flux vidéo est transmis en haute définition…

Voici le code à ajouter dans la boucle principale loop() du croquis pour afficher la température interne du microcontrôleur :

void loop() {
  // Display the ESP32 internal temperature
  Serial.printf("CPU Temp = %.1f\n", temperatureRead());
  // Do nothing. Everything is done in another task by the web server
  delay(10000)
}

Fixer les paramètres de la caméra avec une requête HTML

Afin de vérifier la valeur des paramètres de la caméra, vous pouvez entrer l’adresse suivante dans votre navigateur : http://<adresse-ip-de-la-camera>:80/status

L’ESP32 vous retourne alors la valeur des différents paramètres de la caméra.

Il est possible de fixer les paramètres de la caméra en entrant l’adresse suivante dans votre navigateur : http://<adresse-ip-de-la-camera>:80/control?var=parametre&val=valeur

Par exemple, pour configurer la caméra en VGA (framesize = 640×480), il suffit d’entrer l’adresse suivante dans votre navigateur : http://<adresse-ip-de-la-camera>:80/control?var=framesize&val=8

Valeur de la variable framesizeFramesize
0FRAMESIZE_96X96 (96×96)
1FRAMESIZE_QQVGA (160×120
2FRAMESIZE_QCIF (176×144)
3FRAMESIZE_HQVGA (240×176)
4FRAMESIZE_240X240 (240×240)
5FRAMESIZE_QVGA (320×240)
6FRAMESIZE_CIF (400×296)
7FRAMESIZE_HVGA (480×320)
8FRAMESIZE_VGA (640×480)
9FRAMESIZE_SVGA (800×600)
10FRAMESIZE_XGA (1024×768)
11FRAMESIZE_HD (1280×720)
12FRAMESIZE_SXGA (1280×1024)
13FRAMESIZE_UXGA (1600×1200)

Time-lapse

Pour réaliser un time-lapse (vidéo accélérée), nous n’allons pas utiliser la caméra en mode enregistrement vidéo mais en mode enregistrement de photos. Le concept du time-lapse est en effet de capturer des images toutes les X secondes (X = 1, 5 ou 10 par exemple) et les coller les unes à la suite des autres pour en faire une vidéo à 24 images par seconde. Ce qui donnera l’impression d’une vidéo en accéléré. L’effet d’accéléré va dépendre de l’intervalle entre chaque capture (la période de capture). Pour réaliser la vidéo à partir des photo nous utiliserons le logiciel FFmpeg.

Intervalle entre chaque photoAccélération
1 seconde24x
5 secondes120x
10 secondes240x

Stockage des images sur la carte microSD

Le module caméra de l’ESP32 Sense intègre un support pour carte microSD. Une image au format JPG et de taille FRAMESIZE_HD ayant une taille d’environ 100 Ko, nous allons pouvoir stocker environ 80.000 images sur une carte microSD de 8 Go. Ce qui permet de faire un time-lapse d’une durée de 55 minutes à 24 images par seconde.

Les images sont enregistrées sous le nom “imageXXXXX.jpg“, “XXXXX” étant un numéro sur 5 chiffres qui s’incrémente à chaque capture (les images seront donc nommées “image00001.jpg“, “image00002.jpg“…). Cela va permettre de réaliser plus simplement le time-lapse avec FFmpeg comme nous le verrons à la fin du tuto.

Le stockage sur une carte microSD d’un fichier de 140 ko dure environ 550 millisecondes, il faut tenir compte de cette durée si vous souhaitez gérer précisément l’intervalle entre chaque photo de votre time-lapse.

Voici le code qui m’a permis de mesurer cette durée dans la fonction captureAndSavePicture :

// Capture picture and save it to SD card
void captureAndSavePicture(const char *fileName) {
  unsigned long myTime;
  // Take a picture
  camera_fb_t *fb = esp_camera_fb_get();
  if (!fb) {
    Serial.println("Failed to get camera frame buffer");
    return;
  }
  myTime = millis();
  // Write picture to SD
  if (!writeFile(SD, fileName, fb->buf, fb->len)) {
    Serial.println("Failed to write picture to SD");
  }
  myTime = millis() - myTime;
  Serial.print("Time to write file =");
  Serial.println(myTime); // prints time since program started
  // Release image buffer
  esp_camera_fb_return(fb);
}

Capture régulière des images

Pour réaliser un time-lapse, nous devons capturer régulièrement des images. La périodicité de ces captures va dépendre du sujet que vous souhaitez mettre en avant et de la durée totale du time-lapse. Afin d’avoir à éviter de modifier le programme, j’utilise le fichier texte “period.txt” que je stocke sur la carte microSD et qui contient la période de capture sous forme d’un nombre de secondes.

Exemple de configuration de période de capture de 10 secondes

Au démarrage le programme ouvre ce fichier texte et y lit le nombre qui y est stocké. Ce nombre sera la période de capture des images en secondes.

Alimentation et consommation électrique du module

Le XIAO ESP32S3 Sense avec son module caméra et lecteur de carte microSD consomme environ 177 mA de courant lorsqu’il est alimenté par une batterie externe de 3,7 V.

Le module consomme environ 177 mA lorsqu’il est alimenté par une batterie externe de 3,7 V

Afin de limiter la consommation entre 2 captures, nous pouvons utiliser la fonction de mise en veille et activer son réveil par timer. Le timer sera tout simplement configuré avec la période de capture des images.

esp_sleep_enable_timer_wakeup(capture_period*uS_TO_S_FACTOR);
esp_light_sleep_start();

Mais même mode veille, la consommation du module reste élevée, à environ 90 mA. Il semble que le module caméra et lecteur de carte microSD soit la cause de cette consommation et il ne parait pas aisé de la réduire (je recherche sans succès pour l’instant).

Avec une batterie de type 18650 de 3000 mAh, cela permet toutefois une autonomie d’environ 30 heures.

Le programme

Voici mon programme largement inspiré du wiki de seeed studio :

// Time-lapse with ESP32 Sense
// https://tutoduino.fr/
// Copyleft 2024
#include "esp_camera.h"
#include "FS.h"
#include "SD.h"
#include "SPI.h"
#define MICROSECONDS_PER_SECOND 1000000 /* Conversion factor for micro seconds to seconds */
#define MILLISECONDS_PER_SECOND 1000    /* Conversion factor for milli seconds to seconds */
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 10
#define SIOD_GPIO_NUM 40
#define SIOC_GPIO_NUM 39
#define Y9_GPIO_NUM 48
#define Y8_GPIO_NUM 11
#define Y7_GPIO_NUM 12
#define Y6_GPIO_NUM 14
#define Y5_GPIO_NUM 16
#define Y4_GPIO_NUM 18
#define Y3_GPIO_NUM 17
#define Y2_GPIO_NUM 15
#define VSYNC_GPIO_NUM 38
#define HREF_GPIO_NUM 47
#define PCLK_GPIO_NUM 13
#define LED_GPIO_NUM 21
// Comment this line if you do not want to have debug traces
#define DEBUG
unsigned long imageCount = 1;  // File Counter
bool camera_init = false;      // camera init status
bool sd_init = false;          // sd card status
unsigned int capture_period;   // capture period in seconds
// SD card write file
bool writeFile(fs::FS &fs, const char *path, uint8_t *data, size_t len) {
  size_t rc;
#ifdef DEBUG
  Serial.printf("Writing file: %s (length=%d)\n", path, len);
#endif
  File file = fs.open(path, FILE_WRITE);
  if (!file) {
#ifdef DEBUG
    Serial.println("Failed to open file for writing");
#endif
    return false;
  }
  rc = file.write(data, len);
  if (rc == len) {
#ifdef DEBUG
    Serial.println("File written");
#endif
  } else {
#ifdef DEBUG
    Serial.printf("Write failed (rc=%d)\n", rc);
#endif
    return false;
  }
  file.close();
  return true;
}
// Capture picture and save it to SD card
bool captureAndSavePicture(const char *fileName) {
  // Take a picture
  camera_fb_t *fb = esp_camera_fb_get();
  if (!fb) {
#ifdef DEBUG
    Serial.println("Failed to get camera frame buffer");
#endif
    return false;
  }
  // Write picture to SD
  if (!writeFile(SD, fileName, fb->buf, fb->len)) {
#ifdef DEBUG
    Serial.println("Failed to write picture to SD");
#endif
    return false;
  }
  // Release image buffer
  esp_camera_fb_return(fb);
  return true;
}
// Read picture capture period in file "period.txt" from SD card
unsigned int readCapturePeriod(fs::FS &fs, const char *path) {
  unsigned int period;
  File file = fs.open(path, FILE_READ);
  if (!file) {
#ifdef DEBUG
    Serial.println("Failed to open file containing period");
#endif
    // return 10 seconds period by default
    return 10;
  }
  period = file.parseInt();
  file.close();
  return period;
}
// infinitely blink builtin led in case of init error
void blink_error() {
  while (1) {
    delay(200);
    digitalWrite(LED_BUILTIN, HIGH);
    delay(200);
    digitalWrite(LED_BUILTIN, LOW);
  }
}
void setup() {
  uint8_t cardType;
#ifdef DEBUG
  Serial.begin(115200);
#endif
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;  // for streaming
  config.grab_mode = CAMERA_GRAB_LATEST;
  config.fb_count = 1;
  // if PSRAM is enabled, init with UXGA resolution and higher JPEG quality
  // for larger pre-allocated frame buffer.
  if (psramFound()) {
    config.frame_size = FRAMESIZE_HD;
    config.jpeg_quality = 3;
    config.fb_count = 2;
    config.fb_location = CAMERA_FB_IN_PSRAM;
  } else {
// no PSRAM
#ifdef DEBUG
    Serial.println("PSRAM not found, please enable it");
#endif
    blink_error();
  }
  // camera init
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
#ifdef DEBUG
    Serial.printf("Camera init failed with error 0x%x", err);
#endif
    blink_error();
  }
  camera_init = true;  // Camera initialization check passes
  // Initialize SD card
  if (!SD.begin(21)) {
#ifdef DEBUG
    Serial.println("Card Mount Failed");
#endif
    blink_error();
  }
  cardType = SD.cardType();
  // Determine if the type of SD card is available
  if (cardType == CARD_NONE) {
#ifdef DEBUG
    Serial.println("No SD card attached");
#endif
    blink_error();
  }
#ifdef DEBUG
  if (cardType == CARD_MMC) {
    Serial.println("SD Card Type: MMC");
  } else if (cardType == CARD_SD) {
    Serial.println("SD Card Type: SDSC");
  } else if (cardType == CARD_SDHC) {
    Serial.println("SD Card Type: SDHC");
  } else {
    Serial.println("SD Card Type: UNKNOWN ");
  }
#endif
  sd_init = true;  // sd initialization check passes
  // wait 10 seconds to position the camera correctly before starting to capture pictures
  delay(10000);
  capture_period = readCapturePeriod(SD, "/period.txt");
#ifdef DEBUG
  Serial.printf("XIAO ESP32S3 Sense is starting to capture images every %d seconds \n", capture_period);
#endif
}
void loop() {
  // Camera & SD available, start taking pictures
  if (camera_init && sd_init) {
    char filename[32];
    // the file name ends with the image counter on 5 digits
    sprintf(filename, "/image%05d.jpg", imageCount);
    // take a picture and store it in file filename
    if (captureAndSavePicture(filename)) {
#ifdef DEBUG
      Serial.printf("Saved picture: %s\r\n", filename);
#endif
      // image was correctly taken and saved, increase the image counter
      imageCount++;
    };
    // Wait capture_period seconds
    delay(capture_period * MILLISECONDS_PER_SECOND);
  }
}

The code can be copied from above or from my github.

FFmpeg

Nous allons utiliser le logiciel FFmpeg afin de créer une vidéo à partir des images capturées par l’ESP32. Ce logiciel open source est extrêmement puissant et je vous le recommande vivement. La procédure d’installation est différente en fonction de votre système d’exploitation, mais la commande pour générer la vidéo est la même pour Linux (dans un terminal) et Windows (dans PowerShell).

Installation de FFmpeg sous Linux

Installez FFmpeg via le gestionnaire de paquet de votre distribution :

sudo apt install ffmpeg

Installation de FFmpeg sous Windows

Installez FFmpeg via winget dans un fenêtre PowerShell :

Création du time-lapse avec FFmpeg

Copiez les images depuis la carte microSD vers un répertoire sur votre PC, et lancez la ligne de commande suivante dans ce répertoire (la commande est identique dans un terminal sous Linux et dans un PowerShell sous Windows) :

ffmpeg -framerate 24 -i image%05d.jpg -s:v 1280x720 my-timelapse.mp4

Vous obtiendrez un time-lapse de 24 images par seconde au format MP4 qui inclura toutes les images capturées par votre ESP32.

Voici un exemple de time-lapse à 24 images par seconde avec 623 images capturées toutes les 5 secondes.

Note : si vous avez mal orienté la caméra et que vos images sont à l’envers, sachez que FFmpeg fait des miracles pour traiter les vidéos.

Voici par exemple comment retourner une vidéo lorsque la caméra à été mise à l’envers (haut vers le bas) :

ffmpeg -i my-timelapse.mp4 -vf vflip -c:a copy my-timelapse-flipped.mp4

J’espère que ce tuto vous aura intéressé. N’hésitez pas à donner votre avis en cliquant sur les étoiles ci-dessous. Je ne suis pas rémunéré au nombre d’étoiles mais c’est important pour moi afin de voir que mon travail est utile aux autres 🙂

Votre avis compte !

Note moyenne : 5 / 5. Nombre de votes : 1

Pas encore de vote pour ce tutoriel

Désolé si cet article ne vous a pas intéressé

Merci de commenter afin que je puisse l’améliorer.

Dites-moi comment améliorer cette page.