AWB算法(给定输入图像A->给出输出图像B)

在相机成像系统中,不同光源(如白炽灯、阴天、荧光灯)的光谱分布差异会导致图像出现“偏色”——比如白炽灯下画面偏黄、阴天环境下画面偏蓝。自动白平衡(AWB)作为相机3A(AE自动曝光、AF自动对焦、AWB自动白平衡)核心技术之一,其核心目标是通过算法消除光源干扰,让中性灰物体在任何场景下都呈现真实灰度,实现“色彩归一化”。本文将从理论底层到代码实现,完整拆解AWB算法的工程落地逻辑。

一、AWB核心理论:三个关键认知

在动手实现前,必须先明确AWB算法的三大理论支柱,这是代码逻辑的核心依据。

1.1 色温:光源的“色彩身份证”

色温是描述光源光谱特性的物理量,单位为开尔文(K),其核心规律可总结为“冷光高温、暖光低温”:

低色温(<4000K):如白炽灯(2700K)、蜡烛光,光谱以红光为主,图像易偏黄/偏红;

中色温(4000K-6500K):如正午阳光(5500K),光谱分布均匀,图像色彩最接近人眼感知;

高色温(>6500K):如阴天(6500K)、蓝光灯,光谱以蓝光为主,图像易偏蓝/偏青;

混合色温:如室内“白炽灯+霓虹灯”场景,多光谱叠加导致局部偏色,是AWB的难点。

AWB的本质就是“识别色温→针对性调整色彩通道增益”,让不同色温下的图像色彩统一。

1.2 中性灰锚点:色彩校正的“基准尺”

AWB并非直接“优化白色”,而是以“中性灰”为校准锚点——中性灰的理论特性是R、G、B三通道像素值相等(R=G=B),无论光源如何变化,只要让图像中的中性灰区域满足这一条件,整体画面的色彩平衡就会自然校正。

工程中核心操作是“灰区筛选”:从输入图像中筛选出符合“R/G≈1、B/G≈1”的像素区域(排除高饱和、强反光像素),以此作为色温计算的依据。

1.3 补色原理:快速纠偏的“实战技巧”

当场景中无明显中性灰时,可通过补色原理快速校正偏色,核心对应关系为:

偏红→补青(提升G/B通道增益);

偏蓝→补黄(提升R/G通道增益);

偏黄→补蓝(提升B通道增益);

偏青→补红(提升R通道增益)。

这一原理在主观调试和异常场景修复中应用极广,代码中可作为增益计算的辅助约束。

二、AWB算法全流程:从图像A到图像B的转化链路

AWB算法的核心是“输入原始图像A→输出校准后图像B”的四步流水线,每一步都需兼顾硬件特性与算法精度,流程如下:

输入图像A → 预处理(消除硬件偏差)→ 光源分析(提取色温与灰区)→ 核心校准(增益+CCM调整)→ 后处理(平滑与校验)→ 输出图像B

2.1 预处理:消除硬件“原生偏差”

原始图像A存在传感器暗电流、镜头阴影等硬件缺陷,必须先修正才能进入算法核心流程:

暗电平校正(OB Calibration):扣除传感器暗电流导致的基准偏移,公式为“校正后像素值=原始值-暗电平基准”;

镜头阴影校正(LSC):修正镜头中心与边缘的亮度衰减,通过预存的LSC增益矩阵对边缘像素提亮;

曝光适配:结合AE模块输出的曝光参数,剔除过曝(像素值=255)和欠曝(像素值<10)区域,避免干扰灰区识别。

2.2 光源分析:精准提取“色彩基准”

这是AWB的“感知环节”,核心是从预处理后的图像中提取光源色温与有效灰区:

灰区筛选:遍历图像像素,筛选满足“0.9≤R/G≤1.1”且“0.9≤B/G≤1.1”且“饱和度≤0.3”的像素(饱和度=(max(R,G,B)-min(R,G,B))/max(R,G,B)),这些像素构成潜在中性灰区域;

