当前位置: 首页 > 工具软件 > tfjs > 使用案例 >

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

李捷
2023-12-01

开始

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

不过年底太忙根本没有时间精力去折腾,想着年初再搞,谁知道来了个疫情,突然多出那么多空闲时间,可惜树莓派还没来得及买,浪费了大把时间。

复工后中间还出了次差,这又快到年底了终于克服懒癌晚期,把基本的功能实现出来。

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

树莓派环境

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

 类似资料: