从C++ 使用OpenCV + AI美颜 到 Electron

从C++ 使用OpenCV + AI美颜 到 Electron

前提:

​ OpenCV :跨平台计算机视觉和机器学习软件库。

​ Electron :一个跨平台的、基于 Web 前端技术的桌面 GUI 应用程序开发框架,基于 Chromium 和 Node.js。

目的:如标题,需要摄像头数据经过美颜处理,输出frame buffer 给到 Electron JavaScript 进行渲染。

思路:调用C++ OpenCV 获取原始图像信息,美颜SDK处理,通过NodeJs Addon 把C++ 获取到的 FrameBuffer 转交给Javascript然后调用H5 渲染。

拆解:C++ 使用 OpenCV 采集数据部分,美颜SDK 处理采集后的数据部分,NodeJs C++交互部分。

思路和逻辑都不会很麻烦,所以理论上实现起来问题应该不会很大。事实上,并不是。前期C++业务部分,虽然不太顺利,但相对还好。后期的Electron 部分就比较坑。如果不知道 Electron vue C++ 联调的方式,就会显得比较抓狂。一般情况下,异常部分可以依靠log,在electron vue 中, 仅有的C++的log 没办法打印,无法判定真正出错的地方就会大大增加修改的难度。好不容易找到了NodeJs 跟C++ Debug的方式,NodeJs调试好了之后,Electron 中又出现新的异常。

鉴于开始写记录过程的时候基本处于完工,比较折腾的路线就不再介绍。

做法

需要使用OpenCV , 也需要跨平台,C++ 就不得不是一个选项。

理论来讲使用OpenCV 不是一件特别麻烦的事情,理论和现实可能存在差异。

我们需要在win32 下面来做,因为我们的Electron 项目需要 32 位环境,那么构建 32 位编译调试环境就是第一步。

编译和使用依赖库

库编译
  1. GitHub 中 OpenCV 提供的编译完成的OpenCV 4.5 版本是64 位,无法直接使用,首要解决的问题是编译32 位的 OpenCV 库。CMake 、Visual Studio 相关配置和设置必不可少 。找到合适的编译教程,剩下的只是时间问题,推荐链接 OpenCV Windows编译

OpenCVCmake
OpenCVVisualStudio
OpenCVBuildLibs
opencvBuildOutput
opencvWindowsConfig

  1. 根推荐链接,不出意外,运气好的话大半天儿时间没有了(CMake 过程中会有相关指定库下载,这里存在下载不成功的可能性)。等32位库编译完成,配置好windows 系统环境变量,最终才可以进行OpenCV 的开发。
使用OpenCV
  1. 鉴于计算机视觉在现在的比较广泛的使用,能够从很多的途径拿到C++ 版本如何使用,确认好Demo 中使用的OpenCV 版本,然后根据例子就可以找到简洁易用的使用方法。于是我们可以通过OpenCV 获取到摄像头中的每一帧数据。
  2. OpenCV 获取到的每一帧画面其实可以理解为一张图片,有着长宽和颜色深度的矩阵数据。美颜部分就是拿到这一堆数据之后通过一定的算法把每一帧内容进行数据处理然后输出。比如可以通过TensorFlow Keras 或者Pytorch 神经网络训练库进行数据训练数据处理,处理训练之后生成可用的模型文件,在使用的时候利用框架,载入训练好的模型文件,去处理相应的图形数据。如果不依赖神经网络,特定算法也可以进行图片特征处理。这里我们使用了别人封装好的SDK,在给到输入的数据buffer 之后处理为可使用的美颜格式buffer。神经网络的部分这次先Pass ,我们先按照SDK的方式继续。
  3. 获取到文件buffer,交给JavaScript,依赖于NodeJs Addon 。如果按照Addon 的写法,应该问题也不是特别大。然而,NodeJs Addon 部分却是最耗时和麻烦的地方。
C++ JavaScript 相互通信

思路:Javascript 侧设置回调函数给到C++ , C++持有 Javascript 中的回调函数对象,通过异步回调方式给到Javascript。

前提: Javascript 只有单线程, C++ 可以有多线程,Javascript 设置回调函数调用C++,C++内部执行逻辑不能卡C++ 的主线程。Javascript 跟C++ 交互在于C++ 的主线程,不定时回调内容需要在C++ 中的工作线程(子线程),C++ 内部需要子线程跟主线程通信。Nodejs 能够高效的原因是因为NodeJS的底层是C++,JavaScript 是单线程,想要在Nodejs 中具有多线程就得依赖libuv。C++ 需要在NodeJs 上面的执行最终需要依赖 node-gyp 进行编译。

  1. node-gyp 的使用node-gyp (如果很熟悉这一步,请Pass,如果不是,请一行一行的看)首要解决掉编译和链接才能有下一步的调用,如果没有经过特别精细的设置,唯一的后果就是无法正常生成node文件,也就没办法进行C++ 编译和链接。node-gyp 的配置部分比较麻烦,需要仔细去确认。建议参考别人能够work 的项目工程进行理解,合理设置搜索路径和库链接(系统环境配置和软件运行环境配置最没技术含量,却有出奇的耗时)。
  2. 当我们终于配置完成库编译的时候,如何跟Electron 进行断点调试就是一个问题了。项目中我们这边使用的是Electron Vue ,有着各种的限制,不太容易去直接坐到做到。 但是好消息是,网上提供了可以用于调试C++ 的测试工程。借助这个测试工程,依赖 Nodejs 中的插件 CodeLLDB ,经过详细的插件配置,大半天儿之后NodeJs 和 C++ 可以断点联调了。请参考 如何使用vscode 调试 nodejs + C++
  3. 等以上全部做完,才开始正确的代码开发部分。借助于 Node Nan ,在JavaScript中设置回调函数给到C++, C++ 中接收JavaScript 传参,使用Nan::Persistentv8::Function 可以对JavaScript 中的函数进行持久化保存(避免function 被GC掉)。需求是设置一次JavaScript的回调函数,然后C++ 内部异步多次返回需要的数据。对于Nan 封装的高阶API AsyncWorker 不能使用,原因是在执行异步之后,JavaScript 的 Callback 会被干掉。因为想要得到OpenCV 的逐帧返回内容, C++ 这里获取每一帧内容需要存在于单独线程,那么libuv 就不得不使用了。
