Camera和Image sensor技术基础笔记(4) -- 白平衡White Balance

白平衡到底是什么

        百度搜索“白平衡”,我们会发现有一句话很常见,大概意思是:“白平衡是一种功能,它让图片中的白色看起来就是白色”。啊,什么,白色为啥看起来不是白色?这涉及到人眼看到的白色和相机的sensor所“看到”的白色有什么不同。在解释白平衡之前,我们先来了解一下色温这个概念。

色温

        色温_百度百科

        光源的色温是指光源的辐射在可见光区和绝对黑体的辐射完全相同时黑体的温度就称此光源的色温。嗯,很好,又把人整晕了。我们来换一句不算精确但易于理解的话来说:色温就是光的颜色。

        下图显示了不同色温下对应的颜色。

        在晴天拍照时,光的色温较高,此时照片会偏冷色调;日出日落时,光的色温较低,此时照片会偏暖色调。不同的光有不同的颜色和特性。举例来说,钨丝灯的光偏黄,在多云天气下的太阳光偏蓝。

        人的眼睛会遵循一种“白色的物体看起来就应该是白色的”逻辑,自动进行偏色补偿来抵消环境光的影响。但是在相机所拍摄的图片中,相机所“看到”的颜色就是本身它所感知到的颜色(sensor的像素颜色)。这样的结果就是,由于环境光的不同,在相机所拍摄的图片中白色会有偏色,比如看起来偏黄或偏蓝,这和人眼所“应该看到”的白色是不同的。下面看两张照片:

图1  光源色温较低,白色的碟子偏黄

图2 光源色温较高,白色的雪偏蓝

白平衡

        通过对色温的认识,以及对相机sensor“所见”的白色与人眼所见白色的差异的了解,再来看“白色看起来是白色”的描述就容易理解了,可以理解为让相机“看到”的白色还原成是肉眼看到的白色的功能。

        白平衡的功能使用在相机中一般分为手动和自动两种方式。

        手动方式下,需要先用灰卡或白平衡卡在当前环境中作为相机白平衡功能的一个参考来设置白平衡参数。灰卡目前在专业的拍摄以及视频拍摄中才用到,白平衡卡没有灰卡那么精确。手动白平衡总的来说比自动白平衡对白色的还原度更精准。

        手动白平衡原理不复杂,我们主要来看自动白平衡。

自动白平衡

        如前文所述,人眼对于在不同光线条件下判断什么是白色的非常擅长,但是数码相机如果不做某些调整,则会捕获到带有严重色差的图片。自动白平衡Auto White Balance(AWB)算法所要做的是,使用最小的用户输入,来修正环境光的影响,让结果看起来和人眼所看到的差不多。

        自动白平衡一般通过两个步骤实现:

        1. 估算场景的色温。

        2. 修正图像的颜色(对sensor来说就是计算相关增益参数并设置)。

        主要的算法有:灰度世界算法、完美反射算法和动态阈值算法。

灰度世界算法

        原理:如果一幅图有足够的色彩变化时,可以认为它的RGB分量的值基本是相等的,即R平均 = G平均 = B平均,图像呈现灰色。

        步骤:

                1.\ K = (Ravg + Gavg + Bavg) / 3\ or\ K = MaxGrayValue / 2        

​                2.\ k_r = K/Ravg\ \ k_g = K/Gavg\ \ k_b = K /Bavg

                3.\ R' = R *k_r\ \ G' = G *k_g\ \ B' = B *k_b

        其中,Ravg,Gavg,Bavg是白平衡之前,所有像素各个分量的平均值;

        R、G、B是白平衡之前像素点的原始值; 

        R'、G'、B'是白平衡之后像素点的值

        opencv代码:

#include <opencv2\highgui\highgui.hpp>
#include <imgproc\imgproc.hpp>
using namespace cv;
using namespace std;
 
