Merge branch 'feat/isp_awb_example' into 'master'

Feat/isp awb example

Closes IDF-10498

See merge request espressif/esp-idf!43946
This commit is contained in:
morris
2026-01-09 15:04:52 +08:00
7 changed files with 464 additions and 37 deletions

View File

@@ -8,6 +8,7 @@
This example demonstrates how to use the ISP (image signal processor) to work with esp_driver_cam component. This example will auto-detect camera sensors via [ESP camera sensor driver](https://components.espressif.com/components/espressif/esp_cam_sensor/versions/0.5.3) and capture camera sensor signals via CSI interface and display it via DSI interface. This example enables following ISP functions:
- ISP AWB (auto white balance) & WBG (white balance gain) feature
- ISP AF (auto-focus) feature
- ISP BF (bayer denoise) feature
- ISP BLC (black level correction) feature
@@ -160,23 +161,37 @@ To exit the serial monitor, use `Ctrl` + `]`.
If you see the following console output, your example should be running correctly:
```
I (1425) main_task: Calling app_main()
I (1425) example_dsi_init: Allocating DSI resources with 2 frame buffer(s)
I (1485) example_dsi_init: Frame buffer[0] allocated at: 0x48000a40
I (1485) example_dsi_init: Frame buffer[1] allocated at: 0x481f4a80
I (1485) isp_dsi: Original CSI resolution: 800x640
I (1485) isp_dsi: Display resolution: 800x640, bits per pixel: 16
I (1495) isp_dsi: frame_buffer_size: 2048000
I (1495) isp_dsi: Frame buffers: fb0=0x48000a40, fb1=0x481f4a80
I (1515) ov5647: Detected Camera sensor PID=0x5647
I (1535) sensor_init: fmt[0].name:MIPI_2lane_24Minput_RAW8_800x1280_50fps
I (1535) sensor_init: fmt[1].name:MIPI_2lane_24Minput_RAW8_800x640_50fps
I (1535) sensor_init: fmt[2].name:MIPI_2lane_24Minput_RAW8_800x800_50fps
I (1535) sensor_init: fmt[3].name:MIPI_2lane_24Minput_RAW10_1920x1080_30fps
I (1545) sensor_init: fmt[4].name:MIPI_2lane_24Minput_RAW10_1280x960_binning_45fps
I (2025) sensor_init: Format in use:MIPI_2lane_24Minput_RAW8_800x640_50fps
I (2045) isp_dsi: ISP Crop not configured
I (2105) ili9881c: ID1: 0x98, ID2: 0x81, ID3: 0x5c
I (1457) main_task: Calling app_main()
I (1457) example_dsi_init: Allocating DSI resources with 2 frame buffer(s)
I (1517) isp_dsi: Original CSI resolution: 800x640
I (1517) isp_dsi: Display resolution: 800x640, bits per pixel: 16
I (1517) isp_dsi: Frame buffers: fb0=0x48000a40, fb1=0x481f4a80
I (1517) ov5647: Detected Camera sensor PID=0x5647
I (1537) sensor_init: fmt[0].name:MIPI_2lane_24Minput_RAW8_800x1280_50fps
I (1537) sensor_init: fmt[1].name:MIPI_2lane_24Minput_RAW8_800x640_50fps
I (1537) sensor_init: fmt[2].name:MIPI_2lane_24Minput_RAW8_800x800_50fps
I (1547) sensor_init: fmt[3].name:MIPI_2lane_24Minput_RAW10_1920x1080_30fps
I (1547) sensor_init: fmt[4].name:MIPI_2lane_24Minput_RAW10_1280x960_binning_45fps
I (1617) sensor_init: Format in use:MIPI_2lane_24Minput_RAW8_800x640_50fps
I (1627) isp_pipeline: ISP processor initialized
I (1627) isp_pipeline: BLC module configured
I (1627) isp_pipeline: BF module configured
I (1627) isp_pipeline: LSC module configured
I (1627) isp_pipeline: DEMOSAIC module configured
I (1637) isp_pipeline: CCM module configured
I (1637) isp_pipeline: GAMMA module configured
I (1637) isp_pipeline: SHARPEN module configured
I (1647) isp_pipeline: COLOR module configured
I (1647) isp_pipeline: All ISP pipeline modules initialized
W (1657) ISP_AWB: subwindow size (480 x 384) is not divisible by AWB subwindow blocks grid (5 x 5). Resolution will be floored to the nearest divisible value.
I (1667) isp_wb: AWB and WBG module initialized
I (1677) isp_wb: White balance task started
I (1677) isp_wb: WB enabled: AWB statistics started, WBG module enabled, processing task created
I (1687) isp_af: AF module initialized
I (1687) isp_af: AF task started
I (1697) isp_af: AF task created
I (1757) ili9881c: ID1: 0x98, ID2: 0x81, ID3: 0x5c
I (1877) isp_dsi: ISP DSI example started
```
Below picture is from the video stream of OV5647 and ILI9881C. The camera module is auto-focused and calibrated by ESP on-chip ISP hardware. The edge is over-sharpened as example code configured.

View File

@@ -1,6 +1,7 @@
set(srcs "isp_dsi_main.c"
"example_buffer.c"
"example_af.c"
"example_awb.c"
"example_pipelines.c")
if(CONFIG_EXAMPLE_ISP_CROP_ENABLE)

View File

@@ -187,8 +187,7 @@ static void example_af_task(void *arg)
}
esp_err_t example_isp_af_init(isp_proc_handle_t isp_proc,
esp_sccb_io_handle_t dw9714_io_handle,
const example_isp_af_config_t *config)
esp_sccb_io_handle_t dw9714_io_handle)
{
if (isp_proc == NULL || dw9714_io_handle == NULL) {
ESP_LOGE(TAG, "Invalid arguments");

View File

@@ -38,20 +38,6 @@ typedef struct {
esp_sccb_io_handle_t dw9714_io_handle; // DW9714 VCM SCCB handle
} example_isp_af_task_param_t;
/**
* @brief AF configuration structure
*/
typedef struct {
int window_size; // AF window size (centered window)
int edge_thresh; // Edge threshold for definition calculation
int env_interval; // Environment detector interval (frames)
int first_step_val; // First step value for SA scheme
int first_approx_cycles; // First approximation cycles
int second_step_val; // Second step value for SA scheme
int second_approx_cycles; // Second approximation cycles
int focus_val_max; // Maximum focus value
} example_isp_af_config_t;
/**
* @brief Initialize AF module
*
@@ -60,15 +46,13 @@ typedef struct {
*
* @param[in] isp_proc ISP processor handle
* @param[in] dw9714_io_handle DW9714 VCM SCCB handle
* @param[in] config AF configuration (can be NULL for default values)
* @return
* - ESP_OK: Success
* - ESP_ERR_INVALID_ARG: Invalid arguments
* - ESP_ERR_NO_MEM: Out of memory
*/
esp_err_t example_isp_af_init(isp_proc_handle_t isp_proc,
esp_sccb_io_handle_t dw9714_io_handle,
const example_isp_af_config_t *config);
esp_sccb_io_handle_t dw9714_io_handle);
/**
* @brief Start AF task

View File

@@ -0,0 +1,344 @@
/*
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "esp_log.h"
#include "esp_check.h"
#include "driver/isp_awb.h"
#include "driver/isp_wbg.h"
#include "driver/isp_types.h"
#include "example_config.h"
#include "example_awb.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/idf_additions.h"
#include "esp_heap_caps.h"
static const char *TAG = "isp_awb";
static isp_awb_ctlr_t s_awb_ctlr = NULL;
static isp_proc_handle_t s_isp_proc = NULL;
static QueueHandle_t s_awb_queue = NULL;
static TaskHandle_t s_awb_task_handle = NULL;
// Configuration for gain printing
#define AWB_GAIN_UPDATE_COUNT 5
// White balance gain normalization value (1.0 = neutral gain)
#define AWB_GAIN_NORM 256 // Normalization value for gain calculation
// PI Controller parameters for smooth gain convergence
#define AWB_P_GAIN 0.5f // Proportional gain (0.0-1.0)
/**
* @brief Get default AWB configuration based on image resolution
*/
static void s_get_default_awb_config(esp_isp_awb_config_t *config, uint32_t h_res, uint32_t v_res)
{
// Set sample point: after CCM by default
config->sample_point = ISP_AWB_SAMPLE_POINT_AFTER_CCM;
// Configure window: middle 80% of image to avoid edge overexposure
config->window.top_left.x = h_res * 0.2f;
config->window.top_left.y = v_res * 0.2f;
config->window.btm_right.x = h_res * 0.8f - 1;
config->window.btm_right.y = v_res * 0.8f - 1;
// Configure subwindow: same size as main window
config->subwindow = config->window;
// Configure white patch detection thresholds
// Luminance range: [0, 220*3] to avoid overexposed pixels
config->white_patch.luminance.min = 0;
config->white_patch.luminance.max = 220 * 3;
// Color ratio ranges: wide range to include all possible white patches
config->white_patch.red_green_ratio.min = 0.5f;
config->white_patch.red_green_ratio.max = 1.999f;
config->white_patch.blue_green_ratio.min = 0.5f;
config->white_patch.blue_green_ratio.max = 1.999f;
// Interrupt priority: 0 means auto-allocate
config->intr_priority = 0;
}
/**
* @brief AWB statistics callback (runs in ISR context)
*
* This callback receives AWB statistics and sends them to the processing task via queue.
* It should be lightweight as it runs in ISR context.
*/
static bool IRAM_ATTR s_awb_statistics_callback(isp_awb_ctlr_t awb_ctlr, const esp_isp_awb_evt_data_t *edata, void *user_data)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// Send statistics to queue (from ISR)
if (s_awb_queue != NULL) {
if (xQueueSendFromISR(s_awb_queue, &edata->awb_result, &xHigherPriorityTaskWoken) != pdTRUE) {
ESP_DRAM_LOGE(TAG, "Failed to send AWB statistics to queue (queue full)");
return false;
}
}
return xHigherPriorityTaskWoken == pdTRUE;
}
/**
* @brief PI controller for smooth white balance gain update
*
* This function implements a PI (Proportional-Integral) controller to smoothly update white balance gains, preventing oscillation and ensuring convergence.
*
* Currently we just do the P control, the I control is not used and could be extended in the future.
*
* @param[in] target_gain Target gain calculated from statistics
* @param[in] current_gain Currently applied gain
* @param[out] new_gain Calculated new gain to apply
*/
static void s_pi_controller_update(isp_wbg_gain_t target_gain,
isp_wbg_gain_t current_gain,
isp_wbg_gain_t *new_gain)
{
// Calculate error (target - current)
float error_r = (float)target_gain.gain_r - (float)current_gain.gain_r;
float error_b = (float)target_gain.gain_b - (float)current_gain.gain_b;
// Calculate new gain: current + P*error + I*integral
float new_gain_r = (float)current_gain.gain_r + AWB_P_GAIN * error_r;
float new_gain_b = (float)current_gain.gain_b + AWB_P_GAIN * error_b;
// Round and clamp to valid range
new_gain->gain_r = (uint32_t)(new_gain_r + 0.5f);
new_gain->gain_g = AWB_GAIN_NORM; // G channel is reference
new_gain->gain_b = (uint32_t)(new_gain_b + 0.5f);
}
/**
* @brief Calculate white balance gain from statistics
*
* @param[in] stat_result AWB statistics result
* @param[out] gain Calculated white balance gain
* @return true if calculation successful, false otherwise
*/
static bool s_calculate_awb_gain(const isp_awb_stat_result_t *stat_result, isp_wbg_gain_t *gain)
{
if (stat_result == NULL || gain == NULL) {
return false;
}
// Check if we have enough white patches
if (stat_result->white_patch_num == 0) {
ESP_LOGD(TAG, "No white patches detected, keeping current gain");
return false;
}
// Check if sum values are valid (avoid division by zero)
if (stat_result->sum_r == 0 || stat_result->sum_b == 0) {
ESP_LOGW(TAG, "Invalid sum values (R=%lu, B=%lu), keeping current gain",
stat_result->sum_r, stat_result->sum_b);
return false;
}
// Calculate gains: use G channel as reference
float gain_r_float = ((float)stat_result->sum_g / (float)stat_result->sum_r) * (float)AWB_GAIN_NORM;
float gain_b_float = ((float)stat_result->sum_g / (float)stat_result->sum_b) * (float)AWB_GAIN_NORM;
// Clamp gains to valid range
gain->gain_r = (uint32_t)(gain_r_float + 0.5f); // Round to nearest
gain->gain_g = AWB_GAIN_NORM;
gain->gain_b = (uint32_t)(gain_b_float + 0.5f);
ESP_LOGD(TAG, "Calculated AWB gain: R=%lu, G=%lu, B=%lu (patches=%lu, sum_r=%lu, sum_g=%lu, sum_b=%lu)",
gain->gain_r, gain->gain_g, gain->gain_b,
stat_result->white_patch_num, stat_result->sum_r, stat_result->sum_g, stat_result->sum_b);
return true;
}
/**
* @brief White balance processing task
*
* This task receives AWB statistics from the queue, calculates white balance gains,
* accumulates them over multiple periods, and applies the smoothed gain through PI controller
* to prevent oscillation and ensure convergence.
*/
static void s_awb_task(void *pvParameters)
{
isp_awb_stat_result_t stat_result;
isp_wbg_gain_t gain;
uint32_t gain_count = 0;
uint64_t sum_r = 0, sum_g = 0, sum_b = 0;
// Current applied gain (maintained by PI controller)
isp_wbg_gain_t current_gain = {
.gain_r = AWB_GAIN_NORM,
.gain_g = AWB_GAIN_NORM,
.gain_b = AWB_GAIN_NORM,
};
ESP_LOGI(TAG, "White balance task started");
while (1) {
// Wait for statistics from queue
if (xQueueReceive(s_awb_queue, &stat_result, portMAX_DELAY) == pdTRUE) {
// Calculate white balance gain
if (s_calculate_awb_gain(&stat_result, &gain)) {
// Accumulate gain values for averaging
sum_r += gain.gain_r;
sum_g += gain.gain_g;
sum_b += gain.gain_b;
gain_count++;
// When we have enough samples, calculate average and apply with PI controller
if (gain_count >= AWB_GAIN_UPDATE_COUNT) {
// Calculate average target gain
isp_wbg_gain_t target_gain = {};
target_gain.gain_r = (uint32_t)(sum_r / AWB_GAIN_UPDATE_COUNT);
target_gain.gain_g = (uint32_t)(sum_g / AWB_GAIN_UPDATE_COUNT);
target_gain.gain_b = (uint32_t)(sum_b / AWB_GAIN_UPDATE_COUNT);
// Apply PI controller to calculate new gain
isp_wbg_gain_t new_gain = {};
s_pi_controller_update(target_gain, current_gain, &new_gain);
// Apply new gain through WBG module
esp_err_t ret = esp_isp_wbg_set_wb_gain(s_isp_proc, new_gain);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to set AWB gain: %d", ret);
} else {
// Update current gain
current_gain = new_gain;
// Print final updated gain values
float gain_r_norm = (float)new_gain.gain_r / (float)AWB_GAIN_NORM;
float gain_g_norm = (float)new_gain.gain_g / (float)AWB_GAIN_NORM;
float gain_b_norm = (float)new_gain.gain_b / (float)AWB_GAIN_NORM;
ESP_LOGD(TAG, "AWB Gain Updated - R: %lu (%.3f), G: %lu (%.3f), B: %lu (%.3f) [PI controlled]",
new_gain.gain_r, gain_r_norm,
new_gain.gain_g, gain_g_norm,
new_gain.gain_b, gain_b_norm);
}
// Reset for next cycle
gain_count = 0;
sum_r = 0;
sum_g = 0;
sum_b = 0;
}
}
}
}
ESP_LOGI(TAG, "White balance task exiting");
vTaskDelete(NULL);
}
esp_err_t example_isp_awb_init(isp_proc_handle_t isp_proc)
{
if (isp_proc == NULL) {
ESP_LOGE(TAG, "Invalid arguments: isp_proc is NULL");
return ESP_ERR_INVALID_ARG;
}
if (s_awb_ctlr != NULL) {
ESP_LOGW(TAG, "AWB controller already initialized");
return ESP_OK;
}
s_isp_proc = isp_proc;
esp_isp_awb_config_t awb_config = {};
s_get_default_awb_config(&awb_config, CONFIG_EXAMPLE_MIPI_CSI_DISP_HRES, CONFIG_EXAMPLE_MIPI_CSI_DISP_VRES);
// Create AWB controller for statistics
esp_err_t ret = esp_isp_new_awb_controller(isp_proc, &awb_config, &s_awb_ctlr);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to create AWB controller: %d", ret);
return ret;
}
// Configure WBG module
esp_isp_wbg_config_t wbg_config = {};
ret = esp_isp_wbg_configure(isp_proc, &wbg_config);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to configure WBG: %d", ret);
ESP_ERROR_CHECK(esp_isp_del_awb_controller(s_awb_ctlr));
s_awb_ctlr = NULL;
return ret;
}
ESP_LOGI(TAG, "AWB and WBG module initialized");
return ESP_OK;
}
esp_err_t example_isp_awb_start(isp_proc_handle_t isp_proc)
{
esp_err_t ret = ESP_OK;
if (isp_proc == NULL) {
ESP_LOGE(TAG, "Invalid arguments: isp_proc is NULL");
return ESP_ERR_INVALID_ARG;
}
if (s_awb_ctlr == NULL) {
ESP_LOGE(TAG, "AWB not initialized, call example_isp_awb_init() first");
return ESP_ERR_INVALID_STATE;
}
// Create queue for statistics (use internal RAM for ISR compatibility)
// Ignore the results of frames that are not received (if any)
s_awb_queue = xQueueCreateWithCaps(1, sizeof(isp_awb_stat_result_t), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
if (s_awb_queue == NULL) {
ESP_LOGE(TAG, "Failed to create AWB queue");
return ESP_ERR_NO_MEM;
}
// Register AWB statistics callback
esp_isp_awb_cbs_t awb_cbs = {
.on_statistics_done = s_awb_statistics_callback,
};
ESP_GOTO_ON_ERROR(esp_isp_awb_register_event_callbacks(s_awb_ctlr, &awb_cbs, NULL), err, TAG, "Failed to register AWB callbacks");
ESP_GOTO_ON_ERROR(esp_isp_wbg_enable(isp_proc), err, TAG, "Failed to enable WBG");
// Set initial gain (neutral: all channels equal)
isp_wbg_gain_t initial_gain = {
.gain_r = AWB_GAIN_NORM,
.gain_g = AWB_GAIN_NORM,
.gain_b = AWB_GAIN_NORM,
};
ret = esp_isp_wbg_set_wb_gain(isp_proc, initial_gain);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Failed to set initial AWB gain: %d", ret);
}
// Enable AWB controller
ESP_GOTO_ON_ERROR(esp_isp_awb_controller_enable(s_awb_ctlr), err, TAG, "Failed to enable AWB controller");
// Start continuous statistics
ESP_GOTO_ON_ERROR(esp_isp_awb_controller_start_continuous_statistics(s_awb_ctlr), err, TAG, "Failed to start continuous statistics");
// Create white balance processing task
ESP_GOTO_ON_FALSE(pdPASS == xTaskCreate(s_awb_task, "awb_task", 4096, NULL, 5, &s_awb_task_handle), ESP_FAIL, err, TAG, "Failed to create AWB task");
ESP_LOGI(TAG, "AWB enabled: AWB statistics started, WBG module enabled, processing task created");
return ESP_OK;
err:
ESP_LOGE(TAG, "AWB start failed: %s (0x%x)", esp_err_to_name(ret), ret);
esp_isp_awb_controller_stop_continuous_statistics(s_awb_ctlr);
esp_isp_awb_controller_disable(s_awb_ctlr);
esp_isp_wbg_disable(isp_proc);
if (s_awb_queue != NULL) {
vQueueDeleteWithCaps(s_awb_queue);
s_awb_queue = NULL;
}
if (s_awb_task_handle != NULL) {
vTaskDelete(s_awb_task_handle);
s_awb_task_handle = NULL;
}
return ret;
}

