Unity+OpenCV+Dlib实现换脸+图片生成+上传服务器+生成二维码[纯干货]

news/2024/7/7 20:14:30 标签: unity, opencv, dlib, 换脸, 二维码, 上传图像

Unity+OpenCV+Dlib实现换脸+图片生成+上传服务器+生成二维码

功能描述

一句话描述:让游客体验一下当宇航员的乐趣。
具体功能:游客通过摄像头拍照,生成有着“自己的脸”的宇航员的图片,然后展示二维码,供游客下载。

效果视频

Unity+OpenCV+Dlib实现换脸

实现思路

功能描述起来很简单,但是具体实现起来还是有一点难度的,最基本的问题就是,客户拍照离得远、离得近、拍摄角度不同,都需要完美的将脸“放到那个头盔里”,大小要合适,角度要合适,那么要想实现他:

  • 其实就是原始的图像中有个原来的人脸,然后使用摄像头拍到的人脸进行脸部更换操作,然后生成图片,进行后续的操作。
  • 至于换脸后脸部在头盔里面,是因为这是用了两张图,更换完的人脸图,其实是处于底层,然后在其上面又覆盖了一层带有Alpha通道(头盔的玻璃是透明的)的图。这两图叠加起来,就是最终的效果了。
  • 生成图像后,上传到服务器,然后生成二维码这里难点在于安全性,原因是游客没有身份认证过程,谁都可以拍照上传,那么也就是说,服务器的上传接口是开放的,如何确认上传数据的人是我的Unity客户端,而不是别人伪造的?这就是一个问题,假设有人伪造客户端,上传垃圾数据怎么办??

开发中使用的技术栈

一、换脸

技术原理和步骤:
网上其实有很多换脸的原理和说明,大都是Python和C++的,Unity其实完全一样,只不过使用的插件是OpenCVForUnity,这里简要说一下基本的原理:

  1. 使用OpenCV和Dlib检测人脸,确定目标图(原始图像中的人脸)和源图(摄像头拍到的人脸)中人脸的68个关键点位置。
  2. 分别将两张图中68个关键点位进行三角化处理,即划分成一个一个的小三角形。
  3. 根据目标图中每个三角形的位置和源图中对应的三角形的位置,计算出变换矩阵,然后针对三角形中的每个像素点进行变换操作,这样就可以把源图中的人脸对应变换到目标图中。
  4. 根据目标图的颜色等融合一下,使得颜色相匹配。
示例代码:

由于目标图每次都是一样的,所以只在程序初始化时检测一次即可,这里是在Awake中进行的。

// 创建人脸检测器,使用插件中自带的训练好的模型数据
_faceLandmarkDetector =
    new FaceLandmarkDetector(
        DlibFaceLandmarkDetector.UnityUtils.Utils.getFilePath(
            "DlibFaceLandmarkDetector/sp_human_face_68.dat"));

// 从Unity的Texture2D纹理中创建目标图的Mat,注意:此纹理在Unity中需要勾选"Read/Write Enable"
_faceTargetMat = new Mat(_TargetFaceTex.height, _TargetFaceTex.width, CvType.CV_8UC4);
Utils.texture2DToMat(_TargetFaceTex, _faceTargetMat);

// 先检测人脸的大体位置,获得包含人脸的范围矩形
OpenCVForUnityUtils.SetImage(_faceLandmarkDetector, _faceTargetMat);
var faceRect = _faceLandmarkDetector.Detect();

// 因为目标图中肯定只有一个人脸,所以就是用faceRect[0]
if (faceRect is { Count: > 0 })
    _targetLandmarks = _faceLandmarkDetector.DetectLandmark(faceRect[0]);
else
    throw new Exception("原始纹理中,未检测到人脸");

当游客按下了拍照按钮,执行下面的代码:

