计算机视觉
图像处理

图像处理(七)人物肖像风格转换-Siggraph 2014

一、前言

对于风格转换,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,融合阶段算法首先是构造拉普拉斯金子塔

  1. <span style=“font-size:18px;”>//文献的公式1  创建拉普拉斯金字塔
  2. void CStyleTransfer::CreateLaplacianStack(LAPSACK&lapsack,cv::Mat Img,int n,float first_sigma,int rsize)
  3. {
  4.     lapsack.ori_img=Img;
  5.     //颜色空间转换,文献中提到才lab空间进行处理,效果会比较好,因此需要先转换成lab空间
  6.     cv::Mat tempimg;//=Img;
  7.     cv::cvtColor(Img,tempimg,CV_RGB2Lab);
  8.     //卷积
  9.     vector<cv::Mat>L(n);
  10.     vector<cv::Mat>LG(n);
  11.     for (int i=0;i<n;i++)
  12.     {
  13.         float sigma=pow(first_sigma,i+1);
  14.         cv::Mat lgtemp;
  15.         cv::GaussianBlur(tempimg,lgtemp,cv::Size(sigma+1,sigma+1),sigma);
  16.         lgtemp.convertTo(LG[i],CV_32FC3);
  17.     }
  18.     //构建金字塔
  19.     for (int i=0;i<n;i++)
  20.     {
  21.         if (i==0)
  22.         {
  23.             cv::Mat convertfloat;
  24.             tempimg.convertTo(convertfloat,CV_32FC3);
  25.             L[i]=convertfloat-LG[i];
  26.         }
  27.         else if(i>0&&i<n)
  28.         {
  29.             L[i]=LG[i-1]-LG[i];
  30.         }
  31.     }
  32.     lapsack.laplacian_stack=L;
  33.     lapsack.residual=LG[LG.size()-1];
  34.     //Reconstruction(lapsack.laplacian_stack,lapsack.residual);
  35.     //计算能量图
  36.     for (int i=0;i<lapsack.laplacian_stack.size();i++)
  37.     {
  38.         cv::Mat sqrI;
  39.         cv::Vec3f img00=lapsack.laplacian_stack[i].at<cv::Vec3f>(100,100);
  40.         cv::multiply(lapsack.laplacian_stack[i],lapsack.laplacian_stack[i],sqrI);
  41.         cv::Vec3f img001=sqrI.at<cv::Vec3f>(100,100);
  42.         float sigma=pow(first_sigma,i+1);
  43.         cv::Mat dst;
  44.         cv::GaussianBlur(sqrI,dst,cv::Size(sigma+1,sigma+1),sigma);
  45.         cv::Vec3f img0011=dst.at<cv::Vec3f>(100,100);
  46.         lapsack.pow_map.push_back(dst);
  47.     }
  48. /*
  49.     for (int i=0;i<lapsack.laplacian_stack.size();i++)
  50.     {
  51.         string str;
  52.         std::stringstream stream;
  53.         stream<< i; //将int输入流
  54.         stream >> str; //从stream中抽取前面插入的int值
  55. / *
  56.         cvNamedWindow(str.c_str());
  57.         cv::Mat convert;
  58.         lapsack.laplacian_stack[i].convertTo(convert,CV_8UC3);
  59.         cv::cvtColor(convert,convert,CV_Lab2RGB);
  60.         imshow(str,convert);* /
  61.     }*/
  62. }</span>

上面的金子塔的高斯模糊半径,需要根据自己需要进行调整,如果你希望转换的粒度大一些,那么就把模糊半径选择的小一些。

金子塔重建部分:

  1. <span style=“font-size:18px;”>//金字塔重建
  2. cv::Mat CStyleTransfer::Reconstruction(vector<cv::Mat>LaplacianStack,cv::Mat residual)
  3. {
  4.     cv::Mat result=residual;//=LaplacianStack[LaplacianStack.size()-1].clone();//最后一层
  5.     for (int i=0;i<LaplacianStack.size();i++)
  6.     {
  7.         result=result+LaplacianStack[i];
  8.     }
  9.     cv::Mat r;
  10.     result.convertTo(r,CV_8UC3);
  11.     cv::cvtColor(r,r,CV_Lab2RGB);
  12. //  cv::normalize(result,r, 0, 1.,cv::NORM_MINMAX);
  13.     imshow(“reconstrution”,r);
  14.     return result;
  15. }</span>

