Unity实现照片人脸的人脸特效

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


前言

  之前由于工作需求,需要对照片上的人脸进行处理,实现某种特定的人脸特效,例如变老特效。之后自己寻找相关项目,也很难找到在unity上用C#实现的例子(本人只对C++、C#比较熟悉),所以便自己开始研究,最后终于实现了。本文意在记录自己的实现历程,当然希望也能为想实现相关效果的朋友提供思路!

  下文我将以实现人脸变老特效作为例子,来解释如何一步步实现。


一、实现人脸纹理的变形

  在搜集了许多资料后,我发现人脸变老要么就是用训练好的模型来实现,要么就是直接覆盖人脸的皱纹纹理,而我选择了比较容易实现的后者。之后在掘金上找到类似的实现:https://juejin.cn/post/6844903862881550344

  我们知道,每张照片上的人脸大小、轮廓都各不相同。所以首先,我们要实现根据照片的人脸不同,人脸纹理也要进行适当的变形,可以使用仿射变换,而我使用的是透视变换。当然,在进行变换之前,我们还得对人脸的特征点进行识别,所以需要OpenCV for UnityDlib FaceLandmark Detector两个包。

1.实现

  实现代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using OpenCVForUnity;
using DlibFaceLandmarkDetector;
using OpenCVForUnity.CoreModule;
using OpenCVForUnity.Calib3dModule;
using OpenCVForUnity.ImgprocModule;

public class TextureBlender : MonoBehaviour
{
    [Header("输入输出")]
    public Texture2D srcFace;//需处理的人脸图像
    Texture2D copyTexture;//人脸图像的拷贝
    public Texture2D effectFace;//人脸纹理适配的人脸图像
    public Texture2D effect;//人脸纹理
    public RawImage rawImg;

    //人脸检测器
    FaceLandmarkDetector faceLandmarkDetector;
    string dlibShapePredictorFileName = "sp_human_face_68.dat";

    //图像矩阵
    Mat srcMat;
    Mat copyMat;
    Mat effectMat;

    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 BecomeOld()//通过点击事件调用
    {
        Load();//转化图像为矩阵,之后处理需要用到
        ImageBlend();//混合人脸纹理与需处理的人脸图像
    }

    void Load()
    {
        srcMat = new Mat(srcFace.height, srcFace.width, CvType.CV_8UC4);
        OpenCVForUnity.UnityUtils.Utils.texture2DToMat(srcFace, srcMat);

        effectMat = new Mat(effect.height, effect.width, CvType.CV_8UC4);
        OpenCVForUnity.UnityUtils.Utils.texture2DToMat(effect, effectMat);

        //拷贝需处理的人脸图像
        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);
    }

    void ImageBlend()
    {
        //获取变换后的人脸纹理矩阵
        Mat targetMat = new Mat(srcFace.height, srcFace.width, CvType.CV_8UC4);
        targetMat = GetWarpedTexture();
        if (targetMat == null) return;

        //图像混合
        for(int i = 0; i < srcMat.rows(); i++)
        {
            for(int j = 0; j < srcMat.cols(); j++)
            {
                double[] color1 = targetMat.get(i, j);

                if (color1[3] > 0)//不透明度大于0
                {
                    double[] color2 = srcMat.get(i, j);

                    //像素表现的颜色值人脸纹理与源人脸的权重为0.7:0.3
                    double r = color1[0] * 0.7 + color2[0] * 0.3;
                    double g = color1[1] * 0.7 + color2[1] * 0.3;
                    double b = color1[2] * 0.7 + color2[2] * 0.3;

                    double[] color = new double[] { r, g, b, 255 };
                    copyMat.put(i, j, color);
                    color = null;
                }
            }
        }

        //输出混合后的图像
        OpenCVForUnity.UnityUtils.Utils.matToTexture2D(copyMat, copyTexture);
        rawImg.texture = copyTexture;
    }

    Mat GetWarpedTexture()//返回变换后的人脸纹理矩阵
    {
        //源图像人脸识别
        faceLandmarkDetector.SetImage(srcFace);
        List<UnityEngine.Rect> srcFaces = faceLandmarkDetector.Detect();
        List<Point> srcPoints = new List<Point>();

        if (srcFaces.Count == 0)
        {
            Debug.Log("no faces detected in srcFace");
            return null;
        }
        else
        {
            List<Vector2> points = faceLandmarkDetector.DetectLandmark(srcFaces[0]);
            for(int i = 0; i < points.Count; i++)
            {
                srcPoints.Add(new Point(points[i].x, points[i].y));
            }
        }

        //纹理人脸识别
        faceLandmarkDetector.SetImage(effectFace);
        List<UnityEngine.Rect> effectFaces = faceLandmarkDetector.Detect();
        List<Point> effectPoints = new List<Point>();

        if (effectFaces.Count == 0)
        {
            Debug.Log("no faces found in effectFace");
            return null;
        }
        else
        {
            List<Vector2> points = faceLandmarkDetector.DetectLandmark(effectFaces[0]);
            for(int i = 0; i < points.Count; i++)
            {
                effectPoints.Add(new Point(points[i].x, points[i].y));
            }
        }

        //求单应矩阵
        Mat warpMat = new Mat();
            //用68个特征点拟合求最优单应矩阵
        warpMat = Calib3d.findHomography(new MatOfPoint2f(effectPoints.ToArray()), new MatOfPoint2f(srcPoints.ToArray()));

        //对人脸纹理进行透视变换
        Mat targetMat = new Mat();
        Imgproc.warpPerspective(effectMat, targetMat, warpMat, srcMat.size());

        return targetMat;
    }
}


   项目所在的位置一定不能有中文路径,不然会报错加载失败sp_human_face_68.dat这个文件