private IEnumerator RealTakePhoto()
{
	// 如果摄像头未开始播放,就开始播放
    if(!_webCamTexture.isPlaying)
        _webCamTexture.Play();
    yield return null;

	// 开始倒计时
    float countdown = _CountDown;
    while (countdown > 0)
    {
        _infoText.text = $"{countdown}";
        --countdown;
        yield return new WaitForSeconds(1f);
    }

	// 倒计时完成,等待摄像头完成渲染
    _webCamTexture.Pause();
    yield return new WaitForEndOfFrame();

	// 获取摄像头拍到的照片(源图)
    Texture2D photo = new Texture2D(_webCamTexture.width, _webCamTexture.height, TextureFormat.RGBA32, false);
    photo.SetPixels(_webCamTexture.GetPixels(0, 0, _webCamTexture.width, _webCamTexture.height));
    photo.Apply();

	// 照片转换成Mat,以便在OpenCV中使用
    using Mat photoMat = new Mat(photo.height, photo.width, CvType.CV_8UC4);
    Utils.texture2DToMat(photo, photoMat);

	// 检测人脸,首先检测人脸的个数,以及每个人脸所在的范围矩形
    OpenCVForUnityUtils.SetImage(_faceLandmarkDetector, photoMat);
    var faceRect = _faceLandmarkDetector.Detect();

	// 如果检测到的人脸个数大于零
    if (faceRect is { Count: > 0 })
    {
    	// 获取最大的那个人脸(按照矩形的面积计算)
        var targetRect = GetMaxRect(faceRect);
		
		// 进一步检测68个人脸关键点
        var faceLandmarks = _faceLandmarkDetector.DetectLandmark(targetRect);
		
		// 将目标图进行一次克隆,以免改变原图
        using Mat targetMat = _faceTargetMat.clone();

		// 进行目标图换脸,即摄像头中的脸替换到目标图中
        using DlibFaceChanger faceChanger = new DlibFaceChanger();
        faceChanger.SetTargetImage(targetMat);
        faceChanger.AddFaceChangeData(photoMat, faceLandmarks, _targetLandmarks, 1f);
        faceChanger.ChangeFace();

		// 在目标人脸图的上面,叠加带有Alpha通道的“头盔”图,以完成最终效果
        using Mat dst = targetMat.clone();
        AlphaBlend(targetMat, _pictureOnMarsMat, dst);
		
		// 生成最终的纹理。展示到RawImage中。
        var finalTexture = new Texture2D(dst.width(), dst.height(), TextureFormat.RGBA32, false);
        Utils.matToTexture2D(dst, finalTexture);
        _FinalPhoto.texture = finalTexture;

		// 暂停摄像头,设置拍照完成标志
        _webCamTexture.Pause();
        _IsPhotoTaked = true;
    }
    else
    {
        const string info = "未检测到人脸";
        _infoText.text = info;
    }

	// 清理资源
    Destroy(photo);
}
二、图像Alpha融合(图像叠加)

将第二张图叠加到第一张图的上面,其实就设定第二张图的Alpha通道为a,第一张图乘以(1-a)再加上第二张图乘以a,就是最终的图了。

private static void AlphaBlend(Mat one, Mat two, Mat dst)
{
    List<Mat> channels = new List<Mat>();
	
	// 首先分离第二张图的通道,获取alpha通道,并定义一个1-alpha
    Core.split(two, channels);
    using Mat alpha = channels[3];
    using Mat inv_alpha = new Mat(alpha.width(), alpha.height(), alpha.type());
    Core.bitwise_not(alpha, inv_alpha);

    // 第二张图 * alpha
    using Mat _two = new Mat();
    Core.multiply(alpha, channels[0], channels[0], 1.0 / 255);
    Core.multiply(alpha, channels[1], channels[1], 1.0 / 255);
    Core.multiply(alpha, channels[2], channels[2], 1.0 / 255);
    Core.merge(channels, _two);
    
    // 第一张图 * ( 1 - alpha )
    Core.split(one, channels);
    Core.multiply(inv_alpha, channels[0], channels[0], 1.0 / 255);
    Core.multiply(inv_alpha, channels[1], channels[1], 1.0 / 255);
    Core.multiply(inv_alpha, channels[2], channels[2], 1.0 / 255);
    using Mat _one = new Mat();
    Core.merge(channels, _one);

	// 合并两张图
    Core.add(_two, _one, dst);
}
三、安全的上传

如同“实现思路”中所说的,这里所指的安全的上传,主要是解决有人伪造客户端上传垃圾数据的问题,验证上传者的身份就很重要。我是这么实现的:

采用两次请求进行上传:

  • 第一次,发送一组使用AES加密后的数据,数据中包含了一段固定的字符串,还有当前的时间,加入时间的目的是为了每次构造该数据时,内容都会发生变化,这样就使得数据无法复制,因为复制的数据里面包含的时间是不对的。当服务器收到这组数据的时候,进行解密,如果解密失败,或者解密后,固定的字符串内容不对,或者时间相差5分钟以上,则抛弃数据。否则,随机生成一个KEY,返回给客户端,弊端就是客户端和服务器的时间差不能超过5分钟。
  • 第二次,当客户端收到服务器发回的KEY时,连同照片一起发送给服务器。服务器收到KEY后,与第一次生成的KEY进行对比,如果存在,并且距离生成的时间未超过5秒,那么认为KEY有效,则记录该照片,否则,抛弃数据。

这里贴一下AES加解密的代码:

public static byte[] Encrypt(byte[] key, byte[] iv, string sourceText)
{
    using MemoryStream memoryStream = new MemoryStream();
    using (Aes aes = Aes.Create())
    using (ICryptoTransform transform = aes.CreateEncryptor(key, iv))
    using (CryptoStream cryptoStream = new CryptoStream(memoryStream, transform, CryptoStreamMode.Write))
    using (StreamWriter streamWriter = new StreamWriter(cryptoStream))
    {
        streamWriter.Write(sourceText);
        streamWriter.Flush();
    }

    return memoryStream.ToArray();
}
public static string Decrypt(byte[] key, byte[] iv, byte[] cipherBuffer)
{
    using MemoryStream stream = new MemoryStream(cipherBuffer);
    using Aes aes = Aes.Create();
    using ICryptoTransform transform = aes.CreateDecryptor(key, iv);
    using CryptoStream cryptoStream = new CryptoStream(stream, transform, CryptoStreamMode.Read);
    using StreamReader streamReader = new StreamReader(cryptoStream);
    return streamReader.ReadToEnd();
}
public static byte[] Md5Encrypt(string tex)
{
    var md5 = MD5.Create();
    return md5.ComputeHash(Encoding.UTF8.GetBytes(tex));
}
四、图像的传输和存储

