使用WebAssembly版本opencv实现人脸识别
发布于 6 年前 作者 zy445566 9626 次浏览 来自 分享

最近公司的需求又开始作妖了,说要做用户人脸识别,要知道照片有几个脸,和脸部位置。这需求下来让我这个CURD-BOY有点慌了,果然这个重任又落到了我身上。所以开始研究扣脸技术,之前使用过opencv做过盲水印技术,所以这次就打算继续选取opencv来做这个。

但由于很久没有接触opencv了,之前还是基于2.4做的,现在都4.3了,果然还是逝者如斯夫不舍昼夜。既然如此,重新看官方文档来一遍。

发现新大陆

这个时候居然发现了opencv.js,不看不知道一看高兴坏了。原来opencv.js是opencv利用了emscripten将原本的C++版本编译成了WebAssembly,让js可以直接调用C++版本的opencv方法。

这下省事了,线上部署也方便了。要知道在之前如果要用,线上服务器还要装opencv的开发套还要编写C++扩展,这样非常容易出问题,如果是docker,添加安装脚本前期工作量能让你爆炸。如果是主机,则很容易因为线上linux版本问题和环境问题,导致调用opencv出错。但现在有WebAssembly版本的opencv.js一切都变的不一样了。

所以今天我打算通过opencv.js来实现扣脸技术。

获取opencv.js

获取opencv.js有两种途径

  1. 使用源码构建(教程地址)
  2. 直接下载构建好的版本(下载地址)

两者都可以直接使用在nodejs或js上,区别是源码构建先需要先有emscripten环境,步骤比较麻烦。下载则版本固定且方便,但如果你要修改opencv源码实现特殊功能,那就不行了。

首先实现nodejs服务端版本

其实在官网例子Face Detection using Haar Cascades(例子地址)就有这个例子,但区别是服务端读取图片方式不同,在例子中使用的是前端的canvas读取,后端读取图片主要是使用了jimp库来读取图片。

同时其实在人脸识别中,opencv有一个自带的训练模型haarcascade_frontalface_default.xml,这个模型可以在opencv的代码库中找到(代码库地址)。

既然方法有了,模型也有了,是不是可以直接开撸u,很方便就能实现?

Module = {
 async onRuntimeInitialized() {
 console.log(cv.getBuildInformation())
 await getFace()
 }
 }
const cv = require('./opencv.js');
const fs = require('fs');
const Jimp = require('jimp');
const path = require('path');
async function getFace() {
 var jimpSrc = await Jimp.read(path.join(__dirname,'gx.jpg'));
 let src = cv.matFromImageData(jimpSrc.bitmap);
 let gray = new cv.Mat();
 cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY, 0);
 let faces = new cv.RectVector();
 let faceCascade = new cv.CascadeClassifier();
 // load pre-trained classifiers
 cv.FS_createDataFile(
 '/', 'haarcascade_frontalface_default.xml', 
 fs.readFileSync(path.join(__dirname,'haarcascade_frontalface_default.xml')), 
 true, false, false
 );
 faceCascade.load('haarcascade_frontalface_default.xml');
 // // detect faces
 let msize = new cv.Size(0, 0);
 faceCascade.detectMultiScale(gray, faces, 1.1, 3, 0, msize, msize);
 for (let i = 0; i < faces.size(); ++i) {
 let roiGray = gray.roi(faces.get(i));
 let roiSrc = src.roi(faces.get(i));
 let point1 = new cv.Point(faces.get(i).x, faces.get(i).y);
 let point2 = new cv.Point(faces.get(i).x + faces.get(i).width,
 faces.get(i).y + faces.get(i).height);
 cv.rectangle(src, point1, point2, [255, 0, 0, 255]);
 roiGray.delete(); roiSrc.delete();
 }
 new Jimp({
 width: src.cols,
 height: src.rows,
 data: Buffer.from(src.data)
 }).write('gxOutput.png');
 src.delete(); gray.delete(); faceCascade.delete();
 faces.delete();
}

nodejs实现也就花了45行代码,其中为什么Module要在require(’./opencv.js’)之前是因为在’./opencv.js’文件中执行了全局的Module.onRuntimeInitialized方法。

那么实现效果如下:

  • 输入照片:
    • gx.jpg
  • 识别照片:
    • gxOutput.png

js前端实现

先讲一下为什么要在前端实现,是由于计算量不大不会影响用户体验,同时可以节约上传图片和下载图片的消耗,更重要的是实现也非常方便。

js前端实现的话,基本和nodejs差不多,区别在于读取图像使用canvas和使用ajax请求获取模型文件。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello OpenCV.js</title>
</head>
<body>
<h2>Hello OpenCV.js</h2>
<p id="status">OpenCV.js is loading...</p>
<p><button id="getFaceBtn">获取人脸</button></p>
<div>
 <div class="inputoutput">
 <div>
 <canvas id="canvasInput" width="400" height="400"></canvas>
 </div>
 <div class="caption">输入图片 <input type="file" id="fileInput" name="file" /></div>
 </div>
 <div class="inputoutput">
 <div>
 <canvas id="canvasOutput" width="400" height="400"></canvas>
 </div>
 <div class="caption">输出图片</div>
 </div>
