mirror of
https://github.com/arendst/Tasmota.git
synced 2026-07-28 04:15:53 +00:00
Webcam CSI: allow more complex ISP setups with config files (#24531)
Co-authored-by: Christian Baars <christianbaars@MacStudio-von-Christian.local>
This commit is contained in:
co-authored by
Christian Baars
parent
b57ae100ba
commit
b9beaf6fbd
@@ -1,34 +0,0 @@
|
|||||||
/********************************************************************
|
|
||||||
* Tasmota lib for the ISP of the ESP32-P4 (and maybe successors)
|
|
||||||
*
|
|
||||||
* To use: import ISP`
|
|
||||||
*******************************************************************/
|
|
||||||
#include "be_constobj.h"
|
|
||||||
#include "be_mapping.h"
|
|
||||||
|
|
||||||
#if !defined(USE_BERRY_ISP)
|
|
||||||
|
|
||||||
#include "be_constobj.h"
|
|
||||||
#include "be_mapping.h"
|
|
||||||
|
|
||||||
extern int be_ISP_init(bvm *vm);
|
|
||||||
extern int be_ISP_set_ccm(bvm *vm);
|
|
||||||
extern int be_ISP_set_gamma(bvm *vm);
|
|
||||||
extern int be_ISP_set_sharpen(bvm *vm);
|
|
||||||
extern int be_ISP_set_color(bvm *vm);
|
|
||||||
extern int be_ISP_set_bf(bvm *vm);
|
|
||||||
|
|
||||||
/* @const_object_info_begin
|
|
||||||
module ISP (scope: global) {
|
|
||||||
init, func(be_ISP_init)
|
|
||||||
set_ccm, func(be_ISP_set_ccm)
|
|
||||||
set_gamma, func(be_ISP_set_gamma)
|
|
||||||
set_sharpen, func(be_ISP_set_sharpen)
|
|
||||||
set_color, func(be_ISP_set_color)
|
|
||||||
set_bf, func(be_ISP_set_bf)
|
|
||||||
}
|
|
||||||
@const_object_info_end */
|
|
||||||
|
|
||||||
#include "be_fixed_ISP.h"
|
|
||||||
|
|
||||||
#endif // USE_BERRY_ULP
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"IMX219":{"awb":{"model":0,"min_red_gain_step":0.034,"min_blue_gain_step":0.034,"min_counted":1000,"range":{"green":{"min":128,"max":240},"rg":{"min":0.7,"max":1.3},"bg":{"min":0.7,"max":1.3}},"green_luma_env":"dummy_awb_luma","green_luma_init":140,"green_luma_step_ratio":0.3},"acc":{"ccm":{"low_luma":{"luma_env":"ae.luma.avg","threshold":28,"matrix":[1,0,0,0,1,0,0,0,1]},"table":[{"color_temp":6500,"matrix":[1.9049,-0.5453,-0.3596,-0.2035,1.4471,-0.2436,-0.0028,-0.4219,1.4247]},{"color_temp":2800,"matrix":[1.485,-0.273,-0.212,-0.19,1.337,-0.147,0.127,-0.785,1.658]}]}},"adn":{"bf":[{"gain":1,"param":{"level":2,"matrix":[1,2,1,2,4,2,1,2,1]}},{"gain":8,"param":{"level":4,"matrix":[1,2,1,2,4,2,1,2,1]}},{"gain":32,"param":{"level":6,"matrix":[1,1,1,1,2,1,1,1,1]}}]},"aen":{"gamma":{"use_gamma_param":false,"luma_env":"dummy_gamma_luma","luma_min_step":5,"table":[{"luma":20,"y":[0,66,90,109,125,138,150,161,171,181,190,198,207,215,222,230]},{"luma":50,"y":[0,72,99,119,136,151,164,175,186,197,206,216,224,233,241,248]},{"luma":100,"y":[0,80,113,138,160,179,196,211,226,239,248,253,255,255,255,255]}]},"sharpen":[{"gain":1,"param":{"h_thresh":10,"l_thresh":2,"h_coeff":2.8,"m_coeff":3,"matrix":[1,2,1,2,4,2,1,2,1]}},{"gain":2,"param":{"h_thresh":12,"l_thresh":3,"h_coeff":2.2,"m_coeff":2.5,"matrix":[1,2,1,2,4,2,1,2,1]}},{"gain":4,"param":{"h_thresh":16,"l_thresh":5,"h_coeff":1.5,"m_coeff":1.8,"matrix":[1,2,1,2,4,2,1,2,1]}},{"gain":8,"param":{"h_thresh":24,"l_thresh":7,"h_coeff":0.8,"m_coeff":1.1,"matrix":[1,1,1,1,2,1,1,1,1]}}],"contrast":[{"gain":1,"value":128},{"gain":4,"value":120},{"gain":8,"value":112}]},"ext":{"hue":0,"brightness":0}}}
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
/*
|
|
||||||
xdrv_52_3_berry_isp.ino - Berry scripting language, native functions
|
|
||||||
|
|
||||||
Copyright (C) 2025 Christian Baars & Stephan Hadinger, Berry language by Guan Wenliang https://github.com/Skiars/berry
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
xdrv_52_3_berry_isp.ino - Native Berry ISP Implementation for ESP-IDF 5.5+
|
|
||||||
FIXED:
|
|
||||||
- Replaced designated initializers with direct assignment to fix ordering errors.
|
|
||||||
- Corrected Gamma point assignment (setting .y and .x members).
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifdef USE_BERRY
|
|
||||||
#include <berry.h>
|
|
||||||
|
|
||||||
#ifdef USE_CSI_WEBCAM
|
|
||||||
// Global ISP Processor Handle (Must be shared with xdrv_81)
|
|
||||||
#include "driver/isp.h"
|
|
||||||
isp_proc_handle_t tasmota_wc_isp_handle = NULL; // we do not have stable IDF 5.5 yet, but we need this symbol
|
|
||||||
|
|
||||||
#ifdef PLACEHOLDER_UNTIL_IDF55_ISMATURED //USE_CSI_WEBCAM
|
|
||||||
|
|
||||||
#include "driver/isp.h"
|
|
||||||
#include "driver/isp_bf.h"
|
|
||||||
#include "driver/isp_sharpen.h"
|
|
||||||
#include "driver/isp_color.h"
|
|
||||||
#include "driver/isp_ccm.h"
|
|
||||||
#include "driver/isp_gamma.h"
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
|
|
||||||
// Helper: Parse Berry list to flat float array
|
|
||||||
static int ParseFloatList(bvm *vm, int index, float* out, int max_len) {
|
|
||||||
if (!be_islist(vm, index)) return 0;
|
|
||||||
int count = be_data_size(vm, index);
|
|
||||||
if (count > max_len) count = max_len;
|
|
||||||
be_pushvalue(vm, index);
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
be_pushint(vm, i);
|
|
||||||
be_getindex(vm, -2);
|
|
||||||
out[i] = be_isnumber(vm, -1) ? be_toreal(vm, -1) : 0.0f;
|
|
||||||
be_pop(vm, 1);
|
|
||||||
}
|
|
||||||
be_pop(vm, 1);
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ISP.init(width, height)
|
|
||||||
int be_ISP_init(bvm *vm) {
|
|
||||||
int width = be_toint(vm, 1);
|
|
||||||
int height = be_toint(vm, 2);
|
|
||||||
|
|
||||||
if (tasmota_wc_isp_handle) {
|
|
||||||
be_pushbool(vm, true);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXED: Use memset + direct assignment to bypass C++ designated initializer ordering issues
|
|
||||||
esp_isp_processor_cfg_t isp_config;
|
|
||||||
memset(&isp_config, 0, sizeof(isp_config));
|
|
||||||
|
|
||||||
isp_config.clk_hz = 80 * 1000 * 1000;
|
|
||||||
isp_config.clk_src = ISP_CLK_SRC_DEFAULT;
|
|
||||||
isp_config.input_data_source = ISP_INPUT_DATA_SOURCE_CSI;
|
|
||||||
isp_config.input_data_color_type = ISP_COLOR_RAW8;
|
|
||||||
isp_config.output_data_color_type = ISP_COLOR_YUV422;
|
|
||||||
isp_config.h_res = (uint32_t)width;
|
|
||||||
isp_config.v_res = (uint32_t)height;
|
|
||||||
isp_config.has_line_start_packet = false;
|
|
||||||
isp_config.has_line_end_packet = false;
|
|
||||||
|
|
||||||
esp_err_t ret = esp_isp_new_processor(&isp_config, &tasmota_wc_isp_handle);
|
|
||||||
if (ret == ESP_OK) {
|
|
||||||
esp_isp_enable(tasmota_wc_isp_handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
be_pushbool(vm, (ret == ESP_OK));
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ISP.set_ccm(matrix_list)
|
|
||||||
int be_ISP_set_ccm(bvm *vm) {
|
|
||||||
if (!tasmota_wc_isp_handle) return 0;
|
|
||||||
|
|
||||||
esp_isp_ccm_config_t cfg = {0};
|
|
||||||
float temp_matrix[9];
|
|
||||||
ParseFloatList(vm, 1, temp_matrix, 9);
|
|
||||||
|
|
||||||
int idx = 0;
|
|
||||||
for(int r=0; r<3; r++) {
|
|
||||||
for(int c=0; c<3; c++) {
|
|
||||||
cfg.matrix[r][c] = temp_matrix[idx++];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_isp_ccm_configure(tasmota_wc_isp_handle, &cfg);
|
|
||||||
esp_isp_ccm_enable(tasmota_wc_isp_handle);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ISP.set_gamma(curve_points_list)
|
|
||||||
// curve_points_list: List of 16 Y-values (Output values)
|
|
||||||
int be_ISP_set_gamma(bvm *vm) {
|
|
||||||
if (!tasmota_wc_isp_handle) return 0;
|
|
||||||
|
|
||||||
float temp_points[16];
|
|
||||||
ParseFloatList(vm, 1, temp_points, 16);
|
|
||||||
|
|
||||||
isp_gamma_curve_points_t pts;
|
|
||||||
memset(&pts, 0, sizeof(pts));
|
|
||||||
|
|
||||||
// P4 Gamma Curve has 16 points.
|
|
||||||
// We map the 16 input values to Y, and generate linear X steps.
|
|
||||||
// Assuming 8-bit input (0-255), step is 16.
|
|
||||||
for(int i=0; i<16; i++) {
|
|
||||||
pts.pt[i].x = i * 16; // Auto-generate Input X (0, 16, 32...)
|
|
||||||
pts.pt[i].y = (uint32_t)temp_points[i]; // FIXED: Assign to .y member
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_isp_gamma_configure(tasmota_wc_isp_handle, COLOR_COMPONENT_R, &pts);
|
|
||||||
esp_isp_gamma_configure(tasmota_wc_isp_handle, COLOR_COMPONENT_G, &pts);
|
|
||||||
esp_isp_gamma_configure(tasmota_wc_isp_handle, COLOR_COMPONENT_B, &pts);
|
|
||||||
esp_isp_gamma_enable(tasmota_wc_isp_handle);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ISP.set_sharpen(h_thresh, l_thresh, h_coeff, m_coeff, template_list)
|
|
||||||
int be_ISP_set_sharpen(bvm *vm) {
|
|
||||||
if (!tasmota_wc_isp_handle) return 0;
|
|
||||||
|
|
||||||
esp_isp_sharpen_config_t cfg;
|
|
||||||
memset(&cfg, 0, sizeof(cfg));
|
|
||||||
|
|
||||||
cfg.h_thresh = be_toint(vm, 1);
|
|
||||||
cfg.l_thresh = be_toint(vm, 2);
|
|
||||||
|
|
||||||
float h_val = be_toreal(vm, 3);
|
|
||||||
cfg.h_freq_coeff.integer = (uint8_t)h_val;
|
|
||||||
cfg.h_freq_coeff.decimal = (uint8_t)((h_val - (int)h_val) * 256);
|
|
||||||
|
|
||||||
float m_val = be_toreal(vm, 4);
|
|
||||||
cfg.m_freq_coeff.integer = (uint8_t)m_val;
|
|
||||||
cfg.m_freq_coeff.decimal = (uint8_t)((m_val - (int)m_val) * 256);
|
|
||||||
|
|
||||||
float temp[9];
|
|
||||||
ParseFloatList(vm, 5, temp, 9);
|
|
||||||
int idx = 0;
|
|
||||||
for(int r=0; r<3; r++) {
|
|
||||||
for(int c=0; c<3; c++) {
|
|
||||||
cfg.sharpen_template[r][c] = (uint8_t)temp[idx++];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.padding_mode = ISP_SHARPEN_EDGE_PADDING_MODE_SRND_DATA;
|
|
||||||
|
|
||||||
esp_isp_sharpen_configure(tasmota_wc_isp_handle, &cfg);
|
|
||||||
esp_isp_sharpen_enable(tasmota_wc_isp_handle);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ISP.set_color(contrast, saturation, hue, brightness)
|
|
||||||
int be_ISP_set_color(bvm *vm) {
|
|
||||||
if (!tasmota_wc_isp_handle) return 0;
|
|
||||||
|
|
||||||
esp_isp_color_config_t cfg;
|
|
||||||
memset(&cfg, 0, sizeof(cfg));
|
|
||||||
|
|
||||||
float contrast = be_toreal(vm, 1);
|
|
||||||
cfg.color_contrast.integer = (uint8_t)contrast;
|
|
||||||
cfg.color_contrast.decimal = (uint8_t)((contrast - (int)contrast) * 256);
|
|
||||||
|
|
||||||
float saturation = be_toreal(vm, 2);
|
|
||||||
cfg.color_saturation.integer = (uint8_t)saturation;
|
|
||||||
cfg.color_saturation.decimal = (uint8_t)((saturation - (int)saturation) * 256);
|
|
||||||
|
|
||||||
cfg.color_hue = (be_top(vm) >= 3) ? be_toint(vm, 3) : 0;
|
|
||||||
cfg.color_brightness = (be_top(vm) >= 4) ? be_toint(vm, 4) : 0;
|
|
||||||
|
|
||||||
esp_isp_color_configure(tasmota_wc_isp_handle, &cfg);
|
|
||||||
esp_isp_color_enable(tasmota_wc_isp_handle);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ISP.set_bf(denoising_level, template_list)
|
|
||||||
int be_ISP_set_bf(bvm *vm) {
|
|
||||||
if (!tasmota_wc_isp_handle) return 0;
|
|
||||||
|
|
||||||
esp_isp_bf_config_t cfg;
|
|
||||||
memset(&cfg, 0, sizeof(cfg));
|
|
||||||
|
|
||||||
cfg.denoising_level = be_toint(vm, 1);
|
|
||||||
|
|
||||||
if (be_top(vm) >= 2 && be_islist(vm, 2)) {
|
|
||||||
float temp[9];
|
|
||||||
ParseFloatList(vm, 2, temp, 9);
|
|
||||||
int idx = 0;
|
|
||||||
for(int r=0; r<3; r++) {
|
|
||||||
for(int c=0; c<3; c++) {
|
|
||||||
cfg.bf_template[r][c] = (uint8_t)temp[idx++];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.padding_mode = ISP_BF_EDGE_PADDING_MODE_SRND_DATA;
|
|
||||||
|
|
||||||
esp_isp_bf_configure(tasmota_wc_isp_handle, &cfg);
|
|
||||||
esp_isp_bf_enable(tasmota_wc_isp_handle);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // extern C
|
|
||||||
|
|
||||||
#endif //PLACEHOLDER_UNTIL_IDF55_ISMATURED
|
|
||||||
#endif // USE_CSI_WEBCAM
|
|
||||||
#endif // USE_BERRY
|
|
||||||
@@ -137,6 +137,7 @@ struct {
|
|||||||
SemaphoreHandle_t resume_sem;
|
SemaphoreHandle_t resume_sem;
|
||||||
camera_fail_reason_t fail_reason;
|
camera_fail_reason_t fail_reason;
|
||||||
esp_err_t fail_esp_err;
|
esp_err_t fail_esp_err;
|
||||||
|
uint32_t isp_gamma_y[16];
|
||||||
} core;
|
} core;
|
||||||
|
|
||||||
// --- 2. JPEG Session (POD) ---
|
// --- 2. JPEG Session (POD) ---
|
||||||
@@ -236,6 +237,10 @@ struct {
|
|||||||
uint32_t compression_ratio_x100; // Compression ratio * 100 (e.g., 1500 = 15.00x)
|
uint32_t compression_ratio_x100; // Compression ratio * 100 (e.g., 1500 = 15.00x)
|
||||||
} WcStats;
|
} WcStats;
|
||||||
|
|
||||||
|
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
|
||||||
|
bool WcIspApplyConfig(isp_proc_handle_t handle, const char* sensor_name, int width, int height);
|
||||||
|
#endif
|
||||||
|
|
||||||
#define BOUNDARY "e8b8c539-047d-4777-a985-fbba6edff11e"
|
#define BOUNDARY "e8b8c539-047d-4777-a985-fbba6edff11e"
|
||||||
|
|
||||||
// Command definitions
|
// Command definitions
|
||||||
@@ -266,11 +271,14 @@ static volatile uint32_t cb_finished_count = 0;
|
|||||||
void WcSetFailed(camera_fail_reason_t reason, esp_err_t esp_err = ESP_OK) {
|
void WcSetFailed(camera_fail_reason_t reason, esp_err_t esp_err = ESP_OK) {
|
||||||
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: Entering FAILED state, reason=%d, esp_err=0x%x"), reason, esp_err);
|
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: Entering FAILED state, reason=%d, esp_err=0x%x"), reason, esp_err);
|
||||||
|
|
||||||
// 1. Stop task if running
|
// 1. Stop sensor streaming (harmless if not yet started)
|
||||||
|
callBerryEventDispatcher(PSTR("camera"), PSTR("stream"), 0, nullptr, 0);
|
||||||
|
|
||||||
|
// 2. Gate ISR and wake/join processing task
|
||||||
if (Wc.core.cam_task_handle) {
|
if (Wc.core.cam_task_handle) {
|
||||||
Wc.core.state = CAM_STOPPING;
|
Wc.core.state = CAM_STOPPING;
|
||||||
if (Wc.core.resume_sem) xSemaphoreGive(Wc.core.resume_sem);
|
if (Wc.core.resume_sem) xSemaphoreGive(Wc.core.resume_sem);
|
||||||
if (Wc.core.frame_mutex) xSemaphoreGive(Wc.core.frame_mutex);
|
// NOTE: no xSemaphoreGive(frame_mutex) — mutex, not a signal
|
||||||
xTaskNotifyGive(Wc.core.cam_task_handle);
|
xTaskNotifyGive(Wc.core.cam_task_handle);
|
||||||
|
|
||||||
for (int i = 0; i < 50 && Wc.core.cam_task_handle != NULL; i++) {
|
for (int i = 0; i < 50 && Wc.core.cam_task_handle != NULL; i++) {
|
||||||
@@ -281,11 +289,10 @@ void WcSetFailed(camera_fail_reason_t reason, esp_err_t esp_err = ESP_OK) {
|
|||||||
vTaskDelete(Wc.core.cam_task_handle);
|
vTaskDelete(Wc.core.cam_task_handle);
|
||||||
Wc.core.cam_task_handle = NULL;
|
Wc.core.cam_task_handle = NULL;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Wc.core.state = CAM_STOPPING;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Stop sensor streaming
|
|
||||||
callBerryEventDispatcher(PSTR("camera"), PSTR("stream"), 0, nullptr, 0);
|
|
||||||
|
|
||||||
// 3. Deinitialize hardware pipeline
|
// 3. Deinitialize hardware pipeline
|
||||||
WcDeinitPipeline();
|
WcDeinitPipeline();
|
||||||
|
|
||||||
@@ -322,6 +329,7 @@ void WcSetFailed(camera_fail_reason_t reason, esp_err_t esp_err = ESP_OK) {
|
|||||||
|
|
||||||
// Callback: Provide new buffer for next frame (Ping-Pong Logic)
|
// Callback: Provide new buffer for next frame (Ping-Pong Logic)
|
||||||
static bool IRAM_ATTR csi_on_get_new_vb(esp_cam_ctlr_handle_t handle, esp_cam_ctlr_trans_t *trans, void *user_data) {
|
static bool IRAM_ATTR csi_on_get_new_vb(esp_cam_ctlr_handle_t handle, esp_cam_ctlr_trans_t *trans, void *user_data) {
|
||||||
|
if (Wc.core.state != CAM_STREAMING) return false; // ← ADDED
|
||||||
cb_get_new_count++;
|
cb_get_new_count++;
|
||||||
|
|
||||||
// Switch to the OTHER buffer for the next write
|
// Switch to the OTHER buffer for the next write
|
||||||
@@ -350,7 +358,9 @@ static bool IRAM_ATTR csi_on_trans_finished(esp_cam_ctlr_handle_t handle, esp_ca
|
|||||||
|
|
||||||
// Wake up the processing task immediately
|
// Wake up the processing task immediately
|
||||||
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
|
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
|
||||||
vTaskNotifyGiveFromISR(Wc.core.cam_task_handle, &xHigherPriorityTaskWoken);
|
TaskHandle_t task = Wc.core.cam_task_handle; // safe capture
|
||||||
|
if (!task) return false;
|
||||||
|
vTaskNotifyGiveFromISR(task, &xHigherPriorityTaskWoken);
|
||||||
|
|
||||||
return xHigherPriorityTaskWoken == pdTRUE;
|
return xHigherPriorityTaskWoken == pdTRUE;
|
||||||
}
|
}
|
||||||
@@ -367,7 +377,6 @@ uint32_t WcInitPipeline() {
|
|||||||
Wc.core.frame_buffer[i] = (uint8_t*)heap_caps_aligned_calloc(64, 1, Wc.core.frame_buffer_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
Wc.core.frame_buffer[i] = (uint8_t*)heap_caps_aligned_calloc(64, 1, Wc.core.frame_buffer_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||||
if (!Wc.core.frame_buffer[i]) {
|
if (!Wc.core.frame_buffer[i]) {
|
||||||
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: Frame buffer %d allocation failed"), i);
|
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: Frame buffer %d allocation failed"), i);
|
||||||
WcDeinitPipeline();
|
|
||||||
WcSetFailed(CAM_FAIL_MEMORY);
|
WcSetFailed(CAM_FAIL_MEMORY);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -393,7 +402,6 @@ uint32_t WcInitPipeline() {
|
|||||||
ret = esp_cam_new_csi_ctlr(&csi_config, &Wc.core.cam_handle);
|
ret = esp_cam_new_csi_ctlr(&csi_config, &Wc.core.cam_handle);
|
||||||
if (ret != ESP_OK) {
|
if (ret != ESP_OK) {
|
||||||
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: CSI init failed (0x%x)"), ret);
|
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: CSI init failed (0x%x)"), ret);
|
||||||
WcDeinitPipeline();
|
|
||||||
WcSetFailed(CAM_FAIL_CSI_INIT, ret);
|
WcSetFailed(CAM_FAIL_CSI_INIT, ret);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -405,23 +413,18 @@ uint32_t WcInitPipeline() {
|
|||||||
ret = esp_cam_ctlr_register_event_callbacks(Wc.core.cam_handle, &cbs, &Wc.core.cam_trans);
|
ret = esp_cam_ctlr_register_event_callbacks(Wc.core.cam_handle, &cbs, &Wc.core.cam_trans);
|
||||||
if (ret != ESP_OK) {
|
if (ret != ESP_OK) {
|
||||||
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: Callback registration failed (0x%x)"), ret);
|
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: Callback registration failed (0x%x)"), ret);
|
||||||
WcDeinitPipeline();
|
|
||||||
WcSetFailed(CAM_FAIL_CSI_INIT, ret);
|
WcSetFailed(CAM_FAIL_CSI_INIT, ret);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
ret = esp_cam_ctlr_enable(Wc.core.cam_handle);
|
ret = esp_cam_ctlr_enable(Wc.core.cam_handle);
|
||||||
if (ret != ESP_OK) {
|
if (ret != ESP_OK) {
|
||||||
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: CSI enable failed (0x%x)"), ret);
|
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: CSI enable failed (0x%x)"), ret);
|
||||||
WcDeinitPipeline();
|
|
||||||
WcSetFailed(CAM_FAIL_CSI_INIT, ret);
|
WcSetFailed(CAM_FAIL_CSI_INIT, ret);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. ISP
|
// 4. ISP — always created here with correct output format for session type
|
||||||
if (tasmota_wc_isp_handle) {
|
{
|
||||||
Wc.core.isp_handle = tasmota_wc_isp_handle;
|
|
||||||
esp_isp_enable(Wc.core.isp_handle);
|
|
||||||
} else {
|
|
||||||
isp_color_t isp_output_format = (Wc.core.session_type == SESSION_RTSP_AND_WS || Wc.core.session_type == SESSION_WEBRTC) ? ISP_COLOR_YUV420 : ISP_COLOR_YUV422;
|
isp_color_t isp_output_format = (Wc.core.session_type == SESSION_RTSP_AND_WS || Wc.core.session_type == SESSION_WEBRTC) ? ISP_COLOR_YUV420 : ISP_COLOR_YUV422;
|
||||||
esp_isp_processor_cfg_t isp_config = {
|
esp_isp_processor_cfg_t isp_config = {
|
||||||
.clk_hz = 120 * 1000 * 1000,
|
.clk_hz = 120 * 1000 * 1000,
|
||||||
@@ -434,23 +437,25 @@ uint32_t WcInitPipeline() {
|
|||||||
ret = esp_isp_new_processor(&isp_config, &Wc.core.isp_handle);
|
ret = esp_isp_new_processor(&isp_config, &Wc.core.isp_handle);
|
||||||
if (ret != ESP_OK) {
|
if (ret != ESP_OK) {
|
||||||
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: ISP init failed (0x%x)"), ret);
|
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: ISP init failed (0x%x)"), ret);
|
||||||
WcDeinitPipeline();
|
|
||||||
WcSetFailed(CAM_FAIL_ISP_INIT, ret);
|
WcSetFailed(CAM_FAIL_ISP_INIT, ret);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_isp_enable(Wc.core.isp_handle);
|
esp_isp_enable(Wc.core.isp_handle);
|
||||||
|
|
||||||
|
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
|
||||||
|
WcIspApplyConfig(Wc.core.isp_handle, Wc.core.config.name, Wc.core.config.width, Wc.core.config.height);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Encoders
|
// 5. Encoders
|
||||||
if (Wc.core.session_type == SESSION_MJPEG_HTTP) {
|
if (Wc.core.session_type == SESSION_MJPEG_HTTP) {
|
||||||
if (!WcSetupJpegEncoder()) {
|
if (!WcSetupJpegEncoder()) {
|
||||||
WcDeinitPipeline();
|
|
||||||
WcSetFailed(CAM_FAIL_ENCODER_INIT);
|
WcSetFailed(CAM_FAIL_ENCODER_INIT);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
} else if (Wc.core.session_type == SESSION_RTSP_AND_WS || Wc.core.session_type == SESSION_WEBRTC) {
|
} else if (Wc.core.session_type == SESSION_RTSP_AND_WS || Wc.core.session_type == SESSION_WEBRTC) {
|
||||||
if (!WcSetupH264Encoder()) {
|
if (!WcSetupH264Encoder()) {
|
||||||
WcDeinitPipeline();
|
|
||||||
WcSetFailed(CAM_FAIL_ENCODER_INIT);
|
WcSetFailed(CAM_FAIL_ENCODER_INIT);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -462,6 +467,9 @@ uint32_t WcInitPipeline() {
|
|||||||
|
|
||||||
// De-initialize only the resolution-dependent hardware
|
// De-initialize only the resolution-dependent hardware
|
||||||
void WcDeinitPipeline() {
|
void WcDeinitPipeline() {
|
||||||
|
// 0. AWB must go before ISP processor
|
||||||
|
WcIspDeinitAWB();
|
||||||
|
|
||||||
// 1. Delete Encoder
|
// 1. Delete Encoder
|
||||||
if (Wc.h264.handle) {
|
if (Wc.h264.handle) {
|
||||||
esp_h264_enc_del(Wc.h264.handle); // Only if using helper that doesn't double-free
|
esp_h264_enc_del(Wc.h264.handle); // Only if using helper that doesn't double-free
|
||||||
@@ -483,12 +491,10 @@ void WcDeinitPipeline() {
|
|||||||
Wc.core.cam_handle = NULL;
|
Wc.core.cam_handle = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Delete ISP (Only if we own it)
|
// 3. Delete ISP
|
||||||
if (Wc.core.isp_handle) {
|
if (Wc.core.isp_handle) {
|
||||||
esp_isp_disable(Wc.core.isp_handle);
|
esp_isp_disable(Wc.core.isp_handle);
|
||||||
if (!tasmota_wc_isp_handle) {
|
esp_isp_del_processor(Wc.core.isp_handle);
|
||||||
esp_isp_del_processor(Wc.core.isp_handle);
|
|
||||||
}
|
|
||||||
Wc.core.isp_handle = NULL;
|
Wc.core.isp_handle = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,8 +591,8 @@ uint32_t WcSetup(bool reset_config) {
|
|||||||
AddLog(LOG_LEVEL_INFO, PSTR("CAM: Mutexes created"));
|
AddLog(LOG_LEVEL_INFO, PSTR("CAM: Mutexes created"));
|
||||||
|
|
||||||
// 5. Create processing task based on session type
|
// 5. Create processing task based on session type
|
||||||
TaskFunction_t task_func;
|
TaskFunction_t task_func = nullptr; // ← was uninitialized
|
||||||
const char *task_name;
|
const char *task_name = nullptr; // ← was uninitialized
|
||||||
void WebRTCProcessingTask(void *pvParameters);
|
void WebRTCProcessingTask(void *pvParameters);
|
||||||
|
|
||||||
if (Wc.core.session_type == SESSION_MJPEG_HTTP) {
|
if (Wc.core.session_type == SESSION_MJPEG_HTTP) {
|
||||||
@@ -604,6 +610,17 @@ uint32_t WcSetup(bool reset_config) {
|
|||||||
task_name = "WebRTCTask";
|
task_name = "WebRTCTask";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ← ADDED: guard against unknown/unhandled session type
|
||||||
|
if (!task_func) {
|
||||||
|
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: Unknown session type %d, no task function"), Wc.core.session_type);
|
||||||
|
WcDeinitPipeline();
|
||||||
|
vSemaphoreDelete(Wc.core.frame_mutex);
|
||||||
|
vSemaphoreDelete(Wc.jpeg.mutex);
|
||||||
|
vSemaphoreDelete(Wc.core.resume_sem);
|
||||||
|
WcSetFailed(CAM_FAIL_TASK);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
BaseType_t task_created = xTaskCreatePinnedToCore(
|
BaseType_t task_created = xTaskCreatePinnedToCore(
|
||||||
task_func,
|
task_func,
|
||||||
task_name,
|
task_name,
|
||||||
@@ -694,20 +711,24 @@ uint32_t WcStop(void) {
|
|||||||
|
|
||||||
AddLog(LOG_LEVEL_INFO, PSTR("CAM: Stopping (state=%d)"), Wc.core.state);
|
AddLog(LOG_LEVEL_INFO, PSTR("CAM: Stopping (state=%d)"), Wc.core.state);
|
||||||
|
|
||||||
// 1. Set state to STOPPING - task will exit on next iteration
|
// 1. Stop sensor streaming (MIPI stops; harmless if already idle)
|
||||||
|
callBerryEventDispatcher(PSTR("camera"), PSTR("stream"), 0, nullptr, 0);
|
||||||
|
AddLog(LOG_LEVEL_DEBUG, PSTR("CAM: Called Berry stream stop"));
|
||||||
|
|
||||||
|
// 2. Gate ISR immediately
|
||||||
Wc.core.state = CAM_STOPPING;
|
Wc.core.state = CAM_STOPPING;
|
||||||
|
|
||||||
// 2. If task was paused, release it so it can see STOPPING and exit
|
// 3. Wake paused task so it can see STOPPING and exit
|
||||||
if (Wc.core.resume_sem) xSemaphoreGive(Wc.core.resume_sem);
|
if (Wc.core.resume_sem) xSemaphoreGive(Wc.core.resume_sem);
|
||||||
if (Wc.core.frame_mutex) xSemaphoreGive(Wc.core.frame_mutex);
|
// NOTE: no xSemaphoreGive(frame_mutex) — mutex not a signal; task unblocked by notify below
|
||||||
|
|
||||||
// 3. Wake task and wait for it to exit
|
// 4. Wake task and wait for it to exit
|
||||||
if (Wc.core.cam_task_handle) {
|
if (Wc.core.cam_task_handle) {
|
||||||
xTaskNotifyGive(Wc.core.cam_task_handle);
|
xTaskNotifyGive(Wc.core.cam_task_handle);
|
||||||
|
|
||||||
// Wait for task to exit (task sets handle to NULL before deleting itself)
|
// Wait for task to exit (task sets handle to NULL before deleting itself)
|
||||||
for (int i = 0; i < 50 && Wc.core.cam_task_handle != NULL; i++) {
|
for (int i = 0; i < 50 && Wc.core.cam_task_handle != NULL; i++) {
|
||||||
delay(10);
|
delay(25);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force delete if task didn't exit
|
// Force delete if task didn't exit
|
||||||
@@ -718,10 +739,6 @@ uint32_t WcStop(void) {
|
|||||||
}
|
}
|
||||||
AddLog(LOG_LEVEL_DEBUG, PSTR("CAM: Task stopped"));
|
AddLog(LOG_LEVEL_DEBUG, PSTR("CAM: Task stopped"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Stop sensor streaming via Berry
|
|
||||||
callBerryEventDispatcher(PSTR("camera"), PSTR("stream"), 0, nullptr, 0);
|
|
||||||
AddLog(LOG_LEVEL_DEBUG, PSTR("CAM: Called Berry stream stop"));
|
|
||||||
|
|
||||||
// 5. Deinitialize hardware pipeline (Buffers, CSI, ISP, Encoders)
|
// 5. Deinitialize hardware pipeline (Buffers, CSI, ISP, Encoders)
|
||||||
WcDeinitPipeline();
|
WcDeinitPipeline();
|
||||||
@@ -735,10 +752,10 @@ uint32_t WcStop(void) {
|
|||||||
// 7. Stop RTSP server and close client
|
// 7. Stop RTSP server and close client
|
||||||
WcRtspStop();
|
WcRtspStop();
|
||||||
|
|
||||||
// 7b. Stop WebRTC (UDP, DTLS task, state)
|
// 8. Stop WebRTC (UDP, DTLS task, state)
|
||||||
WcWebRTCStop();
|
WcWebRTCStop();
|
||||||
|
|
||||||
// 8. Delete mutexes
|
// 9. Delete mutexes
|
||||||
if (Wc.core.frame_mutex) {
|
if (Wc.core.frame_mutex) {
|
||||||
vSemaphoreDelete(Wc.core.frame_mutex);
|
vSemaphoreDelete(Wc.core.frame_mutex);
|
||||||
Wc.core.frame_mutex = NULL;
|
Wc.core.frame_mutex = NULL;
|
||||||
@@ -752,7 +769,7 @@ uint32_t WcStop(void) {
|
|||||||
Wc.core.resume_sem = NULL;
|
Wc.core.resume_sem = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. Final state
|
// 10. Final state
|
||||||
Wc.core.state = CAM_IDLE;
|
Wc.core.state = CAM_IDLE;
|
||||||
Wc.jpeg.stream_active = 0;
|
Wc.jpeg.stream_active = 0;
|
||||||
|
|
||||||
@@ -871,6 +888,13 @@ bool Xdrv81(uint32_t function) {
|
|||||||
WcSetup(true); // First boot - reset config to defaults
|
WcSetup(true); // First boot - reset config to defaults
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case FUNC_EVERY_250_MSECOND:
|
||||||
|
if (Wc.core.state == CAM_STREAMING) {
|
||||||
|
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
|
||||||
|
WcIspAwbProcess();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
break;
|
||||||
case FUNC_EVERY_SECOND:
|
case FUNC_EVERY_SECOND:
|
||||||
// Auto-start streaming once WiFi is available (only from INIT, not after FAILED)
|
// Auto-start streaming once WiFi is available (only from INIT, not after FAILED)
|
||||||
if (Wc.core.state == CAM_INIT && !TasmotaGlobal.global_state.network_down) {
|
if (Wc.core.state == CAM_INIT && !TasmotaGlobal.global_state.network_down) {
|
||||||
|
|||||||
@@ -0,0 +1,622 @@
|
|||||||
|
/*
|
||||||
|
xdrv_81_0_esp32_webcam_CSI_isp.ino - ESP32-P4 CSI ISP Configuration
|
||||||
|
|
||||||
|
Copyright (C) 2025 Christian Baars and Theo Arends
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*********************************************************************************************\
|
||||||
|
* ESP32-P4 Hardware ISP Configuration
|
||||||
|
*
|
||||||
|
* Pure C++ implementation - no Berry dependencies
|
||||||
|
*
|
||||||
|
* Responsibilities:
|
||||||
|
* - Load and parse /isp.json from filesystem
|
||||||
|
* - Apply ISP settings: CCM, Gamma, Sharpen, Color, BF (Bilateral Filter)
|
||||||
|
* - Initialize and manage AWB (Auto White Balance) controller
|
||||||
|
* - Run AWB processing loop (called from xdrv_81 core at 4Hz)
|
||||||
|
*
|
||||||
|
* Graceful degradation:
|
||||||
|
* - If isp.json is missing or malformed, ISP runs with bare minimum configuration
|
||||||
|
* - Individual section failures are logged but don't stop other sections
|
||||||
|
\*********************************************************************************************/
|
||||||
|
|
||||||
|
#ifdef ESP32
|
||||||
|
#ifdef USE_CSI_WEBCAM
|
||||||
|
|
||||||
|
#include "esp_idf_version.h"
|
||||||
|
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
|
||||||
|
|
||||||
|
#include "driver/isp.h"
|
||||||
|
#include "driver/isp_bf.h"
|
||||||
|
#include "driver/isp_sharpen.h"
|
||||||
|
#include "driver/isp_color.h"
|
||||||
|
#include "driver/isp_ccm.h"
|
||||||
|
#include "driver/isp_gamma.h"
|
||||||
|
#include "driver/isp_awb.h"
|
||||||
|
|
||||||
|
/*********************************************************************************************/
|
||||||
|
|
||||||
|
// AWB state - file-static, used by init and C processing loop
|
||||||
|
static struct {
|
||||||
|
isp_awb_ctlr_t handle;
|
||||||
|
bool enabled;
|
||||||
|
float base_ccm[3][3]; // Base CCM matrix (before AWB correction)
|
||||||
|
float gain_r, gain_b; // Current WB correction gains
|
||||||
|
float min_r_step; // Minimum gain delta to trigger update
|
||||||
|
float min_b_step;
|
||||||
|
uint32_t min_counted; // Minimum white patches before updating
|
||||||
|
} isp_awb_state = { NULL, false, {{1,0,0},{0,1,0},{0,0,1}}, 1.0f, 1.0f, 0.031f, 0.031f, 2000 };
|
||||||
|
|
||||||
|
/*********************************************************************************************/
|
||||||
|
// Forward declarations
|
||||||
|
|
||||||
|
void WcIspApplyCCM(isp_proc_handle_t handle, JsonParserObject &sensor);
|
||||||
|
void WcIspApplyGamma(isp_proc_handle_t handle, JsonParserObject &sensor);
|
||||||
|
void WcIspApplySharpen(isp_proc_handle_t handle, JsonParserObject &sensor);
|
||||||
|
void WcIspApplyColor(isp_proc_handle_t handle, JsonParserObject &sensor);
|
||||||
|
void WcIspApplyBF(isp_proc_handle_t handle, JsonParserObject &sensor);
|
||||||
|
void WcIspInitAWB(isp_proc_handle_t handle, JsonParserObject &sensor, int width, int height);
|
||||||
|
|
||||||
|
/*********************************************************************************************/
|
||||||
|
|
||||||
|
// Main entry point: Load isp.json and apply all ISP settings
|
||||||
|
// Called from WcSetup() in xdrv_81_0 after ISP handle is created
|
||||||
|
// Returns true if configuration was applied, false otherwise
|
||||||
|
bool WcIspApplyConfig(isp_proc_handle_t handle, const char* sensor_name, int width, int height) {
|
||||||
|
if (!handle) {
|
||||||
|
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: ISP handle is NULL"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
if (!TfsFileExists("/isp.json")) {
|
||||||
|
AddLog(LOG_LEVEL_INFO, PSTR("CAM: No isp.json found, using bare ISP"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get file size and allocate buffer
|
||||||
|
size_t file_size = TfsFileSize("/isp.json");
|
||||||
|
if (file_size == 0 || file_size > 2047) {
|
||||||
|
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: isp.json invalid size: %d"), file_size);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t buf_size = file_size + 1; // +1 for null terminator
|
||||||
|
char *buf = (char*)malloc(buf_size);
|
||||||
|
if (!buf) {
|
||||||
|
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: Failed to allocate %d bytes for JSON"), buf_size);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load file directly into buffer
|
||||||
|
memset(buf, 0, buf_size);
|
||||||
|
if (!TfsLoadFile("/isp.json", (uint8_t*)buf, file_size)) {
|
||||||
|
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: Failed to load isp.json"));
|
||||||
|
free(buf);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON
|
||||||
|
JsonParser parser(buf);
|
||||||
|
JsonParserToken root = parser.getRoot();
|
||||||
|
if (!root) {
|
||||||
|
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: isp.json parse failed"));
|
||||||
|
free(buf);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonParserObject root_obj = root.getObject();
|
||||||
|
if (!root_obj) {
|
||||||
|
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: isp.json root is not an object"));
|
||||||
|
free(buf);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find sensor section
|
||||||
|
JsonParserToken sensor_tok = root_obj[sensor_name];
|
||||||
|
if (!sensor_tok || !sensor_tok.isObject()) {
|
||||||
|
AddLog(LOG_LEVEL_INFO, PSTR("CAM: No ISP section for sensor %s"), sensor_name);
|
||||||
|
free(buf);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonParserObject sensor = sensor_tok.getObject();
|
||||||
|
|
||||||
|
// Apply all ISP settings (each function handles its own errors gracefully)
|
||||||
|
WcIspApplyCCM(handle, sensor);
|
||||||
|
WcIspApplyGamma(handle, sensor);
|
||||||
|
WcIspApplySharpen(handle, sensor);
|
||||||
|
WcIspApplyColor(handle, sensor);
|
||||||
|
WcIspApplyBF(handle, sensor);
|
||||||
|
WcIspInitAWB(handle, sensor, width, height);
|
||||||
|
|
||||||
|
free(buf);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*********************************************************************************************/
|
||||||
|
|
||||||
|
// Helper: Find nearest entry in array by numeric key
|
||||||
|
// Returns index of nearest entry, or -1 if not found
|
||||||
|
int WcIspFindNearest(JsonParserArray &arr, const char* key, int target_value) {
|
||||||
|
if (!arr) return -1;
|
||||||
|
|
||||||
|
int best_idx = -1;
|
||||||
|
int best_dist = INT_MAX;
|
||||||
|
int idx = 0;
|
||||||
|
|
||||||
|
for (auto entry_tok : arr) {
|
||||||
|
if (!entry_tok.isObject()) {
|
||||||
|
idx++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonParserObject entry = entry_tok.getObject();
|
||||||
|
JsonParserToken val_tok = entry[key];
|
||||||
|
if (!val_tok) {
|
||||||
|
idx++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int val = val_tok.getInt();
|
||||||
|
int dist = abs(val - target_value);
|
||||||
|
if (dist < best_dist) {
|
||||||
|
best_dist = dist;
|
||||||
|
best_idx = idx;
|
||||||
|
}
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return best_idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*********************************************************************************************/
|
||||||
|
|
||||||
|
// Apply CCM (Color Correction Matrix)
|
||||||
|
// JSON: {"acc": {"ccm": {"table": [{"color_temp": 6500, "matrix": [flat 9 floats]}, ...]}}
|
||||||
|
void WcIspApplyCCM(isp_proc_handle_t handle, JsonParserObject &sensor) {
|
||||||
|
// Navigate: sensor["acc"]["ccm"]["table"]
|
||||||
|
JsonParserToken acc_tok = sensor["acc"];
|
||||||
|
if (!acc_tok || !acc_tok.isObject()) return;
|
||||||
|
JsonParserObject acc = acc_tok.getObject();
|
||||||
|
|
||||||
|
JsonParserToken ccm_tok = acc["ccm"];
|
||||||
|
if (!ccm_tok || !ccm_tok.isObject()) return;
|
||||||
|
JsonParserObject ccm_obj = ccm_tok.getObject();
|
||||||
|
|
||||||
|
JsonParserToken table_tok = ccm_obj["table"];
|
||||||
|
if (!table_tok || !table_tok.isArray()) return;
|
||||||
|
JsonParserArray table = table_tok.getArray();
|
||||||
|
|
||||||
|
// Find nearest color temperature to 6500K (daylight)
|
||||||
|
int best_idx = WcIspFindNearest(table, "color_temp", 6500);
|
||||||
|
if (best_idx < 0) return;
|
||||||
|
|
||||||
|
JsonParserToken best_tok = table[best_idx];
|
||||||
|
if (!best_tok.isObject()) return;
|
||||||
|
JsonParserObject best = best_tok.getObject();
|
||||||
|
|
||||||
|
JsonParserToken mat_tok = best["matrix"];
|
||||||
|
if (!mat_tok || !mat_tok.isArray()) return;
|
||||||
|
JsonParserArray mat = mat_tok.getArray();
|
||||||
|
|
||||||
|
// Flat 9-element array
|
||||||
|
esp_isp_ccm_config_t cfg;
|
||||||
|
memset(&cfg, 0, sizeof(cfg));
|
||||||
|
int i = 0;
|
||||||
|
for (auto val : mat) {
|
||||||
|
if (i >= 9) break;
|
||||||
|
float fval = val.getFloat(0.0f);
|
||||||
|
cfg.matrix[i/3][i%3] = fval;
|
||||||
|
isp_awb_state.base_ccm[i/3][i%3] = fval;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_isp_ccm_configure(handle, &cfg);
|
||||||
|
esp_isp_ccm_enable(handle);
|
||||||
|
AddLog(LOG_LEVEL_DEBUG, PSTR("CAM: ISP CCM applied"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*********************************************************************************************/
|
||||||
|
|
||||||
|
uint32_t WcIspGammaInterp(uint32_t x) {
|
||||||
|
uint32_t seg = (x * 15) / 255;
|
||||||
|
if (seg >= 15) return Wc.core.isp_gamma_y[15];
|
||||||
|
uint32_t x0 = seg * 255 / 15;
|
||||||
|
uint32_t x1 = (seg + 1) * 255 / 15;
|
||||||
|
uint32_t y0 = Wc.core.isp_gamma_y[seg];
|
||||||
|
uint32_t y1 = Wc.core.isp_gamma_y[seg + 1];
|
||||||
|
uint32_t dx = x1 - x0;
|
||||||
|
return dx ? y0 + (y1 - y0) * (x - x0) / dx : y0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply Gamma Curve
|
||||||
|
// JSON: {"aen": {"gamma": {"table": [{"luma": 50, "y": [0,...15 values...]}, ...]}}}
|
||||||
|
void WcIspApplyGamma(isp_proc_handle_t handle, JsonParserObject &sensor) {
|
||||||
|
JsonParserToken aen_tok = sensor["aen"];
|
||||||
|
if (!aen_tok || !aen_tok.isObject()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonParserObject aen = aen_tok.getObject();
|
||||||
|
JsonParserToken gamma_tok = aen["gamma"];
|
||||||
|
if (!gamma_tok || !gamma_tok.isObject()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonParserObject gamma_obj = gamma_tok.getObject();
|
||||||
|
JsonParserToken table_tok = gamma_obj["table"];
|
||||||
|
if (!table_tok || !table_tok.isArray()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonParserArray table = table_tok.getArray();
|
||||||
|
|
||||||
|
// Find nearest luma to 50 (normal scene)
|
||||||
|
int best_idx = WcIspFindNearest(table, "luma", 50);
|
||||||
|
if (best_idx < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonParserToken best_tok = table[best_idx];
|
||||||
|
if (!best_tok.isObject()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonParserObject best = best_tok.getObject();
|
||||||
|
JsonParserToken y_tok = best["y"];
|
||||||
|
if (!y_tok || !y_tok.isArray()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonParserArray y_arr = y_tok.getArray();
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
for (auto v : y_arr) {
|
||||||
|
if (i >= 16) break;
|
||||||
|
Wc.core.isp_gamma_y[i++] = (uint32_t)v.getInt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
isp_gamma_curve_points_t pts = {};
|
||||||
|
esp_isp_gamma_fill_curve_points(WcIspGammaInterp, &pts);
|
||||||
|
|
||||||
|
// Apply to all color components
|
||||||
|
esp_isp_gamma_configure(handle, COLOR_COMPONENT_R, &pts);
|
||||||
|
esp_isp_gamma_configure(handle, COLOR_COMPONENT_G, &pts);
|
||||||
|
esp_isp_gamma_configure(handle, COLOR_COMPONENT_B, &pts);
|
||||||
|
esp_isp_gamma_enable(handle);
|
||||||
|
AddLog(LOG_LEVEL_DEBUG, PSTR("CAM: ISP Gamma applied (luma=50)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*********************************************************************************************/
|
||||||
|
|
||||||
|
// Apply Sharpen Filter
|
||||||
|
// JSON: {"aen": {"sharpen": [{"gain": 1, "param": {"h_thresh":..., "l_thresh":..., "h_coeff":..., "m_coeff":..., "matrix":[...]}}]}}
|
||||||
|
void WcIspApplySharpen(isp_proc_handle_t handle, JsonParserObject &sensor) {
|
||||||
|
JsonParserToken aen_tok = sensor["aen"];
|
||||||
|
if (!aen_tok || !aen_tok.isObject()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonParserObject aen = aen_tok.getObject();
|
||||||
|
JsonParserToken sh_tok = aen["sharpen"];
|
||||||
|
if (!sh_tok || !sh_tok.isArray()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonParserArray sh_arr = sh_tok.getArray();
|
||||||
|
|
||||||
|
// Find nearest gain to 1
|
||||||
|
int best_idx = WcIspFindNearest(sh_arr, "gain", 1);
|
||||||
|
if (best_idx < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonParserToken best_tok = sh_arr[best_idx];
|
||||||
|
if (!best_tok.isObject()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonParserObject best = best_tok.getObject();
|
||||||
|
JsonParserToken param_tok = best["param"];
|
||||||
|
if (!param_tok || !param_tok.isObject()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonParserObject param = param_tok.getObject();
|
||||||
|
|
||||||
|
// Parse sharpen parameters
|
||||||
|
esp_isp_sharpen_config_t cfg;
|
||||||
|
memset(&cfg, 0, sizeof(cfg));
|
||||||
|
|
||||||
|
cfg.h_thresh = param.getInt("h_thresh", 255);
|
||||||
|
cfg.l_thresh = param.getInt("l_thresh", 128);
|
||||||
|
|
||||||
|
float h_coeff = param.getFloat("h_coeff", 1.0f);
|
||||||
|
cfg.h_freq_coeff.integer = (uint8_t)h_coeff;
|
||||||
|
cfg.h_freq_coeff.decimal = (uint8_t)((h_coeff - (int)h_coeff) * 256);
|
||||||
|
|
||||||
|
float m_coeff = param.getFloat("m_coeff", 1.0f);
|
||||||
|
cfg.m_freq_coeff.integer = (uint8_t)m_coeff;
|
||||||
|
cfg.m_freq_coeff.decimal = (uint8_t)((m_coeff - (int)m_coeff) * 256);
|
||||||
|
|
||||||
|
// Parse 3x3 matrix
|
||||||
|
JsonParserToken mat_tok = param["matrix"];
|
||||||
|
if (mat_tok && mat_tok.isArray()) {
|
||||||
|
JsonParserArray mat = mat_tok.getArray();
|
||||||
|
int i = 0;
|
||||||
|
for (auto v : mat) {
|
||||||
|
if (i >= 9) break;
|
||||||
|
cfg.sharpen_template[i/3][i%3] = (uint8_t)v.getFloat(0);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.padding_mode = ISP_SHARPEN_EDGE_PADDING_MODE_SRND_DATA;
|
||||||
|
|
||||||
|
esp_isp_sharpen_configure(handle, &cfg);
|
||||||
|
esp_isp_sharpen_enable(handle);
|
||||||
|
AddLog(LOG_LEVEL_DEBUG, PSTR("CAM: ISP Sharpen applied (gain=1)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*********************************************************************************************/
|
||||||
|
|
||||||
|
// Apply Color Adjustment
|
||||||
|
// JSON: {"ext": {"hue":0, "brightness":0}, "aen": {"contrast": [{"gain":1, "value":128}]}}
|
||||||
|
void WcIspApplyColor(isp_proc_handle_t handle, JsonParserObject &sensor) {
|
||||||
|
esp_isp_color_config_t cfg;
|
||||||
|
memset(&cfg, 0, sizeof(cfg));
|
||||||
|
|
||||||
|
// contrast from aen.contrast[] nearest gain=1
|
||||||
|
cfg.color_contrast.integer = 1;
|
||||||
|
JsonParserToken aen_tok = sensor["aen"];
|
||||||
|
if (aen_tok && aen_tok.isObject()) {
|
||||||
|
JsonParserObject aen = aen_tok.getObject();
|
||||||
|
JsonParserToken con_tok = aen["contrast"];
|
||||||
|
if (con_tok && con_tok.isArray()) {
|
||||||
|
JsonParserArray con_arr = con_tok.getArray();
|
||||||
|
int best_idx = WcIspFindNearest(con_arr, "gain", 1);
|
||||||
|
if (best_idx >= 0) {
|
||||||
|
int val = con_arr[best_idx].getObject().getInt("value", 128);
|
||||||
|
cfg.color_contrast.integer = (uint8_t)(val >> 7); // scale 0-255 → ISP fixed-point
|
||||||
|
cfg.color_contrast.decimal = (uint8_t)((val & 0x7F) << 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hue + brightness from ext
|
||||||
|
JsonParserToken ext_tok = sensor["ext"];
|
||||||
|
if (ext_tok && ext_tok.isObject()) {
|
||||||
|
JsonParserObject ext = ext_tok.getObject();
|
||||||
|
cfg.color_hue = ext.getInt("hue", 0);
|
||||||
|
cfg.color_brightness = ext.getInt("brightness", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: read from acc.saturation[] once color temperature estimation is available
|
||||||
|
cfg.color_saturation.integer = 1;
|
||||||
|
|
||||||
|
esp_isp_color_configure(handle, &cfg);
|
||||||
|
esp_isp_color_enable(handle);
|
||||||
|
AddLog(LOG_LEVEL_DEBUG, PSTR("CAM: ISP Color applied"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*********************************************************************************************/
|
||||||
|
|
||||||
|
// Apply Bilateral Filter (Denoising)
|
||||||
|
// JSON: {"adn": {"bf": [{"gain":1, "param": {"level":2, "matrix":[flat 9]}}]}}
|
||||||
|
void WcIspApplyBF(isp_proc_handle_t handle, JsonParserObject &sensor) {
|
||||||
|
JsonParserToken adn_tok = sensor["adn"];
|
||||||
|
if (!adn_tok || !adn_tok.isObject()) return;
|
||||||
|
JsonParserObject adn = adn_tok.getObject();
|
||||||
|
|
||||||
|
JsonParserToken bf_tok = adn["bf"];
|
||||||
|
if (!bf_tok || !bf_tok.isArray()) return;
|
||||||
|
JsonParserArray bf_arr = bf_tok.getArray();
|
||||||
|
|
||||||
|
int best_idx = WcIspFindNearest(bf_arr, "gain", 1); // gain=1 = no AGC, normal light
|
||||||
|
if (best_idx < 0) return;
|
||||||
|
|
||||||
|
JsonParserObject best = bf_arr[best_idx].getObject();
|
||||||
|
JsonParserToken param_tok = best["param"];
|
||||||
|
if (!param_tok || !param_tok.isObject()) return;
|
||||||
|
JsonParserObject param = param_tok.getObject();
|
||||||
|
|
||||||
|
esp_isp_bf_config_t cfg;
|
||||||
|
memset(&cfg, 0, sizeof(cfg));
|
||||||
|
cfg.denoising_level = param.getInt("level", 2);
|
||||||
|
|
||||||
|
JsonParserToken mat_tok = param["matrix"];
|
||||||
|
if (mat_tok && mat_tok.isArray()) {
|
||||||
|
JsonParserArray mat = mat_tok.getArray();
|
||||||
|
int i = 0;
|
||||||
|
for (auto v : mat) {
|
||||||
|
if (i >= 9) break;
|
||||||
|
cfg.bf_template[i/3][i%3] = (uint8_t)v.getFloat(0);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg.padding_mode = ISP_BF_EDGE_PADDING_MODE_SRND_DATA;
|
||||||
|
esp_isp_bf_configure(handle, &cfg);
|
||||||
|
esp_isp_bf_enable(handle);
|
||||||
|
AddLog(LOG_LEVEL_DEBUG, PSTR("CAM: ISP BF applied"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*********************************************************************************************/
|
||||||
|
|
||||||
|
// Initialize AWB (Auto White Balance)
|
||||||
|
// JSON: {"awb": {"range": {"green":{min,max}, "rg":{min,max}, "bg":{min,max}}, "min_red_gain_step":..., "min_blue_gain_step":..., "min_counted":...}}
|
||||||
|
void WcIspInitAWB(isp_proc_handle_t handle, JsonParserObject &sensor, int width, int height) {
|
||||||
|
JsonParserToken awb_tok = sensor["awb"];
|
||||||
|
if (!awb_tok || !awb_tok.isObject()) return;
|
||||||
|
JsonParserObject awb = awb_tok.getObject();
|
||||||
|
|
||||||
|
if (isp_awb_state.handle) {
|
||||||
|
esp_isp_awb_controller_disable(isp_awb_state.handle);
|
||||||
|
esp_isp_del_awb_controller(isp_awb_state.handle);
|
||||||
|
isp_awb_state.handle = NULL;
|
||||||
|
isp_awb_state.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read nested range object
|
||||||
|
JsonParserToken range_tok = awb["range"];
|
||||||
|
if (!range_tok || !range_tok.isObject()) return;
|
||||||
|
JsonParserObject range = range_tok.getObject();
|
||||||
|
|
||||||
|
JsonParserObject green_obj = range["green"].getObject();
|
||||||
|
JsonParserObject rg_obj = range["rg"].getObject();
|
||||||
|
JsonParserObject bg_obj = range["bg"].getObject();
|
||||||
|
|
||||||
|
int lum_min = green_obj.getInt("min", 128);
|
||||||
|
int lum_max = green_obj.getInt("max", 240);
|
||||||
|
float rg_min = rg_obj.getFloat("min", 0.7f);
|
||||||
|
float rg_max = rg_obj.getFloat("max", 1.3f);
|
||||||
|
float bg_min = bg_obj.getFloat("min", 0.7f);
|
||||||
|
float bg_max = bg_obj.getFloat("max", 1.3f);
|
||||||
|
|
||||||
|
int x1 = width / 10;
|
||||||
|
int y1 = height / 10;
|
||||||
|
int x2 = width - width / 10;
|
||||||
|
int y2 = height - height / 10;
|
||||||
|
|
||||||
|
esp_isp_awb_config_t awb_cfg;
|
||||||
|
memset(&awb_cfg, 0, sizeof(awb_cfg));
|
||||||
|
awb_cfg.sample_point = (isp_awb_sample_point_t)awb.getInt("sample_point", 1);
|
||||||
|
awb_cfg.window.top_left.x = x1;
|
||||||
|
awb_cfg.window.top_left.y = y1;
|
||||||
|
awb_cfg.window.btm_right.x = x2;
|
||||||
|
awb_cfg.window.btm_right.y = y2;
|
||||||
|
awb_cfg.subwindow = awb_cfg.window;
|
||||||
|
awb_cfg.white_patch.luminance.min = (uint32_t)lum_min;
|
||||||
|
awb_cfg.white_patch.luminance.max = (uint32_t)lum_max;
|
||||||
|
awb_cfg.white_patch.red_green_ratio.min = rg_min;
|
||||||
|
awb_cfg.white_patch.red_green_ratio.max = rg_max;
|
||||||
|
awb_cfg.white_patch.blue_green_ratio.min = bg_min;
|
||||||
|
awb_cfg.white_patch.blue_green_ratio.max = bg_max;
|
||||||
|
|
||||||
|
isp_awb_ctlr_t ctlr = NULL;
|
||||||
|
esp_err_t ret = esp_isp_new_awb_controller(handle, &awb_cfg, &ctlr);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: ISP AWB init failed (0x%x)"), ret);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ret = esp_isp_awb_controller_enable(ctlr);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
esp_isp_del_awb_controller(ctlr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isp_awb_state.handle = ctlr;
|
||||||
|
isp_awb_state.enabled = true;
|
||||||
|
isp_awb_state.gain_r = 1.0f;
|
||||||
|
isp_awb_state.gain_b = 1.0f;
|
||||||
|
isp_awb_state.min_counted = (uint32_t)awb.getInt("min_counted", 2000);
|
||||||
|
isp_awb_state.min_r_step = awb.getFloat("min_red_gain_step", 0.034f);
|
||||||
|
isp_awb_state.min_b_step = awb.getFloat("min_blue_gain_step", 0.034f);
|
||||||
|
AddLog(LOG_LEVEL_DEBUG, PSTR("CAM: ISP AWB initialized"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*********************************************************************************************/
|
||||||
|
|
||||||
|
// AWB Processing Loop - called from xdrv_81 FUNC_EVERY_250_MSECOND (4Hz)
|
||||||
|
// Grey-world algorithm: gain_r = avg_G/avg_R, gain_b = avg_G/avg_B
|
||||||
|
// Applied as diagonal correction on base CCM
|
||||||
|
void WcIspAwbProcess(void) {
|
||||||
|
if (Wc.core.state != CAM_STREAMING) return;
|
||||||
|
if (!isp_awb_state.enabled || !isp_awb_state.handle) return;
|
||||||
|
isp_proc_handle_t isp = Wc.core.isp_handle;
|
||||||
|
if (!isp) return;
|
||||||
|
|
||||||
|
isp_awb_stat_result_t stats;
|
||||||
|
memset(&stats, 0, sizeof(stats));
|
||||||
|
|
||||||
|
esp_err_t ret = esp_isp_awb_controller_get_oneshot_statistics(
|
||||||
|
isp_awb_state.handle, 100, &stats);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.white_patch_num < isp_awb_state.min_counted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float avg_r = (float)stats.sum_r / (float)stats.white_patch_num;
|
||||||
|
float avg_g = (float)stats.sum_g / (float)stats.white_patch_num;
|
||||||
|
if (avg_g < 8.0f) return; // too dark??
|
||||||
|
float avg_b = (float)stats.sum_b / (float)stats.white_patch_num;
|
||||||
|
|
||||||
|
if (avg_r < 1.0f) avg_r = 1.0f;
|
||||||
|
if (avg_b < 1.0f) avg_b = 1.0f;
|
||||||
|
|
||||||
|
float new_gain_r = avg_g / avg_r;
|
||||||
|
float new_gain_b = avg_g / avg_b;
|
||||||
|
|
||||||
|
// Clamp gains to reasonable range
|
||||||
|
if (new_gain_r < 0.5f) new_gain_r = 0.5f;
|
||||||
|
if (new_gain_r > 2.0f) new_gain_r = 2.0f;
|
||||||
|
if (new_gain_b < 0.5f) new_gain_b = 0.5f;
|
||||||
|
if (new_gain_b > 2.0f) new_gain_b = 2.0f;
|
||||||
|
|
||||||
|
// Check if delta exceeds minimum step
|
||||||
|
if (fabsf(new_gain_r - isp_awb_state.gain_r) < isp_awb_state.min_r_step &&
|
||||||
|
fabsf(new_gain_b - isp_awb_state.gain_b) < isp_awb_state.min_b_step) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isp_awb_state.gain_r = new_gain_r;
|
||||||
|
isp_awb_state.gain_b = new_gain_b;
|
||||||
|
|
||||||
|
// Apply: corrected_ccm = diag(gain_r, 1, gain_b) * base_ccm
|
||||||
|
esp_isp_ccm_config_t ccm_cfg;
|
||||||
|
memset(&ccm_cfg, 0, sizeof(ccm_cfg));
|
||||||
|
for (int c = 0; c < 3; c++) {
|
||||||
|
ccm_cfg.matrix[0][c] = isp_awb_state.base_ccm[0][c] * isp_awb_state.gain_r;
|
||||||
|
ccm_cfg.matrix[1][c] = isp_awb_state.base_ccm[1][c];
|
||||||
|
ccm_cfg.matrix[2][c] = isp_awb_state.base_ccm[2][c] * isp_awb_state.gain_b;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_isp_ccm_configure(isp, &ccm_cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*********************************************************************************************/
|
||||||
|
|
||||||
|
// Deinitialize AWB - called when streaming stops
|
||||||
|
void WcIspDeinitAWB(void) {
|
||||||
|
if (!isp_awb_state.handle) return;
|
||||||
|
|
||||||
|
isp_awb_state.enabled = false;
|
||||||
|
|
||||||
|
memset(isp_awb_state.base_ccm, 0, sizeof(isp_awb_state.base_ccm));
|
||||||
|
isp_awb_state.base_ccm[0][0] = 1.0f;
|
||||||
|
isp_awb_state.base_ccm[1][1] = 1.0f;
|
||||||
|
isp_awb_state.base_ccm[2][2] = 1.0f;
|
||||||
|
|
||||||
|
isp_proc_handle_t isp = Wc.core.isp_handle;
|
||||||
|
if (!isp) {
|
||||||
|
isp_awb_state.handle = NULL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_isp_awb_controller_disable(isp_awb_state.handle);
|
||||||
|
esp_isp_del_awb_controller(isp_awb_state.handle);
|
||||||
|
isp_awb_state.handle = NULL;
|
||||||
|
isp_awb_state.gain_r = 1.0f;
|
||||||
|
isp_awb_state.gain_b = 1.0f;
|
||||||
|
AddLog(LOG_LEVEL_DEBUG, PSTR("CAM: ISP AWB deinitialized"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // ESP_IDF_VERSION >= 5.5.0
|
||||||
|
#endif // USE_CSI_WEBCAM
|
||||||
|
#endif // ESP32
|
||||||
Reference in New Issue
Block a user