Unity和服务器之间发送图像时,使用的是Base64编码,这里记录一下使用Base64遇到的坑:

测试时,Unity端发过去的数据,服务器总是解析不成功,后来发现,Base64中编码后的串,个别的字符在传输过程中被改变了,比如“+”。。解决方法也很简单,先使用UnityWebRequest.EscapeURL()处理一下再发送就好了。

服务器端的存储,用的数据库直接存储的,使用的blob字段,还挺好用。

其他

因为Unity中的Texture,会被圆整为2的幂的尺寸,因此将texture如果直接生成图片,可能尺寸是不对的,所以,有时候需要克隆一下,同时改一下尺寸。

private static Texture2D ScaleTexture(Texture2D source, float targetWidth, float targetHeight)
{
    Texture2D result = new Texture2D((int)targetWidth, (int)targetHeight, source.format, false);
    for (int i = 0; i < result.height; ++i)
    {
        for (int j = 0; j < result.width; ++j)
        {
            Color newColor = source.GetPixelBilinear(j / (float)result.width, i / (float)result.height);
            result.SetPixel(j, i, newColor);
        }
    }
    result.Apply();
    return result;
}

最后

由于工程中涉及一些非免费的插件,源码就不分享了。这里只详细记录一下原理,和开发历程。


http://www.niftyadmin.cn/n/5537351.html

相关文章

Elasticsearch 使用聚合进行数据分析

在大数据时代&#xff0c;数据的价值不仅仅在于存储&#xff0c;更在于如何从海量数据中提取出有价值的信息。Elasticsearch&#xff0c;作为一个强大的搜索引擎和数据分析平台&#xff0c;通过其内置的聚合&#xff08;Aggregations&#xff09;功能&#xff0c;为我们提供了一…

数据库详细复习第三章SQL语句

SQL 第三章&#xff1a;SQL语句3.1 SQL概述3.1.3 SQL 语句类型1、数据定义语句2、数据操纵语言3、数据查询语言4、数据控制语言5、事务处理语言 3.1.4 SQL数据类型1、字符串型2、整数型3、浮点数型4、货币型5、日期型 3.2 数据定义语句3.2.1 数据库的定义3.2.2 数据库表对象的定…

大数据面试题之数据库(1)

目录 数据库中的事务是什么&#xff0c;MySQL中是怎么实现的 MySQL事务的特性? 数据库事务的隔离级别?解决了什么问题?默认事务隔离级别? 脏读&#xff0c;幻读&#xff0c;不可重复读的定义 MySQL怎么实现可重复读? 数据库第三范式和第四范式区别? MySQL的…

千益畅行,旅游卡,如何赚钱?

​ 赚钱这件事情&#xff0c;只有自己努力执行才会有结果。生活中没有幸运二字&#xff0c;每个光鲜亮丽的背后&#xff0c;都是不为人知的付出&#xff01; #旅游卡服务#

自定义一个背景图片的高度,随着容器高度的变化而变化,小于图片的高度时裁剪,大于时拉伸100%展示

1、通过js创建<image?>标签来获取背景图片的宽高比&#xff1b; 2、当元素的高度大于原有比例计算出来的高度时&#xff0c;背景图片的高度拉伸自适应100%&#xff0c;否则高度为auto&#xff0c;会自动被裁减 3、背景图片容器高度变化时&#xff0c;自动计算背景图片的…

Arduino - 74HC595 4 位 7 段显示器

Arduino - 74HC595 4 位 7 段显示器 Arduino - 74HC595 4-Digit 7-Segment Display) A standard 4-digit 7-segment display is needed for clock, timer and counter projects, but it usually requires 12 connections. The 74HC595 module makes it easier by only requir…

SpringBoot项目,配置文件pom.xml的结构解析

pom.xml 是 Maven 项目对象模型&#xff08;Project Object Model&#xff09;的配置文件&#xff0c;它定义了 Maven 项目的基本设置和构建过程。以下是 pom.xml 文件的基本结构和一些常见元素的解析&#xff1a; 项目声明 (<project>): <modelVersion>: 通常设置…

python OpenCV 库中的 cv2.Canny() 函数来对图像进行边缘检测,并显示检测到的边缘特征

import cv2# 加载图像 image cv2.imread(4.png)# 使用 Canny 边缘检测算法提取边缘特征 edges cv2.Canny(image, 100, 200)# 显示边缘特征 cv2.imshow(Edges, edges) cv2.waitKey(0) cv2.destroyAllWindows() 代码解析&#xff1a; 导入 OpenCV 库&#xff1a; import cv2加…