ESP32 Camera: Image server

In this tutorial we are going to learn how to setup a HTTP server on the ESP32 that will have an endpoint that will return an image taken from a camera connected to the ESP32.

Introduction

In this tutorial we are going to learn how to setup a HTTP server on the ESP32 that will have an endpoint that will return an image taken from a camera connected to the ESP32.

For this tutorial I’ve used a HW-818 ESP32 board model. It already contains all the electronics needed to connect the camera to the ESP32 and we simply need to plug it in a socket of the board. Additionally, this model also has support for a micro SD card.

The board also ships with 512 KB of internal RAM plus 4MB of external PSRAM. The camera model included with the board is an OV2640.

One of the advantages of this model is that it already has an USB connector, meaning it includes everything we need to get started without additional hardware. You can find the HW-818 at eBay for around 10 euros. You can check it here.

If you haven’t yet tried your board, my recommendation is to start by running the example from the Arduino library. The model HW-818 is not listed in the supported models but you can use the same configurations as the AI THINKER model and it should work.

For the implementation of the code from this tutorial, we will use the async HTTP web server library. For an introductory tutorial on the library, please check here.

If you prefer a video version of this tutorial, please check my YouTube channel:

The code

We will start our code by the library includes. We need the following libraries:

  • WiFi.h: allows to connect the ESP32 to the WiFi network;
  • ESPAsyncWebServer.h: allows to setup a HTTP async web server on the ESP32;
  • esp_camera.h: exposes the functions to initialize the camera and get pictures from it.
#include "WiFi.h"
#include "ESPAsyncWebServer.h"
#include "esp_camera.h"

Then we will define the pins of the board that are connected to the camera. These will be later needed when initializing it. The easiest way to find out the mappings for your camera board model is consulting this file from the Arduino core library examples. As already mentioned in the introductory section, the HW-818 is not listed but we can use the definitions from the AI THINKER model.

The pin defines shown below were taken from the mentioned file and apply to the model I’m using in the tests (the HW-818).

#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

Followed by that we will define two variables to hold the WiFi network credentials, namely the network name and password.

const char* ssid = "yourNetworkName";
const char* password = "yourNetworkPassword";

We will also define an object of class AsyncWebServer, which we will use to setup the HTTP server on the ESP32. As input of the constructor of this class, it receives the port where the service will be listening to incoming requests.

AsyncWebServer server(80);

Moving on to the Arduino setup, we will start by opening a serial connection, so we can output some results from our program.

Serial.begin(115200);

After that, we will take care of initializing the camera. To isolate the camera initialization logic, we will create a function called initCamera that takes no arguments and returns a Boolean value indicating if the procedure was successful. We will check its implementation later.

if(!initCamera()){
    
    Serial.printf("Failed to initialize camera...");
    return;  
}

Assuming the camera initialization procedure is done correctly, we will then connect the ESP32 to the WiFi network, using the credentials defined before.

Note that, at the end of the procedure, we will print the local IP address assigned to the ESP32, which we will need to reach the server.

WiFi.begin(ssid, password);
 
while (WiFi.status() != WL_CONNECTED) {
   delay(1000);
   Serial.println("Connecting to WiFi..");
}
 
Serial.println(WiFi.localIP());

Then we will setup a server route on the “/picture” endpoint. This route will listen to HTTP GET requests and return a picture taken after the request is received. The route handling function, which will have the logic needed to take the picture, will be implemented using the C++ lambda syntax.

server.on("/picture", HTTP_GET, [](AsyncWebServerRequest * request) {
    // Route handling function implementation
});

On the route handling function, we will define a variable that will hold a pointer to a struct of type camera_fb_t. A struct of this type holds the following information (among some additional parameters):

  • A pointer to a buffer that contains the pixel data of the image (uint8_t * data type);
  • The length of the buffer, in bytes;
  • The width and the height of the buffer, in pixels.
camera_fb_t * frame = NULL;

After this we will get an image from the camera with a call to the esp_camera_fb_get function. This will return a pointer to a struct of type camera_fb_t, which we will store in our previously defined variable.

For simplicity, we are not doing any error checking on the returned value but we should do so in a real case scenario, to make sure we obtained the image correctly. Naturally, in case we don’t, we can inform the client by returning an error HTTP status code.

frame = esp_camera_fb_get();

Then we will return the image back to the client. To do so, we need to recall, from previous tutorials where we have used the async HTTP web server, that the route handling function receives as input a pointer to an object of class AsyncWebServerRequest.

This object will allow us to return the response back to the client. To do so, we need to call the send_P method on the object. This method has multiple possible signatures, but we will use the one that allows us to specify a data buffer to be sent back to the client.

As first input we pass an integer with the response status code. In our case we will pass the value 200, which corresponds to OK.

As second parameter of the method, we need to pass a string with the content type of the response. In our case, as we will see below, we will configure our camera to return JPEG images. So, we should use the value “image/jpeg“.

As third parameter we need to pass a pointer to the buffer containing our image. Recall that it is stored in the camera_fb_t pointer we have obtained before.

As mentioned previously, this struct holds a pointer to the actual buffer with the pixels of the image. This pointer is stored in a field called buf. Note that we should do a cast of our buffer to const uint8_t *, which is the data type expected by the send_P method.

As fourth and last parameter of the function, we need to pass the length of the buffer. Once again, we can get it from our camera_fb_t struct, in a field called len.

