泰晓科技 -- 聚焦 Linux - 追本溯源,见微知著!
网站地址:https://tinylab.org

儿童Linux系统,可打字编程学数理化
请稍侯

RISC-V AI 开发:用 D1 进行图片采集和人体识别

JinWen Zhou 创作于 2022/11/10

Corrector: TinyCorrect v0.1-rc3 - [autocorrect epw] Author: Jinwen Zhou zhoujwtony@163.com Date: 2022/08/04 Revisor: Falcon falcon@tinylab.org Project: RISC-V Linux 内核剖析 Sponsor: PLCT Lab, ISCAS

本文介绍在 D1 开发板上实现摄像头实时人体识别功能的过程。首先使能摄像头拍照的功能,接着使能深度学习框架 ncnn,最后结合两部分内容实现实时识别人体功能。

摄像头模块使能

开发板系统选择

如本系列第一部分所述,选择 D1-H 官方的固件 20210804(开机 HDMI 有小企鹅启动 logo):固件下载地址,此固件经测试可满足后续实验要求。

硬件准备

D1-H 开发板、一根 USB 线,一根串口线,一个 USB 摄像头。

软件准备

  • 在运行本例之前,已经成功运行 Hello Word 程序。确保交叉编译工具和 ADB 工具都可正常使用。

  • 下载 USB Camera demo 代码包:D1-H USB camera demo source code,并保存为 cam.c。

硬件连接

首先,请确保连接好需要用的硬件设备:

硬件连接.png

开发板开机,插入 USB 摄像头后,系统会自动识别并打印 USB 摄像头连接的信息,查看 /dev 外设目录:

$ ls /dev/video*

可以发现有 /dev/video0/dev/video1 设备,video1video0 的映射。

查看 video 设备.png

程序编译

通过上一篇配置好的交叉编译工具,可以直接编译如下:

riscv64-unknown-linux-gnu-gcc cam.c -o cam

文件上传

在 Windows 中使用 ADB 工具将其送入开发板中:

adb push cam ./.

程序运行

chmod +x test
./cam

此时程序运行过程中会打印一些内容,我们使用 Ctrl+C 终止程序运行后,可在当前文件夹看到名为 1.jpeg 图片,然后可以用 adb pull 导出来查看。

adb pull /root/1.jpeg .

Pull 成功,这样就可以直接在 Windows 下查看图片了,效果如下:

cam 程序执行结果

D1 上 ncnn 框架使能

ncnn 是腾讯开源的神经网络推理框架,具有轻量级,支持嵌入式平台的特点。它支持深度学习模型 caffe、mxnet、keras、pytorch(onnx)、darknet、tensorflow(mlir) 等框架。它兼容 RISC-V 架构,官方对该框架在 D1-H 上做了适配。故选用此框架进行相关实践。

编译工具下载

去平头哥芯片开放社区下载 工具链-900 系列

下载后,进行解压,并设置环境变量:

tar -zxvf Xuantie-900-gcc-linux-5.10.4-glibc-x86_64-V2.2.6-20220516.tar.gz
export RISCV_ROOT_PATH=/home/nihui/osd/Xuantie-900-gcc-linux-5.10.4-glibc-x86_64-V2.2.6

根据官方说明文档,需要打开 $RISCV_ROOT_PATH/lib/gcc/riscv64-unknown-linux-gnu/10.2.0/include/riscv_vector.h,在文件末尾,可以找到三个 #endif,在文件里添加:

#endif

#define vfrec7_v_f32m1(x, vl) vfrdiv_vf_f32m1(x, 1.f, vl)
#define vfrec7_v_f32m2(x, vl) vfrdiv_vf_f32m2(x, 1.f, vl)
#define vfrec7_v_f32m4(x, vl) vfrdiv_vf_f32m4(x, 1.f, vl)
#define vfrec7_v_f32m8(x, vl) vfrdiv_vf_f32m8(x, 1.f, vl)
#define vfrec7_v_f16m1(x, vl) vfrdiv_vf_f16m1(x, 1.f, vl)
#define vfrec7_v_f16m2(x, vl) vfrdiv_vf_f16m2(x, 1.f, vl)
#define vfrec7_v_f16m4(x, vl) vfrdiv_vf_f16m4(x, 1.f, vl)
#define vfrec7_v_f16m8(x, vl) vfrdiv_vf_f16m8(x, 1.f, vl)

