当前位置: 首页 > 后端技术 > Node.js

使用tfjs-node在树莓派4B上实现基于nodejs实时人脸识别和物体识别

时间:2023-04-04 00:26:51 Node.js

在树莓派4B小助手上使用tfjs-node实现基于nodejs的实时人脸识别和物体识别,类似小爱同学的离线增强版,可以顺便监控自己的小屋。但是年底了,忙得没时间和精力折腾。年初的时候就想过做。谁知来了一场疫情,一下子空闲的时间就多了。可惜没时间买树莓派,浪费了很多时间。复工后,中间出差了,快到年底了,终于战胜了懒癌晚期,实现了基本功能。写到这里做个记录,毕竟年纪大了记性不是很好。树莓派环境我的树莓派是4B,官方系统。nodejs版本是10.21.0,因为后面接的一个小oled屏也是nodejs控制的,但是这个驱动依赖的包太旧了,12以上的版本跑不了,所以降为10。一般情况下,12左右的版本可以运行tfjs。相机是某宝带小支架的20CSI接口相机,支持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核心库是不支持的。在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来实现整个过程。识别方法是获取摄像头拍摄的每一帧图像。一开始使用的是树莓派自带的camera命令,但是camera每张图片都需要等camera打开取景器才能抓取。大约需要一秒钟,这太慢了。一开始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端口,然后用nodejsffmpeg截流执行命令raspivid-t0-w640-h480-pfhigh-fps24-b2000000-o-|nc-k-l8090这时候可以通过播放地址端口来测试推流是否成功。可以用ffplay测试一下ffplaytcp://你的地址:8090如果一切顺利,应该可以看到你的大脸了。Nodejs拉流先通过ffmpeg拉端口push传入的tcp流varffmpeg=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){throwerr;});ffmpeg.on('close',function(code){console.log('ffmpegexitedwithcode'+code);});ffmpeg.stderr.on('data',function(data){//console.log('stderr:'+数据);});ffmpeg.stderr.on('exit',function(data){//console.log('退出:'+数据);});此时nodejs可以处理mjpeg推送的每一帧图片ffmpeg.stdout.on('data',function(data){varframe=newBuffer(data).toString('base64');console.log(frame);});至此,人脸识别和物体识别的处理可以写成一个进程,但是如果某处报错或者溢出整个程序就会挂掉,所以我把人脸识别和物体识别分别写成两个文件,处理它们通过socket通信,这样如果一个进程挂了再单独重启也不会影响一切,所以pull将接收到的流推送到需要识别的socket,准备接收返回的识别数据constnet=require('net');letisFaceInDet=false,isObjInDet=false,faceBox=[],objBox=[],faceHasBlock=0,objHasBlock=0;ffmpeg.stdout.on('data',function(data){varframe=newBuffer(data).toString('base64');console.log(frame);});letclientArr=[];constserver=net.createServer();//3绑定连接事件server.on('connection',(person)=>{console.log(clientArr.length);//记录连接过程person.id=clientArr.length;clientArr.push(person);//person.setEncoding('utf8');//客户端套接字进程绑定事件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;脸框=[];}else{faceHasBlock++;}}isFaceInDet=false;})person.on('close',(p1)=>{clientArr[p1.id]=null;})person.on('error',(p1)=>{clientArr[p1.id]=null;})})server.listen(8990);letclientOgjArr=[];constserverOgj=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-apidemo稍作改动接下来需要先接收传来的图片buffer,处理后返回识别数据letclient;const{canvas,faceDetectionNet,faceDetectionOptions,saveFile}=require('./commons/index.js');const{createCanvas}=require('canvas')const{Image}=canvas;constcanvasCtx=createCanvas(1280,760)constctx=canvasCtx.getContext('2d')asyncfunctioninit(){if(!img){//预加载模型awaitloadRes();}client=net.connect({port:8990,host:'127.0.0.1'},()=>{console.log('=-=-=-=')});让海峡=假;client.on('data',(chunk)=>{//console.log(chunk);//处理图像检测(chunk);})client.on('end',(chunk)=>{str=false})client.on('error',(e)=>{console.log(e.message);})}init();asyncfunctiondetect(buffer){//buffer转换为canvas对象letqueryImage=新图片();queryImage.onload=()=>ctx.drawImage(queryImage,0,0);queryImage.src=缓冲区;console.log('queryImage',queryImage);try{//识别结果Query=awaitfaceapi.detectAllFaces(queryImage,faceDetectionOptions)}catch(e){console.log(e);}letoutQuery='';//console.log(resultsQuery);//返回结果到socketclient.write(JSON.stringify(resultsQuery))return;if(resultsQuery.length>0){}else{console.log('donotdetectFacesresultsQuery')outQuery=faceapi.createCanvasFromMedia(queryImage)}}官方文档和例子中有更多的参数和细节。物体识别,参考同一个官方例子,对传过来的图片进行处理。letclient,img=false,myModel;asyncfunctioninit(){if(!img){//建议下载模型保存到本地,否则每次初始化都会从远程拉取模型,耗时很多-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'},});awaitmyModel.init(['cocoSsd','mobileNet']);img=真;}client=net.connect({port:8991,host:'127.0.0.1'},()=>{console.log('=-=-=-=')client.write(JSON.stringify([]))});让海峡=假;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();asyncfunctiondetect(imgBuffer){letresults=awaitmyModel.detect(图像缓冲区);client.write(JSON.stringify(results))return;}此时将获取的图片流推送到两个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);})}}varframe=newBuffer(data).toString('base64');console.log(frame);});这时候摄像头拍摄的每一帧图片和识别数据都有了,可以通过websocket返回给网页。然后网页通过canvas在页面上绘制每一帧图片和识别的数据,就实现了最基本的效果。当然在网页上我也不太放心,毕竟会涉及到隐私,所以你可以用react-native或者weex,用起来也方便。我尝试用rn的原图来展示,但是加载的时候图片一直闪,效果很差。我也用了rn的canvas,性能太差了。落后。最好直接用webview运行。最后开始做这个,打算研究tfjs,做一个小屋的监控预警。这部分只需要在人脸识别部分加入自己的人脸检测,识别出不是我的时候给我发信息。这样一个简单的监控和防护就实现了。本来想做一个可以识别简单操作的机器人,类似小爱同学,但是某宝上的硬件基本都是和其他平台对接的,没有可编程的可以玩。让我们制作一个可以进行简单对话的小型机器人。另外,识别出来的数据可以先由nodejs处理,然后push到ffmpeg,再转成rtmp流。这时候可以同时添加和推送音频流,这样效果会更好,但是我觉得自己没有脑细胞可以烧了,平时的工作就够了~以后有经验的还是要折腾又是小机器人。技术栈差不多,就是脑子不够用。