基于 MAX78000FTHR 实现可以人脸识别的血氧饱和度测试仪
使用 MAX78000FTHR 的摄像头拍摄图像并在显示屏上进行实时显示,同时将图像发送给CNN进行识别,可以识别是否有人脸以及人脸是谁,同时板卡外接一只血氧饱和度测试模块,可以用来测量识别到的人的血氧饱和度。
标签
嵌入式系统
MCU
MAX78000
topgear
更新2023-01-31
817

一、项目描述

使用 MAX78000FTHR 的摄像头拍摄图像并在显示屏上实时显示,同时将图像发送给CNN。CNN计算此图像的向量,与预设的向量表内的数据进行距离比较,根据比较结果可以检测图像中是否存在人脸或者图像中的人脸是否为向量表中的对象,同时板卡外接一只血氧饱和度测试模块,可以用来测量识别到的人的血氧饱和度。

二、硬件介绍

  • 摄像头(板载):用于拍摄图像(人脸);
  • CNN(板载):用于图像的向量计算;
  • 串口(板载):用于输出调试信息;
  • 显示屏(DFR0665):2.8英寸的TFT电阻式触摸显示屏,用于实时显示摄像头拍摄的图像信息以及显示血氧饱和度相关的数据;
  • 血氧饱和度测量模块(MAX30102):用于测量血氧饱和度。

三、框图

FpzmOCc4mCgYSzJvDAR6mr0oE9WC

四、管脚定义

  • DFR0665 - 2.8' 320x240 TFT LCD(这款屏不接RESET管脚也可以工作)

MAX78000FTHR 板卡数据手册中的管脚框图将P0_11管脚标识为SPI0_SS0,而我查询MAX78000的数据手册,发现P0_11的复选功能2应该为SPI0_SS1。

MAX78000FTHR   DFR0665  
3V3   VCC 电源
GND   GND 接地
P0_7 SPI0_SCK SCLK 时钟
P0_5 SPI0_MOSI MOSI 数据(主机发送从机接收)
P0_6 SPI0_MISO MISO 数据(主机接收从机发送)
P0_11 SPI0_SS1 CS 屏幕片选
P0_8 SPI0_SDIO2 DC 数据/命令
P0_19   TOUCH_CS 触摸片选
P1_6   INT 触摸中断
  • MAX30102
MAX78000FTHR   MAX30102  
3V3   VCC 电源
GND   GND 接地
P0_16 I2C1_SCL SCL 串行时钟线
P0_17 I2C1_SDA SDA 串行数据线

五、外设功能熟悉以及驱动开发

  • 摄像头

通过 Camera_IF 工程范例来熟悉摄像头的使用,调试过程中,关闭ENABLE_TFT宏定义,使用工程自带的 pc_utility 脚本,通过串口接收摄像头捕获的图像并在PC上进行显示。

调试过程中,发现pc_utility显示的图像有问题,后来发现是DAPLINK的固件版本的问题,通过群里小伙伴提供的固件版本解决了该问题。max32625_max78000fthr_if_crc_v1.0.2.bin
  • DFR0665

DFRobot 出品的 2.8" 320x240 TFT电阻触摸显示屏:驱动芯片为 ILI9341,触摸芯片XPT2046。

初始化代码如下,为了支持触控功能,对示例代码做了一些调整:

/* Initialize TFT display */
// RESET和BL管脚没有连接,所以都置NULL
// TFT DC 管脚在 MXC_TFT_Init() 接口函数中初始化,默认使用的P0_8,作为通用驱动,这里最好也能像RESET管脚一样可以在接口外定义并传入
MXC_TFT_Init(MXC_SPI0, 1, NULL, NULL);
MXC_TFT_SetRotation(ROTATE_0);
MXC_TFT_SetBackGroundColor(4);
MXC_TFT_SetForeGroundColor(WHITE); // set font color to white

