ESP32-CAM Camera Settings: Frame Size, JPEG Quality, XCLK and Buffers

Most ESP32-CAM sketches copy the same camera_config_t block without changing it. That works, but it hides the choices that decide image detail, file size, frame rate and memory use. If your stream is slow, your photos are blurry or esp_camera_fb_get() sometimes returns NULL, the answer is usually in these few settings.

This tutorial explains each important field of camera_config_t and the most useful run-time sensor controls, then gives a test sketch that captures at several frame sizes and quality values and prints the JPEG size and capture time, so you can choose numbers based on your own measurements.

Settings Fixed at Initialisation

These fields are read once by esp_camera_init(). Changing them needs esp_camera_deinit() and a new init.

FieldTypical valueEffect
xclk_freq_hz20000000Clock sent to the OV2640 on GPIO 0. 20 MHz is the standard; 10 MHz can help if you see stripes or corrupt frames, at a lower frame rate
pixel_formatPIXFORMAT_JPEGThe OV2640 compresses in hardware. RGB565 or GRAYSCALE give raw pixels but need far more memory
frame_sizeFRAMESIZE_UXGA when PSRAM is presentSets the size of the buffers allocated at init; you can later switch smaller but not larger
jpeg_quality10 to 120 to 63, lower means better quality and bigger files
fb_count1 for stills, 2 for streamingNumber of frame buffers; 2 lets the driver fill one while you send the other
fb_locationCAMERA_FB_IN_PSRAMLarge JPEG buffers need PSRAM; DRAM only suits small frames
grab_modeCAMERA_GRAB_LATESTLATEST always returns the newest frame; WHEN_EMPTY returns frames in order and can hand you an old image

Common Frame Sizes on the OV2640

ConstantResolutionTypical use
FRAMESIZE_QQVGA160 x 120Very fast processing, thumbnails
FRAMESIZE_QVGA320 x 240Smooth streaming, face detection
FRAMESIZE_CIF400 x 296Streaming on a weak Wi-Fi link
FRAMESIZE_VGA640 x 480Good balance for live video
FRAMESIZE_SVGA800 x 600Clear stills, slower stream
FRAMESIZE_XGA1024 x 768Detailed stills
FRAMESIZE_SXGA1280 x 1024High-detail stills
FRAMESIZE_UXGA1600 x 1200Full sensor resolution, stills only

Run-Time Sensor Controls

After init, esp_camera_sensor_get() returns a sensor_t pointer whose function pointers change sensor registers immediately. The most useful ones are listed here.

  • set_framesize(s, FRAMESIZE_VGA) and set_quality(s, 10) for size and compression.
  • set_brightness, set_contrast, set_saturation: values from -2 to 2.
  • set_whitebal(s, 1) turns automatic white balance on; set_wb_mode picks presets such as sunny or office.
  • set_exposure_ctrl(s, 1) enables auto exposure; with it off, set_aec_value sets a manual exposure.
  • set_gain_ctrl(s, 1) enables automatic gain; set_gainceiling limits noise in low light.
  • set_hmirror and set_vflip correct a camera mounted sideways or upside down.
  • set_lenc(s, 1) enables lens correction, which reduces dark corners.

Measuring Size and Speed

The sketch initialises at UXGA, then loops through several frame sizes and two quality values. For each combination it throws away one frame (so the new setting takes effect), captures another and prints the resolution, byte count and time taken.

Benchmark JPEG size and capture time for different frame sizes and quality values
#include "esp_camera.h"

// AI-Thinker ESP32-CAM pin map
#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

