
一、问题根源解析
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 差值对应的视觉色差是不均匀的,无法对应人眼感知。
二、代码实现
cpp
#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);
}