开始
年前的时候就有这个想法,想做一个可能人脸识别和物体辨认,并且能简略对话辨认指令的助手,相似小爱同学离线增强版,顺便能监控本人的小屋。
不过年底太忙基本没有工夫精力去折腾,想着年初再搞,谁晓得来了个疫情,忽然多出那么多闲暇工夫,惋惜树莓派还没来得及买,节约了大把工夫。
停工后两头还出了次差,这又快到年底了终于克服懒癌早期,把根本的性能实现进去。
这里写下来做个记录,毕竟年级大了忘性不太好。
树莓派环境
我的树莓派是 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。
留神如果你是在 X86 架构的 Linux 或者 win 零碎下间接应用是没有问题的,但如果应用 arm 架构的零碎比方树莓派,那么 2.0 之前的 tfjs 外围库是不反对的
这里坑了良久,在 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
如果一切顺利应该就能看到本人的大脸了
nodejs 拉流
先通过 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 流,这时能够加上音频流同时推送,这样成果会更好,不过感觉我曾经没有脑细胞烧了,平时工作曾经够够的了~,前面有经验的话应该会再折腾下小机器人吧,技术栈曾经看的差不多,就是脑袋不够用了