色温估算:计算灰区像素的平均R/G、B/G比值,结合预存的“色温-比值映射表”(如2700K对应R/G=1.2、B/G=0.8;6500K对应R/G=0.8、B/G=1.2),通过插值法得到当前场景色温;

场景判断:若灰区像素占比<5%(无有效灰区),则触发“场景识别 fallback”,基于图像主色调匹配预设场景(如人像场景优先肤色校准)。

2.3 核心校准:实现“色彩归一化”

这是AWB的“执行环节”,通过增益调整和色彩校正矩阵(CCM)实现精准校色:

增益计算:目标是让灰区R=G=B,计算公式为“Rgain=1/平均R/G比值,Bgain=1/平均B/G比值,Ggain=1.0”(G通道作为基准);同时加入约束:增益最大值≤2.0(避免低照度下噪点放大);

CCM矩阵应用:修正传感器光谱响应偏差,基于当前色温调用预存的3×3 CCM矩阵(如2700K CCM矩阵),计算方式为:
# CCM矩阵计算示例(R'=R*ccm[0][0]+G*ccm[0][1]+B*ccm[0][2],G'、B'同理)

ccm_2700k = [[1.05, -0.05, 0.0],

[-0.1, 1.1, 0.0],

[0.0, -0.05, 1.05]]

肤色优化(可选):若场景含人脸,提取人脸区域Lab值,微调Rgain/Bgain使肤色落在“L=60-70、a=8-15、b=15-25”(亚洲人典型范围)。

2.4 后处理:保障“视觉稳定性”

避免动态场景下色彩跳变,同时校验输出质量:

增益平滑:若当前色温与上一帧差值>1000K,采用线性插值过渡增益(如“当前增益=上一帧增益×0.3+计算增益×0.7”);

质量校验:计算输出图像灰区的R/G、B/G比值,若偏离1.0超过0.1,则回溯调整CCM矩阵;

像素裁剪:将校准后像素值裁剪至0-255范围,避免色彩溢出。

理论环节 代码实现函数 / 逻辑 核心对应点
预处理(暗电平 + LSC)
preprocess
扣除暗电平、2×2 分块 LSC 校正、像素裁剪
光源分析(灰区 + 色温)
lightSourceAnalysis
灰区筛选(R/G/B/G + 饱和度约束)、线性插值估色温
核心校准(增益 + CCM)
coreCalibration
增益计算(1 / 平均比值)、CCM 矩阵插值与应用
后处理(平滑 + 格式转换)
postprocess
+ 增益平滑逻辑
增益线性平滑、RGB→BGR 格式适配

3.1 依赖库安装

3.1 环境配置详解(适配OpenCV 4.x)

代码核心依赖 OpenCV 4.x 库(用于图像读取、矩阵运算、显示等),环境配置的核心目标是:让编译器(VS/g++)能找到OpenCV的头文件和库文件。

3.1.1 通用前置:下载OpenCV 4.x库

无论何种平台,先下载对应版本的OpenCV库:

下载地址:OpenCV官方下载页https://opencv.org/releases/(选择4.x版本,如4.8.0,推荐LTS长期支持版);

版本选择: Windows:下载“Windows”安装包(.exe格式,本质是解压包);

3.1.2 Windows平台(Visual Studio 2019/2022)

Visual Studio(简称VS)是Windows下最主流的C++开发工具,配置流程如下:

步骤1:安装OpenCV并配置系统环境变量

运行下载的OpenCV .exe文件,选择解压路径(如
D:OpenCV
,路径建议无中文/空格);

解压后,核心文件路径为
D:OpenCVuildx64vc16
(vc16对应VS2019/2022,vc15对应VS2017);

配置系统环境变量: ① 右键“此电脑”→“属性”→“高级系统设置”→“环境变量”; ② 在“系统变量”→“Path”中添加:
D:OpenCVuildx64vc16in
; ③ 重启电脑(环境变量生效)。

步骤2:创建VS项目并关联OpenCV

