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