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

最近公司的需求又开始作妖了,说要做用户人脸识别,要知道照片有几个脸,和脸部位置。这需求下来让我这个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 看来要看看了。

回到顶部