</div>
<script type="text/javascript">
let inputElement = document.getElementById('fileInput');
let faceBtn = document.getElementById('getFaceBtn');
let img = new Image();
inputElement.addEventListener('change', (e) => {
 img.src = URL.createObjectURL(e.target.files[0]);
}, false);
img.onload = function() {
 let inCanvas = document.getElementById('canvasInput')
 let inCanvasCtx = inCanvas.getContext('2d')
 inCanvasCtx.drawImage(img,0,0,img.width,img.height,0,0,400,400);
 if(img.width!==400 || img.height!=400) {
 inCanvas.toBlob(function(blob) {
 img.src = URL.createObjectURL(blob);
 })
 }
};
faceBtn.addEventListener('click', (e) => {
 let outCanvas = document.getElementById('canvasOutput')
 let outCanvasCtx = outCanvas.getContext('2d');
 let src = cv.imread('canvasInput');
 let gray = new cv.Mat();
 cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY, 0);
 let faces = new cv.RectVector();
 let faceCascade = new cv.CascadeClassifier();
 // load pre-trained classifiers
 faceCascade.load('haarcascade_frontalface_default.xml');
 // // detect faces
 let msize = new cv.Size(0, 0);
 faceCascade.detectMultiScale(gray, faces, 1.1, 3, 0, msize, msize);
 for (let i = 0; i < faces.size(); ++i) {
 let roiGray = gray.roi(faces.get(i));
 let roiSrc = src.roi(faces.get(i));
 const offest = 0
 let point1 = new cv.Point(faces.get(i).x, faces.get(i));
 let point2 = new cv.Point(faces.get(i).x + faces.get(i).width,
 faces.get(i).y + faces.get(i).height);
 outCanvasCtx.drawImage(img, 
 faces.get(i).x,
 faces.get(i).y,
 faces.get(i).width,
 faces.get(i).height,
 0,0,400,400)
 roiGray.delete(); roiSrc.delete();
 }
 src.delete(); gray.delete(); faceCascade.delete();
 faces.delete();
})
function onOpenUtilsReady() {
 let utils = new Utils('errorMessage');
 utils.loadOpenCv(() => {
 document.getElementById('status').innerHTML = 'OpenCV.js is ready.';
 let faceCascadeFile = 'haarcascade_frontalface_default.xml';
 utils.createFileFromUrl(faceCascadeFile, faceCascadeFile, () => {
 console.log('加载模型成功')
 });
});
}
</script>
<script async src="./utils.js" onload="onOpenUtilsReady();" type="text/javascript"></script>
<style>
.inputoutput{
 display: inline-block;
}
</style>
</body>
</html>

效果如下:

webOut.png

实例地址展示地址,需翻墙

最后的选型

虽然nodejs服务端和JS前端都实现了扣脸功能,若直接使用前端实现扣脸,可以实现扣脸保证用户体验,又节约图片上传和下载的带宽,为用户和公司节约了资源,充分利用边缘计算优势。

但若前端实现,考虑到opencv.js的wasm文件过大,需要做进度条加载文件比较麻烦,前端进度很可能来不及,且对于前端复杂度度提升有风险超过前端所能承受的范围,所以最终还是使用nodejs服务端的加载opencv.js实现。

同时能实现前端和后端的服务,也不禁感叹一声WebAssembly牛逼!emscripten牛逼!opencv.js牛逼!Node.js牛逼!JS牛逼!

9 回复

@jxycbjhc 记仇了,看看这篇有没有比原来少1%的水

@zy445566 老铁,稳健,图片是我辈楷模啊。。。 我要是帅哥,会特么少女朋友。截屏2020年06月03日 上午11.11.40.png 看下了效果正面没毛病。

@jxycbjhc 其实识别了多个,但我只选了最后一个输出。你看到的案例是直接在浏览器里实现的,没服务端

@zy445566 这玩意投入回报比咋样?太低直接就和老板说了不写了,我是来赚钱的。

WebAssembly牛逼!emscripten牛逼!opencv.js牛逼!Node.js牛逼!JS牛逼!深以为然。 可以用 JavaScript 来写的应用,最终都会用 JavaScript 来写。

@jxycbjhc 你是说人脸识别?还是wasm?如果是人脸识别,回报比不是很强。但如果是wasm,尤其是已经移植到wasm的库或简单的C++改的wasm来替代c++扩展的话,回报比爆表。

@kamibababa 简单的C++扩展完全可以使用wasm替代,可维护性比原生大十倍。复杂的比如调用很多大的C++库不方便移植或调用V8本身的特性再考虑C++扩展了。

前几天还把我数字水印的库移植到了wasm,现在服务器不用装C++库也不需要依赖node-gyp编译了。而且一般来说只要编译一次C++库,以后都可以直接复用。

https://github.com/zy445566/node-digital-watermarking/pull/8/files

@zy445566 wasm 看来要看看了。

回到顶部

AltStyle によって変換されたページ (->オリジナル) /