bool initCamera() {
  camera_config_t c = {};
  c.ledc_channel = LEDC_CHANNEL_0;
  c.ledc_timer   = LEDC_TIMER_0;
  c.pin_d0 = Y2_GPIO_NUM;  c.pin_d1 = Y3_GPIO_NUM;
  c.pin_d2 = Y4_GPIO_NUM;  c.pin_d3 = Y5_GPIO_NUM;
  c.pin_d4 = Y6_GPIO_NUM;  c.pin_d5 = Y7_GPIO_NUM;
  c.pin_d6 = Y8_GPIO_NUM;  c.pin_d7 = Y9_GPIO_NUM;
  c.pin_xclk = XCLK_GPIO_NUM;   c.pin_pclk = PCLK_GPIO_NUM;
  c.pin_vsync = VSYNC_GPIO_NUM; c.pin_href = HREF_GPIO_NUM;
  c.pin_sccb_sda = SIOD_GPIO_NUM; c.pin_sccb_scl = SIOC_GPIO_NUM;
  c.pin_pwdn = PWDN_GPIO_NUM;   c.pin_reset = RESET_GPIO_NUM;
  c.xclk_freq_hz = 20000000;           // try 10000000 if frames are corrupt
  c.pixel_format = PIXFORMAT_JPEG;
  c.frame_size   = FRAMESIZE_UXGA;     // allocate for the largest size
  c.jpeg_quality = 10;
  c.fb_count     = 1;
  c.fb_location  = CAMERA_FB_IN_PSRAM;
  c.grab_mode    = CAMERA_GRAB_LATEST;
  return esp_camera_init(&c) == ESP_OK;
}

const framesize_t sizes[] = {FRAMESIZE_QVGA, FRAMESIZE_VGA, FRAMESIZE_SVGA,
                             FRAMESIZE_XGA, FRAMESIZE_UXGA};
const int qualities[] = {10, 30};

void setup() {
  Serial.begin(115200);
  delay(500);
  if (!psramFound() || !initCamera()) {
    Serial.println("PSRAM missing or camera init failed");
    while (true) delay(1000);
  }
  sensor_t *s = esp_camera_sensor_get();
  s->set_lenc(s, 1);                   // lens correction on

  Serial.println("width x height | quality | bytes | ms");
  for (framesize_t fs : sizes) {
    for (int q : qualities) {
      s->set_framesize(s, fs);
      s->set_quality(s, q);
      camera_fb_t *fb = esp_camera_fb_get();   // discard: old settings
      if (fb) esp_camera_fb_return(fb);

      unsigned long t0 = millis();
      fb = esp_camera_fb_get();
      unsigned long dt = millis() - t0;
      if (!fb) {
        Serial.println("capture failed");
        continue;
      }
      Serial.printf("%4u x %4u | %2d | %6u | %lu\n",
                    fb->width, fb->height, q, fb->len, dt);
      esp_camera_fb_return(fb);
    }
  }
}

void loop() {
}

Run it pointed at a busy scene and then at a plain wall. JPEG size depends strongly on image content, so a detailed scene can be several times larger than a blank one at the same settings. Use the worst case when planning SD card space, upload time or Telegram limits.

Reading the Results

Capture time is mostly the time to read out one frame from the sensor, so it rises with frame size. JPEG size rises with both frame size and image detail, and drops quickly as the quality number goes up. In practice, quality values between 10 and 15 look almost the same on a phone screen, while values above 30 show visible blocks around edges. For a stream, aim for frames that are small enough to send many times per second over your Wi-Fi link; for a stored photo, choose the largest size and lowest quality number that still captures reliably.

Record your measurements in a small table for your own board and lighting. Numbers from other people's boards are only a rough guide, because sensor batches, lenses and Wi-Fi conditions differ.

Choosing Settings and Avoiding Problems

  • fb_get returns NULL at high resolution with low quality numbers: the JPEG does not fit the buffer. Raise the quality number to 12 or more, or reduce the frame size.
  • Greenish or dark first photo: auto exposure and white balance need a few frames to settle. Discard two or three frames after power-up or after changing settings.
  • Old image returned for a single photo: you are using CAMERA_GRAB_WHEN_EMPTY with buffers filled earlier. Use CAMERA_GRAB_LATEST or discard one frame first.
  • Choppy stream: drop to VGA or CIF and quality 12 to 15 before blaming Wi-Fi.
  • Out of memory errors: two UXGA buffers plus Wi-Fi need PSRAM; without PSRAM stay at QVGA or smaller with one buffer in DRAM.

Summary

Frame size and JPEG quality decide image detail and file size, fb_count and grab_mode decide whether you get fresh frames and smooth streaming, and xclk_freq_hz trades speed for stability. Initialise at the largest size you need, adjust with the sensor functions at run time, discard a frame after changes, and measure with the benchmark sketch rather than guessing.