打开VS,创建“控制台应用”(C++),项目名称如“AWB_Project”;

设置项目为“x64”架构(OpenCV默认仅提供x64库,右上角“解决方案平台”选择“x64”,若无则新建);

配置“包含目录”(让编译器找到OpenCV头文件): ① 右键项目→“属性”→“配置属性”→“C/C++”→“常规”; ② “附加包含目录”中添加:
D:OpenCVuildinclude
; ③ 点击“应用”。

配置“库目录”(让编译器找到OpenCV库文件): ① 同一属性页→“链接器”→“常规”; ② “附加库目录”中添加:
D:OpenCVuildx64vc16lib
; ③ 点击“应用”。

配置“附加依赖项”(指定链接的库文件名): ① 同一属性页→“链接器”→“输入”; ② “附加依赖项”中添加以下2个库名(根据配置选择):  Debug模式:
opencv_world480d.lib
(480对应OpenCV 4.8.0,d代表Debug); – Release模式:
opencv_world480.lib
; ③ 点击“确定”保存属性。



#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <map>
#include <algorithm>
#include <cmath>
 
using namespace cv;
using namespace std;
 
class AWBAlgorithm {
private:
    // 1. 预定义参数(工程中可从OTP读取或配置文件加载)
    int dark_level_;                  // 暗电平基准值
    Mat lsc_matrix_;                  // LSC增益矩阵(简化2x2分块)
    map<int, pair<float, float>> temp_ratio_map_;  // 色温-比值映射表
    map<int, Mat> ccm_map_;           // 分色温CCM矩阵
    float prev_r_gain_;               // 上一帧R通道增益(用于平滑)
    float prev_b_gain_;               // 上一帧B通道增益(用于平滑)
 
    /**
     * 辅助函数:根据R/G、B/G比值估算色温(线性插值)
     * @param avg_r_g 灰区平均R/G比值
     * @param avg_b_g 灰区平均B/G比值
     * @return 估算的色温值(K)
     */
    int estimateTemperature(float avg_r_g, float avg_b_g) {
        // 提取排序后的色温关键节点
        vector<int> temps;
        vector<pair<float, float>> ratios;
        for (auto& item : temp_ratio_map_) {
            temps.push_back(item.first);
            ratios.push_back(item.second);
        }
        sort(temps.begin(), temps.end());
        sort(ratios.begin(), ratios.end(), [](const pair<float, float>& a, const pair<float, float>& b) {
            return a.first > b.first;  // 按R/G降序(低色温R/G大)
        });
 
        // 边界处理:低于最低色温或高于最高色温
        if (avg_r_g >= ratios[0].first) return temps[0];
        if (avg_r_g <= ratios.back().first) return temps.back();
 
        // 中间区间线性插值
        for (size_t i = 1; i < temps.size(); ++i) {
            int t1 = temps[i-1], t2 = temps[i];
            float r1 = ratios[i-1].first, r2 = ratios[i].first;
            if (avg_r_g <= r1 && avg_r_g >= r2) {
                float ratio = (avg_r_g - r1) / (r2 - r1);
                return static_cast<int>(round(t1 + (t2 - t1) * ratio));
            }
        }
        return 5500;  // 默认中间色温
    }
 