mxc_gpio_cfg_t ts_cs_pin = {MXC_GPIO0, MXC_GPIO_PIN_19, MXC_GPIO_FUNC_OUT,
                                    MXC_GPIO_PAD_NONE, MXC_GPIO_VSSEL_VDDIOH};
MXC_GPIO_Config(&ts_cs_pin);

// 官方的示例代码中触控芯片使用了 SPI0_SS0(P0_4),而SPI0_SS0 在FTHR板卡上连接的是Micro_SD 的CS管脚
// 所以我通过修改ss_idx来禁用了SS0的功能,并且另外定义了一个 P0_19 作为 TOUCH_CS。
mxc_ts_spi_config ts_spi_config = {
        .regs   = MXC_SPI0,
        .gpio   = {MXC_GPIO0, MXC_GPIO_PIN_5 | MXC_GPIO_PIN_6 | MXC_GPIO_PIN_7 /*| MXC_GPIO_PIN_4*/,
                 MXC_GPIO_FUNC_ALT1, MXC_GPIO_PAD_NONE, MXC_GPIO_VSSEL_VDDIOH},
        .freq   = 1000000,
        .ss_idx = 3,
    };

    mxc_gpio_cfg_t int_pin = {MXC_GPIO1, MXC_GPIO_PIN_6, MXC_GPIO_FUNC_IN, MXC_GPIO_PAD_NONE,
                              MXC_GPIO_VSSEL_VDDIOH};
    /* Initialize Touch Screen controller */
    MXC_TS_PreInit(&ts_spi_config, &int_pin, NULL);

    /* Initialize Touch Screen controller */
    MXC_TS_Init();
    MXC_TS_Start();

同时 \MaximSDK\Libraries\MiscDrivers\Touchscreen\tsc2046.c 中也要加上P0_19的片选功能代码:

// 修改 x、y分辨率
#define X_RES_T	240
#define Y_RES_T	320

extern mxc_gpio_cfg_t ts_cs_pin;
static void spi_transmit_tsc2046(mxc_ts_touch_cmd_t datain, unsigned short* dataout)
{
    int i;
    uint8_t rx[2] = {0, 0};
    mxc_spi_req_t request;

    request.spi = t_spi;
    request.ssIdx = t_ssel;
    request.ssDeassert = 0;
    request.txData = (uint8_t*)(&datain);
    request.rxData = NULL;
    request.txLen = 1;
    request.rxLen = 0;

    MXC_SPI_SetFrequency(t_spi, t_spi_freq);
    MXC_SPI_SetDataSize(t_spi, 8);

    // cs->low
    MXC_GPIO_OutClr(ts_cs_pin.port, ts_cs_pin.mask);

    MXC_SPI_MasterTransaction(&request);

    // Wait to clear TS busy signal
    for (i = 0; i < 100; i++) {
        __asm volatile("nop\n");
    }

    request.ssDeassert = 1;
    request.txData = NULL;
    request.rxData = (uint8_t*)(rx);
    request.txLen = 0;
    request.rxLen = 2;

    MXC_SPI_MasterTransaction(&request);

    // cs->high
    MXC_GPIO_OutSet(ts_cs_pin.port, ts_cs_pin.mask);

    if (dataout != NULL) {
        *dataout = (rx[1] | (rx[0] << 8)) >> 4;
    }
}

// x、y分辨率修改
static int tsGetXY(unsigned short* x, unsigned short* y)
{
    unsigned short tsX, tsY, tsZ1;
    int ret;

    spi_transmit_tsc2046(TSC_DIFFZ1, &tsZ1);

    if (tsZ1 & 0x7F0) {
        spi_transmit_tsc2046(TSC_DIFFX, &tsX);
        *x = tsX * 240 / 0x7FF;
        spi_transmit_tsc2046(TSC_DIFFY, &tsY);
        *y = tsY * 320 / 0x7FF;

        // Wait Release
        do {
            spi_transmit_tsc2046(TSC_DIFFZ1, &tsZ1);
        }
        while (tsZ1 & 0x7F0);

#if (FLIP_SCREEN == 1)
        *x = X_RES_T - *x;
        *y = Y_RES_T - *y;
#elif (ROTATE_SCREEN == 1)
        unsigned short swap = *x;
        *x = 240-*y-1;
        *y = swap;
#endif
        ret = 1;

    } else {

#if (FLIP_SCREEN == 1)
	*x = X_RES_T;
	*y = Y_RES_T;
#elif (ROTATE_SCREEN == 1)
	*x = Y_RES_T;
	*y = X_RES_T;
#else
	*x = 0;
	*y = 0;
#endif

        ret = 0;
    }

    return ret;
}
  • MAX30102

