关于javascript:使用tfjsnode在树莓派4B上实现基于nodejs实时人脸识别和物体识别

32次阅读

共计 6923 个字符,预计需要花费 18 分钟才能阅读完成。

开始

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

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

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

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

树莓派环境

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

正文完
 0