    /**
     * 辅助函数:根据色温获取CCM矩阵(线性插值)
     * @param temp 当前估算色温
     * @return 插值后的CCM矩阵(3x3 float)
     */
    Mat getCCMByTemp(int temp) {
        if (temp <= 2700) return ccm_map_[2700];
        if (temp >= 6500) return ccm_map_[6500];
        
        // 2700K到6500K线性插值
        float ratio = (temp - 2700.0f) / (6500.0f - 2700.0f);
        Mat ccm_2700 = ccm_map_[2700];
        Mat ccm_6500 = ccm_map_[6500];
        Mat ccm;
        addWeighted(ccm_2700, 1 - ratio, ccm_6500, ratio, 0.0f, ccm);
        return ccm;
    }
 
public:
    /**
     * 构造函数:初始化AWB算法参数
     */
    AWBAlgorithm() {
        dark_level_ = 10;
        // 初始化LSC增益矩阵(2x2分块,边缘提亮)
        lsc_matrix_ = (Mat_<float>(2, 2) << 1.0f, 1.2f, 1.2f, 1.3f);
        // 色温-比值映射表(key:色温,value:(R/G, B/G))
        temp_ratio_map_[2700] = make_pair(1.2f, 0.8f);
        temp_ratio_map_[5500] = make_pair(1.0f, 1.0f);
        temp_ratio_map_[6500] = make_pair(0.8f, 1.2f);
        // 分色温CCM矩阵(3x3 float矩阵)
        ccm_map_[2700] = (Mat_<float>(3, 3) << 1.05f, -0.05f, 0.0f,
                                                  -0.1f, 1.1f, 0.0f,
                                                  0.0f, -0.05f, 1.05f);
        ccm_map_[5500] = Mat::eye(3, 3, CV_32F);
        ccm_map_[6500] = (Mat_<float>(3, 3) << 0.95f, 0.05f, 0.0f,
                                                  0.05f, 1.0f, 0.0f,
                                                  0.0f, 0.05f, 0.95f);
        // 初始化上一帧增益(平滑用)
        prev_r_gain_ = 1.0f;
        prev_b_gain_ = 1.0f;
    }
 
    /**
     * 步骤1:预处理(暗电平校正+LSC校正+曝光适配)
     * @param img 输入RGB图像(CV_8UC3)
     * @return 预处理后的图像(CV_32FC3)
     */
    Mat preprocess(const Mat& img) {
        Mat img_float;
        img.convertTo(img_float, CV_32FC3);  // 转为float类型避免溢出
 
        // 1. 暗电平校正
        img_float -= dark_level_;
 
        // 2. LSC校正(2x2分块提亮边缘)
        int h = img.rows, w = img.cols;
        int h_block = h / 2, w_block = w / 2;
        // 左上(中心区域)
        img_float(Rect(0, 0, w_block, h_block)) *= lsc_matrix_.at<float>(0, 0);
        // 右上(右边缘)
        img_float(Rect(w_block, 0, w - w_block, h_block)) *= lsc_matrix_.at<float>(0, 1);
        // 左下(下边缘)
        img_float(Rect(0, h_block, w_block, h - h_block)) *= lsc_matrix_.at<float>(1, 0);
        // 右下(边角区域)
        img_float(Rect(w_block, h_block, w - w_block, h - h_block)) *= lsc_matrix_.at<float>(1, 1);
 
        // 3. 曝光适配(裁剪过曝/欠曝像素)
        clip(img_float, 0.0f, 255.0f);
 
        return img_float;
    }
 