使用I2C1,初始化代码如下:

/***** Definitions *****/
#define I2C_MASTER  MXC_I2C1 // SCL P0_16; SDA P0_17
#define I2C_SCL_PIN 16
#define I2C_SDA_PIN 17

#define I2C_FREQ 60000 // 100kHZ

static mxc_i2c_req_t i2c_req;

int error;
uint8_t counter = 0;

//Setup the I2CM
error = MXC_I2C_Init(I2C_MASTER, 1, 0);
if (error != E_NO_ERROR) {
    printf("-->Failed master\n");
    return -1;
} else {
    MXC_I2C_SetFrequency(I2C_MASTER, I2C_FREQ);
    printf("\n-->I2C Master Initialization Complete\n");
}

i2c_req.i2c      = I2C_MASTER;
i2c_req.addr     = 0;
i2c_req.tx_buf   = NULL;
i2c_req.tx_len   = 0;
i2c_req.rx_buf   = NULL;
i2c_req.rx_len   = 0;
i2c_req.restart  = 0;
i2c_req.callback = NULL;

for (uint8_t address = 8; address < 120; address++) {
    i2c_req.addr = address;
    printf(".");

    if ((MXC_I2C_MasterTransaction(&i2c_req)) == 0) {
        printf("\nFound slave ID %03d; 0x%02X\n", address, address);
        counter++;
    }
    MXC_Delay(MXC_DELAY_MSEC(20));
}

printf("\n-->Scan finished. %d devices found\n", counter);

// resets the MAX30102
maxim_max30102_reset();

MXC_Delay(MXC_DELAY_MSEC(20));

// read and clear status register
maxim_max30102_read_reg(0, &uch_dummy);

// initializes the MAX30102
maxim_max30102_init();

六、一次最终失败的训练

插一句经验教训总结:以后做任何项目一定要先跑通示例代码,我在做本项目的过程中花了太多时间在训练环境的搭建和模型的训练上,并且在训练环境上耗费了近200元来购买硬件资源,但是最终训练出来的模型所生成的代码却无法实现人脸识别,导致后面做应用开发时又去使用SDK提供的训练模型代码,浪费了太多时间。

  • 购买GPU云服务器:

一开始我准备使用 windows 10 笔记本的WSL部署开发环境,配置环境的过程中出现了各种不可控的问题,并且考虑到笔记本只有集成显卡,于是参考群里小伙伴尝试找一个GPU云服务器进行训练。我选择的是腾讯GPU云服务器,在使用初期因为我的操作失误导致无法参加“1元试用15天”的活动,后来我重新购买了竞价实例,但是发现训练太耗时了,即使使用价格低廉的竞价实例也会导致使用成本很高,并且竞价实例还面临硬件资源被释放的风险。最后狠心花了120元购买了一个月的服务,但在训练过程中还是因为硬盘资源不够被迫扩容到300G容量,合计花费近200元。。。

  • 操作系统
Ubuntu 18.04 + CUDA11 + GPU驱动版本 460.106.00
Pytorch 1.9.1 + torchvision0.10.0 + Miniconda + OpenCV 4 + Python 3.8
  • 硬件资源

CPU - Intel(R) Xeon(R) Platinum 8255C CPU @ 2.50GHz 8核

Memory - 32G

Harddisk - 300G

GPU -  Nvidia Tesla T4 16G

  • 在云服务器上安装软件工具包
