Node.js
使用 node-seeta 为你的项目引入 SeetafaceEngine
8/31/2018
·
24
·
profile photo
早在去年,中科院的山世光老师团队就开源了其论文中实现的人脸识别框架 SeetaFaceEngine,而在今年早些时候,中科视拓团队又为我们带来了性能更为优越的 SeetaFaceEngine2。我在随后便封装了一个 Node.js 插件版的模块 node-seeta,以便于在我的项目中使用它。
经过了半年左右的修改与重构,现在这个模块也还算是稳定了,所以现在也想来分享一下这个模块的开发心得,以及如何使用这个模块在你的 Node.js 应用中引入 SeetaFaceEngine。
现在 node-seeta 的版本是 0.2.4,如果你使用的是 0.2.2 <= 的版本,暴露的 API 则全部是同步操作 (不需要 asyncawait 关键字),当然 >= 0.2.3 也保留了同步的版本:detectSynccompareSync 和 recognizeSync
⚠️ 请不要使用 0.1.x,他们是已经废弃的版本。

下载安装

源码地址

系统需求

编译依赖

这个模块的编译依赖于 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())
简单吧,后端的逻辑就这么多。

前端页面

前端页面的代码我就不贴了,大家可以到示例仓库里自取,这里就只放几张演示截图:
notion image
notion image

小结

本来编写这个模块的初衷只是为了完成我在实验室遗留的一个项目,后来觉得有必要好好学习一下 node.js 的 c++ 插件开发,现在经过了反复的修改成为了一个小有规模的项目。如果你对这个项目感兴趣,也欢迎联系我加入到本项目的开发中来;本文写作时间仓促,如有不慎遗漏或者有谬误之处,还请不吝指正。

参考链接