前言:
濒危动物的追踪识别一直是动物保护和研究的难题,传统的跟踪手法主要是通过研究濒危动物的分布,根据动物的粪便,食物,大致定位动物可能的活动范围,再布置摄像机进行24小时监控,效率较低,尤其是24小时的摄录监控,需要占用大量的存储卡以及需要人工进行繁重的筛选,也不利于大范围分布式布点跟踪。
本项目提出了一种使用深度学习技术进行目标识别的方法,深度神经网络会对摄像头采集到的图像进行计算,对学习到的濒危动物进行自动特征提取,符合标签的,自动进行视频录制,并可以通过AIOT的平台,将检测到的数据回传服务器端,相比传统方式,不再需要24小时实时录制,由深度学习算法,替代人工进行了目标识别和分类,不再需要人工的进行筛选,非目标濒危动物自动忽略且不进行录制,极大的提高了存储利用率。降低了成本,方便在野外大范围部署。对濒危动物的保护和检测,起到基础支撑设施的作用。
视频
PPT
总体设计:
该比赛项目设计主要分为三个部分:
目标检测模型——Yolo v3
首先让我们来看一下标准的Yolo v3模型结构
标准的Yolo v3模型大小高达200多MB,无法放在RT1062上运行,NOR Flash也不够存放这么大的模型,计算时间更是难以估计。因此有必要对Yolo v3做出修改,降低计算量和参数量,能够在单片机上运行。
修改后的模型如下:
因模型结构图神似猴赛雷,因此我们就将他称为Yolo-猴赛雷模型。猴赛雷模型相较于原版yolo v3的改变主要有以下几点:
1、缩小输入分辨率
yolo输入通常为416*416分辨率,猴赛雷模型将模型输入分辨率降低到160*160,在牺牲一定的精度后计算量大幅下降。可以根据实际需要适当扩大或缩小输入分辨率。
2、轻量化骨干网络
yolo的骨干网络使用darknet网络,该网络虽然精度表现很好,但计算量和参数量过大,不适合在MCU设备上运行。猴赛雷模型骨干网络采用深度可分离卷积和残差连接组成,大幅降低计算量和参数量。同时激活函数由leaky relu替换为relu6,这使得模型在量化后精度下降少一点。
3、修改输出层
yolo输出包含三个特征层,分别负责检测不同尺度的信息,但由于TensorFlow Lite Micro目前不支持上采样算子(虽然可以自己实现),而使用反卷积计算量又太大了,所以yolo输出层之间没有进行特征融合,这导致在实际调试过程中发现中间的特征层输出置信度基本上达不到阈值,因此将中间的输出特征层砍掉,只保留两个输出特征层。
模型部署到MCU
1、移植TensorFlow Lite Micro
模型部署采用TensorFlow Lite Micro推理框架,这个框架在NXP的MCUXpresso中也提供支持,免去自己手动移植,并且CMSIS-NN也适配了。
2、量化并生成tflite格式模型文件
模型部署前首先要对模型进行量化,采用TensorFlow框架对模型进行量化并保存为tflite格式,代码如下:
代码语言:python代码运行次数:0复制converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_data_gen
# Ensure that if any ops can't be quantized, the converter throws an error
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
# Set the input and output tensors to uint8 (APIs added in r2.3)
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8
tflite_model_quant = converter.convert()
# Save the model.
with open('xxx.tflite', 'wb') as f:
f.write(tflite_model_quant)
3、加载模型
模型文件有了,这么在单片机上获取这个模型文件呢?这些模型本质上就是一堆二进制数据,只需要给tflm提供这堆数据的首地址就可以了,大小都不需要提供。本文提供几种方式去加载这些模型:
1、使用xxd指令将tflite编程c语言源文件,以数组形式存放模型,这也是官方提供的做法。
2、编译的时候链接tflite文件,方便一点。
3、以上两种方法都是将模型保存在flash中,每次下载程序都要老久了,其实不必将模型保存在flash中,可以通过将模型保存在MicroSD卡中,单片机将存在MicroSD卡中的文件复制到RAM中即可,也可以用USB将内存虚拟成U盘,直接将模型文件从电脑上拖到单片机内存中,实现模型和单片机程序的解耦。主要实现代码如下:
代码语言:c复制/* 从sd卡中获取tflite模型 */
int fatfs_get_model(void* pModel, uint32_t size)
{
DIR DirInfo;
FILINFO FilInfo;
FIL File;
FRESULT error;
UINT bytesRead;
UINT bytesModel = 0;
char name[64];
if(f_opendir(&DirInfo, (const TCHAR*)"/") == FR_OK) /* 打开文件夹目录成功,目录信息已经在dir结构体中保存 */
{
while(f_readdir(&DirInfo, &FilInfo) == FR_OK) /* 读文件信息到文件状态结构体中 */
{
if(!FilInfo.fname[0])
break; /* 如果文件名为‘ ',说明读取完成结束 */
if(strstr(FilInfo.fname, "tflite") != NULL)
{
printf("-> %s | %lu KB <-rn", FilInfo.fname, FilInfo.fsize / 1024);
break;
}
}
bytesModel = FilInfo.fsize;
strncpy(name, FilInfo.fname, sizeof(name));
f_closedir(&DirInfo);
}
if(bytesModel > size)
{
printf("tmodel size errorrn");
return -1;
}
error = f_open(&File, name, FA_READ);
if (error)
{
printf("tOpen file failed.rn");
return -1;
}
error = f_read(&File, pModel, FilInfo.fsize, &bytesRead);
if (error)
{
printf("tRead file failed. rn");
return -1;
}
if(bytesRead != FilInfo.fsize)
{
printf("tRead size %lu %d.rn", FilInfo.fsize, bytesRead);
return -1;
}
printf("tRead model file ok. rn");
return 0;
}
void setup(void* pModel) {
// Set up logging. Google style is to avoid globals or statics because of
// lifetime uncertainty, but since this has a trivial destructor it's okay.
// NOLINTNEXTLINE(runtime-global-variables)
static tflite::MicroErrorReporter micro_error_reporter;
error_reporter = µ_error_reporter;
// Map the model into a usable data structure. This doesn't involve any
// copying or parsing, it's a very lightweight operation.
model = tflite::GetModel(pModel);
if (model->version() != TFLITE_SCHEMA_VERSION) {
TF_LITE_REPORT_ERROR(error_reporter,
"Model provided is schema version %d not equal "
"to supported version %d",
model->version(), TFLITE_SCHEMA_VERSION);
return;
}
// This pulls in all the operation implementations we need.
// NOLINTNEXTLINE(runtime-global-variables)
static tflite::AllOpsResolver resolver;
// Build an interpreter to run the model with.
static tflite::MicroInterpreter static_interpreter(
model, resolver, tensor_arena, kTensorArenaSize, error_reporter);
interpreter = &static_interpreter;
// Allocate memory from the tensor_arena for the model's tensors.
TfLiteStatus allocate_status = interpreter->AllocateTensors();
if (allocate_status != kTfLiteOk) {
TF_LITE_REPORT_ERROR(error_reporter, "AllocateTensors() failed");
return;
}
// Obtain pointers to the model's input and output tensors.
input = interpreter->input(0);
/* yolo 有多个输出层 */
for(uint32_t i = 0; i < interpreter->outputs_size(); i ) {
output[i] = interpreter->output(i);
}
}
4、跑跑跑
之前的工作准备好后运行起来就很简单了,首先准备输入数据,调用invoke()函数,最后进行后处理即可,关键部分代码如下。
代码语言:javascript复制/* 模型输出结果的处理关键部分 */
for(uint32_t i = 0; i < interpreter->outputs_size(); i ) {
printf("[output %ld]rn", i);
uint32_t len_x = output[i]->dims->data[1];
uint32_t len_y = output[i]->dims->data[2];
uint32_t len_z = output[i]->dims->data[3];
for(uint32_t i_x = 0; i_x < len_x; i_x ) {
for(uint32_t i_y = 0; i_y < len_y; i_y ) {
for(uint32_t i_z = 0; i_z < len_z; i_z ) {
if(i_z % 6 == 4) {
uint32_t grid_size = input->dims->data[1] / len_x;
uint32_t k = (i_x*len_y i_y)*len_z i_z;
float conf = sigmoid(((float)output[i]->data.int8[k] - output[i]->params.zero_point) * output[i]->params.scale);
if(conf > 0.7) {
float anchor_x = anchor[i][i_z/6][0];
float anchor_y = anchor[i][i_z/6][1];
int32_t pos_x = grid_size * (i_y sigmoid((output[i]->data.int8[k-3] - output[i]->params.zero_point) * output[i]->params.scale));
int32_t pos_y = grid_size * (i_x sigmoid((output[i]->data.int8[k-4] - output[i]->params.zero_point) * output[i]->params.scale));
int32_t pos_w = anchor_x * expf((output[i]->data.int8[k-2] - output[i]->params.zero_point) * output[i]->params.scale);
int32_t pos_h = anchor_y * expf((output[i]->data.int8[k-1] - output[i]->params.zero_point) * output[i]->params.scale);
printf("[conf %.2f] [%ld %ld %ld %ld]rn", conf, pos_x, pos_y, pos_w, pos_h);
push_top_n(conf, pos_x - pos_w/2, pos_y - pos_w/2, pos_x pos_w/2, pos_y pos_w/2, i);
}
}
}
}
}
}
/* Non-Maximum Suppression */
int nms(float threshold, uint32_t* frame)
{
int cnt = 0;
for(uint32_t i = 0; i < TOP_N_SIZE; i ) {
if(top[i].conf < 0.01)
continue;
for(uint32_t n = i 1; n < TOP_N_SIZE; n ) {
if(top[n].conf < 0.01)
break;
float iou = IoU(top[i].x_1, top[i].y_1, top[i].x_2, top[i].y_2,
top[n].x_1, top[n].y_1, top[n].x_2, top[n].y_2);
if(iou > threshold) {
top[n].conf = 0;
}
}
}
for(uint32_t i = 0; i < TOP_N_SIZE; i ) {
if(top[i].conf < 0.01)
continue;
uint32_t color;
if(top[i].layer == 0)
color = 0x00ff0000; // 5*5 红色
else if(top[i].layer == 1)
color = 0x0000ff00; // 10*10 绿色
else
color = 0x000000ff; // 20*20 蓝色
cnt ;
display_draw_rect(top[i].x_1, top[i].y_1, top[i].x_2, top[i].y_2, frame, color);
display_print(frame, top[i].x_1 3, top[i].y_1 3, color, "%.2f", top[i].conf);
}
return cnt;
}
YOLO ESP8266 = AIoT
IoT实现参考直播课的MQTT,主要功能是检测到东北虎后将检测信息上传到腾讯云平台,方便管理人员查看东北虎活动情况。
其他
保存检测到东北虎的照片
本项目除了实现上述功能外,还实现将检测到东北虎的照片保存在MicroSD卡中,采用BMP库,将摄像头原始数据编码成BMP格式图片写入MicroSD卡中。
不同RAM对推理速度的影响
可以看到模型放在DTCM和OCRAM中跑明显比放在SDRAM中快,如果模型消耗内存小建议放在DTCM或OCRAM中运行。
虽然OCRAM工作频率比TCM低,但并不意味着放在OCRAM上的数据访问效率永远都比TCM访问效率低。TCM因为速度与L1 Cache一样,因此系统设计里其不会被L1 Cache缓存,但OCRAM是可以挂在L1 Cache上,有了Cache助阵,OCRAM上数据访问效率并不一定比TCM慢。(引用自痞子衡嵌入式:恩智浦i.MX RT1xxx系列MCU外设那些事(2)- 百变星君FlexRAM_痞子衡嵌入式-CSDN博客)
最后感谢腾讯和恩智浦提供这次比赛的机会以及群里各位大佬的支持。
Gitee
ziniu/AIoT应用创新大赛 (gitee.com)