前言
在很多美颜相机啊,抖音啊,都会有一些放大眼睛的效果,今天就来实现如何放大眼睛。
思路
1、首先使用OpenCV定位到人脸
2、根据定位到的人脸去检测人脸关键点,进而获取到人眼睛的位置。
3、根据眼睛位置,对眼睛进行放大。
实现
定位人脸
人脸的定位追踪,在之前文章中已经写过OpenCv实现人脸追踪 当时是在xCode上写的,把里面的代码移植到Android中就可以了,这个是C写的,所以需要移植到JNI中。这里的人脸模型,我采用的是OpenCV中提供的,当然也可以自己去训练模型。
检测人脸关键点
人脸关键点的检测,有很多三方的sdk,比如说face++等等,都是要收费的,face++是检测了68个关键点,这68个关键点都不是随意分布的,都是有规律的。如下图:
但是像face++这种,都是需要收费的,我从GitHub上找了一个免费的叫SeetaFaceEngine它这个里面有三个模块:人脸检测模块(SeetaFace Detection)、面部特征点定位模块(SeetaFace Alignment)以及人脸特征提取与比对模块(SeetaFace Identification)。这里我用到的是SeetaFace Alignment,用来检测人脸的关键点。
它这里面并不是定位了68个关键点,而是定位了5个关键点,即左眼(0)、右眼(1)、鼻子(2)、嘴巴左边(3)、嘴巴右边(4),这个分布也是有规律的,并不是随便的点。代码如下:将定位到的人脸,送去进行关键点的检测
if (faces.size()){
Rect face = faces[0];
rects.push_back(Rect2f(face.x,face.y,face.width,face.height));
//关键点定位
//保存5个关键点
//0:左眼 1:右眼 2:鼻头 3:嘴巴左边,4 :嘴巴右边
seeta::FacialLandmark points[5];
//图像数据
seeta::ImageData image_data(src.cols,src.rows);
image_data.data = src.data;
//指定人脸部位 去定位眼睛
seeta::FaceInfo faceInfo;
seeta::Rect bbox; //是一个矩形边框,用来微调定位到的人脸的窗口,有时候可能会定位的不准确
bbox.x = face.x;
bbox.y = face.y;
bbox.width = face.width;
bbox.height = face.height;
faceInfo.bbox = bbox;
// 定位人脸关键点方法,第一个参数是你需要检测的灰度图,
// 第二个参数是The face bounding box,脸部边框
// 第三个参数是检测到的关键点集合
faceAlignment->PointDetectLandmarks(image_data,faceInfo,points);
for (int i = 0; i < 5; ++i) {
//把点放入集合中 ,点是没有宽和高的
rects.push_back(Rect2f(points[i].x,points[i].y,0,0));
}
}
放大眼睛
这里是根据网上论文中的一个公式实现的,http://www.gson.org/thesis/warping-thesis.pdf, 大概在45页左右有个这样的描述,以及公式,就是根据这个来实现的。
4.4.2. Local scaling warps
The mapping used for a local scaling warp maps each point within the area
of influence onto a point in the source image whose distance vector from
the center of the area has the same orientation as that in the destination
image but whose length is a function fs® of the length r of the vector
The function chosen for fs®
where the parameter a is controlled by the current horizontal position of
the mouse ranging from when the mouse is at the left edge of the
area of interest to when the mouse is at the right edge of the area of
interest Note that the function reduces to the identity function fs® = r
when a =0.In Figure the function f(s)r for rmax has been plotted for a number of values of a in the range
这个公式是什么意思呢?这个公式求出的值是采集的改变后的点距离眼睛中心点的位置,rmax表示的是最大放大的区域,r表示原来的点距离眼睛中心点的位置,a表示的是放大系数,所以当a=0的时候,公式的结果就是r,也就是没有变化,不放大。
原理是什么呢,就是在片元着色器,把眼睛周边的需要放大的点取值成对应的眼睛内部的点的值,然后把这个值的坐标赋值给gl_FragColor,这样就完成了眼睛的放大;
片元着色器:
// r:原来的点距离眼睛中心点的位置
//rmax: 放大区域
float fs(float r,float rmax){
//放大系数
float a = 0.4;
return (1.0 - pow((r/rmax -1.0),2.0) *a);
}
//根据需要采集的点 aCoord 计算新的点(可能是需要改变为眼睛内部的点,完成放大的效果)
vec2 newCoordDistance(vec2 coord,vec2 eye,float rmax){
vec2 newCoord = coord;
//获得当前需要采集的点与眼睛的距离
//distance是glsl中的内置函数
float r = distance(coord,eye);
//在范围内 才放大
if(r < rmax){
//想要方法需要采集的点 与 眼睛中心点的距离
float fsr = fs(r,rmax);
// 新点-眼睛 / 老点-眼睛 = 新距离/老距离
//(newCoord - eye) / (coord-eye) = fsr/r;
//(newCoord - eye) = fsr/r * (coord-eye)
newCoord = fsr * (coord - eye) +eye;
}
return newCoord;
}
void main(){
//最大作用半径 rmax
//计算两个点的距离
float rmax = distance(left_eye,right_eye)/2.0;
// 如果属于 左眼 放大区域的点 得到的就是 左眼里面的某一个点(完成放大效果)
// 如果属于 右眼放大区域的点 或者都不属于 ,那么 newCoord还是 aCoord
vec2 newCoord= newCoordDistance(aCoord,left_eye,rmax);
newCoord = newCoordDistance(newCoord,right_eye,rmax);
// 采集到 RGBA 值
gl_FragColor = texture2D(vTexture,newCoord);
}
效果图
用最爱的明星做个效果图,可以看出来眼睛部位是有明显的放大的