人物肖像风格转换
原文地址:http://blog.csdn.net/hjimce/article/details/45534333
作者:hjimce
一、前言
对于风格转换,2014年siggraph上面出了一篇比较不错的paper:《Style Transfer for Headshot Portraits》 ,这篇文献涉及到的算法非常多,可以说,如果要把这篇paper的代码从头到尾写过一遍,相当复杂。即使是paper作者本人,也只是通过代码拼凑实现的。因为这篇文章涉及到十几篇paper的算法。我这边主要讲解这篇文献的总流程,如果你打算把这篇文献完全看懂,那么对于图像融合、抠图、sift、图像变形等这些基础算法都要非常熟悉,因为这篇paper就是通过这些基础算法组合在一起实现的。当然如果自己把这篇paper搞过一遍,那么真的是可以学到好多经典算法。
先看一下paper的效果图:
这篇paper如果不考虑速度问题,那么真的是有很广泛的工程应用,paper实现的功能:输入一副图片Input,然后在输入一张Example图片,然后通过算法,可以把input图片的光照风格转换成Example图片的风格,感觉牛逼哄哄的样子。
它的工程应用,文章为我们列出了几个,我在这里简单讲解一下它的可能工程应用:
1、首先是化妆,我们知道现在天天P图、美妆等一系列软件化妆软件最近挺火的,天天p图的就是因为武媚娘妆而成名。利用本篇算法我们可以实现基于模板的化妆算法:
如图所示,也就是输入一张用户图片Input,然后输入一张化妆好的模板图片,可以实现用户图片Input的化妆,把Example的化妆结果,传输到Input上。不过这个我个人感觉很难达到工程应用,因为它对图片的对齐要求很高,而且用户输入的图片的背景也会跟着Example的背景进行转换,所以这就是为什么paper中演示的图片实例,基本上都是用单一背景图片。当然虽然不能达到工程应用,但是学了这篇paper我们可以学到很多东西,所以非常值得学习,特别是对于搞图片美化、美容软件的算法人,更必须学习。
2、瞳孔光照转换
这个在现有的美图秀秀、天天p图就有类似的功能,就是“亮眼”功能,当然这篇paper的亮眼比较高级,因为它不仅仅可以实现亮眼,而且可以把Exmaple的眼睛风格转换到Input图片上,如果Example的眼睛是蓝色的,那么Input也会跟着转换,牛逼哄哄。在paper的主页上,有很多眼睛光照转换结果,看一下下面一张效果图片:
总之,学完这篇paper你会感觉自己学到了n多种算法,虽然因为背景问题,很难被用于工程APP中,但是对于我们学算法的,必须好好解读。
二、算法流程
开始这篇paper之前,我们需要知道paper的主要创新点,paper的主要创新点是提出了基于拉普拉金字塔的图像风格转换,因此主要创新点是利用图像融合算法,实现风格转换。下面开始讲解算法流程:
1、对齐阶段
人脸对齐,这一步涉及到稠密sift对齐、基于特征线的图像变形算法,对应的文献分别为:《Sift flow: Dense correspondence across scenes and its applications》、《Feature-based image metamorphosis》。这一步算法主要流程:
(1)先通过人脸特征点检测算法,检测到68个face landmark,这一步如果要自己实现,可以用AAM算法,或者用CNN,相关的人脸特征点检测的paper很多,近几年单单face++就发表了好几篇高精度人脸特征点检测的算法。基于CNN人脸特征点定位精度,从《Deep Convolutional Network Cascade for Facial Point Detection》的效果看是精度挺高的,不过要实现这一步,对于我们仅仅只是为了实现风格转换,要花很长时间。所以还是建议直接到face++官网注册免费人脸特征点检测吧。
(2)以face landmark作为控制顶点,利用图像变形算法,对Example图像进行变形,把Example的人脸特征点对齐到Input的人脸特征点上,这一步我们又称之为粗对齐。对齐算法采用《Feature-based image metamorphosis》,这个变形算法,测试了一下,相比于《As-rigid-as-possible_shape_manipulation》、《Image Deformation Using Moving Least Squares》变形算法来说,其特点是可以实现保证图像人脸变形弧度比较大的时候,脸型的曲线过度比较自然。因此这个变形算法,可以用于瘦脸、眼睛放大等算法中。
(3)精对齐。这一步使用Dense sift flow进行对齐。其实我感觉这一步可以省了,因为用Dense sift flow进行对齐,有的时候感觉对齐效果很不好。
通过对齐步骤,我们可以把Example图片,变形到与Input一样形状,如下图所示:
最左边的图片便是Example变形结果了。图像对齐这一步不是paper的创新点,我们可以选择粗略的看一下就好了。
2、融合阶段
这一步的算法很重要,因为它是paper的主要创新点,是最值得我们学习的地方,需要好好琢磨,因为这一步就是实现风格转换的原理实现。主要这一步算法的主要流程如下:
算法总流程图
(1)构建拉普拉斯金字塔。对两幅图像:input、Example分别进行多尺度分解,说的简单一点就是构造拉普拉斯金子塔。这个算法如果了解金字塔图像融合算法的人,应该是挺熟悉的。当然拉普拉斯金字塔和高斯金字塔有点区别,高斯金字塔包含采样,在图像融合领域里的一大经典算法,这个扯得有点离题了。具体多尺度分解方法如下:
以高斯卷积核的卷积半径第0层为2,根据2的n次方依次递增。也就是第0,1,2……的卷积半径依次为2,4,8,……进行高斯卷积,这就是所谓的多尺度,因为卷积核σ不同,所以人们又把它称之为多尺度。
根据如下公式进行构建拉普拉斯金子塔:
a.金子塔0~n-1层。金子塔从底层开始(L=0)计算,每一层的计算方法如下:
b.金字塔最后一层(L=n)为:
最后一层,我们又称之为残差层。
说白了就是,用不同的卷积半径对一张图片分别进行卷积,然后进行相邻层之间相减,这样得到的多张图片就称之为金字塔了。如上面的公式说是,I代表输入图片,G(2)就是卷积σ为2的高斯核,然后上面的运算符就是卷积的意思了。这样把金字塔的每一层相加在一起,你可以发现刚好等于原图像I,而这个过程就称之为金字塔重建。而金字塔融合的原理,就是对金字塔的每一层进行融合处理,然后进行重建。而本篇paper实现光照风格转换的原理,就是对金子塔的每一层进行相关处理,然后再把处理后的每一层相加在一起,就可以得到重建结果。
记住,这一步对Input、Example都要构建金字塔。金字塔的层数,paper默认选择了6层,还有最后一层的残差层。
2、计算金字塔每层的能量图。
这个能量的计算,是根据上面计算得到的金字塔进行计算的。
能量计算公式如下:
对于Input image 根据上面的式子,就可以计算出每一层的能量图了。上面的公式,说的简单一点,就是对金子塔的每一层进行平方,然后在进行高斯卷积。得到的图像,有称之为能量图
对于Example image来说,我们也需要计算出其每一层的能量图(后面变形的时候,在对能量图进行变形)。
也就是先计算能量图,然后对能量图进行W运算,W指的是变形操作。
3、风格转换
对Input image金子塔的每一层(除了最后一层残差层之外),进行风格转换,每一层的计算公式如下:
其中ε为较小的数,取,文章介绍,如果直接使用5b的计算结果,代入公司5a中,会出现一些相关的问题。因此在计算完5b之后,还需进一步的处理:
其中:
最后一层残差层,直接为Example的残差层图像。
看上面的算法流程,需要结合算法总流程图。上面的卷积核大小,并不一定要是2^n,卷积半径大小,对效果影响挺大的,这个paper有讲到,当卷积半径太小的时候,会出现转换过度。如果卷积半径太大了,又会出现转换不足的现象,可以看一下下面的半径对结果的影响图:
当然paper为了使得过度更好,还用了mask,需要用到grab cut算法。不过那都是效果提升阶段了。其实算法上面的这一步骤,就可以看见比较粗糙的风格转换结果了,因此我就讲到这里。
三、算法实现
我这里只贴一下我自己写的paper的主要创新点部分,也就是上面的步骤2(融合阶段),因为只要实现了这一步,就可以看效果了。OK,融合阶段算法首先是构造拉普拉斯金子塔:
<span style="font-size:18px;">//文献的公式1 创建拉普拉斯金字塔 void CStyleTransfer::CreateLaplacianStack(LAPSACK&lapsack,cv::Mat Img,int n,float first_sigma,int rsize) { lapsack.ori_img=Img; //颜色空间转换,文献中提到才lab空间进行处理,效果会比较好,因此需要先转换成lab空间 cv::Mat tempimg;//=Img; cv::cvtColor(Img,tempimg,CV_RGB2Lab); //卷积 vector<cv::Mat>L(n); vector<cv::Mat>LG(n); for (int i=0;i<n;i++) { float sigma=pow(first_sigma,i+1); cv::Mat lgtemp; cv::GaussianBlur(tempimg,lgtemp,cv::Size(sigma+1,sigma+1),sigma); lgtemp.convertTo(LG[i],CV_32FC3); } //构建金字塔 for (int i=0;i<n;i++) { if (i==0) { cv::Mat convertfloat; tempimg.convertTo(convertfloat,CV_32FC3); L[i]=convertfloat-LG[i]; } else if(i>0&&i<n) { L[i]=LG[i-1]-LG[i]; } } lapsack.laplacian_stack=L; lapsack.residual=LG[LG.size()-1]; //Reconstruction(lapsack.laplacian_stack,lapsack.residual); //计算能量图 for (int i=0;i<lapsack.laplacian_stack.size();i++) { cv::Mat sqrI; cv::Vec3f img00=lapsack.laplacian_stack[i].at<cv::Vec3f>(100,100); cv::multiply(lapsack.laplacian_stack[i],lapsack.laplacian_stack[i],sqrI); cv::Vec3f img001=sqrI.at<cv::Vec3f>(100,100); float sigma=pow(first_sigma,i+1); cv::Mat dst; cv::GaussianBlur(sqrI,dst,cv::Size(sigma+1,sigma+1),sigma); cv::Vec3f img0011=dst.at<cv::Vec3f>(100,100); lapsack.pow_map.push_back(dst); } /* for (int i=0;i<lapsack.laplacian_stack.size();i++) { string str; std::stringstream stream; stream<< i; //将int输入流 stream >> str; //从stream中抽取前面插入的int值 / * cvNamedWindow(str.c_str()); cv::Mat convert; lapsack.laplacian_stack[i].convertTo(convert,CV_8UC3); cv::cvtColor(convert,convert,CV_Lab2RGB); imshow(str,convert);* / }*/ }</span>
金子塔重建部分:
<span style="font-size:18px;">//金字塔重建 cv::Mat CStyleTransfer::Reconstruction(vector<cv::Mat>LaplacianStack,cv::Mat residual) { cv::Mat result=residual;//=LaplacianStack[LaplacianStack.size()-1].clone();//最后一层 for (int i=0;i<LaplacianStack.size();i++) { result=result+LaplacianStack[i]; } cv::Mat r; result.convertTo(r,CV_8UC3); cv::cvtColor(r,r,CV_Lab2RGB); // cv::normalize(result,r, 0, 1.,cv::NORM_MINMAX); imshow("reconstrution",r); return result; }</span>
<span style="font-size:18px;">//计算Gain cv::Mat CStyleTransfer::RobustGain(cv::Mat input_pow,cv::Mat example_pow,int layer) { int heigth=input_pow.rows; int width=input_pow.cols; cv::Mat outimg(heigth,width,CV_32FC3); float thetah=4; float thetal=0.25; float beta=3; for (int i=0;i<heigth;i++) { for (int j=0;j<width;j++) { float s=0.01*0.01; cv::Vec3f pow_example=example_pow.at<cv::Vec3f>(i,j); cv::Vec3f pow_input=input_pow.at<cv::Vec3f>(i,j)+cv::Vec3f(s,s,s); cv::Vec3f gain;//(1,1,1); cv::divide(pow_example,pow_input,gain); cv::sqrt(gain,gain); cv::Vec3f mingain=cv::Vec3f(min(gain[0],thetah),min(gain[1],thetah),min(gain[2],thetah)); cv::Vec3f maxgain=cv::Vec3f(max(mingain[0],thetal),max(mingain[1],thetal),max(mingain[2],thetal)); outimg.at<cv::Vec3f>(i,j)=maxgain; } } float sigma=3*pow(2.f,layer+1); cv::Mat robustgain; cv::GaussianBlur(outimg,robustgain,cv::Size(sigma+1,sigma+1),sigma); return robustgain; </span>这一部分得到的robustgain,模糊半径选得越大,那么过度越自然,不会出现过度不连续的现象。
这个图片可以看到转换上基本可以了,然而你可以看到在结果图片的最下方,过渡有点不自然,这个时候,就是因为选择的robustgain这一步的模糊半径选择太小了。在看另外一张结果图片:
这个就是我第一次得到的结果,刚开始一直以为自己代码写错了,想了n久。从图片上看,它实现了部分的转换,但是转换又不够彻底,皮肤的颜色还是有点黄。这是因为我在计算能量图的时候,根据作者的公式进行写,连卷积的半径也是根据公式来,最后经过把计算能量图的:
<span style="font-size:18px;"> //计算能量图 for (int i=0;i<lapsack.laplacian_stack.size();i++) { cv::Mat sqrI; cv::Vec3f img00=lapsack.laplacian_stack[i].at<cv::Vec3f>(100,100); cv::multiply(lapsack.laplacian_stack[i],lapsack.laplacian_stack[i],sqrI); cv::Vec3f img001=sqrI.at<cv::Vec3f>(100,100); float sigma=pow(first_sigma,i+1); cv::Mat dst; cv::GaussianBlur(sqrI,dst,cv::Size(sigma+1,sigma+1),sigma); cv::Vec3f img0011=dst.at<cv::Vec3f>(100,100); lapsack.pow_map.push_back(dst); }</span>中的高斯模糊的半径扩大了一倍,才得到最后的结果。于是去查看了作者写的matlab源码,果然,作者源码中的卷积半径也是比较大的,paper作者也并不是根据文献所写的一模一样的卷积半径进行实现,这个paper也提到了,卷积半径对于结果的影响非常大,然而papaer也没有给出比较合理的卷积半径的计算方法,估计是经过调参。具体paper相关的测试效果还有相关的源码在paper作者的主页上可以看到,这里就不罗嗦了。
**********************作者:hjimce 时间:2015.5.6 联系QQ:1393852684 地址:http://blog.csdn.net/hjimce 原创文章,版权所有,转载请保留本行信息********************
参考文献:
1、《Style Transfer for Headshot Portraits》