request->send_P(200, "image/jpeg", (const uint8_t *)frame->buf, frame->len);

To finalize the implementation of the route handling function, we will call the esp_camera_fb_return function, passing as input the pointer to the camera_fb_t struct we have used. This function call will allow the image buffer to be reused again.

esp_camera_fb_return(frame);

The complete route declaration can be seen below.

server.on("/picture", HTTP_GET, [](AsyncWebServerRequest * request) {

    camera_fb_t * frame = NULL;
    frame = esp_camera_fb_get();

    request->send_P(200, "image/jpeg", (const uint8_t *)frame->buf, frame->len);

    esp_camera_fb_return(frame);
});

To finish the setup function, we simply need to call the begin method on our server object, so it starts listening to incoming requests.

server.begin();

The complete Arduino setup can be seen below.

void setup() {
  Serial.begin(115200);

  if(!initCamera()){
    
    Serial.printf("Failed to initialize camera...");
    return;  
  }
  
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }

  Serial.println(WiFi.localIP());

  server.on("/picture", HTTP_GET, [](AsyncWebServerRequest * request) {

    camera_fb_t * frame = NULL;
    frame = esp_camera_fb_get();

    request->send_P(200, "image/jpeg", (const uint8_t *)frame->buf, frame->len);

    esp_camera_fb_return(frame);
  });

  server.begin();
}

Since we are working with an async HTTP web server, no periodic call needs to be done for it to work. Consequently, we can leave the Arduino main loop empty.

void loop(){}

To finalize, we will check the implementation of the initCamera function we have used in the Arduino setup. As mentioned before, the function takes no arguments and returns a Boolean value indicating if the procedure was completed with success.

bool initCamera(){
   // function implementation
}

We will start by declaring a variable of type camera_config_t. This type corresponds to a struct that will hold the camera configurations.

camera_config_t config;

A big part of the configs correspond to specifying the pins of the controller that are connected to the pins of the camera. In short, for those, we will be assigning the values we have defined in the beginning of our code.

For some other fields, such as the camera clock frequency, the ledc timer and ledc channel, we will use the values also used on the Arduino core camera example.

Then we will setup the following configurations:

  • Pixel format: we will pass the value PIXFORMAT_JPEG, so we get the images in the JPEG format, like we are specifying in the return of our route handling function.
  • Frame size: we will pass the value FRAMESIZE_SVGA, to get the frame in the SVGA format (800×600).
  • JPEG quality: an integer value between 0 and 63 indicating the quality of the output JPEG. Lower numbers mean higher quality. We will pass the value 10.
  • Frame buffer count: Number of frame buffers to be allocated. We will use the value 1.
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; 
config.frame_size = FRAMESIZE_SVGA;
config.jpeg_quality = 10;
config.fb_count = 1;

After that we will call the esp_camera_init function, passing as input the address of our struct containing the camera definitions. This will take care of the camera initialization.

As output, it returns a value of type esp_err_t, which we will use for error checking. In case the value is different from ESP_OK, it means something failed.

The complete function can be seen below.

bool initCamera(){
  
  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; 
  config.frame_size = FRAMESIZE_SVGA;
  config.jpeg_quality = 10;
  config.fb_count = 1;
   
  esp_err_t result = esp_camera_init(&config);
  
  if (result != ESP_OK) {
    return false;
  }

  return true;
}

The full code can be seen below.

#include "WiFi.h"
#include "ESPAsyncWebServer.h"
#include "esp_camera.h"

#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

const char* ssid = "yourNetworkName";
const char* password = "yourNetworkPassword";

AsyncWebServer server(80);

bool initCamera(){
  
  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; 
  config.frame_size = FRAMESIZE_SVGA;
  config.jpeg_quality = 10;
  config.fb_count = 1;
   
  esp_err_t result = esp_camera_init(&config);
  
  if (result != ESP_OK) {
    return false;
  }

  return true;
}

void setup() {
  Serial.begin(115200);

  if(!initCamera()){
    
    Serial.printf("Failed to initialize camera...");
    return;  
  }
  
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }

  Serial.println(WiFi.localIP());

  server.on("/picture", HTTP_GET, [](AsyncWebServerRequest * request) {

    camera_fb_t * frame = NULL;
    frame = esp_camera_fb_get();

    request->send_P(200, "image/jpeg", (const uint8_t *)frame->buf, frame->len);

    esp_camera_fb_return(frame);
  });

  server.begin();
}

void loop(){}

Testing the code

To test the code, simply compile it and upload it to your device using the Arduino IDE. When the procedure finishes, open the IDE serial monitor to confirm everything was correctly initialized.

Note that the IP address of the ESP32 should get printed once the device connects to the WiFi network. Copy it, since it will be needed to reach the server.

Then, open a web browser of your choice and type the following in the URL bar, changing #yourDeviceIp# by the IP address you have just copied:

http://#yourDeviceIp#/picture

After accessing the URL, you should obtain a result similar to the one shown in figure 1. As can be seen, we have obtained a picture from the server, taken by the camera. You can refresh the page to take new pictures.

Picture taken by the ESP32 camera and returned by the server.
Figure 1 – Picture taken by the ESP32 camera and returned by the server.

One Reply to “ESP32 Camera: Image server”

  1. That one line : send_P(200, “image/jpeg”, (const uint8_t *)frame->buf, frame->len)
    made such a difference in quickly loading the image to a webpage.

    Thank you.

Leave a Reply