一、问题根源解析
1. YUV(NV12/YUYV)微小 UV 值失效的核心原因
- YUV 中 U/V 量化是整数 8bit(0~255),中性灰中心点严格为 127,量化步长 = 1。
- 差值仅为 2(U:127 vs 125),在 8bit 整数下只能做到整数级修正,无法做亚像素级微调。
- UV 通道只代表色度分量,没有和亮度 L 耦合:亮度轻微不一致会放大人眼色度感知。人眼对色度的感知是和亮度绑定的,单纯 UV 差≤3 时,数值差很小,但结合亮度后视觉色差会被放大。
- 8bit YUV 色度存在量化截断误差,微小色偏被整数量化掩盖,数值看起来接近,实际视觉差异显著。
比如如下图,肉眼可见又色差,但是yuv域计算处于容忍范围内,无法再做进一步校正。
2. Lab 空间为什么能捕捉到人眼可见的微小色差
CIE Lab 是均匀视觉空间:
- L:亮度;a:红 - 绿轴;b:蓝 - 黄轴。
- Lab 满足ΔE 色差公式,数值差异直接等价于人眼视觉感受。 你给出的两组数据:
基准:L=73.50, a=-0.89, b=0.26 副目:L=73.84, a=-1.27, b=2.41行业标准:
- ΔE<1:人眼不可分辨;
- 1<ΔE<3:轻微可察觉;
- ΔE>3:明显偏色。 你当前 Δ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 i=0; i<3; i++) { g_I_a[i] = 0.0f; g_I_b[i] = 0.0f; } // 清空滤波缓存 for(int i=0; i<MAX_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 i=0; i<order; 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); }