2.效果

  下面看一下实现的效果:

  首先我先找一张正脸的人脸,然后在PS上照着他的脸绘制一个简单的黑色纹理。

在这里插入图片描述


  输出这个图层,得到人脸纹理(注意:图片分辨率,必须与适配人脸的分辨率一致,否则之后的人脸变换会出问题)。

在这里插入图片描述
在这里插入图片描述



  导入图片到unity内(注意:Advanced内的Read/Write记得要勾上,Compression我是改成none的,最好也改一下)。

在这里插入图片描述



  放入程序运行,需处理的人脸就是正在显示的这张:

在这里插入图片描述



  处理后,黑色的人脸纹理就附在上面了,效果还是可以的。

在这里插入图片描述


二、实现人脸纹理的自然融合

  掘金的项目为我提供了思路,可以使用类似ps里柔光的混合模式来处理人脸和皱纹纹理的图片,使他们能够比较自然的融合在一起,但柔光的混合模式具体底层是如何实现的,我还是需要找相关的资料,最后在知乎上找到了对混合模式解释得不错的文章:https://zhuanlan.zhihu.com/p/643960643


1.相关知识

  这里我就讲一下我们需要用到的知识:

图层、颜色

图层混合模式 一共涉及三个图层分别是
1、基础图层或者叫做底图就是下方的图层,我们使用英文单词below的开头字母B来表示,符号是LayerB
2、混合图层或者叫做调整图层绘画图层混合图层或者叫绘画图层,就是上方的图层,我们使用英文单词above的开头字母A来表示,符号是LayerA
3、结果图层就是以何种方式处理之后的结果,他是通过LayerB和LayerA的结合来表示。

  那也就是说, LayerC=BlendMode(LayerB,LayerA)

  具体到一个像素点上,就是c=BlendMode(b,a),其中c为结果色,b为混合色,a为基色


不透明度、填充

  但是,某个像素表现出来的颜色,除了跟结果色有关,还跟另外两个值有关,一个是不透明度(Opacity),另一个就是填充(fill)

在PS中,混合模式旁边就有不透明度和填充这两个参数


  具体两者是如何相互影响的,不同的混合模式有不同的公式,但两者一起产生效果的原则公式如下:

Fill(b,a)=BlendMode(b,a * fill)

