ESP32-CAM quickstart

ESP32-CAM quickstart

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.

EspKit library

The EspKit library for Arduino defines a set of abstractions that make the ESP32 features easily accessible with few lines of code. Regarding the camera access, here's a list of what's included, to give you an idea:

  • pin assignment
  • sensor configuration
  • fast jpeg decoding
  • motion detection
  • face detection & recognition
  • object detection using Edge Impulse

Installation

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

About the syntax

Before moving on, a quick note about the syntax style of the library. The library follows the object-oriented paradigm, sometimes brought to the extreme. Everything is an object, possibly nested under other objects. I like this style and it looks really clean to me.

So, instead of

void disableBrownoutDetector() {
    WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
}

you write

camera.config.brownout.disable();

If you're not familiar at all with this kind of programming, you may have a hard time using the library. But you can still copy-paste the code snippets from this blog and make minor changes to adapt them to you needs.

Camera configuration

The config component of the camera object (which is provided for you in the global namespace) covers pin assignment, clock speed, brownout detector (which will reboot the board if a high voltage is detected - which happens pretty often!), image quality and resolution.

Pinout assignment

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

Thanks to the espkit 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.

// set camera model directly
// alternatives are:
// - aithinker
// - xiao
// - wroom_s3
// - ttgo_plus
// - ttgo_pir
// - m5
// - m5_fisheye
// - m5_timerx
// - espeye
// - espeye_s3
// - wrover
camera.config.pinout.aithinker();

// ...or select from a list
camera.config.pinout.prompt();

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

Sometimes, you may not be sure about which model you should select. In that case, you can try to guess() the model automatically. This process will cycle throughout all the known pinouts and see if the camera initializes correctly. At the end of the process, it will print the model that was found, so you can place the initialization line in your sketch.

camera.config.pinout.guess();

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)
camera.config.pixformat.jpeg();

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
camera.config.quality.set(100);
// or use a qualitative measure
// alternatives are: best, high, mid, low, lowest
camera.config.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
camera.config.resolution.vga();

// ...or choose from a list
camera.config.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 (!camera.begin()) {
  Serial.print("Camera init failed");
}

// ...or abort execution
// if an error occur, raise() will alt the program
// otherwise it will do nothing
camera.begin();
camera.raise();

Capture frame

To grab a new frame, you just call camera.grab(). This will return a CameraFrame 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 = camera.grab();

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

  // print info about the frame
  // frame.size() returns the size in bytes of the frame
  // frame.fb gives access to the underlying camera_fb_t*
  ESP_LOGI(
        "App",
        "Frame size: %d bytes, t=%d",
        frame.size(),
        frame.fb->timestamp.tv_usec / 1000
  );
}

Complete sketch

Here's what a complete sketch looks like.

/**
 * Get started with the EloquentEsp32 library.
 * Configure camera and grab frames.
 *
 * Turn on INFO logging in the Tools menu to see
 * debug messages.
 *
 * Rembember to enable PSRAM in the Tools menu!
 */
#include <espkit.h>
#include <espkit/camera.h>


void setup() {
    delay(3000);
    Serial.begin(115200);
    ESP_LOGI("APP", "Start");

    // set camera pins
    // let user choose from a list (open Serial Monitor!)
    // after you choose the pinout, a line will be printed
    // to avoid choosing again in the future
    // e.g. `camera.config.pinout.aithinker()`
    camera.config.pinout.prompt();
    // ...or, if you don't know which model to select
    // you can try to guess
    // (WARNING: will probably cause reboots until a match is found!)
    camera.config.pinout.guess();

    // enable JPEG mode
    // alternatives are:
    // - gray() for grayscale mode
    // - rgb() for RGB565 mode
    // - raw() for RGB888
    // NOTE: not all boards support all modes!
    camera.config.pixformat.jpeg();

    // when in JPEG mode, you can set the quality
    // of the image. Higher quality requires
    // more memory.
    // alternatives are:
    // - best() for highest quality
    // - mid()
    // - low()
    // - worst()
    // - set(0..100) to set a custom quality
    //   where 0 is worst and 100 is best
    camera.config.quality.high();

    // set frame resolution
    // choosing from a list of options
    //camera.config.resolution.prompt();
    // or set directly
    camera.config.resolution.qvga();

    // if you prefer, you can set all config values using a string
    // the options above translate to the following:
    //camera.config("format:jpeg q:high res:square pinout:prompt");
    // init camera
    // will return "falsy" if something goes wrong
    camera.begin();
    // if something went wrong, abort execution
    // (will print the error message forever)
    camera.raise();
}

void loop() {
    // grab new frame
    // will return "falsy" if something goes wrong
    auto frame = camera.grab();

    if (!frame) {
        Serial.println(frame.reason());
        delay(1000);
        return;
    }

    // print info about the frame
    // frame.size() returns the size in bytes of the frame
    // frame.fb gives access to the underlying camera_fb_t*
    ESP_LOGI(
            "App",
            "Frame size: %d bytes, t=%d",
            frame.size(),
            frame.fb->timestamp.tv_usec / 1000
    );

    delay(1000);
}
Next: ESP32-CAM streaming