在相机成像系统中,不同光源(如白炽灯、阴天、荧光灯)的光谱分布差异会导致图像出现“偏色”——比如白炽灯下画面偏黄、阴天环境下画面偏蓝。自动白平衡(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) | |
扣除暗电平、2×2 分块 LSC 校正、像素裁剪 |
| 光源分析(灰区 + 色温) | |
灰区筛选(R/G/B/G + 饱和度约束)、线性插值估色温 |
| 核心校准(增益 + CCM) | |
增益计算(1 / 平均比值)、CCM 矩阵插值与应用 |
| 后处理(平滑 + 格式转换) | + 增益平滑逻辑 |
增益线性平滑、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
解压后,核心文件路径为 (vc16对应VS2019/2022,vc15对应VS2017);
D:OpenCVuildx64vc16
配置系统环境变量: ① 右键“此电脑”→“属性”→“高级系统设置”→“环境变量”; ② 在“系统变量”→“Path”中添加:; ③ 重启电脑(环境变量生效)。
D:OpenCVuildx64vc16in
步骤2:创建VS项目并关联OpenCV
打开VS,创建“控制台应用”(C++),项目名称如“AWB_Project”;
设置项目为“x64”架构(OpenCV默认仅提供x64库,右上角“解决方案平台”选择“x64”,若无则新建);
配置“包含目录”(让编译器找到OpenCV头文件): ① 右键项目→“属性”→“配置属性”→“C/C++”→“常规”; ② “附加包含目录”中添加:; ③ 点击“应用”。
D:OpenCVuildinclude
配置“库目录”(让编译器找到OpenCV库文件): ① 同一属性页→“链接器”→“常规”; ② “附加库目录”中添加:; ③ 点击“应用”。
D:OpenCVuildx64vc16lib
配置“附加依赖项”(指定链接的库文件名): ① 同一属性页→“链接器”→“输入”; ② “附加依赖项”中添加以下2个库名(根据配置选择): Debug模式:(480对应OpenCV 4.8.0,d代表Debug); – Release模式:
opencv_world480d.lib; ③ 点击“确定”保存属性。
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函数作为测试入口,支持传入图像路径、显示原始 / 校准结果、保存输出、计算客观指标(灰平衡比值);依赖明确:基于 OpenCV 4.x,提供了 g++ 和 VS 的编译命令,配置环境后可直接编译运行。
main

















暂无评论内容