开始

年前的时候就有这个想法,想做一个可能人脸识别和物体辨认,并且能简略对话辨认指令的助手,相似小爱同学离线增强版,顺便能监控本人的小屋。

不过年底太忙基本没有工夫精力去折腾,想着年初再搞,谁晓得来了个疫情,忽然多出那么多闲暇工夫,惋惜树莓派还没来得及买,节约了大把工夫。

停工后两头还出了次差,这又快到年底了终于克服懒癌早期,把根本的性能实现进去。

这里写下来做个记录,毕竟年级大了忘性不太好。

树莓派环境

我的树莓派是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流,这时能够加上音频流同时推送,这样成果会更好,不过感觉我曾经没有脑细胞烧了,平时工作曾经够够的了~,前面有经验的话应该会再折腾下小机器人吧,技术栈曾经看的差不多,就是脑袋不够用了