#define vfrsqrt7_v_f32m1(x, vl) vfrdiv_vf_f32m1(vfsqrt_v_f32m1(x, vl), 1.f, vl)
#define vfrsqrt7_v_f32m2(x, vl) vfrdiv_vf_f32m2(vfsqrt_v_f32m2(x, vl), 1.f, vl)
#define vfrsqrt7_v_f32m4(x, vl) vfrdiv_vf_f32m4(vfsqrt_v_f32m4(x, vl), 1.f, vl)
#define vfrsqrt7_v_f32m8(x, vl) vfrdiv_vf_f32m8(vfsqrt_v_f32m8(x, vl), 1.f, vl)
#define vfrsqrt7_v_f16m1(x, vl) vfrdiv_vf_f16m1(vfsqrt_v_f16m1(x, vl), 1.f, vl)
#define vfrsqrt7_v_f16m2(x, vl) vfrdiv_vf_f16m2(vfsqrt_v_f16m2(x, vl), 1.f, vl)
#define vfrsqrt7_v_f16m4(x, vl) vfrdiv_vf_f16m4(vfsqrt_v_f16m4(x, vl), 1.f, vl)
#define vfrsqrt7_v_f16m8(x, vl) vfrdiv_vf_f16m8(vfsqrt_v_f16m8(x, vl), 1.f, vl)

#endif
#endif

ncnn 下载和编译

git clone https://github.com/Tencent/ncnn.git
cd ncnn
mkdir build-c906
cd build-c906
cmake -DCMAKE_TOOLCHAIN_FILE=../toolchains/c906-v226.toolchain.cmake \
    -DCMAKE_BUILD_TYPE=release -DNCNN_OPENMP=OFF -DNCNN_THREADS=OFF -DNCNN_RUNTIME_CPU=OFF -DNCNN_RVV=ON \
    -DNCNN_SIMPLEOCV=ON -DNCNN_BUILD_EXAMPLES=ON ..
cmake --build .
cmake --build . --target install

测试 example

这里准备实现对图片上的物体进行检测,ncnn 支持大部分常用的 CNN 网络,这里选择测试 nanodet 模型,这是一个可在移动设备上进行实时超快速高精度物体检测的模型。需要的文件有:

  • ncnn/build-c906/examples/nanodet
  • 测试图片 test.jpg
  • nanodet 模型文件,找到 nanodet 对应的 .bin.param 文件,它们分别为识别所需网络和训练过的参数。

将上述文件通过 ADB 工具下载到 D1-H 上,然后在开发板上执行:

./nanodet test.jpg

输出结果会保存在 image.png 中。

把 image.png 下载到本地查看,就可以看到检测结果。

图片检测结果.png

摄像头人体实时检测功能实现

接下来将综合前两个部分的功能,实现摄像头定时的对出现的画面进行人体检测。每隔一秒钟进行拍摄一张图片,交给 ncnn 模块进行识别。

模块示意图.png

此功能即可为后续视频监控提供上传数据的信号,又可避免直接对视频使用深度学习检测带来的开销,适合在嵌入式平台上使用。

修改 ncnn 模块的例程

接着对例程中 nanodet.c 进行修改,使其成为独立的模块,该模块收到处理图片请求后对图片进行检测,并返回结果。

我们将采用管道通信的方式,让本模块作为独立的服务端程序,收到请求的第一个字节为功能号,1 代表人体识别功能。返回的数据第二个字节为识别结果,1 代表图片中有人,0 代表没有人。本模块目前只判断是否有人,后续只需增加与之通信的协议并增加相关实现便可对识别模块进行功能拓展。

打开 ncnn/examples/nanodet.cpp 文件,新增一个函数,根据容器 objects 中的 label 确定是否有人,该 objects 存储了识别结果。