    /**
     * 步骤2:光源分析(灰区筛选+色温估算)
     * @param img_float 预处理后的图像(CV_32FC3)
     * @param temp 输出:估算的色温(K)
     * @param avg_r_g 输出:灰区平均R/G比值
     * @param avg_b_g 输出:灰区平均B/G比值
     */
    void lightSourceAnalysis(const Mat& img_float, int& temp, float& avg_r_g, float& avg_b_g) {
        int h = img_float.rows, w = img_float.cols;
        vector<float> r_g_list, b_g_list;
 
        for (int y = 0; y < h; ++y) {
            const Vec3f* row_ptr = img_float.ptr<Vec3f>(y);
            for (int x = 0; x < w; ++x) {
                float r = row_ptr[x][0];  // RGB通道:0=R,1=G,2=B
                float g = row_ptr[x][1];
                float b = row_ptr[x][2];
 
                // 避免除零错误
                if (g < 1e-6f) continue;
 
                // 计算R/G、B/G比值
                float rg = r / g;
                float bg = b / g;
 
                // 计算饱和度:saturation = (max - min) / max
                float max_rgb = max(max(r, g), b);
                float min_rgb = min(min(r, g), b);
                float saturation = (max_rgb - min_rgb) / (max_rgb + 1e-6f);
 
                // 灰区筛选条件:0.9≤R/G≤1.1,0.9≤B/G≤1.1,饱和度≤0.3
                if (rg >= 0.9f && rg <= 1.1f && bg >= 0.9f && bg <= 1.1f && saturation <= 0.3f) {
                    r_g_list.push_back(rg);
                    b_g_list.push_back(bg);
                }
            }
        }
 
        // 灰区不足5%,触发fallback(默认5500K)
        if (r_g_list.size() < 0.05f * h * w) {
            temp = 5500;
            avg_r_g = 1.0f;
            avg_b_g = 1.0f;
            return;
        }
 
        // 计算灰区平均比值
        avg_r_g = 0.0f;
        avg_b_g = 0.0f;
        for (size_t i = 0; i < r_g_list.size(); ++i) {
            avg_r_g += r_g_list[i];
            avg_b_g += b_g_list[i];
        }
        avg_r_g /= r_g_list.size();
        avg_b_g /= r_g_list.size();
 
        // 估算色温
        temp = estimateTemperature(avg_r_g, avg_b_g);
    }
 
    /**
     * 步骤3:核心校准(增益计算+CCM矩阵应用)
     * @param img_float 预处理后的图像(CV_32FC3)
     * @param temp 估算的色温(K)
     * @param avg_r_g 灰区平均R/G比值
     * @param avg_b_g 灰区平均B/G比值
     * @return 校准后的图像(CV_32FC3)
     */
    Mat coreCalibration(const Mat& img_float, int temp, float avg_r_g, float avg_b_g) {
        // 1. 计算增益(目标:R/G=1,B/G=1)
        float r_gain = 1.0f / avg_r_g;
        float b_gain = 1.0f / avg_b_g;
 
        // 增益约束:0.5~2.0(避免噪点或色彩溢出)
        r_gain = clamp(r_gain, 0.5f, 2.0f);
        b_gain = clamp(b_gain, 0.5f, 2.0f);
 
        // 增益平滑(动态场景防跳变:30%上一帧+70%当前计算值)
        r_gain = prev_r_gain_ * 0.3f + r_gain * 0.7f;
        b_gain = prev_b_gain_ * 0.3f + b_gain * 0.7f;
 
        // 更新上一帧增益
        prev_r_gain_ = r_gain;
        prev_b_gain_ = b_gain;
 
        // 2. 应用增益
        Mat calibrated = img_float.clone();
        for (int y = 0; y < calibrated.rows; ++y) {
            Vec3f* row_ptr = calibrated.ptr<Vec3f>(y);
            for (int x = 0; x < calibrated.cols; ++x) {
                row_ptr[x][0] *= r_gain;  // R通道增益
                row_ptr[x][2] *= b_gain;  // B通道增益
            }
        }
        clip(calibrated, 0.0f, 255.0f);
 
        // 3. 应用CCM矩阵(修正光谱响应偏差)
        Mat ccm = getCCMByTemp(temp);
        int h = calibrated.rows, w = calibrated.cols;
        Mat calibrated_reshaped = calibrated.reshape(1, h * w);  // 转为1行多列(3通道合并)
        Mat ccm_t;
        transpose(ccm, ccm_t);  // CCM矩阵转置(适配矩阵乘法)
        Mat calibrated_ccm_reshaped;
        gemm(calibrated_reshaped, ccm_t, 1.0f, Mat(), 0.0f, calibrated_ccm_reshaped, 0);  // 矩阵乘法
        Mat calibrated_ccm = calibrated_ccm_reshaped.reshape(3, h);  // 恢复为3通道图像
 
        return clip(calibrated_ccm, 0.0f, 255.0f);
    }
 
