双目拼接中色差消除优化 一、问题根源解析1. YUVNV12/YUYV微小 UV 值失效的核心原因YUV 中 U/V 量化是整数 8bit0~255中性灰中心点严格为 127量化步长 1。差值仅为 2U127 vs 125在 8bit 整数下只能做到整数级修正无法做亚像素级微调。UV 通道只代表色度分量没有和亮度 L 耦合亮度轻微不一致会放大人眼色度感知。人眼对色度的感知是和亮度绑定的单纯 UV 差≤3 时数值差很小但结合亮度后视觉色差会被放大。8bit YUV 色度存在量化截断误差微小色偏被整数量化掩盖数值看起来接近实际视觉差异显著。比如如下图肉眼可见又色差但是yuv域计算处于容忍范围内无法再做进一步校正。2. Lab 空间为什么能捕捉到人眼可见的微小色差CIE Lab 是均匀视觉空间L亮度a红 - 绿轴b蓝 - 黄轴。Lab 满足ΔE 色差公式数值差异直接等价于人眼视觉感受。 你给出的两组数据基准L73.50, a-0.89, b0.26 副目L73.84, a-1.27, b2.41行业标准ΔE1人眼不可分辨1ΔE3轻微可察觉ΔE3明显偏色。 你当前 ΔE≈2.21正好对应 “数值 UV 几乎无差别但肉眼能看出偏色”。 YUV 是非均匀空间2 个灰度级的 UV 差值对应的视觉色差是不均匀的无法对应人眼感知。二、代码实现#include stdint.h #include math.h // 【量产可调参数区】 // PI积分全局限幅 #define INT_LIMIT 3.0f // 光照分段亮度阈值 #define LIGHT_DARK_LUM_THRESH 35.0f #define LIGHT_HIGH_LUM_THRESH 70.0f // 光照档位切换积分衰减系数越小过渡越平缓 #define LIGHT_STAGE_ATTEN_FACTOR 0.5f // Lab中性灰筛选明度区间 #define L_GRAY_MIN 60.0f #define L_GRAY_MAX 85.0f #define SAT_THRESHOLD 8.0f #define VALID_PIX_THRES 50 // 三层置信权重 #define WEIGHT_SINGLE_NO_GRAY 0.20f #define WEIGHT_DOUBLE_NO_GRAY 0.10f // 色差转白平衡倍率灵敏度 #define CALIB_KRA 0.025f #define CALIB_KBB 0.022f // ISP Q8定点增益 #define Q8_SCALE 256 #define ISP_GAIN_MIN 128 #define ISP_GAIN_MAX 512 #define GAIN_RATIO_MIN 0.80f #define GAIN_RATIO_MAX 1.20f // 死区内残差缓慢累积比例核心优化3 #define DEAD_ZONE_RESIDUAL_RATIO 0.1f // 光照档位枚举 typedef enum { LIGHT_STAGE_DARK 0, // 暗光 L 35 LIGHT_STAGE_NORMAL 1, // 标准光 35 ≤ L ≤70 LIGHT_STAGE_BRIGHT 2 // 强光 L 70 } LightStage; // 单档光照PI参数结构体 typedef struct { float kp_fast; float kp_slow; float ki; float dead_zone_de; uint8_t filter_order; // 自适应滤波阶数 2/3/5 } PiLightParam; // 三档固定PI参数表新增自适应滤波阶数 static const PiLightParam g_pi_light_table[] { // LIGHT_STAGE_DARK 暗光低KP、低KI、大死区、5阶滤波降噪 {0.40f, 0.20f, 0.05f, 1.3f, 5}, // LIGHT_STAGE_NORMAL 标准光平衡收敛速度与稳定3阶滤波 {0.60f, 0.30f, 0.08f, 1.2f, 3}, // LIGHT_STAGE_BRIGHT 强光高KP、高KI、小死区、2阶低滞后滤波 {0.75f, 0.35f, 0.10f, 1.0f, 2} }; // 【全局静态状态】 // 优化2分光照三档独立积分 I_a[3], I_b[3] static float g_I_a[3] {0.0f, 0.0f, 0.0f}; static float g_I_b[3] {0.0f, 0.0f, 0.0f}; // 自适应多阶滤波最大缓存5阶 #define MAX_FILTER_ORDER 5 // a/b双通道独立滤波缓存、独立索引解决异步相位错位 static float buf_a[MAX_FILTER_ORDER] {0.0f}; static float buf_b[MAX_FILTER_ORDER] {0.0f}; static uint8_t filter_idx_a 0; static uint8_t filter_idx_b 0; // 上一帧光照档位用于平滑积分衰减 static LightStage last_light_stage LIGHT_STAGE_NORMAL; // 【对外公共接口】 /** * brief IRCUT/亮度阶跃突变清空全部档位积分、全部滤波缓存索引 */ void BinoResetPiIntegral(void) { for(int i0; i3; i) { g_I_a[i] 0.0f; g_I_b[i] 0.0f; } // 清空滤波缓存 for(int i0; iMAX_FILTER_ORDER; i) { buf_a[i] 0.0f; buf_b[i] 0.0f; } filter_idx_a 0; filter_idx_b 0; } // 【底层工具函数】 static float st_GammaInv(float val) { if (val 0.04045f) return powf((val 0.055f) / 1.055f, 2.4f); return val / 12.92f; } static float st_XyzFFunc(float val) { const float thr powf(6.0f / 29.0f, 3.0f); if (val thr) return powf(val, 1.0f / 3.0f); return (841.0f / 108.0f) * val 4.0f / 29.0f; } /** * brief 自适应N阶滑动均值滤波阶数由外部参数传入 */ static float st_AdaptiveAvgFilter(float new_val, float buf[], uint8_t *idx, uint8_t order) { buf[*idx] new_val; *idx (*idx 1U) % order; float sum 0.0f; for(uint8_t i0; iorder; i) { sum buf[i]; } return sum / (float)order; } /** * brief NV12 UV读取无宏无告警 */ static void st_GetNv12Uv(uint8_t *uv_buf, int roi_w, int i, int j, uint8_t *pU, uint8_t *pV) { int uv_r i / 2; int uv_c j / 2; int pix_idx uv_r * (roi_w / 2) uv_c; int byte_off pix_idx * 2; *pU uv_buf[byte_off]; *pV uv_buf[byte_off 1]; } static void st_Yuv2Rgb(uint8_t Y, uint8_t U, uint8_t V, float *R, float *G, float *B) { float y (float)Y; float u (float)U - YUV_CHROMA_OFF; float v (float)V - YUV_CHROMA_OFF; *R y 1.402f * v; *G y - 0.34414f * u - 0.71414f * v; *B y 1.772f * u; } static void st_Rgb2Lab(float R, float G, float B, float *L, float *a, float *b) { R / 255.0f; G / 255.0f; B / 255.0f; R st_GammaInv(R); G st_GammaInv(G); B st_GammaInv(B); float X R * 0.4124f G * 0.3576f B * 0.1805f; float Y R * 0.2127f G * 0.7152f B * 0.0722f; float Z R * 0.0193f G * 0.1192f B * 0.9503f; const float Xn 0.95047f; const float Yn 1.00000f; const float Zn 1.08883f; float fx st_XyzFFunc(X / Xn); float fy st_XyzFFunc(Y / Yn); float fz st_XyzFFunc(Z / Zn); *L 116.0f * fy - 16.0f; *a 500.0f * (fx - fy); *b 200.0f * (fy - fz); } /** * brief 中性灰统计 L饱和度双重筛选 */ static void st_CalcNeutralLab(uint8_t *y_buf, uint8_t *uv_buf, int roi_w, int roi_h, float *L_out, float *a_out, float *b_out, int *valid_cnt) { float sumL 0.0f, suma 0.0f, sumb 0.0f; int cnt 0; uint8_t U, V; for (int i 0; i roi_h; i) { for (int j 0; j roi_w; j) { uint8_t Y y_buf[i * roi_w j]; st_GetNv12Uv(uv_buf, roi_w, i, j, U, V); float R, G, B, L, a, b; st_Yuv2Rgb(Y, U, V, R, G, B); st_Rgb2Lab(R, G, B, L, a, b); float sat_C sqrtf(a * a b * b); if (L L_GRAY_MIN L L_GRAY_MAX sat_C SAT_THRESHOLD) { sumL L; suma a; sumb b; cnt; } } } *valid_cnt cnt; if (cnt 0) { float inv_cnt 1.0f / (float)cnt; *L_out sumL * inv_cnt; *a_out suma * inv_cnt; *b_out sumb * inv_cnt; } else { *L_out 0.0f; *a_out 0.0f; *b_out 0.0f; } } /** * brief 兜底全画面统计过滤高低暗像素无中间调全像素兜底 */ static void st_CalcAllPixelLab(uint8_t *y_buf, uint8_t *uv_buf, int roi_w, int roi_h, float *L_out, float *a_out, float *b_out) { float sumL_valid 0.0f, suma_valid 0.0f, sumb_valid 0.0f; int valid_cnt 0; float sumL_all 0.0f, suma_all 0.0f, sumb_all 0.0f; int total_pix roi_w * roi_h; uint8_t U, V; for (int i 0; i roi_h; i) { for (int j 0; j roi_w; j) { uint8_t Y y_buf[i * roi_w j]; st_GetNv12Uv(uv_buf, roi_w, i, j, U, V); float R, G, B, L, a, b; st_Yuv2Rgb(Y, U, V, R, G, B); st_Rgb2Lab(R, G, B, L, a, b); sumL_all L; suma_all a; sumb_all b; if (L L_GRAY_MIN L L_GRAY_MAX) { sumL_valid L; suma_valid a; sumb_valid b; valid_cnt; } } } if (valid_cnt 0) { float inv_valid 1.0f / (float)valid_cnt; *L_out sumL_valid * inv_valid; *a_out suma_valid * inv_valid; *b_out sumb_valid * inv_valid; } else { float inv_total 1.0f / (float)total_pix; *L_out sumL_all * inv_total; *a_out suma_all * inv_total; *b_out sumb_all * inv_total; } } /** * brief 改造后PI自适应滤波阶数、分档位独立积分、死区内残差缓慢累积 */ static void st_PidCompute(float am, float bm, float as, float bs, float Kra, float Kbb, uint8_t diff_level, const PiLightParam *param, LightStage stage, float *kr, float *kb) { float delta_a_raw as - am; float delta_b_raw bs - bm; // 优化1每帧同步滤波同步PI计算完全同步无延迟 float da_filt st_AdaptiveAvgFilter(delta_a_raw, buf_a, filter_idx_a, param-filter_order); float db_filt st_AdaptiveAvgFilter(delta_b_raw, buf_b, filter_idx_b, param-filter_order); float deltaE sqrtf(da_filt * da_filt db_filt * db_filt); float ia, ib; // 读取当前光照档位独立积分 ia g_I_a[stage]; ib g_I_b[stage]; float P_a, P_b; float kp (diff_level 1) ? param-kp_fast : param-kp_slow; P_a kp * da_filt; P_b kp * db_filt; // 优化3死区内保留微量残差持续积分消除长期稳态色差 if (deltaE param-dead_zone_de) { da_filt * DEAD_ZONE_RESIDUAL_RATIO; db_filt * DEAD_ZONE_RESIDUAL_RATIO; } // 迭代当前档位独立积分 ia param-ki * da_filt; ib param-ki * db_filt; // 积分限幅 if (ia INT_LIMIT) ia INT_LIMIT; if (ia -INT_LIMIT) ia -INT_LIMIT; if (ib INT_LIMIT) ib INT_LIMIT; if (ib -INT_LIMIT) ib -INT_LIMIT; // 写回对应档位积分 g_I_a[stage] ia; g_I_b[stage] ib; float out_a P_a ia; float out_b P_b ib; float kr_tmp 1.0f Kra * out_a; float kb_tmp 1.0f Kbb * out_b; if (kr_tmp GAIN_RATIO_MAX) kr_tmp GAIN_RATIO_MAX; if (kr_tmp GAIN_RATIO_MIN) kr_tmp GAIN_RATIO_MIN; if (kb_tmp GAIN_RATIO_MAX) kb_tmp GAIN_RATIO_MAX; if (kb_tmp GAIN_RATIO_MIN) kb_tmp GAIN_RATIO_MIN; *kr kr_tmp; *kb kb_tmp; } /** * brief ISP定点增益换算 */ static void st_CalcTargetCalcGain(uint16_t curr_calc_r, uint16_t curr_calc_b, float kr, float kb, uint16_t *out_target_r, uint16_t *out_target_b) { float fr (float)curr_calc_r * kr; float fb (float)curr_calc_b * kb; uint16_t t_r (uint16_t)(fr 0.5f); uint16_t t_b (uint16_t)(fb 0.5f); if (t_r ISP_GAIN_MIN) t_r ISP_GAIN_MIN; if (t_r ISP_GAIN_MAX) t_r ISP_GAIN_MAX; if (t_b ISP_GAIN_MIN) t_b ISP_GAIN_MIN; if (t_b ISP_GAIN_MAX) t_b ISP_GAIN_MAX; *out_target_r t_r; *out_target_b t_b; } // 顶层业务主逻辑 void BinoNv12ColorMatch(uint8_t *m_y, uint8_t *m_uv, uint8_t *s_y, uint8_t *s_uv, int roi_w, int roi_h, uint16_t curr_calc_r, uint16_t curr_calc_b, uint16_t *target_calc_r, uint16_t *target_calc_b) { if (roi_w 0 || roi_h 0 || m_y NULL || s_y NULL) { *target_calc_r curr_calc_r; *target_calc_b curr_calc_b; return; } float L_m, a_m, b_m; float L_s, a_s, b_s; int valid_m, valid_s; float weight 1.0f; st_CalcNeutralLab(m_y, m_uv, roi_w, roi_h, L_m, a_m, b_m, valid_m); st_CalcNeutralLab(s_y, s_uv, roi_w, roi_h, L_s, a_s, b_s, valid_s); if (valid_m 0 valid_s 0) { st_CalcAllPixelLab(m_y, m_uv, roi_w, roi_h, L_m, a_m, b_m); st_CalcAllPixelLab(s_y, s_uv, roi_w, roi_h, L_s, a_s, b_s); weight WEIGHT_DOUBLE_NO_GRAY; } else if (valid_m 0 || valid_s 0) { weight WEIGHT_SINGLE_NO_GRAY; } else { int min_valid (valid_m valid_s) ? valid_m : valid_s; if (min_valid VALID_PIX_THRES) { weight (float)min_valid / (float)VALID_PIX_THRES; } } // 判定当前光照档位 LightStage curr_stage; const PiLightParam *curr_param; if (L_m LIGHT_DARK_LUM_THRESH) { curr_stage LIGHT_STAGE_DARK; curr_param g_pi_light_table[LIGHT_STAGE_DARK]; } else if (L_m LIGHT_HIGH_LUM_THRESH) { curr_stage LIGHT_STAGE_NORMAL; curr_param g_pi_light_table[LIGHT_STAGE_NORMAL]; } else { curr_stage LIGHT_STAGE_BRIGHT; curr_param g_pi_light_table[LIGHT_STAGE_BRIGHT]; } // 光照跨档位平滑衰减积分分档位独立积分衰减 if (curr_stage ! last_light_stage) { g_I_a[curr_stage] * LIGHT_STAGE_ATTEN_FACTOR; g_I_b[curr_stage] * LIGHT_STAGE_ATTEN_FACTOR; // 衰减后限幅 if(g_I_a[curr_stage] INT_LIMIT) g_I_a[curr_stage] INT_LIMIT; if(g_I_a[curr_stage] -INT_LIMIT) g_I_a[curr_stage] -INT_LIMIT; if(g_I_b[curr_stage] INT_LIMIT) g_I_b[curr_stage] INT_LIMIT; if(g_I_b[curr_stage] -INT_LIMIT) g_I_b[curr_stage] -INT_LIMIT; last_light_stage curr_stage; } float raw_da (a_s - a_m) * weight; float raw_db (b_s - a_m) * weight; float abs_sum fabs(raw_da) fabs(raw_db); uint8_t diff_level (abs_sum 1.5f) ? 1 : 0; float Kra_w CALIB_KRA * weight; float Kbb_w CALIB_KBB * weight; float kr, kb; // 传入当前光照档位读取对应独立积分、自适应滤波阶数 st_PidCompute(a_m, b_m, a_s, b_s, Kra_w, Kbb_w, diff_level, curr_param, curr_stage, kr, kb); st_CalcTargetCalcGain(curr_calc_r, curr_calc_b, kr, kb, target_calc_r, target_calc_b); } // 任务调度入口 static void IspWriteCalcAwbGain(uint16_t r_gain, uint16_t b_gain) { } static void IspReadCalcAwbGain(uint16_t *r_out, uint16_t *b_out) { *r_out 256; *b_out 256; } void BinocularAwbMatchTask(uint8_t ircut_trig, uint8_t bright_jump) { if (ircut_trig || bright_jump) { BinoResetPiIntegral(); } const int roi_w 320; const int roi_h 240; uint8_t *m_y, *m_uv, *s_y, *s_uv; uint16_t curr_calc_r, curr_calc_b; IspReadCalcAwbGain(curr_calc_r, curr_calc_b); uint16_t target_r, target_b; BinoNv12ColorMatch(m_y, m_uv, s_y, s_uv, roi_w, roi_h, curr_calc_r, curr_calc_b, target_r, target_b); IspWriteCalcAwbGain(target_r, target_b); }