$ sudo apt-get install -y make build-essential libssl-dev zlib1g-dev \
  libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm \
  libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev \
  libsndfile-dev portaudio19-dev
  • 训练和综合的SDK配置

在本地主机上通过代理下载 ai8x-training 和 ai8x-synthesis ,然后通过 SCP 远程导入到云服务器。

  • 验证训练环境是否能够工作 - kws20_v3训练

为了验证训练环境是否能够工作,参考群里不少小伙伴跑通的kws20训练,我直接执行了./script/train_kws20_v3.sh,大概训练了三个多小时结束了。训练完的数据将近11GB,执行script/evaluate_kws20_v3.sh可以校验,ai8x-synthesis/train/ 目录的ai85-kws20_v3-qat8-q.pth.tar 会被更新,执行./script/quantize_kws20_v3.sh和./script/gen_kws20_v3_max78000.sh 会在sdk/example目录生成新的kws20_v3源代码。通过kws20_v3的训练说明训练环境是工作的。

FjL9E9hdDIwVlgDXMAd5s3jYxM9w

  • FACEID 训练 - 数据集下载

vggface2_train.zip - 下载地址 

vggface2_test.zip - 下载地址

YouTubeFaces.tar.gz - 下载地址   用户名:wolftau 密码:wtal997 

  • FACEID 训练 - 数据集vggface2处理
// vggface2 数据集处理

cd ai8x-training/data/FACEID
unzip vggface2_train.zip
unzip vggface2_test.zip
cd ai8x-training

// 生成向量
python datasets/face_id/gen_vggface2_embeddings.py -r data/FACEID/vggface2_train/ -d data/FACEID/vggface2/embeddings --type train
python datasets/face_id/gen_vggface2_embeddings.py -r data/FACEID/vggface2_test/ -d data/FACEID/vggface2/embeddings --type test

// vggface2 目录
vggface2
└── embeddings
    ├── test
    │   └── temp
    └── train
        └── temp

// 合并向量
python datasets/face_id/merge_vggface2_dataset.py -p data/FACEID/vggface2/embeddings  --type train
python datasets/face_id/merge_vggface2_dataset.py -p data/FACEID/vggface2/embeddings  --type test


mkdir -p data/FACEID/VGGFace-2
mkdir -p data/FACEID/VGGFace-2/train
mkdir -p data/FACEID/VGGFace-2/test