/* 新增 is_person 函数 */
static int is_person(const std::vector<Object>& objects)
{
    int ret = 0;
    /* 图片中可识别目标存在 objects 容器中 */
    for (size_t i = 0; i < objects.size(); i++)
    {
        const Object& obj = objects[i];
        /* 目标结构体中 label 为 0 代表目标为人 */
        if (obj.label == 0) {
            ret = 1
            break;
        }

    }

    return ret;
}

#define FIFO_1 "/tmp/1"
#define FIFO_2 "/tmp/2"

int main(int argc, char** argv)
{
    char buffer[80];
    int fd_w;
    int fd_r;
    int ret;
    int datalen;
    unlink(FIFO_1);
    mkfifo(FIFO_1,0666);
    unlink(FIFO_2);
    mkfifo(FIFO_2,0666);// 建立有名管道

    fd_w = open(FIFO_1,O_WRONLY);
    fd_r = open(FIFO_2,O_RDONLY);
    while(1)
    {
        memset(buffer,0,80);
        /* 收取客户端发来的识别请求 */
        datalen = read(fd_r,buffer,80);
        if (datalen == -1)
            continue;
        if (buffer[0] == 1)
        {
            //start detect
            if (argc != 2)
            {
                fprintf(stderr, "Usage: %s [imagepath]\n", argv[0]);
                return -1;
            }
            /* 摄像头模块采集图片的保存路径 */
            const char* imagepath = argv[1];

            cv::Mat m = cv::imread(imagepath, 1);
            if (m.empty())
            {
                fprintf(stderr, "cv::imread %s failed\n", imagepath);
                return -1;
            }

            std::vector<Object> objects;
            /* 使用 nanodet 框架进行识别,识别结果保存在 objects 容器中 */
            detect_nanodet(m, objects);
            ret = is_person(objects);
            buffer[1] = ret;
            write(fd_w,buffer,80);

        }

    }
    return 0;
}

修改 camera 模块例程

修改摄像头模块代码 cam.c,让其在保存照片后通知 ncnn 模块,并等待检测结果:

int main(int argc, char* argv[])
{
    char buffer[80];
    int fd_w;
    int fd_r;
    int datalen;
    fd_r = open(FIFO_1,O_RDONLY);
    fd_w = open(FIFO_2,O_WRONLY);
    v4l2_init();
    while(1)
    {
        v4l2Grab();
        buffer[0] = 1;
        /* 通知 ncnn 模块已经采集到图片并存储 */
        datalen = write(fd_w,buffer,80);
        if(datalen == -1)
            continue;

        /* 等待 ncnn 发回来的识别结果 */
         if(read(fd_r,buffer,80))
        {
            if(buffer[1] == 1)
                printf("person!\n");
            else
                printf("no person!\n");
        }
        sleep(1);
    }
    v4l2_close();
    return 0;
}

编译

首先进入 ncnn/build-c906/ 路径,输入如下命令编译 ncnn 检测程序 nanodet.cpp 文件:

make nanodet

编译成功,显示:

[  0%] Built target ncnn-generate-spirv
[100%] Built target ncnn
[100%] Built target nanodet

接着使用 D1 SDK 源码中给的编译器编译 cam.c 文件:

riscv64-unknown-linux-gnu-gcc cam.c -o cam

编译完成后,准备运行的文件,放到一个文件夹 demo 里:

  • ncnn/build-c906/examples 路径下的 nanodet
  • nanodet 模型文件,找到 nanodet 对应的 .bin.param 文件
  • cam.c 的编译文件 cam

将上述文件通过 ADB 工具发送到 D1-H 上,发送命令:

adb push  <本机路径/demo> <开发板目标路径>

传输成功后,在开发板上找到 demo 文件夹,要实现两个进程之间的通信,首先在该路径下执行 nanodet,加 & 让他后台运行,如果没有权限,首先输入 chmod 命令:

chmod +x nanodet
./nanodet 1.jpeg &

然后执行 cam:

chmod +x cam
./cam

运行成功的情况下,当摄像头中出现人时,会检测到有人,输出 person!

实时检测输出.png

小结

本文简单实现了对摄像头采集图片的一个实时检测,后续可对识别模块进行优化拓展。

参考资料



Read Album:

Read Related:

Read Latest: