ESP32-CAM: take first picture

ESP32-CAM: take first picture

The ESP32-CAM is a nice piece of hardware. At only 5 USD on Aliexpress, it is by far the easiest and cheapest way to get your hands on embedded vision.

Sadly, most tutorials you find online are really poorly written... They are lengthy, intricate and hard to customize for your specific needs. And since they're almost a copy-past of each other, you run the risk to get used to that style of programming. But it doesn't have to be like that. There's a better, cleaner, more efficient way to use the ESP32 camera.

Using the ESPx library

This project makes use of the espx library. The espx library for Arduino defines a set of abstractions that make the ESP32 features easily accessible with few lines of code. Some of the features are:

  • WiFi connection (wifix)
  • HTTP client (httpx)
  • JSON generator (jsonx)
  • Camera manipulation (camx)

Installation

Install the latest version of espx from the Arduino Library Manager.

The code examples on this page have been tested with version 1.0.2: if you get weird errors about missing variables or method, double check that you have the correct version.

If the error persists, please open an issue on GitHub.

The ugly way to initialize a camera

You may be familiar with the following offense to the public decency, included in all your ESP32-CAM sketches:

#if defined(CAMERA_MODEL_WROVER_KIT)
#define PWDN_GPIO_NUM    -1
#define RESET_GPIO_NUM   -1
#define XCLK_GPIO_NUM    21
#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      19
#define Y4_GPIO_NUM      18
#define Y3_GPIO_NUM       5
#define Y2_GPIO_NUM       4
#define VSYNC_GPIO_NUM   25
#define HREF_GPIO_NUM    23
#define PCLK_GPIO_NUM    22

#elif defined(CAMERA_MODEL_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

// ...other defines...


#else
#error "Camera model not selected"
#endif

You should be accustomed to copy-paste those 100+ lines of pin definitions from project to project. Then some more 38 lines.

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_sccb_sda = SIOD_GPIO_NUM;
config.pin_sccb_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; 

if(psramFound()){
  config.frame_size = FRAMESIZE_UXGA; // FRAMESIZE_ + QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA
  config.jpeg_quality = 10;
  config.fb_count = 2;
} else {
  config.frame_size = FRAMESIZE_SVGA;
  config.jpeg_quality = 12;
  config.fb_count = 1;
}

// Init Camera
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
  Serial.printf("Camera init failed with error 0x%x", err);
  return;
}

If you ask me, this is a shame...

The ESPx way to configure the camera

Thanks to the espx library, you can set the pinout of the camera with a much cleaner syntax. You can also choose at runtime the model if you want to port the same exact sketch across different boards. You have a global camera instance available at camx.

// set camera model directly
// alternatives are:
// - aithinker
// - xiao
// - wrooms3
// - ttgoplus
// - ttgopir
// - m5
// - m5fisheye
// - m5timerx
// - espeye
// - espeyes3
// - wrover
camx.model.aithinker();

// ...or choose from a list
camx.model.prompt();

In case of prompt(), you have to open the Serial Monitor and enter your choice. 

Pixel format

Most boards allow you to capture frames in different formats (JPEG, grayscale, RGB...). You can choose the proper format from the pixformat object.

// choose pixel format
// alternatives are:
// - jpeg
// - gray
// - rgb (a.k.a. RGB 565)
// - raw (a.k.a. RGB 888)
camx.pixformat.jpeg();
// ...or choose from a list
camx.pixformat.prompt();

If you opt for JPEG, you can set the quality. Lower quality means lower file size if you want to store the frames on an SD or send them over the network. Quality ranges from 0 to 100.

// set manually
camx.quality.set(100);
// or use a qualitative measure
// alternatives are: best, high, base, low, lowest
camx.quality.high();

Resolution

Same logic applies to choosing a resolution. Not all cameras support all resolutions, so you may need to experiment to see if the one you chose is supported. Mainstream resolutions (QQVGA, QVGA, VGA) are well supported across models.

// set manually
camx.resolution.vga();

// ...or choose from a list
camx.resolution.prompt();

Init camera

After the camera has been configured, it needs to be initialized. Calling begin() will do the job. Like many other components of the library, this action can fail. You can choose to handle this failure in two ways:

  1. check the returned value and act, if it is a non critical error
  2. abort the program execution, if it is a critical error

Here's how to handle both cases.

// handle failure manually
if (!camx.begin()) {
  Serial.print("Camera init failed");
}

// ...or abort execution
// if an error occurs, raise() will alt the program
// otherwise it will do nothing
camx.begin().raise();
// here the camera init succeeded

Capture frame

To grab a new frame, you just call camera.grab(). This will return a Image object, which is a wrapper around the camera_fb_t type from the ESP-IDF core. As per the camera initialization, this operation can fail too, so you can handle the error. Usually, a failure to grab a frame may be a recoverable error, so you rarely will use raise() in this context.

void loop() {
  auto frame = camx.grab();

  // also frame capture may fail
  if (!frame) {
    Serial.print("Capture error: ");
    Serial.println(frame.failure());
    return;
  }

  // print info about the frame
  // frame.length returns the size in bytes of the frame
  // frame.fb gives access to the underlying camera_fb_t*
  Serial.printf(
        "Frame size: %d bytes, ms=%d\n",
        frame.size(),
        frame.fb->timestamp.tv_usec / 1000
  );
}

Complete sketch

Here's what a complete sketch looks like.

/**
 * Take a picture
 */
#include <espx.h>
#include <espx/camx.h>


void setup() {
    delay(1000);
    Serial.begin(115200);
    Serial.println("Camx example: take photo");
    
    // configure camx through Serial Monitor
    camx.model.prompt();
    camx.pixformat.prompt();
    camx.quality.prompt();
    camx.resolution.prompt();
    
    // initialize camx,
    // enter endless loop on error
    camx.begin().raise();
}


void loop() {
    auto frame = camx.grab();

    if (!frame) {
      Serial.println(frame.failure());
      return;
    }

    Serial.printf("Frame size: %d bytes\n", frame.length);
}
Next: ESP32-CAM streaming server