从数码相机直接输出的未经过处理过的RAW图到平常看到的JEPG图有一系列复杂的图像信号处理过程,称作ISP(Image Signal Processing)。这个过程会经过图像处理和压缩。
参考文章1:http://t.csdn.cn/LvHH5
参考文章2:http://t.csdn.cn/c97t5
参考文章3:http://t.csdn.cn/UbAOu
http://t.csdn.cn/HuI67参考文章4:http://t.csdn.cn/HuI67
参考文章(下面整理的笔记):Understanding White Balance Control - 知乎 (zhihu.com)
人眼具有颜色恒常性,可以避免光源变化带来的颜色变化,但是图像传感器不具备这种特性,从而造成色偏,白平衡就是需要校正这个颜色的偏差。
颜色恒常性是指在照度发生变化的条件下人们对物体表面颜色的知觉趋于稳定的心理倾向。
色温描述的是具有一定表面温度的“黑体”(blackbody)的辐射光的光谱特性。简单的理解就是颜色随温度的变化规律,比如生铁就是黑色,加热会变成橘红色,继续加热到液态会呈现偏白的颜色,这种随温度而产生的颜色变化就光谱特性。
白平衡校正
-
手动白平衡:在拍照前通过拍摄一个18度灰的卡片,然后计算出当时环境的白平衡增益值对后面的图片进行校正;
-
自动白平衡:相机通过本身的算法,通过获取的图像自动计算出增益值对图像进行校正的方式。
AWB(Automatic White Balance),自动白平衡理解
为了将拍摄场景中的白色物体在显示时正确还原为白色,首先需要知道真实的白色物体在sensor RGB 空间中呈现什么颜色。常用的方法是通过实验标定标准白色在一些典型色温下呈现的sensor RGB 颜色,然后通过几个标定的色温点可以外推白色在所有可能色温下呈现的颜色,从而建立场景色温与sensor RGB 白色的对应关系,这个对应关系通常用白平衡增益来描述。
白平衡增益通常记为(R/G, B/G),即红色和蓝色相对于绿色的比例。目前对白平衡增益存在两种不同的定义:
1.一部分厂家将(R/G, B/G)定义为sensor捕捉到的图像中红色和蓝色的统计值,因此在高色温(如D65)下B/G大于R/G。
海思的光源的色温曲线(普朗克曲线)表现为下图所示的形状。
通过对不同sensor的色温曲线标定数据可知:在D50-D65之间 R/G、B/G的比值基本相近,相差不大。在低色温的时候,主要是G和B分量在改变,在高色温的时候主要是G和B分量在改变。
以海思的色温曲线为例:从直角双曲线的两个坐标方向来看,在D50-D65这个R/G、B/G为分界点,沿着R/G的方向看,最后应该会有一个B/G的饱和点。同理,沿B/G方向看,R/G也会有一个饱和点,饱和区域的时候其两个坐标方向上看应该呈现一条类似接近直线的曲线段。
或者也可以这么理解:
色温越低,R/G的比值越大,B/G的比值越小,当R/G达到某一阈值后,B/G则缓慢变小直至达到一个最小值而不再改变。
同理,色温越高,B/G的比值越大,R/G的比值越小,当B/G达到某一阈值后,R/G则缓慢变小直至达到一个最小值而不再改变。
直角双曲线靠近坐标的部分与坐标轴近似平行的直线段,比较符合色温曲线的变化规律。
根据G/R和G/B用直角双曲线函数拟合的图像和海思的普朗克曲线十分相似,如下图所示。
在这种定义下,D65光源下的白色具有较大的B/G统计值。
2.另一部分厂家则将(R/G, B/G)定义成为了使sensor捕捉的图像达到白平衡需要施加的增益系数,因此在高色温下B/G小于R/G。
在这种定义下,D65光源下的白色需要较大的R/G增益才能达到白平衡。
以R/G,B/G值代表增益系数,而非图像的统计值,在此定义下,白平衡标定的大致过程是:
- 以标定好的若干个色温点(A, TL84, D50, D65, D75等)为核心可以拟合出一条色温曲线。
- 以色温曲线上的点为圆心,以一定半径画椭圆,可以得到一些列椭圆。
- 所有椭圆所覆盖的(R/G,B/G)集合就认为是真实场景中的白色物体在全部色温下的可能值。不落在这个范围内的颜色不认为是白色。
某些特殊光源(如CWF)光谱特征偏离黑体光谱较大,因此需要单独标定。
一个好的白平衡算法需要能够检测出画面中存在的特殊场景并加以针对性的强化。点云分析是一种非常有效的提取图像特征的方法,但由于需要分析每一个像素的白平衡增益,所以计算量非常大。
理想的白平衡控制时序如下图所示,其基本流程是:
- 在第N帧图像结束时ISP硬件采集关于白平衡的统计信息
- 白平衡算法根据第N帧的统计数据预测第N+1帧的控制参数
- 摄像机固件在第N+1帧图像到达之前将白平衡控制参数写入ISP硬件寄存器
- 第N+1帧图像使用新的白平衡参数进行曝光
但是实际上由于两帧的时间间隔往往很小,不容易保证算法完成一系列的计算和配置,因此实际的白平衡控制往往采用隔帧生效的时序,其基本流程如下图所示
- 在第N帧图像结束时ISP硬件采集关于白平衡的统计信息
- 在第N+1帧开始后,白平衡算法根据第N帧的统计数据预测第N+2帧的控制参数,需要在第N+1帧结束前完成所有计算
- 摄像机固件在第N+2帧图像到达之前将新的白平衡控制参数写入ISP硬件寄存器
- 第N+2帧图像使用新的白平衡参数进行曝光
白平衡校正算法
1.灰度世界算法
基于一个假说:任一幅图像,当它有足够多的色彩变化,则它的RGB分量的均值会趋于相等。这是一个在自动白平衡方面应用极为广泛的理论。对此算法的流程如下:
- 计算各个颜色通道的平均值;
- 寻找一个参考值K,一般情况选取Gmean;
- 计算Rgain = Gmean/Rmean, Bgain = Gmean/Bmean;
- 对图像中的每个像素都乘以对应的gain值进行校正;
- 以下C++代码,不知道本人缺少什么运行条件,在自己电脑上最后显示结果不太理想;大家也可以试试,该代码出自:http://t.csdn.cn/edydD
//自动白平衡 //灰度世界算法
void GrayWorldAlgorithm(Mat& src,Mat& dst)
{
assert(3==src.channels());
//求BGR分量均值
auto mean = mean(src);
//需要调整的BGR分量的增益
float gain_B(0),gain_G(0),gain_R(0);
float K = (mean[0]+mean[1]+mean[2])/3.0f;
gain_B = K/mean[0];
gain_G = K/mean[1];
gain_R = K/mean[2];
vector<Mat> channels;
split(src,channels);
//调整三个通道各自的值
channels[0] = channels[0]*gain_B;
channels[1] = channels[1]*gain_G;
channels[2] = channels[2]*gain_R;
//通道合并
cv::merge(channels,dst);
}
下面的灰度世界法是本人自己根据理解写的,但是显示结果存疑。有点怀疑是不是BGR的赋值跟输入图像的拜尔分布有关,赋值顺序有误?有能力者找到错误的可以帮我纠出(感谢感谢):
//灰度世界法,输入三通道彩色图
Mat awbgray(Mat img_rgb8)
{
Mat img_awb = Mat::zeros(height, width, CV_32FC3);
Mat imgR = Mat::zeros(height, width, CV_8UC1);
Mat imgG = Mat::zeros(height, width, CV_8UC1);
Mat imgB = Mat::zeros(height, width, CV_8UC1);
double Rsum=0, Gsum=0,Bsum=0;
/*unsigned char* srcdata;
unsigned char *dstdata;*/
for (int row = 0; row < height; row++)
{
//ptr得到行数据的头指针,得到row行指针
/*uchar* data = img_awb.ptr<uchar>(row);*/
Vec3b* inptr = img_rgb8.ptr<Vec3b>(row);
for (int col = 0; col < width; col++)
{
//BGGR分布?
//imgB.at<Vec3b>(row, col)[0] b通道
/*uchar bdata = data[col * img_awb.channels() + 0];*/
imgB.at<uchar>(row, col) = (*(inptr + col))[0];
imgG.at<uchar>(row, col) = (*(inptr + col))[1];
imgR.at<uchar>(row, col) = (*(inptr + col))[2];
Bsum += imgB.at<uchar>(row, col);
Gsum += imgG.at<uchar>(row, col);
Rsum += imgR.at<uchar>(row, col);
}
}
double Rmean=0, Gmean=0, Bmean = 0;
Rmean = Rsum / (height * width);
Gmean = Gsum / (height * width);
Bmean = Bsum / (height * width);
double K = (Rmean + Gmean + Bmean) / 3;
cout << "Rmeanvalue:" << Rmean << endl;
cout << "Gmeanvalue:" << Gmean << endl;
cout << "Bmeanvalue:" << Bmean << endl;
cout << "rgbmeanvalue:" << K << endl;
double Rgain = K / Rmean;
double Ggain = K / Gmean;
double Bgain = K / Bmean;
//重新调整计算RGB值
for (int row = 0; row < height; row++)
{
Vec3b* inptr = img_rgb8.ptr<Vec3b>(row);
Vec3f* outptr = img_awb.ptr<Vec3f>(row);
for (int col = 0; col < width; col++)
{
(*(outptr + col))[2] = Rgain * (*(inptr + col))[2];
(*(outptr + col))[1] = Ggain * (*(inptr + col))[1];
(*(outptr + col))[0] = Bgain * (*(inptr + col))[0];
}
}
//convertScaleAbs(img_awb, img_awb);//转为CV_8UC1
/*imshow("img_awb", img_awb);
waitKey(0);*/
return img_awb;
}
该方法运行得到的结果,通过图像监视观察函数中的图像变化,放大可以查看像素值,没有在VS安装的可以自行安装Imagewatch,安装教程:VS2022安装Image Watch插件_image watch for visual studio 2022_Aqder的博客-CSDN博客
感觉处理后的结果不太理想,不知道是不是代码有误,可指出。
(左为输入,右为输出,不是一一对应的截图):
2.完美反射法
(perfect Reflector)基于这样一种假设,一幅图像中最亮的像素相当于物体有光泽或镜面上的点,它传达了很多关于场景照明条件的信息。如果景物中有纯白的部分,那么就可以直接从这些像素中提取出光源信息。因为镜面或有光泽的平面本身不吸收光线,所以其反射的颜色即为光源的真实颜色,这是因为镜面或有光泽的平面的反射比函数在很长的一段波长范围内是保持不变的。那么在这个假设下,图像中就一定存在一个纯白色的的像素或者最亮的点。
完美反射法就是利用这种特性来对图像进行调整。算法执行时,将待检测图像中亮度最高的像素作为参考白点,以此点为基础就可计算出gain值从而进行校正。完美反射算法流程如下:
(1)遍历原始图像,统计RGB三通道之和的直方图;
(2)遍历原始图像,找到RGB三通道各自的最大值Bmax、Gmax、Rmax
(3)设定比例 r ,对RGB之和的直方图进行倒叙遍历,找到使白点像素个数超过总像素个数比例的阈值,T;
(4)遍历原始图像,计算RGB之和大于 T 的像素,各个通道取平均,得到Bavg、Gavg、Ravg;
(5)遍历原始图像,分别计算RGB三通道的调整值Aout=A / Aavg * Amax;
(6)防溢出处理,这里可以采用简单的截断即可。
有看到其他博文资料不使用比例和阈值的完美反射法,但是运行结果和上面一样存疑。
设定比例r为10%,使用C++,代码如下:
//白平衡校正,完美反射法,使用比例r和阈值T,
Mat awbreflect2(Mat img_rgb8)
{
int histrgbsum[255 * 3 + 1] = { 0 };
double Rmax = 0, Gmax = 0, Bmax = 0;
//uchar maxrgb[3] = { 0 };
for (int row = 0; row < height; row++)
{
const uchar* inptr = img_rgb8.ptr<uchar>(row);
for (int col = 0; col < width; col++)
{
//统计RGB三通道之和的直方图
int sum = *(inptr + 3 * col) + *(inptr + 3 * col + 1) + *(inptr + 3 * col + 2);
histrgbsum[sum]++;
//找到RGB三通道各自的最大值Bmax、Gmax、Rmax
Bmax = max(Bmax, (double)*(inptr + 3 * col));
Gmax = max(Gmax, (double)*(inptr + 3 * col + 1));
Rmax = max(Rmax, (double)*(inptr + 3 * col + 2));
/*maxrgb[0] = max(maxrgb[0], *(inptr + 3 * col));
maxrgb[1] = max(maxrgb[1], *(inptr + 3 * col+1));
maxrgb[2] = max(maxrgb[2], *(inptr + 3 * col+2));*/
}
}
//设定比例r为10%
double num = 0,ratio=0.1;
int threshold = 0;
int len = 0;
len=sizeof(histrgbsum) / sizeof(histrgbsum[0]);
//int len = end(histrgbsum) - begin(histrgbsum);
cout << "histagram length:" << len << endl;
for (len; len >= 0; len--)
{
num += histrgbsum[len];
//计算R+G+B的数量超过像素总数的ratio的像素值
if (num > height * width * ratio)
{
//使白点像素个数超过总像素个数的比例时,为阈值T
threshold = len;
break;
}
}
//计算RGB之和大于 T 的像素,对大于阈值的像素各通道取平均,得到Bavg、Gavg、Ravg;
double Rsum = 0, Gsum = 0, Bsum = 0;
double Ravg = 0, Gavg = 0, Bavg = 0;
int pixnum = 0;
for (int row = 0; row < height; row++)
{
const uchar* inptr = img_rgb8.ptr<uchar>(row);
for (int col = 0; col < width; col++)
{
//计算RGB之和,上面的局部变量又用一遍
int sum = *(inptr + 3 * col) + *(inptr + 3 * col + 1) + *(inptr + 3 * col + 2);
if (sum > threshold)
{
Bsum += *(inptr + 3 * col);
Gsum += *(inptr + 3 * col + 1);
Rsum += *(inptr + 3 * col + 2);
pixnum++;
}
}
}
Ravg = Rsum / (double)pixnum;
Gavg = Gsum / (double)pixnum;
Bavg = Bsum / (double)pixnum;
//创建与输入图像一样大小类型的矩阵
Mat img_awb = Mat::zeros(img_rgb8.size(), img_rgb8.type());
//量化0-255,重新计算RGB值,分别计算RGB三通道的调整值Aout=A / Aavg * Amax
double Rout = 0, Gout = 0, Bout = 0;
for (int row = 0; row < height; row++)
{
const uchar* inptr = img_rgb8.ptr<uchar>(row);
uchar* outptr = img_awb.ptr<uchar>(row);
for (int col = 0; col < width; col++)
{
Bout = (double)*(inptr + 3 * col) / Bavg * Bmax;
Gout = (double)*(inptr + 3 * col+1) / Gavg * Gmax;
Rout = (double)*(inptr + 3 * col+2) / Ravg * Rmax;
Bout = min(max((double)0, Bout), (double)255);
Gout = min(max((double)0, Gout), (double)255);
Rout = min(max((double)0, Rout), (double)255);
//将计算好的RGB值赋给新矩阵
*(outptr + 3 * col) = (uchar)Bout;
*(outptr + 3 * col+1) = (uchar)Gout;
*(outptr + 3 * col+2) = (uchar)Rout;
}
}
return img_awb;
}
断点运行之后,图像监视下白平衡处理后得到的结果(左输入,右输出,不是一一对应的截图):
3.动态阈值法
YUV颜色空间(亦称YCrCb)主要用于优化彩色视频信号的传输,Y表示亮度,U和V表示色度(色调和饱和度)。亮度是通过RGB输入信号来建立的,方法是将RGB信号的特定部分叠加到一起。“色度”则定义了颜色的两个方面─色调与饱和度,分别用Cr和Cb来表示。其中,Cr反映了RGB输入信号红色部分与RGB信号亮度值之间的差异。而Cb反映的是RGB输入信号蓝色部分与RGB信号亮度值之同的差异。
动态阈值算法通过将RGB变化到YCrCb颜色空间进行分析来确定白点,其选择参考白点的阈值是动态变化的。我们通过对图片的YCrCb坐标空间的分析,可以找到一个接近白色的区域,该区域是包含着参考白点的,通过设定一个阈值来规定某些点为参考白点。因此该算法是一个动态的自适应白平衡算法。
白平衡算法通常分为两步:白色点的检测,白色点的调整。本方法采用一个动态的阀值来检测白色点。详细算法过程参考如下:
-
把图像w*h从RGB空间转换到YCrCb空间。转换公式如下:
-
通过限定YUV的区域来判断是否为白点,通过四个限制条件俩限制白点,满足条件的点就是白点,参与后续的计算,否则,点直接舍弃。 (1)首先,为了增强算法的鲁棒性,将图像分为12部分,把图像分成宽高比为4:3个块(块数可选)。 (2)然后对每个块,分别计算Cr,Cb的平均值Mr,Mb。 (3)再对每个块,根据Mr,Mb,用下面公式分别计算Cr,Cb的方差Dr,Db。(4)最后 判定每个块的近白区域(near-white region)。判别准则为:其中sign为符号函数,即正数返回1,负数返回0。
-
设一个“参考白色点”的亮度矩阵RL,大小为w*h。
若符合判别式,则作为“参考白色点”,并把该点(i,j)的亮度(Y分量)值赋给RL(i,j);若不符合,则该点的RL(i,j)值为0。
-
上面几步为白点检测,下面几步为白点调整:
(1)选取参考“参考白色点”中最大的10%的亮度(Y分量)值,并选取其中的最小值Lu_min;
(2)调整RL,若RL(i,j)<Lu_min, RL(i,j)=0; 否则,RL(i,j)=1;
(3)分别把R,G,B与RL相乘,得到R2,G2,B2。 分别计算R2,G2,B2的平均值,得到Rav,Gav,Bav;
(4)得到调整增益: Ymax=double(max(max(Y));
Rgain=Ymax/Rav;
Ggain=Ymax/Gav;
Bgain=Ymax/Bav;
(5)调整原图像:Ro= R*Rgain; Go= G*Ggain; Bo= B*Bgain。
实现代码本人未尝试,C++可以参考(过程较为复杂):
OpenCV图像处理专栏十一 | IEEE Xplore 2015的图像白平衡处理之动态阈值法 (qq.com)
4.其他算法
除了上面比较常见的几种,还有基于模糊逻辑,基于色温,基于边缘和多方法融合法等等。