双目拼接中色差消除优化

一、问题根源解析

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);
}
相关推荐
Sagittarius_A*6 小时前
Command Injection 命令注入漏洞:系统命令拼接缺陷与执行利用技术
网络·安全·web安全·dvwa·命令注入漏洞
曙光_deeplove7 小时前
海康機器人工業相機(丢失帧、空帧和残帧/不完整帧)
网络
Android洋芋7 小时前
漏洞挖掘与赏金攻略
网络·安全·web安全
王燕龙(大卫)7 小时前
fastdds笔记
网络·c++·笔记
meilindehuzi_a8 小时前
浏览器端 HTTP 流式通信详解:从 SSE 到 EventSource、fetch 与 ReadableStream
网络·网络协议·http
数据知道8 小时前
SDN 与软件定义网络的安全边界在哪里?
网络·安全·网络安全
REDcker8 小时前
用 eBPF 观测 SSL/TLS 明文:原理、钩点与边界
网络·网络协议·ssl
数据知道8 小时前
VLAN 与安全隔离:从配置错误到 VLAN Hopping 攻击
网络·安全·网络安全·智能路由器
Tsuki_tl9 小时前
UDP传输协议
网络·网络协议·udp·传输层·udp校验和·tcp与udp区别·面向数据报