年前的时候就有这个想法,想做一个能够人脸识别和物体识别,并且能简单对话识别指令的助手,类似小爱同学离线增强版,顺便能监控自己的小屋。
不过年底太忙根本没有时间精力去折腾,想着年初再搞,谁知道来了个疫情,突然多出那么多空闲时间,可惜树莓派还没来得及买,浪费了大把时间。
复工后中间还出了次差,这又快到年底了终于克服懒癌晚期,把基本的功能实现出来。
这里写下来做个记录,毕竟年级大了记性不太好。
我的树莓派是4B,官方系统。nodejs版本是10.21.0,因为后面又接入个oled的小屏也是使用nodejs控制,但是这个驱动依赖的包太老,12以上的版本跑不起来所以降到了10。正常情况12左右的版本都能跑起来tfjs。
摄像头是某宝的20块带个小支架的CSI接口摄像头,支持1080p,价格惊到我了。同样型号不限只要带摄像头能获取到视频流就行
如果没有树莓派,在Linux或者win系统也能正常实现功能
人脸识别用的是face-api.js,是一个基于tfjs的js库,tfjs就是TensorFlow的js版,支持web端和nodejs端。这个库大致原理是取人脸面部的68个点去做对比,识别率挺高的,并且能够检测性别,年龄(当然相机自带美颜的今天娱乐下就好)还有面部表情。
这个库上次的维护时间是八个月前,不知道是不是老美那闹的太欢的原因已经很久没维护了,tfjs核心库已经更新到2.6左右,这个库还使用的是1.7。
这里坑了好久,在Windows和Linux上跑着好好的,到树莓派安装npm包就会报错,看了下tfjs源码报错原因是1.7版本还没有添加对arm架构的支持。虽然他可以不依赖tfjs核心库去跑,但效率感人,200ms的识别时间在不使用tfjs核心库的树莓派上需要花10秒左右。肯定是不考虑这种方式的。
所以要在树莓派上跑的话要做的首先就是下载face-api.js的源码更新下tfjs的版本然后重新编译一次,我更新到了2.6的版本会有一个核心库方法被弃用,那块注释掉就好了,当然如果懒得改动的也可以用我改动编译过的版本face-api
物体识别用的也是基于tfjs的库recognizejs,同理也需要将tfjs升级到2.0以上,这个库只是将tfjs物体识别库coco-ssd和mobilenet做了简单应用,所以直接下载下来改下tfjs的版本就好了。
这里试过好几种方法,毕竟是想用nodejs去实现全部流程,那么识别方法就是获取摄像头捕捉到的每一帧,一开始使用的树莓派自带的拍照命令,但是拍照每张都要等相机打开取景再捕获,需要一秒左右太慢了,ffmpeg一开始无法直接将获取到视频流给nodejs,如果改用Python之类的话感觉做这个差点意思。
后来发现ffmpeg是可以把流传给nodejs的,只不过nodejs不好直接处理视频流,所以只需要将ffmpeg推送的格式转为mjpeg,这样nodejs拿到的每一帧直接是图片,不需要做其余处理。
首先安装ffmpeg,百度一大把,就不贴了
然后安装相关nodejs依赖
{
"dependencies": {
"@tensorflow/tfjs-node": "^2.6.0",
"babel-core": "^6.26.3",
"babel-preset-env": "^1.7.0",
"canvas": "^2.6.1",
"nodejs-websocket":"^1.7.2",
"fluent-ffmpeg": "^2.1.2"
}
}
注意安装canvas库的时候会依赖很多包,可以根据报错信息去安装对应的包,也可以直接百度树莓派node-canvas需要的包
拉取摄像头的流
我用的方法是先用自带的摄像头工具将流推倒8090端口,然后用nodejs ffmpeg截取流
执行命令
raspivid -t 0 -w 640 -h 480 -pf high -fps 24 -b 2000000 -o - | nc -k -l 8090
这时候可以通过播放该地址端口测试推流是否成功
可以使用ffplay测试
ffplay tcp://你的地址:8090
如果一切顺利应该就能看到自己的大脸了
先通过ffmpeg拉取端口推过来的tcp流
var ffmpeg = require('child_process').spawn("ffmpeg", [
"-f",
"h264",
"-i",
"tcp://"+‘自己的ip和端口’,
"-preset",
"ultrafast",
"-r",
"24",
"-q:v",
"3",
"-f",
"mjpeg",
"pipe:1"
]);
ffmpeg.on('error', function (err) {
throw err;
});
ffmpeg.on('close', function (code) {
console.log('ffmpeg exited with code ' + code);
});
ffmpeg.stderr.on('data', function (data) {
// console.log('stderr: ' + data);
});
ffmpeg.stderr.on('exit', function (data) {
// console.log('exit: ' + data);
});
这时候nodejs就能处理到mjpeg推过来的每一帧图片了
ffmpeg.stdout.on('data', function (data) {
var frame = new Buffer(data).toString('base64');
console.log(frame);
});
到这里可以把人脸识别和物体识别的处理写到一个进程里,但这样如果某个地方报错或者溢出了整个程序就会挂掉,所以我把人脸识别和物体识别单独写到两个文件,通过socket通信去处理,这样某个进程挂了单独重启他就好了,不会影响所有
所以要将拉到的流推给需要识别的socket,并且准备接收返回的识别数据
const net = require('net');
let isFaceInDet = false,isObjInDet = false,faceBox=[],objBox=[],faceHasBlock=0,objHasBlock=0;
ffmpeg.stdout.on('data', function (data) {
var frame = new Buffer(data).toString('base64');
console.log(frame);
});
let clientArr = [];
const server = net.createServer();
// 3 绑定链接事件
server.on('connection',(person)=>{
console.log(clientArr.length);
// 记录链接的进程
person.id = clientArr.length;
clientArr.push(person);
// person.setEncoding('utf8');
// 客户socket进程绑定事件
person.on('data',(chunk)=>{
// console.log(chunk);
if(JSON.parse(chunk.toString()).length>0){
//识别后的数据
faceBox = JSON.parse(chunk.toString());
}else{
if(faceHasBlock>5){
faceHasBlock = 0;
faceBox = [];
}else{
faceHasBlock++;
}
}
isFaceInDet = false;
})
person.on('close',(p1)=>{
clientArr[p1.id] = null;
} )
person.on('error',(p1)=>{
clientArr[p1.id] = null;
})
})
server.listen(8990);
let clientOgjArr = [];
const serverOgj = net.createServer();
// 3 绑定链接事件
serverOgj.on('connection',(person)=>{
console.log(clientOgjArr.length);
// 记录链接的进程
person.id = clientOgjArr.length;
clientOgjArr.push(person);
// person.setEncoding('utf8');
// 客户socket进程绑定事件
person.on('data',(chunk)=>{
// console.log(chunk);
if(JSON.parse(chunk.toString()).length>0){
objBox = JSON.parse(chunk.toString());
}else{
if(objHasBlock>5){
objHasBlock = 0;
objBox = [];
}else{
objHasBlock++;
}
}
isObjInDet = false;
})
person.on('close',(p1)=>{
clientOgjArr[p1.id] = null;
} )
person.on('error',(p1)=>{
clientOgjArr[p1.id] = null;
})
})
serverOgj.listen(8991);
把face-api官方的demo干下来稍微改动下
需要先接收传过来的图片buffer,处理完后返回识别数据
let client;
const { canvas, faceDetectionNet, faceDetectionOptions, saveFile }= require('./commons/index.js');
const { createCanvas } = require('canvas')
const { Image } = canvas;
const canvasCtx = createCanvas(1280, 760)
const ctx = canvasCtx.getContext('2d')
async function init(){
if(!img){
//预加载模型
await loadRes();
}
client = net.connect({port:8990,host:'127.0.0.1'},()=>{
console.log('=-=-=-=')
});
let str=false;
client.on('data',(chunk)=>{
// console.log(chunk);
//处理图片
detect(chunk);
})
client.on('end',(chunk)=>{
str=false
})
client.on('error',(e)=>{
console.log(e.message);
})
}
init();
async function detect(buffer) {
//buffer转为canvas对象
let queryImage = new Image();
queryImage.onload = () => ctx.drawImage(queryImage, 0, 0);
queryImage.src = buffer;
console.log('queryImage',queryImage);
try{
//识别
resultsQuery = await faceapi.detectAllFaces(queryImage, faceDetectionOptions)
}catch (e){
console.log(e);
}
let outQuery ='';
// console.log(resultsQuery);
//将结果返回给socket
client.write(JSON.stringify(resultsQuery))
return;
if(resultsQuery.length>0){
}else{
console.log('do not detectFaces resultsQuery')
outQuery = faceapi.createCanvasFromMedia(queryImage)
}
}
官方文档和示例里有更多的参数细节
同样的参考官方示例,处理传过来的图片
let client,img=false,myModel;
async function init(){
if(!img){
//建议将模型下载下来保存到本地,否则每次初始化都会从远程拉取模型,消耗很多时间
myModel = new Recognizejs({
mobileNet: {
version: 1,
// modelUrl: 'https://hub.tensorflow.google.cn/google/imagenet/mobilenet_v1_100_224/classification/1/model.json?tfjs-format=file'
modelUrl: 'http://127.0.0.1:8099/web_model/model.json'
},
cocoSsd: {
base: 'lite_mobilenet_v2',
// modelUrl: 'https://hub.tensorflow.google.cn/google/imagenet/mobilenet_v1_100_224/classification/1/model.json?tfjs-format=file'
modelUrl: 'http://127.0.0.1:8099/ssd/model.json'
},
});
await myModel.init(['cocoSsd', 'mobileNet']);
img = true;
}
client = net.connect({port:8991,host:'127.0.0.1'},()=>{
console.log('=-=-=-=')
client.write(JSON.stringify([]))
});
let str=false;
client.on('data',(chunk)=>{
// console.log(chunk);
console.log(n);
detect(chunk);
})
client.on('end',(chunk)=>{
str=false
})
client.on('error',(e)=>{
console.log(e.message);
})
}
init();
async function detect(imgBuffer) {
let results = await myModel.detect(imgBuffer);
client.write(JSON.stringify(results))
return;
}
这时候在推流的js里将获取的图片流推给这两个socket
ffmpeg.stdout.on('data', function (data) {
//同时只处理一张图片
if(!isFaceInDet){
isFaceInDet = true;
if(clientArr.length>0){
clientArr.forEach((val)=>{
// 数据写入全部客户进程中
val.write(data);
})
}
}
if(!isObjInDet){
isObjInDet = true;
if(clientOgjArr.length>0){
clientOgjArr.forEach((val)=>{
// 数据写入全部客户进程中
val.write(data);
})
}
}
var frame = new Buffer(data).toString('base64');
console.log(frame);
});
这个时候摄像头获取的每一帧图片和识别到的数据就都有了,可以通过websocket返回给网页了,网页再将每一帧图片和识别的数据通过canvas绘制到页面上,最基本的效果就实现了。
当然放在网页上心里不放心,毕竟会涉及到隐私,所以可以用react-native或者weex之类的包个壳,这样用起来也方便,我试过用rn原生的图片去展示,但是图片加载的时候一直闪,效果很不好,还用过rn的canvas,性能差太多,一会就卡死了。还是直接用webview跑效果比较好。
我最开始做这个的打算研究下tfjs并做一个小屋的监控预警
预警这块只需要在人脸识别这块加上自己的人脸检测,识别到不是自己的时候给我发消息,这样一个简单的监控守卫就实现了
本来还想做一个能识别简单操作的机器人,类似小爱,但是某宝上的硬件基本都是接入到别人平台的,没有可编程的给玩玩,那只能先做一个能简单对话的小机器人了。
还有识别后的数据可以先在nodejs处理完然后再推给ffmpeg后转成rtmp流,这时可以加上音频流同时推送,这样效果会更好,不过感觉我已经没有脑细胞烧了,平时工作已经够够的了~,后面有经历的话应该会再折腾下小机器人吧,技术栈已经看的差不多,就是脑袋不够用了