前言
上篇文章介绍了基于VC++的人脸美颜软件的具体原理和编程细节,其中提到了美白效果由于难度和时间关系,借用了另一套图像增强-非锐化掩蔽算法。本篇将着重介绍白平衡人脸美白算法,并给出基于MATLAB和C++的两种算法实现代码。
开发环境
Visual Studio 2012 + Opencv 2.4.9 +MFC框架
MATLAB R2016a
算法原理
白平衡法美白主要分为两个阶段
(1)近白色点的检测,作为参考白点
(2)根据近白点算出RGB调整比例
实现步骤
YCrCb空间介绍
YCrCb又称YUV,主要用于优化彩色视频信号的传输,使其向后兼容老式黑白电视。
Y分量表示图像的亮度成分
Cr分量表示图像红色部分与亮度值之间的差异
Cb分量表示图像蓝色部分与亮度值之间的差异
以lena图为例,分别展示YCrCb三通道的实际分离效果
YCrCb的应用
(摘自百度百科)
在人脸检测中也常常用到YCrCb空间,因为一般的图像都是基于RGB空间的,在RGB空间里人脸的肤色受亮度影响相当大,所以肤色点很难从非肤色点中分离出来,也就是说在此空间经过处理后,肤色点是离散的点,中间嵌有很多非肤色,这为肤色区域标定(人脸标定、眼睛等)带来了难题。如果把RGB转为YCrCb空间的话,可以忽略Y(亮度)的影响,因为该空间受亮度影响很小,肤色会产生很好的类聚。这样就把三维的空间降为二维的CrCb,肤色点会形成一定得形状,如:人脸的话会看到一个人脸的区域,手臂的话会看到一条手臂的形态,对处理模式识别很有好处,根据经验某点的CrCb值满足:133≤Cr≤173,77≤Cb≤127 那么该点被认为是肤色点,其他的就为非肤色点。
参考白点检测
根据公式所计算得到的亮点矩阵Rl如下图所示,其中判别阈值Lu_min在180~190左右。
RGB增益调整
原始图像经调整后的图像如下所示:
MATLAB实现代码
im = imread('te.jpg');
im1=rgb2ycbcr(im);%将图片的RGB值转换成YCbCr值%
YY=im1(:,:,1);
Cb=im1(:,:,2);
Cr=im1(:,:,3);
[x y z]=size(im);
tst=zeros(x,y);
Mb=mean(mean(Cb));
Mr=mean(mean(Cr));
%计算Cb、Cr的均方差%
Tb = Cb-Mb;
Tr = Cr-Mr;
Db=sum(sum((Tb).*(Tb)))/(x*y);
Dr=sum(sum((Tr).*(Tr)))/(x*y);
%根据阀值的要求提取出near-white区域的像素点%
cnt=1;
for i=1:x
for j=1:y
b1=Cb(i,j)-(Mb+Db*sign(Mb));
b2=Cr(i,j)-(1.5*Mr+Dr*sign(Mr));
if (b1<abs(1.5*Db) && b2<abs(1.5*Dr))
Ciny(cnt)=YY(i,j);
tst(i,j)=YY(i,j);
cnt=cnt+1;
end
end
end
cnt=cnt-1;
iy=sort(Ciny,'descend');%将提取出的像素点从亮度值大的点到小的点依次排列%
nn=round(cnt/10);
Ciny2(1:nn)=iy(1:nn);%提取出near-white区域中10%的亮度值较大的像素点做参考白点%
%提取出参考白点的RGB三信道的值%
mn=min(Ciny2);
for i=1:x
for j=1:y
if tst(i,j)<mn
tst(i,j)=0;
else
tst(i,j)=1;
end
end
end
R=im(:,:,1);
G=im(:,:,2);
B=im(:,:,3);
R=double(R).*tst;
G=double(G).*tst;
B=double(B).*tst;
%计算参考白点的RGB的均值%
Rav=mean(mean(R));
Gav=mean(mean(G));
Bav=mean(mean(B));
Ymax=double(max(max(YY)))*0.15;%计算出图片的亮度的最大值%
%计算出RGB三信道的增益%
Rgain=Ymax/Rav;
Ggain=Ymax/Gav;
Bgain=Ymax/Bav;
%通过增益调整图片的RGB三信道%
im(:,:,1)=im(:,:,1)*Rgain;
im(:,:,2)=im(:,:,2)*Ggain;
im(:,:,3)=im(:,:,3)*Bgain;
imshow(im);
C++实现代码
/*读取原始图片*/
Mat src;
int width = input_image.cols;
int heigh = input_image.rows;
input_image.copyTo(src);
/*将图片从RGB空间转换至YCrCb空间*/
Mat Y,Cr,Cb;
Mat dst_YcrCb;
vector<Mat> channels;
cvtColor(src,dst_YcrCb,CV_BGR2YCrCb);
split(dst_YcrCb,channels);
Y = channels.at(0);
Cr = channels.at(1);
Cb = channels.at(2);
/*分别计算Cr,Cb的平均值Mr,Mb*/
double Mr,Mb;
Mr = mean(Cr)[0]; //求Cr均值
Mb = mean(Cb)[0]; //求Cb均值
/*分别计算Cr,Cb的方差Dr,Db*/
double Dr,Db;
Mat Tr,Tb;
Tr = Cr - Mr;
Tb = Cb - Mb;
Db = sum(Tb.mul(Tb))[0]/(width*heigh);
Dr = sum(Tr.mul(Tr))[0]/(width*heigh);
/*
设一个大小与图片相等的矩阵Rl,根据公式判断白色参考点
Cb(i,j)-(Mb+Db) < 1.5*Db && Cr(i,j)-(1.5Mr+Dr) < 1.5*Dr
若符合公式,把该点的亮度(Y分量)赋给Rl(i,j)
若不符合公式,则该点的Rl(i,j)值为0
*/
Mat Rl(heigh,width,CV_8UC1,Scalar(0));
Mat Ciny(1,width*heigh,CV_8UC1,Scalar(0));
double b1,b2;
int cnt = 0;
for(int i = 0;i< heigh;i++)
{
for(int j = 0;j< width;j++)
{
b1 = Cb.at<uchar>(i,j) - (Mb + Db);
b2 = Cr.at<uchar>(i,j) - (1.5 * Mr + Dr);
if(b1 < 1.5*Db && b2 < 1.5*Dr)
{
Ciny.at<uchar>(0,cnt) = Y.at<uchar>(i,j);
Rl.at<uchar>(i,j) = Y.at<uchar>(i,j);
cnt = cnt + 1;
}
}
}
/*提取参考白点中亮度较大的前10%,并选取其中的最小值Lu_min*/
cv::sort(Ciny,Ciny,CV_SORT_DESCENDING);
int nn = cvRound(cnt/10);
Mat Ciny2(1,nn,CV_8UC1,Scalar(0));
for(int k = 0;k<nn;k++)
{
Ciny2.at<uchar>(0,k) = Ciny.at<uchar>(0,k);
}
double maxx, minn;
minMaxIdx(Ciny2, &minn, &maxx);
/*调整Rl,若Rl(i,j)<Lu_min,Rl(i,j)=0,否则,Rl(i,j)=1*/
for(int i = 0;i<heigh;i++)
{
for(int j = 0;j<width;j++)
{
if(Rl.at<uchar>(i,j) < minn)
{
Rl.at<uchar>(i,j) = 0;
}else
{
Rl.at<uchar>(i,j) = 255;
}
}
}
imshow("Rl",Rl);
/*分别把R,G,B与Rl点乘,得到Rm,Gm,Bm*/
Mat R,G,B;
vector<Mat> channels_RGB;
split(src,channels_RGB);
R = channels_RGB.at(0);
G = channels_RGB.at(1);
B = channels_RGB.at(2);
Mat Rm,Gm,Bm;
Rm = R.mul(Rl/255.0);
Gm = G.mul(Rl/255.0);
Bm = B.mul(Rl/255.0);
/*分别计算其均值,得到Rav,Gav,Bav*/
double Rav,Bav,Gav;
Rav = mean(Rm)[0];
Gav = mean(Gm)[0];
Bav = mean(Bm)[0];
/*Ymax = max(Y)*0.15,计算出图片中亮度最大值,其中Y为亮度值矩阵*/
double Ymax;
minMaxIdx(Y, NULL, &Ymax);
Ymax = Ymax*0.15;
/*计算调整增益*/
double Rgain,Ggain,Bgain;
Rgain = Ymax/Rav;
Ggain = Ymax/Gav;
Bgain = Ymax/Bav;
/*调整RGB通道*/
channels_RGB.at(0) = channels_RGB.at(0) * Rgain;
channels_RGB.at(1) = channels_RGB.at(1) * Ggain;
channels_RGB.at(2) = channels_RGB.at(2) * Bgain;
/*合并RGB通道*/
Mat output_image;
merge(channels_RGB,output_image);
imshow("in",input_image);
总结
1.将图片从RGB空间转换至YCrCb空间时使用到cvtColor函数
cvtColor(src,dst_YcrCb,CV_BGR2YCrCb);
该函数有CV_BGR2YCrCb和CV_RGB2YCrCb两种参数,使用时注意区别
另:opencv和matlab在处理彩色图像时,RGB通道的存储顺序是相反的
matlab的顺序是R,G,B,opencv的顺序是B,G,R
Mat Rl(heigh,width,CV_8UC1,Scalar(0));
其中CV_8UC1表示图像的数据类型,8U为无符号整型8bit数据(uint8),C1表示1个通道
3.在对参考白点进行排序处理时,注意区别sort函数的命名空间是cv还是std
cv::sort(Ciny,Ciny,CV_SORT_DESCENDING);
4.C++代码中的这两段中,设置Rl(i,j)为255是为了查看亮度矩阵的检测效果,实际运行中去除这两步后,结果是一致的。
for(int i = 0;i<heigh;i++)
{
for(int j = 0;j<width;j++)
{
if(Rl.at<uchar>(i,j) < minn)
{
Rl.at<uchar>(i,j) = 0;
}else
{
Rl.at<uchar>(i,j) = 255; // = 1
}
}
}
//imshow("Rl",Rl);
Mat Rm,Gm,Bm;
Rm = R.mul(Rl/255.0); //Rl
Gm = G.mul(Rl/255.0); //Rl
Bm = B.mul(Rl/255.0) //Rl
心得体会
关于人脸美颜软件的研究算是告一段落了,第一次听说这个题目还是在18年,据学长说他使用二维指针完成了美白算法,因为自己指针学的一直不好,所以潜意识里觉得美白算法很难。
去年自己做的时候,尝试不用二维指针写白平衡美白算法,但是在写判断参考白点的时候卡住,只能作罢,答辩的时候老师问用到的美白原理是什么,也是支支吾吾。
这次重写美白算法花了两天时间,总结下来,主要是由于对数据类型和基本矩阵运算函数不熟悉导致当时未能完成,比如遍历数组时用到的uchar类型和Vec3d类型,以及sum求和,mean求均值,sort排序等等。此外,搭界面写算法只花了三天时间,剩下时间都在带别人做和优化自己的界面上,花费了不少精力。
数字图像处理这门课如果好好学,还是很有意思的,当时熬了一个通宵连做了6份作业,而且大部分内容借用opencv库函数都能完成,所以上手难度并不大。
最后附上《数字图像处理(第三版)》(何东健著)的相关参考代码的的网盘链接。
链接: https://pan.baidu.com/s/13bDzqUxY26oyRqNJYNugMw 提取码: udnf