Opacity(b,a)=op * b+(1 - op) * Fill(b,a)

  最后输出的这个Opacity(b,a)就是像素表现的颜色,由以上公式我们也可知道填充影响混合模式的效果,填充越小,混合模式越弱。而不透明度则是底图和结果图层的权重,以此控制结果图层表现出来的效果。


柔光(Soft Light)计算公式

  那么柔光这种混合模式的底层计算公式是什么呢,经过一番寻找,在知乎这篇文章上https://zhuanlan.zhihu.com/p/108820522,找到一条效果比较好的公式,就是下面这张图:

在这里插入图片描述

  当然,他这里的B是指混合色,A是指底色,到时候实现的时候要注意。

  了解这些之后,我们就可以着手开始实现了。


2.实现

代码实现

  首先,我们把原来混合图片的函数ImageBlend稍微修改一下,顺便加上新的变量fill

[Range(0, 1)] public double fill;//填充

void ImageBlend()
    {
        //获取变换后的人脸纹理矩阵
        Mat targetMat = new Mat(srcFace.height, srcFace.width, CvType.CV_8UC4);
        targetMat = GetWarpedTexture();
        if (targetMat == null) return;

        //图像混合
        for(int i = 0; i < srcMat.rows(); i++)
        {
            for(int j = 0; j < srcMat.cols(); j++)
            {
                double[] color1 = targetMat.get(i, j);

                if (color1[3] > 0)//不透明度大于0
                {
                    double[] color2 = srcMat.get(i, j);

                    //柔光混合
                    double r = SoftLightBlend(color1[0], color2[0], color1[3], fill);
                    double g = SoftLightBlend(color1[1], color2[1], color1[3], fill);
                    double b = SoftLightBlend(color1[2], color2[2], color1[3], fill);

                    double[] color = new double[] { r, g, b, 255 };
                    copyMat.put(i, j, color);
                    color = null;
                }
            }
        }


  然后,我们再实现柔光混合的函数SoftLightBlend:

double SoftLightBlend(double _a, double _b, double _opacity, double _fill)
    {
        //标准化
        _a /= 255;
        _b /= 255;
        _opacity /= 255;

        //_a *= _fill;

        double res = 0;
        if (_a > 0.5)
        {
            res = 2 * _b * (1 - _a) + (2 * _a - 1) * Mathf.Sqrt((float)_b);
        }
        else
        {
            res = 2 * _a * _b + _b * _b * (1 - 2 * _a);
        }

        res = _fill * res + (1 - _fill) * _b;
        res = _opacity * res + (1 - _opacity) * _b;

        //映射回原来的色彩值
        res *= 255;
        return res;
    }

  这里fillopacity的式子是我自己摸索出来的,但是效果还是不错的。


人脸纹理制作

  做到这里,我们的代码部分已经完成了,剩下的就是最后一步,制作一个充满皱纹的人脸纹理。首先,先找一张老年人的脸部照片(注意:尽量正脸,且轮廓都在照片内),这是我找的图,已经把背景去掉了:

在这里插入图片描述

  经过一系列处理后,得到下面的PNG图片:

在这里插入图片描述

  具体在PS的处理步骤如下:

  1. 羽化人脸边缘,使混合后边缘不会过于突兀。
  2. 使用印章工具,用其他部位的纹理覆盖眼睛、鼻子、嘴巴、眉毛这些区域,再模糊掉,以减少纹理对这些明显特征部位的影响。
  3. 适当降低亮度,尽量拉高对比度,以加深皱纹的纹理。

3.效果

  终于到了激动人心的地步,这次我们选择一张没什么皱纹的人脸作为待处理的人脸,然后再将上面两张人脸作为参数放进脚本,调整fill的值,处理!

在这里插入图片描述

处理前

在这里插入图片描述

处理后


  我们再试试别人脸:
在这里插入图片描述

处理前

在这里插入图片描述

处理后


  可以看到,效果还是可以的,即使稍微侧脸也能处理得比较好。但也有不足的地方

  1. 人脸纹理边缘轮廓过渡不够自然
  2. 人脸皱纹不够明显

  对人脸纹理边缘过渡的问题,我的两种解决方法:

  1. 在PS中提高人脸边缘的羽化程度
  2. 降低fill的值

  对人脸皱纹不明显的问题,我的两种解决方法:

  1. 在PS中提高人脸的对比度,适当降低亮度
  2. 提高fill的值

  由上可知,fill的取值需要根据自己的情况去调整。调高fill值皱纹确实会更明显,但是人脸纹理的边缘会变突兀;调低fill值虽然边缘过渡会更自然,但是皱纹的纹理也会变淡。


总结

  虽说以上实现的是人脸老化的效果,但实际上只要你能做出相对应的人脸纹理,其他的人脸效果也能做出来,比如黑眼圈、眼窝之类的。所以只要理解了以上的思路,基本都能靠这种方法做出来。

  这次是我第一次在 csdn写文章,所以文章的长度、排版之类的都把握不好,对相关知识点的解释可能也不是很好,有什么不好或者不对的地方,都欢迎大家的意见和建议!

  • 10
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 内容概要 《计算机试卷1》是一份综合性的计算机基础和应用测试卷,涵盖了计算机硬件、软件、操作系统、网络、多媒体技术等多个领域的知识点。试卷包括单选题和操作应用两大类,单选题部分测试学生对计算机基础知识的掌握,操作应用部分则评估学生对计算机应用软件的实际操作能力。 ### 适用人群 本试卷适用于: - 计算机专业或信息技术相关专业的学生,用于课程学习或考试复习。 - 准备计算机等级考试或职业资格认证的人士,作为实战演练材料。 - 对计算机操作有兴趣的自学者,用于提升个人计算机应用技能。 - 计算机基础教育工作者,作为教学资源或出题参考。 ### 使用场景及目标 1. **学习评估**:作为学校或教育机构对学生计算机基础知识和应用技能的评估工具。 2. **自学测试**:供个人自学者检验自己对计算机知识的掌握程度和操作熟练度。 3. **职业发展**:帮助职场人士通过实际操作练习,提升计算机应用能力,增强工作竞争力。 4. **教学资源**:教师可以用于课堂教学,作为教学内容的补充或学生的课后练习。 5. **竞赛准备**:适合准备计算机相关竞赛的学生,作为强化训练和技能检测的材料。 试卷的目标是通过系统性的题目设计,帮助学生全面复习和巩固计算机基础知识,同时通过实际操作题目,提高学生解决实际问题的能力。通过本试卷的学习与练习,学生将能够更加深入地理解计算机的工作原理,掌握常用软件的使用方法,为未来的学术或职业生涯打下坚实的基础。
### 内容概要 这份《计算机试卷1》包含多个部分,主要覆盖了计算机基础知识、操作系统应用、文字处理、电子表格、演示文稿制作、互联网应用以及计算机多媒体技术。试卷以单选题开始,涉及计算机历史、基本概念、硬件组成、软件系统、网络协议等。接着是操作应用部分,要求考生在给定的软件环境中完成一系列具体的计算机操作任务。 ### 适用人群 本试卷适用于计算机科学与技术、信息技术相关专业的学生,以及准备计算机水平考试或职业资格认证的人士。它适合那些希望检验和提升自己计算机操作能力的学习者,也适用于教育工作者作为教学评估工具。 ### 使用场景及目标 1. **学习评估**:作为教育机构的课程评估工具,帮助教师了解学生对计算机基础知识的掌握程度。 2. **自学检验**:供个人自学者检验自己的计算机操作技能和理论知识,为进一步学习提供方向。 3. **职业发展**:为职场人士提供计算机技能的自我提升途径,增强其在信息时代的竞争力。 4. **考试准备**:为准备计算机相关考试的考生提供实战演练的机会,加强考试自信。 5. **教学资源**:教师可以将其作为教学资源,设计课程和实验,提高教学效果。 试卷的目标是通过理论知识的测试和实践技能的操作,全面提升考生的计算机应用能力。考生应掌握从基础的计算机组成原理到复杂的数据处理、演示文稿制作、网络应用以及多媒体技术处理等多方面技能。通过本试卷的学习与练习,考生将能够更加熟练地使用计算机解决实际问题,为未来的学术或职业生涯打下坚实的基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值