int main()
{
     Mat originImage = imread("test.jpg");
     if (!originImage.empty())
     {
          vector<Mat> imageRGB;
          // 分离RGB通道
          split(originImage, imageRGB);
		  
          // 求RGB分量的均值(opencv中排列顺序是B,G,R)
          double Ravg, Gavg , Bavg ;
		  
          Bavg  = mean(imageRGB[0])[0]; 
          Gavg  = mean(imageRGB[1])[0];
          Ravg = mean(imageRGB[2])[0];
 
          // 计算各分量的增益
		  double RGBavg = (Ravg + Gavg  + Bavg ) / 3;
          double k_r, k_g, k_b;
          k_r = RGBavg / Ravg;
          k_g = RGBavg / Gavg;
          k_b = RGBavg / Bavg;
 
 
          // 计算各通道变换后的灰度值
          imageRGB[0] = imageRGB[0] * k_b;
          imageRGB[1] = imageRGB[1] * k_g;
          imageRGB[2] = imageRGB[2] * k_r;
 
          // 输出图像
          merge(imageRGB, imageSource);
          imshow("Original image", imageSource);
          imshow("AWB using gray world algo", imageSource);
		  
          waitKey();
 
     }
     return 0;
}

        这个算法比较简单,处理一般的场景效果还行,但是当图片的颜色单一,或者单一色块面积占比较大时,灰度世界算法的假设是不满足的,结果会出现较大的偏差。

 完美反射算法(镜面法)

        原理:假设图像中最亮的点是白点,以这个白点为参考对图像进行计算,最亮点定义为(R,G,B)中的最大值。

        步骤:

        1. 遍历原图,得到RGB三通道之和的直方图RGBsum_hist,这个直方图统计了三通道之和的分布情况
           这个直方图实际上理解为C/C++的一个数组,数组下标代表了R+G+B之和,
           对应下标的值代R+G+B之和为这个下标的总的像素个数,
           比如对于一个像素R=1,G=1,B=1,那么这个像素会被统计到RGBsum_hist[3]这个位置,如果有n个这样的像素,那么RGBsum_hist[3] = n
        2. 遍历原图,找到所有像素点中的R,G,B的最大值BGRmax
        3. 设置一个比例r(例如10%),反向遍历RGBsum_hist直方图,统计像素个数n,如果n > 总像素个数 * r
           停止遍历,记录此时的阈值T,T = 反向遍历停止时,走到RGBsum_hist的下标  
        4. 遍历原图,找到RGB之和大于T的像素,对各通道取平均得到Ravg,Gavg,Bavg
        5. 遍历原图,计算每个像素R、G、B的值,Xout = X / Xavg * Xmax(X代表R、G、B)如果Xout溢出,则截断即可

        opencv代码

#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>
#include <time.h>
 
using namespace cv;
using namespace std;
 
