
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
#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;
}
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();
// 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();
// 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();
// 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();
// 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:
- check the returned value and act, if it is a non critical error
- 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
// 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
);
}
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);
}
/**
* 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);
}