mv data/FACEID/vggface2/embeddings/train/*.pkl data/FACEID/VGGFace-2/train/
mv data/FACEID/vggface2/embeddings/test/*.pkl data/FACEID/VGGFace-2/test/

// VGGFace-2目录
VGGFace-2
├── test
│   ├── whole_set_00.pkl
│   └── whole_set_01.pkl
└── train
    ├── whole_set_00.pkl
    ├── whole_set_01.pkl
    ├── whole_set_02.pkl
    └── whole_set_03.pkl
  • FACEID 训练 - 数据集YouTubeFaces 处理
cd ai8x-trainning/data/FACEID
tar zxvf YouTubeFaces.tar.gz
cd ai8x-trainning
// 生成向量
python datasets/face_id/gen_youtubefaces_embeddings.py -r YouTubeFaces/ -d YouTubeFaces/ --type test
// 合并向量
python datasets/face_id/merge_youtubefaces_dataset.py -p data/FACEID/YouTubeFaces/  --type test

// YouTubeFaces 目录会生成下列pkl文件
whole_set_01.pkl
whole_set_02.pkl
whole_set_03.pkl
whole_set_04.pkl
whole_set_05.pkl
whole_set_06.pkl
whole_set_07.pkl

// YouTubeFaces/test/ 目录会生成系列pkl文件
whole_set_00.pkl
  • FACEID 训练
./scripts/train_faceid.sh --data data/FACEID
  • FACEID 量化
cd ai8x-synthesis
python quantize.py ../ai8x-training/latest_log_dir/qat_best.pth.tar trained/ai85-faceid-qat8-q.pth.tar --device MAX78000 -v
  • 生成代码
cd ai8x-synthesis

//
./gen-demos-max78000_faceid.sh
// gen_demos-max78000.sh会调用ai8xize.py
python ./ai8xize.py -e --verbose --top-level cnn -L --test-dir ../ai8x-training/data/FACEID/  --prefix faceid --checkpoint-file ./trained/ai85-faceid-qat8-q.pth.tar --config-file networks/faceid.yaml --device MAX78000 --fifo --compact-data --mexpress --display-checkpoint --unload

ubuntu@VM-0-14-ubuntu:~/max78000/ai8x-synthesis$ ./gen-demos-max78000_faceid.sh
NOTICE: Upstream repository on GitHub has updates on the develop branch! (Use --no-version-check to disable this check.)
Configuring device: MAX78000
Reading networks/faceid.yaml to configure network...
Reading trained/ai85-faceid-qat8-q.pth.tar to configure network weights...
Checkpoint for epoch 10, model ai85faceidnet - weight and bias data:
 InCh OutCh  Weights         Quant Shift  Min  Max    Size Key                                       Bias       Quant  Min  Max Size Key
    3    16  (48, 3, 3)          8    -1  -73  104     432 conv1.op.weight                           N/A            0    0    0    0 N/A     
   16    32  (512, 3, 3)         8    -1 -125   81    4608 conv2.op.weight                           N/A            0    0    0    0 N/A     
   32    32  (1024, 3, 3)        8    -1 -120   84    9216 conv3.op.weight                           N/A            0    0    0    0 N/A     
   32    64  (2048, 3, 3)        8    -1 -111   94   18432 conv4.op.weight                           N/A            0    0    0    0 N/A     
   64    64  (4096, 3, 3)        8    -1 -126  122   36864 conv5.op.weight                           N/A            0    0    0    0 N/A     
   64    64  (4096, 3, 3)        8     0  -74   62   36864 conv6.op.weight                           N/A            0    0    0    0 N/A     
   64    64  (4096, 3, 3)        8     0  -73   67   36864 conv7.op.weight                           N/A            0    0    0    0 N/A     
   64   512  (32768, 1, 1)       8     0  -81   76   32768 conv8.op.weight                           N/A            0    0    0    0 N/A     
TOTAL: 8 parameter layers, 176,048 parameters, 176,048 bytes
Configuring data set: FaceID.
faceid...
NOTICE: --overwrite specified, writing to sdk/Examples/MAX78000/CNN/faceid even though it exists.
Arranging weights... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100%
Storing weights...   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100%
Creating network...  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100%
  • weights.h 文件中的CNN配置代码需要替换到faceid project中的weights.h文件中去,我的项目工程是基于 Examples\MAX78000\CNN\faceid_evkit 上修改的,经过测试我的weights.h 文件替换之后,人脸识别的功能无法工作。

七、基于 faceid_evkit 项目合并新的人脸向量

虽然训练出来的模型无法工作,应用还是得继续做,为了偷懒,直接将我本人和张艺谋的人脸向量合并到 faceid_evkit 工程中的人脸向量表中,具体步骤如下:

  • 人脸数采集

张艺谋(Zhang_Yimou) - 从网上找6张图片

Fo1B8_fUmNrHncOA-4yLJixR0SEy

我本人(topgear) - 自拍6张

按照如下目录生成mydb

Fq91xm7OLj9OhIO87FddYB2Ddr0Q

  • 生成向量

Examples/MAX78000/CNN/faceid_evkit/db_gen 目录执行 generate_face_db,注意要选择刚才创建的数据集“mydb”

ubuntu@VM-0-14-ubuntu:~/max78000/ai8x-synthesis/sdk/Examples/MAX78000/CNN/faceid_evkit/db_gen$ python generate_face_db.py --db /home/ubuntu/max78000/ai8x-training/data/FACEID/mydb/ --db-filename embeddings --include-path ../include/
Running on device: cuda:0
Configuring device: AI85, simulate=True.
Processing subject: Zhang_Yimou
        File: 3.jpg
/home/ubuntu/max78000/ai8x-synthesis/sdk/Examples/MAX78000/CNN/faceid_evkit/db_gen/mtcnn/utils/detect_face.py:70: UserWarning: The given NumPy array is not writeable, and PyTorch does not support non-writeable tensors. This means you can write to the underlying (supposedly non-writeable) NumPy array using the tensor. You may want to copy the array to protect its data or make it writeable before converting it to a tensor. This type of warning will be suppressed for the rest of this program. (Triggered internally at  /pytorch/torch/csrc/utils/tensor_numpy.cpp:180.)
  imgs = torch.as_tensor(imgs, device=device) # pylint: disable=no-member
/usr/local/miniconda3/lib/python3.8/site-packages/torch/nn/functional.py:718: UserWarning: Named tensors and all their associated APIs are an experimental feature and subject to change. Please do not use them for anything important until they are released as stable. (Triggered internally at  /pytorch/c10/core/TensorImpl.h:1156.)
  return torch.max_pool2d(input, kernel_size, stride, padding, dilation, ceil_mode)
        File: 5.jpg
        File: 1.jpg
        File: 6.jpg
        File: 4.jpg
        File: 2.jpg
Processing subject: topgear
        File: 3.jpg
        File: 5.jpg
        File: 1.jpg
        File: 6.jpg
        File: 4.jpg
        File: 2.jpg
A new DB with
        Zhang_Yimou: 6 images
        topgear: 5 images
has been created!
Binary embedding file is saved to "/home/ubuntu/max78000/ai8x-synthesis/sdk/Examples/MAX78000/CNN/faceid_evkit/db_gen/embeddings.bin".
Embedding file is saved to ../include/embeddings.h

embedding.h 就是包含我和张艺谋人脸数据所对应的向量文件。

  • 向量表的格式说明
typedef struct __attribute__((packed)) {
    uint8_t numberOfSubjects;
    uint16_t lengthOfEmbeddings;
    uint16_t numberOfEmbeddings;
    uint16_t imageWidth;
    uint16_t imageHeight;
    uint16_t lengthOfSubjectNames;
} tsFaceIDFile;

根据代码可以分析出示例项目中的向量表结构,如下图显示,示例的向量表中包含6位西方明星:

FkBORzCZIJIO09acZ836Z-RIHJIh

我新生成的向量表如下:

FsyDyGEj6WtSfMYUI0VKPOhSOttc

  • 合并向量表

直接进行合并,生成新的向量表,包含我、张艺谋和6位西方明星:

FiLEPJflFhg175wtbEYLrvh-1U0V

八、应用开发

  • 屏幕home页面新增图片的转换方法

从网上找一张血氧仪的图片,使用sdk自带的转换工具将图片转换为代码:

// 进入 faceid_evkit\TFT\fthr\Utility 目录

python bmp2c.py oximeter.bmp -f

FlY1OvjE576mEmr03i0V01H0n6VG

  • 字体大小和位置调整

将字体调整为12px,可以在屏幕上展示更多信息,同时调整字体的显示位置,让展示页面更加美观;

  • 人脸识别的流程基本按照示例代码执行,只不过 embedding_process 处理的是上面合并出来的新的 embeddings;
  • 血氧饱和度和心跳算法参考模块卖家提供的示例代码,但是我这边测不出正确的数据,后来找了网上的算法,也没有测出,所以这个demo暂时只能实时展示IR和RED数据,血氧和心跳算法还需要花一些时间去调试。

九、功能展示

详细的功能展示见视频,这里展示一下home页面和faceid页面:

  • home页面

FhnkkYoP8YYofonMRIPbZfLnsRp-

  • faceid页面

FmvzkOAaGGxaZ_VTw0zuYtyavUjh

附件下载
faceid_oximeter.zip
faceid oximeter 固件代码
团队介绍
在职软件工程师
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号