Mat AWB_perfect_reflection_algo(Mat src) {
     int row = src.rows;
     int col = src.cols;
     Mat dst(row, col, CV_8UC3);
     int RGB_sumHist[767] = { 0 };
     int BGRmax = {0};
     int sum;
 
     cout << __func__ << "Finding RGB max" << endl;
	 //找到Rmax,Gmax,Bmax以及生成RGB三通道之和的直方图
     for (int i = 0; i < row; i++) {
          for (int j = 0; j < col; j++) {
                BGRmax = max(BGRmax, (int)src.at<Vec3b>(i, j)[0]);
                BGRmax = max(BGRmax, (int)src.at<Vec3b>(i, j)[1]);
                BGRmax = max(BGRmax, (int)src.at<Vec3b>(i, j)[2]);
 
                sum = src.at<Vec3b>(i, j)[0] + src.at<Vec3b>(i, j)[1] + src.at<Vec3b>(i, j)[2];
                RGB_sumHist[sum]++;
          }
 
     }
 
     int threshold = 0;
     int pixel_count = 0;
     double pixel_ratio = 0.1;
 
     cout << __func__ << "Finding threshold" << endl;
     sum = 0;
	 //反向遍历直方图,找出阈值T
     for (int i = 766; i >= 0; i--) {
          sum += RGB_sumHist[i];
          if (sum > (int)(row * col * pixel_ratio)) {
                threshold = i;
                break;
          }
     }
 
     float Bavg = 0;
     float Gavg = 0;
     float Ravg = 0;
     int cnt = 0;
 
     cout << "Threshold is: " << threshold << endl;
     cout << __func__ << "Finding R,G,B avarage" << endl;
	 //找出大于阈值T的所有像素的R,G,B平均值
     for (int i = 0; i < row; i++) {
          for (int j = 0; j < col; j++) {
                int sumP = src.at<Vec3b>(i, j)[0] + src.at<Vec3b>(i, j)[1] + src.at<Vec3b>(i, j)[2];
 
                if (sumP > threshold) {
                      Bavg += src.at<Vec3b>(i, j)[0];
                      Gavg += src.at<Vec3b>(i, j)[1];
                      Ravg += src.at<Vec3b>(i, j)[2];
                      cnt++;
                }
          }
     }
 
     Bavg /= cnt;
     Gavg /= cnt;
     Ravg /= cnt;
     float Bgain = (float)BGRmax / Bavg;
     float Ggain = (float)BGRmax / Gavg;
     float Rgain = (float)BGRmax / Ravg;
	 
     cout << __func__ << "Generating dst image..." << endl;
	 //对每个像素进行转换
     for (int i = 0; i < row; i++) {
          for (int j = 0; j < col; j++) {
                int Bnew = src.at<Vec3b>(i, j)[0] * Bgain;
                int Gnew = src.at<Vec3b>(i, j)[1] * Ggain;
                int Rnew = src.at<Vec3b>(i, j)[2] * Rgain;
 
                if (Rnew > 255) {
                      Rnew = 255;
                }
 
                if (Gnew > 255) {
                      Gnew = 255;
                }
 
                if (Bnew > 255) {
                      Bnew = 255;
                }
 
                dst.at<Vec3b>(i, j)[0] = Bnew;
                dst.at<Vec3b>(i, j)[1] = Gnew;
                dst.at<Vec3b>(i, j)[2] = Rnew;
          }
     }
 
     return dst;
}
 
int main() {
     Mat src = imread("lena.jpg"); 
     Mat dst = AWB_perfect_reflection_algo(src);
 
     imshow("Original image", src);
     imshow("AWB using perfect reflection algo", dst);
     waitKey(0);
 
     return 0;
}

        完美反射算法比灰度世界算法稍好,但效果依赖于比例参数的选择。此算法对于最亮区域不是白色的图来说效果不好。

动态阈值算法

        原理:将RGB变化到YCrCb色彩空间,分析确定参考白点的阈值,这个阈值是动态变化的,因此算法叫做动态阈值算法。

        步骤:

        1. 将图像从RGB空间转换到YCrCb空间

        2. 分析确定白色参考点:

            a. 图像分块,分块的数量可以自定义,比如按照宽高比4:3划分为12块

            b. 每个块分别计算Cr,Cb的平均值Mr,Mb,根据Mr,Mb用公式分别计算Cr,Cb和绝对偏差的均值Dr,Db。

            M_r = \sum C_r(i,j) / N\ \ \ M_b = \sum C_b(i,j) /N

            Dr = \sum |C_r(i,j) - M_r| / N\ \ \ Db = \sum |C_b(i,j) - M_b| / N

             可选:如果Db,Dr过于小(比如0.01),则忽略这个块。Db,Dr偏小说明这块颜色分布均匀。

            c. 选择候选白点,某个像素的Cb,Cr如果满足以下条件则会被选入候选白点:

            |C_b(i,j) - (M_b + D_b * sign(M_b))| < |1.5 * D_b|

            |C_r(i,j) - (1.5*M_r + D_r * sign(M_r))| < |1.5 * D_r|

            d. 选择参考白点,将候选白点的像素亮度值由高到低排序,取出候选白点亮度值前10%的白点作为参考白点。这10%的点里面,亮度最小的值作为阈值T。

       3.  遍历原图,找出白点并标记对应坐标i,j的像素点待处理,白点的定义是其亮度值Y > 阈值T。

            标记的实现可以使用一个数组辅助比如is_white_point[i][j] = 1表示是白点,0表示非白点

        4. 遍历原图,计算所有白点的R,G,B分量平均值

            依次对标记为白点的像素点,取R,G,B分别相加,然后除以参考白点的个数(d步骤里的白点数量)得到Ravg,Gavg,Bavg

        5.  获得Ymax亮度最大值,计算R,G,B增益Rgain,Ggain,Bgain

             Rgain = Ymax /Ravg

             Ggain = Gmax /Gavg

             Bgain = Bmax / Bavg

        6. 遍历原图,对每个像素点应用增益参数

            R' = R * Rgain\ \ \ G' = G * Ggain\ \ \ B = B * Bgain

            R、G、B为原始像素点的分量值,R'、G'、B'为调整后的值

        这个算法用opencv写的话代码较长,这里提供matlab代码参考

        

