在OpenGL中利用shader进行实时瘦脸大眼等脸型微调

3,655 阅读7分钟

前言

由于原创图片加载失败,添加图片记录一下方便学习!原创地址

目前是准备做一个美颜相机类的项目,后续会更新瘦脸大眼等脸型微调实现部分。 代码已上传MagicCamera

正文

在现在这个靠脸吃饭的时代,如果你没有一张瓜子脸一双大眼睛,那还怎么去吃饭呢,而现在一些直播视频App相机应用基本都会有瘦脸大眼效果.本文是在OpenGl环境下,在shader中通过对像素位置进行偏移来实现放大缩小的,实现起来快速简单,也是各大主流应用裁刘德基本方式.

举个栗子

首先在这里给大家看一个效果图

脸型微调之后

细心的朋友应该可以看出来两张图片的区别,图二明显脸更尖了,更趋近于瓜子脸,眼睛也稍微大了点.这里其实就是用到了最常见的拉伸和缩放处理.

原理解析

其实整个过程可以分成一下三种类型的处理

  • 圆内放大
  • 圆内缩小
  • 向某一点拉伸

其实拉伸缩放理解并不困难,大家都对Bitmap有进行拉伸或者缩放过,但是这里的拉伸和缩放同Bitmap的拉伸缩放最大的区别就是要考虑到处理范围的周边像素,要使整个图片看起来过度正常,如果只是使用Bitmap的拉伸缩放必然会在边界处像素差别很大,能看到一条很明显的像素分割线,而且这样还会丢失一些像素,我们来看一个简单的放大操作效果.

原图

放大后

这是普通的放大操作,存在很明显的一条圆形边界,而且放大前的好多像素直接被放大后的像素给替代(覆盖)了,像素丢失了,眼睛的其他部位都没有了,这显然不是我们想要的结果.这种简单的放大操作实现起来也很简单,但是他的适用也很局限

float dis=distance(vec2(gPosition.x,gPosition.y/uXY),vec2(centerPoint.x,centerPoint.y));
if(dis< RADIUS){
    gl_FragColor=texture2D(vTexture,vec2(aCoordinate.x/2.0+0.25,aCoordinate.y/2.0+0.25));
}
gl_FragColor=orignalColor;

而我们这里用到的是局部拉伸和缩放,显然用这种算法是不合理的.

局部微调中用到的算法原理更多是把局部区域进行微小的挤压,将一些像素进行缩放拉伸的同时会对另一些像素进行挤压,基本不会产生像素的丢失和明显的分界线.本质是对像素的座标值进行偏移(或者说是对每个位置上的像素进行偏移).

一个形象的比如:我们可以将一幅图像的所有像素点映射到一张面片上,面片的厚度表示单位面积上原始像素的多少,开始时面片厚度是一致的,我们可以设为1 ,越厚则便是这里堆积的原始像素的密度越大,局部放大就是将面片局部进行擀薄,且厚薄区域的过度圆滑,该区域的原始像素值变少并且向外扩张,这样原本离中心距离为1的像素跑到距离为3的位置,距离为2的像素跑到距离为5的位置,以此类推,就会产生放大的效果;但是这种放大并不会有明显的过度.假设我们的放大半径为100,这样离中心35的像素移动到离中心71的位置,而后面的29个距离上却堆积了原来从36到100的像素,并且产生一种平稳的过度.

下面看一下具体的实现.

圆内放大

vec2 enlargeFun(vec2 curCoord,vec2 circleCenter,float radius,float intensity,float curve) 
{ 
    float currentDistance = distance(curCoord,circleCenter);

    { 
        float weight = currentDistance/radius;

        weight = 1.0-intensity*(1.0-pow(weight,curve));//默认curve为2 ,当curve越大时,会放大得越大的, 
        weight = clamp(weight,0.0,1.0); 
        curCoord = circleCenter+(curCoord-circleCenter)*weight; 
    } 
    return curCoord; 
}

