Unity实现照片人脸瘦脸特效

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

  之前做项目的时候需要Unity实现人脸的瘦脸特效,如今把实现的方法记录在这里。秉着能简单实现就简单实现的原则,我没有去找相关模型,而是直接使用了于泓老师局部平移算法,这是他在b站的视频:https://www.bilibili.com/video/BV1PS4y1574S/?spm_id_from=333.337.searchcard.all.click&vd_source=2913f7d9809d6e3bc495a90385756b1d

  实际上实现起来并不难,只要理解其中的逻辑就能做出来,但是做出来的效果还是可以的。由于需要用到人脸识别,所以也需要导入OpenCV for UnityDlib FaceLandmark Detector这两个包。


一、基本原理

  先简单说一下大概的实现思路,并引用于泓老师视频中的截图辅以说明:


1.检测人脸

  首先我们先要识别出照片中人脸的68个特征点,具体部位对应的序号如下图所示:

在这里插入图片描述


2.画出需要处理的像素区域

  实际上瘦脸并不需要整张脸的像素都进行处理,而是只要脸颊部分处理就行了。我们设定需处理区域为点4为圆心,点4到6为半径的圆和点14为圆心,点14到12为半径的圆

![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/298ad16d9198481bbe0b9b8a5d72e46d.p


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值调大。


总结

  以上就是瘦脸特效的实现过程,实际上并不算太难,只要搞懂局部平移算法的思路,把公式带进去就行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值