首发:旷视研究院
作者:旷视研究院
导语
面对琳琅满目的618促销商品,激动搓搓小手的你,是不是在“番茄红”“烂番茄”“甜柿红”“草莓红”“枫叶红”等看起来都差不多的口红面前踌躇不前,纠结不断。就连狂刷主播试色和美妆博主试色,都无法决定哪款才是“真爱”。
那么,不妨试试在线虚拟试妆,它可以成功拯救“选择困难症”。在虚拟试妆过程中,妆容可以精准匹配面部合适位置,且始终实时自然变动和贴合。
在惊叹于人工智能试妆技术之后,本期“技术的真相”将带你揭秘这个打破了线上与线下“次元壁”的神奇技术背后的实现细节。
01 项目背景
“H5试妆”项目的目标功能为基于浏览器的实时虚拟试妆。用户以手机等移动设备的浏览器访问H5页面,保持前置摄像头拍摄面部,即可在页面中实时观察体验叠加于面部的虚拟试妆效果。
本文中“试妆”一词指代一系列面部美化操作的集合,包括基础美颜效果、虚拟妆容渲染(如口红上妆)、美型效果(如脸型调整)等。
项目运行流程如上图所示,为基于JS的串行步骤循环。依次为:
- 摄像头帧数据预处理
- TensorFlow.js(以下简称TFJS)模型推理
- 对模型输出人脸关键点等数据后处理、平滑等
- 算法逻辑以及WebGL渲染至屏幕
本文介绍针对最后步骤“算法+渲染”的优化实践:将其以C++实现并编译为WASM模块。
02 待解决的问题与选择WebAssembly
按照以往的实现方式,算法+渲染步骤与前序步骤一样以JS实现,缺点在于代码逻辑完全暴露,即使进行JS混淆也无法做到绝对的安全。此外,本步骤涉及计算量较大的矩阵运算,以JS实现无法保证速度。
而我们的需求是:核心算法的加密;尽量快的运算速度,同时满足模块体积、加载速度的基本限制;最好可以复用旷视美妆项目已有C/C++代码及架构。
WebAssembly则完全能满足上述需求。
03 WebAssembly(WASM)是什么
WebAssembly(WASM)是一种运行在网络浏览器中的新型二进制格式代码。它设计的目的是为诸如C/C++和Rust等低级源语言提供一个高效的编译目标。它的巨大意义在于为浏览器提供了一种在网络平台以接近本地速度的方式运行多种语言编写的代码的方式;在这之前,浏览器是不可能做到的。
WebAssembly的优点
- 体积小,加载快。
- 它名为“Assembly”但并不是一种汇编语言。然而它很接近机器码,因此可直接被映射为当前硬件的机器码。兼具可移植性与运行速度。
- 灵活性:可由C/C++、Rust等语言编译得到。
- 可被JS调用,进入JS上下文。
- 保密性:作为二进制文件,WASM被反编译的难度很大。
WebAssembly被设计为与JS协同工作,而不是用来取代JS。
事实上,在本项目中,模型推理部分也与WASM有些关系。TFJS已经提供了WASM backend*,作为已有的JavaScript CPU backend与WebGL backend的可选替代。WASM backend完全基于CPU,但速度比JS CPU backend快10-30倍;与WebGL backend相比,虽然对于大多数模型,WebGL backend性能更优,但WASM backend在超轻量的模型上速度更快。且WASM backend不依赖GPU,因此据统计可运行于全球90%的设备。
但本文内容并不会具体涉及此部分的WASM。
*TFJS backend —— 可以理解为TFJS的一种实现,部分中文资料中翻译为“后端”。不同backend的区别在于模型推理的环境与计算实现,JavaScript CPU backend与WASM backend基于CPU,WebGL backend则基于GPU。选用哪个backend取决于不同的设备硬件条件与用户场景。
以WASM方式实现的结构
我们将算法+WebGL渲染步骤以C++实现并编译为WASM模块,模块输入为摄像头图像纹理+关键点等数据,输出为渲染到屏幕。
04 在WASM中使用WebGL
Emscripten
Emscripten编译器可以将C/C++代码项目编译为WASM,具体产出包括:一个.wasm模块(Module)+ 一个.js代码文件。后者包含“胶水”代码,提供了调用模块的JS接口。
Emscripten SDK提供的API定义在_emscripten.h_和_html5.h_两个头文件中。
考虑到支持更多设备,包括更老旧的设备与浏览器,我们基于WebGL实现而非WebGL2。
WebGL对应OpenGL ES 2,因此我们要在编译指令中增加 FULL\_ES2=1 标志。为了能够处理大尺寸的纹理/图像,增加_ALLOW\_MEMORY\_GROWTH=1_ 标志。
此外还需要通过 EXTRA\_EXPORTED\_RUNTIME\_METHODS 显式导出辅助函数_ccall/stringToUTF8_,这两个函数的作用后面会讲到。
包含上述内容的完整编译命令为:
_\> emcc -o out\_name.js source\_name.cpp -O3 -s ALLOW\_MEMORY\_GROWTH=1 -s FULL\_ES2=1 -s WASM=1 std=c++1z -s EXTRA\_EXPORTED\_RUNTIME\_METHODS="\['ccall','stringToUTF8'\]"_
在C++代码中引入头文件:
#include <emscripten.h>
#include <string>
#include <GLES2/gl2.h>
#include <EGL/egl.h>
extern "C" {
#include "html5.h" // emscripten module
}
在C++代码中,首先我们必须要实现的接口之一是一个初始化函数:_void createContext(char \* id)_,在调用时创建名为_id_的上下文:
EMSCRIPTEN_KEEPALIVE
void createContext (char * id) {
// 上下文配置参数
EmscriptenWebGLContextAttributes attrs;
// ... attrs参数设置
std::string id_str = id;
std::string sharp_id_str = "#" + id_str;
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE context = emscripten_webgl_create_context(sharp_id_str.c_str(), &attrs);
emscripten_webgl_make_context_current(context);
free(id);
}
其中,
EMSCRIPTEN\_KEEPALIVE 是一个宏,用于防止C/C++编译器把没有被调用的函数或代码段删除。
emscripten\_webgl\_create\_context(const char *target, const EmscriptenWebGLContextAttributes *attributes) —— 为指定id的画布和属性实例化一个新的上下文。
emscripten\_webgl\_destroy\_context(EMSCRIPTEN\_WEBGL\_CONTEXT\_HANDLE context) —— 与之对应的上下文销毁函数。
emscripten\_webgl\_make\_context\_current(EMSCRIPTEN\_WEBGL\_CONTEXT\_HANDLE context) —— 将上下文指定为当前WebGL渲染到的上下文。
相应的,我们要在调用WASM模块的JS代码中创建canvas,并将其id作为参数传递给WASM模块的_createContext_接口,从而实现在模块中渲染到画布对应的上下文。
![](https://pic2.zhimg.com/v2-f4e2b722e2b9a90f6f3691738f44f655_b.png)
其中,为了实现传递字符串给WASM模块,需要先通过_Module.\_malloc()_ 分配一段内存,再通过_Module.stringToUTF8()_ 将字符串写入其中,并将地址作为数字通过_Module.ccall()_ 方法传递给WASM模块。
_05_ **向WASM模块传参**
其实我们已经在上一段代码中实现了JS向WASM模块传参。下面再举一例。
类似的,我们需要实现的另一个接口是将试妆所必需的关键点数组(float32类型)传递给WASM模块中的函数:
_void beautify(float\* buf, int bufSize)_
在调用模块的web端JS代码中,如下实现
![](https://pic3.zhimg.com/v2-f01d177d6eef1210415d7594ef5416fe_b.png)
与传递字符串一致,数组也是通过一段连续内存传递的。
_06_ **离屏渲染**
如本文开始所提到的,“试妆”是将多个维度的美化操作依次叠加。我们通过多步骤的离屏渲染实现对这些美化效果的层层叠加。
我们需要创建2个附加纹理的帧缓存对象(FBO)。多步骤离屏渲染过程中,每一步以保存上一步渲染结果的FBO纹理为输入,并根据关键点与算法给出的其他参数,将叠加了新一步美化操作的纹理渲染到另一个FBO,如此往复。
以依次完成“基础美颜、口红上妆、脸型调整”的试妆过程为例,流程图如下:
![image.png](/img/bV2kX)
_07_ **实测性能**
如上述实践,我们将算法+WebGL渲染步骤编译为WASM模块,从而实现了核心代码加密、加载速度与运行速度的需求。
具体运行速度方面,试妆功能(基础美颜、口红上妆、脸型调整)全部启用,在骁龙660机型上,WASM模块实测平均耗时数据为单次迭代2ms以内。
**参考资料**
\[1\] [https://developer.mozilla.org/zh-CN/docs/WebAssembly/Concepts](https://link.zhihu.com/?target=https%3A//developer.mozilla.org/zh-CN/docs/WebAssembly/Concepts)
\[2\] [https://en.wikipedia.org/wiki/Emscripten](https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Emscripten)
\[3\] [https://blog.enixjin.net/webassembly-introduction/](https://link.zhihu.com/?target=https%3A//blog.enixjin.net/webassembly-introduction/)
\[4\] [https://segmentfault.com/a/1190000008686643](https://link.zhihu.com/?target=https%3A//segmentfault.com/a/1190000008686643)
\[5\] [https://cloud.tencent.com/developer/article/1646410](https://link.zhihu.com/?target=https%3A//cloud.tencent.com/developer/article/1646410)
\[6\] [https://www.freecodecamp.org/news/how-to-use-webgl-shaders-in-webassembly-1e6c5effc813/](https://link.zhihu.com/?target=https%3A//www.freecodecamp.org/news/how-to-use-webgl-shaders-in-webassembly-1e6c5effc813/)
**专栏文章推荐**
* [**论文要怎么读?研究院院长孙剑的最新体会都在这里了**](https://aijishu.com/a/1060000000206651)
* [**MegEngine TensorCore 卷积算子实现原理**](https://aijishu.com/a/1060000000206213)
* [**分享实录下篇:利用 MegEngine 分布式通信算子实现复杂的并行训练**](https://aijishu.com/a/1060000000203467)
> 欢迎关注[**旷视研究院极术社区专栏**](https://aijishu.com/blog/kuangshiyanjiuyu),定期更新最新旷视研究院成果