早在去年,中科院的山世光老师团队就开源了其论文中实现的人脸识别框架
SeetaFaceEngine,而在今年早些时候,中科视拓团队又为我们带来了性能更为优越的 SeetaFaceEngine2。我在随后便封装了一个 Node.js 插件版的模块 node-seeta,以便于在我的项目中使用它。经过了半年左右的修改与重构,现在这个模块也还算是稳定了,所以现在也想来分享一下这个模块的开发心得,以及如何使用这个模块在你的 Node.js 应用中引入 SeetaFaceEngine。
现在
node-seeta 的版本是 0.2.4,如果你使用的是 0.2.2 <= 的版本,暴露的 API 则全部是同步操作 (不需要 async, await 关键字),当然 >= 0.2.3 也保留了同步的版本:detectSync, compareSync 和 recognizeSync。⚠️ 请不要使用 0.1.x,他们是已经废弃的版本。
下载安装
源码地址
- node-seeta: https://github.com/Mitscherlich/node-seeta
- SeetaFaceEngine2: https://github.com/seetaface/SeetaFaceEngine2
系统需求
编译依赖
这个模块的编译依赖于
OpenCV,使用 OpenCV 是由于 SeetaFaceEngine2 自身的历史包袱 (延伸阅读: 为什么 Node.js 读取图片这么难)。不过幸好 SeetaFaceEngine2 对于 OpenCV 的依赖仅仅是读取图片,所以理论上 OpenCV >= 2.4 的版本都可以使用,但要注意预编译的库文件使用了 gcc 6.20 和 protobuf 2.6,而这两个库在 Ubuntu boinc 上不是默认的版本,你可以从源码编译安装,也可以参考我的安装指南来购配置构建环境。参考下面的命令来配置你的安装环境,可能会有所帮助:
- For Ubuntu xenial
$ sudo apt update $ sudo apt install build-essential dpkg-dev $ sudo apt install cmake git libopenblas-dev libprotobuf-dev libssl-dev libgtk2.0-dev pkg-config libavcodec-dev libavformat-dev libswscale-dev
准备好之后,你可以参考我的的安装脚本来一键配置 OpenCV 和 protobuf。
哦对了,使用这个模块依然需要 SeetaFaceEngine 预训练的模型文件:
开发实践
光说不练假把式,现在我们就从一个简易的人脸检测/对齐/识别项目入手,来简单了解一下这个模块如何将 SeetaFaceEngine 带到 Node.js 的世界。
下面我编写了一个使用了 koa/seeta 的人脸识别后端服务,并设计前端页面用来测试它的功能。
🚧 你可以从以下地址获取示例代码:https://github.com/Mitscherlich/koa-seeta-demo
后端接口
首先在你的项目目录下初始化项目所需要的依赖:
# 使用 npm $ npm i -S koa koa-logger koa-router koa-multer koa-static-cache seeta # 当然你也可以使用 yarn $ yarn add koa koa-logger koa-router koa-multer koa-static-cache seeta
使用 koa 作为后端 Web 框架,koa-logger 用于打印请求日志,koa-multer 用于处理上传文件,koa-static-cache 用于托管静态文件,最后自然是项目核心 node-seeta 模块。
接着创建工程目录结构:
. ├── app.js ├── router.js ├── models │ ├── SeetaFaceDetector2.0.ats │ ├── SeetaFaceRecognizer2.0.ats │ └── SeetaPointDetector2.0.pts5.ats ├── package.json └── public ├── uploads ├── js | └── main.js └── index.html
编写入口文件:
// app.js const static = require('koa-static') const logger = require('koa-logger') const seeta = require('seeta') const path = require('path') const fs = require('fs') const Koa = require('koa') const app = new Koa() const fdModel = path.join(__dirname, 'models/SeetaFaceDetector2.0.ats') const faModel = path.join(__dirname, 'models/SeetaPointDetector2.0.pts5.ats') const frModel = path.join(__dirname, 'models/SeetaFaceRecognizer2.0.ats') const port = process.env.PORT || 3000 const dev = process.env.NODE_ENV === 'development' // 由于 `SeetaFaceEngine2` 的限制,所有的 seeta 对象都只能为单例 // 这里使用了 koa 框架的反模式,将 seeta 对象挂载到 context 下 // 在路由中这样使用:ctx.detector/pointer/recognizer app.context.detector = new seeta.FaceDetector(fdModel) app.context.pointer = new seeta.PointDetector(faModel) app.context.recognizer = new seeta.FaceRecognizer(frModel) app.use(static(path.join(__dirname, 'public'), { maxAge: 14 * 24 * 60 * 60 // 两周 })) if (dev) app.use(logger()) require('./router')(app) app.listen(port) console.log(`app running on http://127.0.0.1:${port}`)
编写路由文件:
// router.js const Multer = require('koa-multer') const Router = require('koa-router') const { Image } = require('seeta') const fs = require('fs') const store = Multer.diskStorage({ // 保存路径 destination: (req, file, cb) => cb(null, 'public/uploads/'), // 修改文件名称 filename: (req, file, cb) => { const suffix = (file.originalname).split('.').pop() const timestamp = Date.now() cb(null, timestamp + '.' + suffix) } }) const upload = Multer({ storage: store }) const router = new Router({ prefix: '/api' }) /** * /apit/seeta/face 人脸识别请求接口 * method: POST * files: avatar * 接受客户端发送的图片并进行人脸检测与识别,返回检测结果与相似度结果 */ router.post('/seeta/face', upload.single('avatar'), async ctx => { // 读取上传的图片 const image = new Image(__dirname + '/public/uploads/' + ctx.req.file.filename) // 检测/对齐/识别 try { if (ctx.recognizer.opertaional) { const { count, faces } = await ctx.pointer.detect(image, ctx.detector) if (count < 1) { ctx.body = { success: 0, msg: '您上传的图片好像不包含人脸' } return } const similars = await ctx.recognizer.recognize(image, ctx.detector, ctx.pointer) ctx.body = { success:1, faces, similars } } else ctx.body = { success: 0, msg: '请等待服务端初始化完成' } } catch (err) { console.error(err) } }) /** * /apit/seeta/faces 获取人脸数据库 (入库的图片) * method: GET * 返回已经入库的图片路径 */ router.get('/seeta/faces', async ctx => { // 遍历整个上传文件夹 const avatars = [] const walk = path => { fs.readdirSync(path).forEach(file => { const newPath = path + '/' + file const stat = fs.statSync(newPath) if (stat.isFile()) { if (/(.*)\.(png|jpg)/.test(file)) avatars.push(newPath) } else if (stat.isDirectory()) walk(newPath) }) } walk('public/uploads') ctx.body = { success: 1, avatars } }) /** * /apit/seeta/faces 清空人脸数据库 * method: DELETE * 清空人脸数据库 */ router.delete('/seeta/faces', async ctx => { if (ctx.recognizer.operational) { const walk = path => { fs.readdirSync(path).forEach(file => { const newPath = path + '/' + file const stat = fs.statSync(newPath) if (stat.isFile()) { if (/(.*)\.(png|jpg)/.test(file)) fs.unlinkSync(newPath) } else if (stat.isDirectory()) walk(newPath) }) } walk('public/uploads') ctx.recognizer.clear() ctx.body = { success: 1, msg: '清除成功' } } else ctx.body = { success: 0, msg: '请等待服务端初始化完成' } }) module.exports = app => app.use(router.routes(), router.allowedMethods())
简单吧,后端的逻辑就这么多。
前端页面
前端页面的代码我就不贴了,大家可以到示例仓库里自取,这里就只放几张演示截图:
小结
本来编写这个模块的初衷只是为了完成我在实验室遗留的一个项目,后来觉得有必要好好学习一下 node.js 的 c++ 插件开发,现在经过了反复的修改成为了一个小有规模的项目。如果你对这个项目感兴趣,也欢迎联系我加入到本项目的开发中来;本文写作时间仓促,如有不慎遗漏或者有谬误之处,还请不吝指正。
参考链接
- Mitscherlich/node-seeta: https://github.com/Mitscherlich/node-seeta
- seetaface/SeetaFaceEngine2: https://github.com/seetaface/SeetaFaceEngine2
- koajs/koa: https://github.com/koajs/koa
- koajs/static: https://github.com/koajs/static
- koa-modules/multer: https://github.com/koa-modules/multer
- alexmingoia/koa-Routre: https://github.com/alexmingoia/koa-router