1
2
3
4
5
6
 // 用于主线程监听,在收到libuv 回调之后在合适时机执行
static void async_on_message(uv_async_t *handle);
// 用 Nan::Persistent 来持久化 JavaScript 提供的CallBack
Nan::Persistent<v8::Function> m_callBack;
// Async handle 用于触发,唤起 回调函数, 可以从其他线程触发
uv_async_t m_async;
libuv 使用方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// libuv 回调
void BeautyFaceInterface::async_on_message(uv_async_t *handle)
{
Nan::HandleScope m_scope;
// isolate 需要临时获取,一定不要存储
v8::Isolate *isolate = v8::Isolate::GetCurrent();
v8::HandleScope scope(isolate);
//get finalResult
...
...

v8::Local<v8::Integer> v8_width = v8::Integer::New(isolate, (int)finalResult.cols);
v8::Local<v8::Integer> v8_height = v8::Integer::New(isolate, (int)finalResult.rows);
// CopyBuffer 注意内存, 深度拷贝, 因为nan 使用了 c++ 跟 JavaScript Buffer 共享, 需要在 JavaScript 使用完成Buffer 之后置空操作,让内存释放
v8::Local<v8::Object> buffer = Nan::CopyBuffer((char *)finalResult.data, len).ToLocalChecked();
v8::Local<v8::Value> argv[] = {v8_width, v8_height, buffer};
const unsigned argc = 3;

v8::Local<v8::Function> m_call = Nan::New(BeautyFaceInterface::getInstance()->m_callBack);
TryCatch try_catch;
m_call->Call(isolate->GetCurrentContext()->Global(), argc, argv);
if (try_catch.HasCaught())
{
FatalException(try_catch);
}
}

// libuv Callback 初始化和 回调函数设置
void BeautyFaceInterface::start(const Nan::FunctionCallbackInfo<v8::Value> &args)
{
Isolate *isolate = args.GetIsolate();
v8::HandleScope scope(isolate);
Local<Value> arg = args[0];
if (!arg->IsFunction())
{
return;
}
//构造 持久化 JavaScript function
v8::Local<v8::Function> func = v8::Local<v8::Function>::Cast(args[0]);
BeautyFaceInterface::getInstance()->m_callBack.Reset(func);
// libuv uv_async_init 设置回调函数
uv_async_init(uv_default_loop(), &BeautyFaceInterface::getInstance()->m_async, BeautyFaceInterface::async_on_message);
}

/*
下面是在任意的函数内部执行,目的是为了启动和唤醒 async_on_message
*/

// 设置timer
Timer *t = new Timer();
t->setInterval([&]() {
//新的子线程中 fire m_async uv_async_send 线程安全 这里可能会出现多次fire ,但是 回调地方只会有一次的现象,这里不能使用 m_async 来进行传值,需要在这里交给主线程数据的话需要使用线程安全的队列来执行。
uv_async_send(&BeautyFaceInterface::getInstance()->m_async);
},
30); // 30 Ms


以上是正常的使用方法,在之前的时候出现过一些奇异的异常,比如在NodeJs 中可以完美运行,但是 载入Electron Vue 之后就在等待一段时间之后出现异常的现象。

libuv 异常部分
  1. 如下图,不能正常fire Async handle

ElectronIsolate

  1. 在回调函数中也遇到过一下的崩溃,这个是需要运行一段实现以后才会出现。
1
2
0x0eccee98 {isolate_=0xdddddddd {...} context_={...} m_callBack=0xdddddddd {handle_={...} } ...}
Nan::ObjectWrap:{refs_=-572662307 handle_={...} }

很明显能看出来 isolate_ 无法访问了 , m_callBack 也无法访问。 猜测大概率原因是经过了一些时间的运行,JavaScript 的虚拟环境发生了改变,或者 回调函数被GC 掉了。

最后查明原因是 isolate 不能进行存储(网上的Demo 有一些很不负责),JavaScript 提供的CallBack 需要使用 Nan::Persistent 来进行持久化(避免GC)。

所以会好奇如何拿到这些数据的对吧,配置调试环境放在第一位!!! 配置调试环境放在第一位!!! 配置调试环境放在第一位!!!

会很耗时间,但是总好过完全不知道错误原因

结尾

OK 到此为止,从C++ 获取OpenCV 数据 到 Electron 中经过 Canvas 渲染,最终能把画面呈现到 Electron 上面。

Demo 地址: 先等等,需要配置出包环境。