提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
之前做项目的时候需要Unity实现人脸的瘦脸特效,如今把实现的方法记录在这里。秉着能简单实现就简单实现的原则,我没有去找相关模型,而是直接使用了于泓老师局部平移算法,这是他在b站的视频:https://www.bilibili.com/video/BV1PS4y1574S/?spm_id_from=333.337.searchcard.all.click&vd_source=2913f7d9809d6e3bc495a90385756b1d
实际上实现起来并不难,只要理解其中的逻辑就能做出来,但是做出来的效果还是可以的。由于需要用到人脸识别,所以也需要导入OpenCV for Unity和Dlib FaceLandmark Detector这两个包。
一、基本原理
先简单说一下大概的实现思路,并引用于泓老师视频中的截图辅以说明:
1.检测人脸
首先我们先要识别出照片中人脸的68个特征点,具体部位对应的序号如下图所示:
2.画出需要处理的像素区域
实际上瘦脸并不需要整张脸的像素都进行处理,而是只要脸颊部分处理就行了。我们设定需处理区域为点4为圆心,点4到6为半径的圆和点14为圆心,点14到12为半径的圆。
3.局部平移
确定好处理范围后,我们就可以对范围内像素进行处理,实现图像往鼻子那边拉过去的效果,这也就是它叫局部平移算法的原因。
当然除此之外,范围内每个像素平移的距离是不一样的。越靠近圆心,平移的距离会越远,越远离圆心,平移的距离会越近。通过这种方式,可以让平移后的图像不会过于突兀。
大致思路如下图所示:
那么我们该怎么处理需处理的像素呢?实际上我们只要取别的点的颜色值就可以了。那要取的这个点的坐标改怎么求出来呢?我引用于泓老师的公式:
其中:
- U—我们要求的点的坐标
- x—当前待处理的点的坐标
- R—当前点所在的圆的半径
- C—当前点所在的圆的圆心的坐标
- M—鼻子处特征点,即特征点31的坐标
二、实现
以下是代码的实现:
using OpenCVForUnity.CoreModule;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using DlibFaceLandmarkDetector;
using OpenCVForUnity.ImgprocModule;
public class FaceThin : MonoBehaviour
{
[Header("输入输出")]
public Texture2D srcFace;
Texture2D copyTexture;
public RawImage rawImg;
[Header("瘦脸参数")]
public float a;
//人脸检测器
FaceLandmarkDetector faceLandmarkDetector;
string dlibShapePredictorFileName = "sp_human_face_68.dat";
//图像矩阵
Mat srcMat;
Mat copyMat;
void Start()
{
//初始化人脸检测器
faceLandmarkDetector = new FaceLandmarkDetector(DlibFaceLandmarkDetector.UnityUtils.Utils.getFilePath(dlibShapePredictorFileName));
if (faceLandmarkDetector == null)
{
Debug.LogError("faceLandmarkDetector is not found");
}
else
{
Debug.Log("faceLandmarkDetector is found");
}
rawImg = GetComponent<RawImage>();
rawImg.texture = srcFace;
rawImg.SetNativeSize();
}
public void BecomeThin()
{
Load();
ThinFace();
}
void Load()//转化图像为矩阵
{
srcMat = new Mat(srcFace.height, srcFace.width, CvType.CV_8UC4);
OpenCVForUnity.UnityUtils.Utils.texture2DToMat(srcFace, srcMat);
//拷贝源人脸图像
copyTexture = new Texture2D(srcFace.width, srcFace.height, TextureFormat.RGBA32, false);
copyTexture.SetPixels32(srcFace.GetPixels32());
copyTexture.Apply();
copyMat = new Mat(copyTexture.height, copyTexture.width, CvType.CV_8UC4);
OpenCVForUnity.UnityUtils.Utils.texture2DToMat(copyTexture, copyMat);
}
public void ThinFace()
{
//识别源人脸
faceLandmarkDetector.SetImage(srcFace);
List<UnityEngine.Rect> faces = faceLandmarkDetector.Detect();
//获取所需的特征点
#region
Vector2 four = Vector2.zero;
Vector2 fourteen = Vector2.zero;
Vector2 six = Vector2.zero;
Vector2 twelve = Vector2.zero;
Vector2 thirty_one = Vector2.zero;
if (faces.Count == 0)
{
Debug.Log("no faces detected");
return;
}
else
{
List<Vector2> points = faceLandmarkDetector.DetectLandmark(faces[0]);
//获取特殊点,注意:数组下标=序号-1
four = new Vector2(points[3].x, points[3].y);
fourteen = new Vector2(points[13].x, points[13].y);
six = new Vector2(points[5].x, points[5].y);
twelve = new Vector2(points[11].x, points[11].y);
thirty_one = new Vector2(points[30].x, points[30].y);
}
#endregion
//获取蒙版
//以点4和点14作为圆心,点4-6和点14-12的距离为半径,朝向点31
Mat mask = Mat.zeros(copyMat.rows(), copyMat.cols(), copyMat.type());//初始化为零矩阵,即全黑色
float radius1 = (four - six).magnitude;
float radius2 = (four - six).magnitude;
Imgproc.circle(mask, new Point(four.x, four.y), (int)radius1, new Scalar(255, 255, 255), -1);//右脸的圆为白色
Imgproc.circle(mask, new Point(fourteen.x, fourteen.y), (int)radius2, new Scalar(255, 255, 0), -1);//左脸的圆为黄色
//局部平移
#region
for (int i = 0; i < copyMat.rows(); i++)
{
for (int j = 0; j < copyMat.cols(); j++)
{
double[] color = mask.get(i, j);
if (color[0] == 255 && color[1] == 255 && color[2] == 255)//颜色为白色
{
//注意坐标系之间的转换
//Mat用的参数是矩阵的行、列,Vector2储存的是以图像左上角为原点的x、y坐标
Vector2 U_point = GetWarpPoint(new Vector2(j-1+0.5f, i-1+0.5f), four, thirty_one, radius1, a);
if ((int)U_point.x < 0 || (int)U_point.x > copyMat.cols() - 1 || (int)U_point.y < 0 || (int)U_point.y > copyMat.rows() - 1) continue;//防止出界
//线性取值
//copyMat.put(i, j, BiLinearInsert(U_point, sourceMat));
//临近取值,使用临近取值就足够了
copyMat.put(i, j, srcMat.get((int)U_point.y, (int)U_point.x));
}
else if (color[0] == 255 && color[1] == 255 && color[2] == 0)//颜色为黄色
{
Vector2 U_point = GetWarpPoint(new Vector2(j-1+0.5f, i-1+0.5f), fourteen, thirty_one, radius2, a);
if ((int)U_point.x < 0 || (int)U_point.x > copyMat.cols() - 1 || (int)U_point.y < 0 || (int)U_point.y > copyMat.rows() - 1) continue;//防止出界
//线性取值
//copyMat.put(i, j, BiLinearInsert(U_point, sourceMat));
//临近取值,使用临近取值就足够了
copyMat.put(i, j, srcMat.get((int)U_point.y, (int)U_point.x));
}
}
}
#endregion
//输出图像
OpenCVForUnity.UnityUtils.Utils.matToTexture2D(copyMat, copyTexture);
rawImg.texture = copyTexture;
}
//局部平移算法
Vector2 GetWarpPoint(Vector2 _x, Vector2 _C_point, Vector2 _M_point, float _radius, float _a)
{
Vector2 direction = _M_point - _C_point;
//求(M-C)前面的因子
float midFactor = _radius * _radius - (_x - _C_point).sqrMagnitude;
float factor = Mathf.Pow(midFactor / (midFactor + _a * (_M_point - _C_point).sqrMagnitude), 2f);
Vector2 res = _x - factor * (_M_point - _C_point);
return res;
}
//线性插值求像素点值
double[] BiLinearInsert(Vector2 _x, Mat _mat)
{
Vector2 tl = new Vector2((int)_x.x, (int)_x.y);
Vector2 tr = new Vector2((int)_x.x, (int)_x.y + 1);
Vector2 bl = new Vector2((int)_x.x + 1, (int)_x.y);
Vector2 br = new Vector2((int)_x.x + 1, (int)_x.y + 1);
double[] v1 = _mat.get((int)tl.x, (int)tl.y);
double[] v2 = _mat.get((int)tr.x, (int)tr.y);
double[] v3 = _mat.get((int)bl.x, (int)bl.y);
double[] v4 = _mat.get((int)br.x, (int)br.y);
float dist1 = (_x - tl).magnitude;
float dist2 = (_x - tr).magnitude;
float dist3 = (_x - bl).magnitude;
float dist4 = (_x - br).magnitude;
float rate1 = dist1 / (dist1 + dist2 + dist3 + dist4);
float rate2 = dist2 / (dist1 + dist2 + dist3 + dist4);
float rate3 = dist3 / (dist1 + dist2 + dist3 + dist4);
float rate4 = 1 - rate1 - rate2 - rate3;
return new double[] { rate1 * v1[0] + rate2 * v2[0] + rate3 * v3[0] + rate4 * v4[0], rate1 * v1[1] + rate2 * v2[1] + rate3 * v3[1] + rate4 * v4[1], rate1 * v1[2] + rate2 * v2[2] + rate3 * v3[2] + rate4 * v4[2], 255 };
}
}
其中,像素取值的时候我虽然也实现了线性取值,但后面发现和临近取值差不多效果,就改用简单的临近取值了。
三、效果
我们看一下效果,先把图片导入Unity,记得把Read/Write勾选,Compression这里我选的是none,不选应该也没什么问题。
这里设置的a值为0.8:
看着还不算太突兀。
如果你想提高瘦脸效果,那就把a值调小;想降低瘦脸效果,那就把a值调大。
总结
以上就是瘦脸特效的实现过程,实际上并不算太难,只要搞懂局部平移算法的思路,把公式带进去就行。