    /**
     * 步骤4:后处理(格式转换+色域校正)
     * @param img_float 校准后的图像(CV_32FC3)
     * @return 最终输出图像(CV_8UC3,BGR格式,适配OpenCV显示)
     */
    Mat postprocess(const Mat& img_float) {
        Mat img_uint8;
        img_float.convertTo(img_uint8, CV_8UC3);  // 转为8位无符号整数
        Mat img_bgr;
        cvtColor(img_uint8, img_bgr, COLOR_RGB2BGR);  // RGB转BGR(OpenCV默认格式)
        return img_bgr;
    }
 
    /**
     * AWB算法主函数
     * @param img_rgb 输入RGB图像(CV_8UC3)
     * @param temp 输出:估算的色温(K)
     * @return 校准后的BGR图像(CV_8UC3)
     */
    Mat run(const Mat& img_rgb, int& temp) {
        // 步骤1:预处理
        Mat img_pre = preprocess(img_rgb);
        // 步骤2:光源分析
        float avg_r_g, avg_b_g;
        lightSourceAnalysis(img_pre, temp, avg_r_g, avg_b_g);
        cout << "估算色温:" << temp << "K,灰区平均R/G:" << fixed << setprecision(2) << avg_r_g 
             << ",B/G:" << avg_b_g << endl;
        // 步骤3:核心校准
        Mat img_calibrated = coreCalibration(img_pre, temp, avg_r_g, avg_b_g);
        // 步骤4:后处理
        return postprocess(img_calibrated);
    }
};
 
 
int main(int argc, char** argv) {
    // 1. 检查输入参数(传入测试图像路径)
    if (argc != 2) {
        cout << "用法:" << argv[0] << " test_2700k.jpg)" << endl;
        return -1;
    }
 
    // 2. 读取输入图像(OpenCV默认BGR,需转RGB)
    Mat img_bgr = imread(argv[1]);
    if (img_bgr.empty()) {
        cout << "无法读取图像,请检查路径!" << endl;
        return -1;
    }
    Mat img_rgb;
    cvtColor(img_bgr, img_rgb, COLOR_BGR2RGB);
 
    // 3. 初始化AWB算法并执行
    AWBAlgorithm awb;
    int temp;
    Mat img_calibrated_bgr = awb.run(img_rgb, temp);
 
    // 4. 计算校准后客观指标(灰平衡偏差)
    Mat img_calibrated_rgb;
    cvtColor(img_calibrated_bgr, img_calibrated_rgb, COLOR_BGR2RGB);
    Mat img_float;
    img_calibrated_rgb.convertTo(img_float, CV_32FC3);
 
    float avg_r_g = 0.0f, avg_b_g = 0.0f;
    int count = 0;
    for (int y = 0; y < img_float.rows; ++y) {
        const Vec3f* row_ptr = img_float.ptr<Vec3f>(y);
        for (int x = 0; x < img_float.cols; ++x) {
            float r = row_ptr[x][0];
            float g = row_ptr[x][1];
            float b = row_ptr[x][2];
            if (g < 1e-6f) continue;
            avg_r_g += r / g;
            avg_b_g += b / g;
            count++;
        }
    }
    avg_r_g /= count;
    avg_b_g /= count;
    cout << "校准后平均R/G:" << fixed << setprecision(2) << avg_r_g 
         << ",B/G:" << avg_b_g << "(越接近1越好)" << endl;
 
    // 5. 显示并保存结果
    imshow("原始RAW", img_bgr);
    imshow(format("AWB校准后(%dK)", temp), img_calibrated_bgr);
    imwrite("wawawa.jpg", img_calibrated_bgr);
 
    waitKey(0);
    destroyAllWindows();
    return 0;
}

核心逻辑:
AWBAlgorithm
类封装了预处理、光源分析、核心校准、后处理四大模块,覆盖理论中的完整流程;可执行性:附带
main
函数作为测试入口,支持传入图像路径、显示原始 / 校准结果、保存输出、计算客观指标(灰平衡比值);依赖明确:基于 OpenCV 4.x,提供了 g++ 和 VS 的编译命令,配置环境后可直接编译运行。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容