original_image=im2double(imread('test.jpg'));
[m,n,k] = size(original_image);
%获取三通道值
R = original_image(:,:,1);
G = original_image(:,:,2);
B = original_image(:,:,3);

%YUV -> RGB
Y = 0.257*R+0.504*G+0.098*B+16/255;
Cb = -0.148*R-0.291*G+0.439*B+128/255;
Cr = -0.439*R-0.368*G-0.071*B+128/255;
 
%分块
row = m/3;
col = n/4; 
 
count = 1;
Mb = 0;
Mr = 0;
Db = 0;
Dr = 0;
 
for i=1:row:m
    for j=1:col:n
        Ib = Cb(i:1:i+row-1,j:1:j+col-1); %每分块的Cb值
        Ir = Cr(i:1:i+row-1,j:1:j+col-1);
 
        Mb_block = mean(mean(Ib)); % 分块的Cb均值
        Mr_block = mean(mean(Ir)); % 分块的Cr均值
 
        Db_block = sum(sum(abs(Ib-Mb_block)))/(row*col);% 分块的绝对偏差
        Dr_block = sum(sum(abs(Ir-Mr_block)))/(row*col);

        Mb(count) = Mb_block;
        Mr(count) = Mr_block; 
%         if Db_block>0.01 && Dr_block>0.01  % 可选:判断该分块的绝对偏差是否够大
            Db(count) = Db_block;
            Dr(count) = Dr_block;
            count = count+1;
%         end
    end
end
  
%计算整体的Cb,Cr均值和绝对偏差
Mb = mean(Mb); 
Mr = mean(Mr);
Db = mean(Db);
Dr = mean(Dr);

% 记录候选白点的位置信息,若(i,j)位置为1,代表该位置的像素为候选白点
J = zeros(m,n); 
   
for i=1:1:m
    for j=1:1:n
        bv = abs(Cb(i,j)-(Mb+Db*sign(Mb)));
        rv = abs(Cr(i,j)-(1.5*Mr+Dr*sign(Mr)));

        if (bv<1.5*Db) && (rv<1.5*Dr)
            J(i,j) = 1;
        end
    end
end


white_point_candidates = reshape(Y.*J,m*n,1);
% 候选白点的亮度值Y从大到小排序
white_point_candidates = sort(white_point_candidates,'descend'); 

first_ten_percent_index = round(sum(sum(J))*0.1);
% 得到前10%的最小值
min_v = white_point_candidates(first_ten_percent_index); 
  
%找出亮度大于阈值的点
Y1 = (Y>(ones(m,n)*min_v));  
R1 = R.*Y1; 
G1 = G.*Y1;
B1 = B.*Y1;
%计算白点的RGB三分量平均值
Ravg = sum(sum(R1))/sum(sum(Y1)); 
Gavg = sum(sum(G1))/sum(sum(Y1));
Bavg = sum(sum(B1))/sum(sum(Y1));

%找出亮度最大值
Ymax = double(max(max(Y)));
 
% 计算增益
Rgain = Ymax/Ravg; 
Ggain = Ymax/Gavg;
Bgain = Ymax/Bavg;
 
%应用增益,做白平衡计算
original_image(:,:,1) = original_image(:,:,1)*Rgain;
original_image(:,:,2) = original_image(:,:,2)*Ggain;
original_image(:,:,3) = original_image(:,:,3)*Bgain;

        

  • 4
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

亦枫Leonlew

希望这篇文章能帮到你

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值