View File

@@ -0,0 +1,68 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @file example_awb.h
* @brief Auto White Balance (AWB) Functionality
*
* This module implements ISP white balance functionality using hardware AWB controller
* for statistics and WBG module for gain adjustment.
*
* Please note that there are two methods to achieve auto white balance tuning:
* 1. use AWB module to get white balance statistics + tune camera sensor gain registers
* 2. use AWB module to get white balance statistics + tune the output gain of ISP pipeline by WBG module
*
* This example uses method 2 to achieve auto white balance tuning.
*
* How it works:
* - ISP hardware AWB controller detects white patches in the configured window
* - White patches are identified based on luminance and color ratio thresholds
* - AWB controller provides statistics (sum_r, sum_g, sum_b) through continuous mode
* - Statistics are sent to a FreeRTOS task via queue
* - The task calculates white balance gains based on statistics
* - Gains are applied through the WBG (White Balance Gain) module
*
* @note Please note that `AWB` and `WBG` refers to 2 different modules in the ISP pipeline, while this example uses both of them to achieve auto white balance tuning.
*/
#pragma once
#include "esp_err.h"
#include "driver/isp.h"
#include "driver/isp_awb.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Initialize White Balance module
*
* This function creates and configures the AWB controller for statistics and WBG module
* for gain adjustment. It uses default configuration for AWB controller and WBG module.
*
* @param[in] isp_proc ISP processor handle
* @return
* - ESP_OK: Success
* - ESP_ERR_INVALID_ARG: Invalid arguments
* - ESP_ERR_NO_MEM: Out of memory
*/
esp_err_t example_isp_awb_init(isp_proc_handle_t isp_proc);
/**
* @brief Enable White Balance module
*
* @param[in] isp_proc ISP processor handle
* @return
* - ESP_OK: Success
* - ESP_ERR_INVALID_ARG: Invalid arguments
* - ESP_ERR_INVALID_STATE: Invalid state
*/
esp_err_t example_isp_awb_start(isp_proc_handle_t isp_proc);
#ifdef __cplusplus
}
#endif

View File

@@ -26,6 +26,7 @@
// Include modular ISP components
#include "example_buffer.h"
#include "example_af.h"
#include "example_awb.h"
#include "example_pipelines.h"
#ifdef CONFIG_EXAMPLE_ISP_CROP_ENABLE
#include "example_crop.h"
@@ -222,8 +223,23 @@ void app_main(void)
return;
}
#if CONFIG_ESP32P4_REV_MIN_FULL >= 300
//---------------White Balance Init------------------//
ret = example_isp_awb_init(isp_proc);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "AWB init fail[%d]", ret);
return;
}
ret = example_isp_awb_start(isp_proc);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "AWB enable fail[%d]", ret);
return;
}
#endif /* CONFIG_ESP32P4_REV_MIN_FULL >= 300 */
//---------------AF Init and Start------------------//
ret = example_isp_af_init(isp_proc, dw9714_io_handle, NULL);
ret = example_isp_af_init(isp_proc, dw9714_io_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "AF init fail[%d]", ret);
return;