然后计算Gain的部分的代码如下:

  1. <span style=“font-size:18px;”>//计算Gain
  2. cv::Mat CStyleTransfer::RobustGain(cv::Mat input_pow,cv::Mat example_pow,int layer)
  3. {
  4.     int heigth=input_pow.rows;
  5.     int width=input_pow.cols;
  6.     cv::Mat outimg(heigth,width,CV_32FC3);
  7.     float thetah=4;
  8.     float thetal=0.25;
  9.     float beta=3;
  10.     for (int i=0;i<heigth;i++)
  11.     {
  12.         for (int j=0;j<width;j++)
  13.         {
  14.             float s=0.01*0.01;
  15.             cv::Vec3f pow_example=example_pow.at<cv::Vec3f>(i,j);
  16.             cv::Vec3f pow_input=input_pow.at<cv::Vec3f>(i,j)+cv::Vec3f(s,s,s);
  17.             cv::Vec3f gain;//(1,1,1);
  18.             cv::divide(pow_example,pow_input,gain);
  19.             cv::sqrt(gain,gain);
  20.             cv::Vec3f mingain=cv::Vec3f(min(gain[0],thetah),min(gain[1],thetah),min(gain[2],thetah));
  21.             cv::Vec3f maxgain=cv::Vec3f(max(mingain[0],thetal),max(mingain[1],thetal),max(mingain[2],thetal));
  22.             outimg.at<cv::Vec3f>(i,j)=maxgain;
  23.         }
  24.     }
  25.     float sigma=3*pow(2.f,layer+1);
  26.     cv::Mat robustgain;
  27.     cv::GaussianBlur(outimg,robustgain,cv::Size(sigma+1,sigma+1),sigma);
  28.     return robustgain;
  29. </span>

这一部分得到的robustgain,模糊半径选得越大,那么过度越自然,不会出现过度不连续的现象。
效果测试:我这边贴一下,最粗糙的版本,也就是不经过对齐,不经过mask等处理,仅仅使用了paper的创新点部分的算法,进行验证测试,因为没有经过对齐,所以我选择了两张脸型,还有位置看起来比较对齐的图片作为测试图片,如下:

这个图片可以看到转换上基本可以了,然而你可以看到在结果图片的最下方,过渡有点不自然,这个时候,就是因为选择的robustgain这一步的模糊半径选择太小了。在看另外一张结果图片:

这个就是我第一次得到的结果,刚开始一直以为自己代码写错了,想了n久。从图片上看,它实现了部分的转换,但是转换又不够彻底,皮肤的颜色还是有点黄。这是因为我在计算能量图的时候,根据作者的公式进行写,连卷积的半径也是根据公式来,最后经过把计算能量图的:

  1. <span style=“font-size:18px;”>    //计算能量图
  2.     for (int i=0;i<lapsack.laplacian_stack.size();i++)
  3.     {
  4.         cv::Mat sqrI;
  5.         cv::Vec3f img00=lapsack.laplacian_stack[i].at<cv::Vec3f>(100,100);
  6.         cv::multiply(lapsack.laplacian_stack[i],lapsack.laplacian_stack[i],sqrI);
  7.         cv::Vec3f img001=sqrI.at<cv::Vec3f>(100,100);
  8.         float sigma=pow(first_sigma,i+1);
  9.         cv::Mat dst;
  10.         cv::GaussianBlur(sqrI,dst,cv::Size(sigma+1,sigma+1),sigma);
  11.         cv::Vec3f img0011=dst.at<cv::Vec3f>(100,100);
  12.         lapsack.pow_map.push_back(dst);
  13.     }</span>

中 的高斯模糊的半径扩大了一倍,才得到最后的结果。于是去查看了作者写的matlab源码,果然,作者源码中的卷积半径也是比较大的,paper作者也并不 是根据文献所写的一模一样的卷积半径进行实现,这个paper也提到了,卷积半径对于结果的影响非常大,然而papaer也没有给出比较合理的卷积半径的 计算方法,估计是经过调参。具体paper相关的测试效果还有相关的源码在paper作者的主页上可以看到,这里就不罗嗦了。

 参考文献:

1、《Style Transfer for Headshot Portraits》

转载注明来源:CV视觉网 » 图像处理(七)人物肖像风格转换-Siggraph 2014

分享到:更多 ()
扫描二维码,给作者 打赏
pay_weixinpay_weixin

请选择你看完该文章的感受:

0不错 0超赞 0无聊 0扯淡 0不解 0路过

评论 4

评论前必须登录!