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

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

功能描述

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

效果视频

Unity+OpenCV+Dlib实现换脸

实现思路

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

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

开发中使用的技术栈

一、换脸

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

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

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

// 创建人脸检测器,使用插件中自带的训练好的模型数据
_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]);
elsethrow 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-alphaCore.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);// 第二张图 * alphausing 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[] rgbKey, byte[] rgbIV, string sourceText)
{using MemoryStream memoryStream = new MemoryStream();using (Aes aes = Aes.Create())using (ICryptoTransform transform = aes.CreateEncryptor(rgbKey, rgbIV))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[] rgbKey, byte[] rgbIV, byte[] cipherBuffer)
{using MemoryStream stream = new MemoryStream(cipherBuffer);using Aes aes = Aes.Create();using ICryptoTransform transform = aes.CreateDecryptor(rgbKey, rgbIV);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.mzph.cn/bicheng/37279.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python学习笔记五

1.当循环执行完整后&#xff0c;就会执行else里面的代码 s0 i1 while i<100:sii1 else:print(s) 当循环不完整就会如下 s0 i1 while i<100:sii1if s6:break; else:print(s) 2. 实现密码匹配&#xff0c;可以输入三次&#xff0c;若输入三次错误会退出&#xff0c;或者输…

部分力扣题记

1.. - 力扣&#xff08;LeetCode&#xff09; 这题用到了map和栈的知识点 我们利用map的特性&#xff0c;将&#xff08;&#xff09;【】{}&#xff0c;分别一一对应 然后遍历&#xff0c;如果map&#xff08;char&#xff09;为真&#xff0c;就意味着是左边的部分&#x…

界面组件DevExpress WinForms v24.1 - 支持DateOnly TimeOnly类型

DevExpress WinForms拥有180组件和UI库&#xff0c;能为Windows Forms平台创建具有影响力的业务解决方案。DevExpress WinForms能完美构建流畅、美观且易于使用的应用程序&#xff0c;无论是Office风格的界面&#xff0c;还是分析处理大批量的业务数据&#xff0c;它都能轻松胜…

1976 ssm 营地管理系统开发mysql数据库web结构java编程计算机网页源码Myeclipse项目

一、源码特点 ssm 营地管理系统是一套完善的信息系统&#xff0c;结合springMVC框架完成本系统&#xff0c;对理解JSP java编程开发语言有帮助系统采用SSM框架&#xff08;MVC模式开发&#xff09;&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开…

Pycharm一些问题解决办法

研究生期间遇到关于Pycharm一些问题报错以及解决办法的汇总 ModuleNotFoundError: No module named sklearn’ 安装机器学习库&#xff0c;需要注意报错的sklearn是scikit-learn缩写。 pip install scikit-learnPyCharm 导包提示 unresolved reference 描述&#xff1a;模块…

网络爬虫的架构

网络爬虫的架构 网络爬虫的架构&#xff0c;犹如一座精心设计的桥梁&#xff0c;连接着海量的互联网数据与我们的需求。在网络爬虫的世界里&#xff0c;每一个组件都扮演着至关重要的角色&#xff0c;它们协同工作&#xff0c;确保数据的高效获取与处理。 在爬虫架构的顶层&a…

时序(流式)图谱数据仓库AbutionGraph功能介绍-Streaming Graph OLAM Database

AbutionGraph是一款端到端的流式数据实时分析的图谱数据库&#xff0c;实时&#xff08;流式写入实时、高QPS决策分析实时、流式预处理实时&#xff09;表现在&#xff1a; 构建实时查询QPS响应时长与历史数据量无关的图模型&#xff1b;接入流式数据并实时更新图计算指标&…

Spring Cloud Consul作为配置中心实践

官网地址&#xff1a;https://docs.spring.io/spring-cloud-consul/docs/current/reference/html/#spring-cloud-consul-config 先说个人总结&#xff1a;作为配置中心对比config、Consul以及Nacos后&#xff0c;建议使用 Nacos。 Consul 提供了一个键/值存储用于保存配置和其…

Vite: 代码分割与拆包

概述 在生产环境下&#xff0c;为了提高页面加载性能&#xff0c;构建工具一般将项目的代码打包(bundle)到一 起&#xff0c;这样上线之后只需要请求少量的 JS 文件&#xff0c;大大减少 HTTP 请求。当然&#xff0c;Vite 也不例 外&#xff0c;默认情况下 Vite 利用底层打包引…

简单的本地局域网的前后端接口联调

由于项目被赶进度了&#xff0c;急于前后端联调接口&#xff0c;但是我又没钱买服务器&#xff08;主要我也不会部署&#xff09;&#xff0c;所以我这里就紧急找一个后端的大神朋友请教了一下&#xff1a;苏泽SuZe-CSDN博客 提示&#xff1a;这里不讲后端怎么写接口、前端怎么…

Java的异常处理体系

目录 异常处理1、Java的异常类层次结构2、try-catch-finally 使用注意事项3、在Web应用中如何实现全局异常处理机制 异常处理 1、Java的异常类层次结构 其中Error表示程序运行错误 常见的错误类型有&#xff1a; OutOfMemoryError (内存溢出错误) StackOverFlowError (栈内存溢…

【408计算机组成原理】计算机系统层次结构

计算机系统层次结构 计算机系统由硬件和软件两大部分组成&#xff0c;它们相互作用&#xff0c;共同完成信息处理任务。计算机系统可以分为多个层次&#xff0c;每一层次都有其特定的功能和作用。 硬件层次 输入设备&#xff1a;这些设备负责将用户的指令和数据输入到计算机…

论文学习:基于知识图谱的RAG进行客服问答

1.简介 文章名称&#xff1a; Retrieval-Augmented Generation with Knowledge Graphs for Customer Service Question Answering&#xff08;基于知识图谱的RAG进行客服问答&#xff09; 2.摘要ABSTRACT 在客户服务技术支持中&#xff0c;迅速准确地检索相关的过往问题对于有…

Qt Creator创建一个用户登录界面

目录 1 界面设计 2 代码 2.1 登录界面 2.2 注册界面 2.3 登陆后的界面 3 完整资源 这里主要记录了如何使用Qt Creator创建一个用户登录界面&#xff0c;能够实现用户的注册和登录功能&#xff0c;注册的用户信息存储在了一个文件之中&#xff0c;在登录时可以比对登录信息…

Go 常用文件操作

查找文件/目录 os.Stat(String)组合路径 dir, _ : homedir.Dir() filename : args[0] path : filepath.Join(dir, filename)homedir.Dir()为home根目录。 filepath.Join 会自动处理分隔符&#xff0c;将目录和文件名组合成文件路径。 检查是否含有后缀.json strings.HasSu…

【深度学习】常用命令行指令汇总

这些指令对于管理深度学习环境、监控资源使用、调试程序等方面 查看显卡使用情况 要实时监控NVIDIA显卡的状态,可以使用命令: nvidia-smi -l 1这条命令会每秒刷新一次显卡的使用情况,包括GPU利用率、显存使用情况等。 查看当前Python环境 查看当前使用的Python环境,可…

堆排序(手写堆)

堆排序 输入一个长度为 n的整数数列&#xff0c;从小到大输出前 m小的数。 输入格式 第一行包含整数 n和 m。 第二行包含 n个整数&#xff0c;表示整数数列。 输出格式 共一行&#xff0c;包含 m个整数&#xff0c;表示整数数列中前 m小的数。 数据范围 1≤m≤n≤105&…

【运维】如何在Ubuntu中设置一个内存守护进程来确保内存不会溢出

文章目录 前言增加守护进程1. 编写监控脚本2. 创建 systemd 服务文件3. 启动并启用服务4. 验证服务是否运行注意事项 如何修改守护进程1. 修改监控脚本2. 重新加载并重启服务3. 验证服务是否运行总结 如何设置一个日志文件来查看信息1. 修改监控脚本以记录日志方法一&#xff1…

利用代理IP实现高效大数据抓取的策略与技巧

在当今信息爆炸的时代&#xff0c;数据对于各行各业都至关重要。而数据的获取往往需要通过网络爬取。然而随着网络安全意识的提高和反爬虫机制的加强&#xff0c;传统的数据爬取方式可能会受到限制。在这种情况下&#xff0c;代理IP技术的应用就显得尤为重要。本文将探讨代理IP…

C语言 | Leetcode C语言题解之第204题计数质数

题目&#xff1a; 题解&#xff1a; int countPrimes(int n) {if (n < 2) {return 0;}int isPrime[n];int primes[n], primesSize 0;memset(isPrime, 0, sizeof(isPrime));for (int i 2; i < n; i) {if (!isPrime[i]) {primes[primesSize] i;}for (int j 0; j < …