这是圆内放大算法,

输入:座标,放大中心座标,放大半径,放大比例系数,放大算法参数.
返回:放大之后应该取的像素的位置.

为了分析这个算法到底是怎么实现的,我们先可以简单的另intensity = 1.0,curve = 2.0;此时再看这个算法其实就是pow函数,也就是平方函数了,这个平方函数是怎么实现放大的呢.

其实从图像中大家就能明白原理了,座标经过平方处理之后,本来A点应该取A1像素,结果取的是A2像素,也就是离中心0.25位置的像素B1被放在了离中心0.5的位置A上,这显然就是放大的操作.由于横纵座标一一对应,所以该算法并不会造成像素的丢失,只是会在放大区域的边缘内有一部分像素比较密集的区域.

上面的分析是建立在特殊值下的,我们再回到这个函数本身,
curve是我们的pow函数的次方值,由于我们考虑的都是[0,1]区间的,所以该值越大,离中心点向外扩散也越厉害,放大效果越大.
intensity的取值是[0,1]当它取1时就是我们上面分析的情况,会最大化的利用pow次方产生的座标偏移来取像素,若它为0,则不会产生任何缩放效果,intensity是一个影响因子,一个对pow函数产生的座标偏移的采用度,intensity越大则会更大化利用pow函数产生座标便宜作为最后的座标偏移.

圆内缩小

vec2 narrowFun(vec2 curCoord,vec2 circleCenter,float radius,float intensity,float curve) 
{ 
    float currentDistance = distance(curCoord,circleCenter);

    { 
        float weight = currentDistance/radius; 
        weight = 1.0-intensity*(1.0-pow(weight,curve));//默认curve为2 ,当curve越大时,会缩小得越小的, 
        weight = clamp(weight ,0.0001,1.0); 
        curCoord = circleCenter+(curCoord-circleCenter)/weight; 
    } 
    return curCoord; 
}

上面分析了圆内放大,看一下缩小的代码,其实也不难理解,也是利用pow函数进行像素座标的偏移.这里就不多家分析了.

向某一点拉伸

vec2 stretchFun(vec2 textureCoord, vec2 originPosition, vec2 targetPosition, float radius,float curve) 
{ 
    vec2 offset = vec2(0.0); 
    vec2 result = vec2(0.0);

    vec2 direction = targetPosition - originPosition;


    float infect = distance(textureCoord, originPosition)/radius;

    infect = pow(infect,curve);//默认curve为1,这个值越大,拉伸到指定点越圆润,越小越尖
    infect = 1.0-infect; 
    infect = clamp(infect,0.0,1.0); 
    offset = direction * infect; 
    result = textureCoord - offset;

    return result; 
}

输入:座标,拉伸中心座标,拉伸目标座标,拉伸半径,拉伸算法参数.
返回:放大之后应该取的像素的位置.

拉伸和缩放原理其实是一样的,只是理解起来有些差距.我们还是设curve = 2,对其原理进行分析.

上图是我对拉伸原理的一个简单描述,A为拉伸中心座标,B为拉伸目标座标,经过拉伸后,圆1上的像素会被平移到圆1',圆2上的像素会被平移到圆2'.以A为圆心的同心圆经过拉伸之后都会被平移,离A越近平移的距离越远,A点直接平移到B点, 而R处则不会平移.看到这里应该已经知道怎样将一个圆脸变成瓜子脸了吧.当然这都是微调,如果调整过大会产生不自然的效果.curve 值越大,拉伸到指定点越圆润,越小越尖.

上面只是我对人脸变形时的原理进行分析,要想使用变形,首先要确定人脸特征点,有了这些特征点,你才知道缩放中心半径等等,而使用时往往不是一步就能达到理想效果,比如说我们大眼一般是首先对一个比较大的包含眼睛的区域进行放大,然后再对眼睛中心瞳孔位置进行进一步放大.实际使用时为了达到某种效果一般都是对这几种操作进行组合使用,而且是多次操作.