第六篇 目标检测模型部署篇
文章目录
8.YOLOV5目标检测算法
YOLOV5目标检测算法是YOLO算法的第五次迭代,YOLO全称为You Only Look Once(你只需看一次),YOLO在2015年提出将物体检测作为回归问题求解,论文地址:https://arxiv.org/pdf/1506.02640.pdf。
这里介绍该论文的引言部分,简单了解YOLO思想:YOLO算法主要来自于人类生活中的日常行为,当人脸瞥一眼图像就立刻知道该图像中的内容,知道该图像中所突出的物体以及物体与图像中的其他内容的相互作用,人类的视觉就是如此的发达且快速精准,这是十分符合我们实际的应用中遇到各种场景的。
例如:在自动驾驶中车辆在行驶过程中我们很少用意识思维,即经过思考后才做出的决定,而是应该下意识就应该做出的反应。这种快速且准确的物体检测算法才能满足计算机以及嵌入式设备在没有专用的传感器的情况下驾驶汽车,并且可以让辅助设备向人类传达实时的场景信息,有望设计出通用的且反应灵敏的机器人系统。
YOLOV5是2020年Ultralytics在GitHub上发布的单阶段目标检测算法,源码地址:https://github.com/ultralytics/yolov5。该算法是YOLO算法(卷积神经网络)革命性的迭代,该算法在YOLOv4的基础上添加了一些新的改进思路,使其速度与精度都得到了极大的性能提升。该目标检测算法是基于Pytorch深度学习框架上搭建的,所以具有易用、可附加功能和高性能的特性。
8.1 部署yolov5源码
①获取源码:git clone https://github.com/ultralytics/yolov5
这里推荐直接下载源码压缩包:https://github.com/ultralytics/yolov5/archive/refs/tags/v6.0.tar.gz
②下载完成后,在Ubuntu中解压yolov5-6.0.tar.gz源码压缩包:
tar –xzvf yolov5-6.0.tar.gz
解压完成后获取如下目录:
③下载模型文件:https://github.com/ultralytics/yolov5/releases/download/v6.0/yolov5s.pt
④传入Ubuntu中的yolov5-6.0目录下
测试YOLOV5-6.0:
python detect.py
8.2 修改模型文件并导出模型
接下来我们会修改模型文件,导出模型用于K510的端侧部署,进入model目录下修改
yolo.py
修改完成后导出yolov5s.pt模型文件为ONNX格式,输入:
python export.py --weights yolov5s.pt --include onnx --dynamic
将官方的yolov5s的pytorch模型导出为动态输入的onnx模型文件。
由于之前我们导出模型文件为动态输入的,我们需要固定输入尺寸,所以需要简化模型:
python -m onnxsim yolov5s.onnx yolov5s-sim.onnx --input-shape 1,3,320,320
在yolov5-6.0目录下可以查看简化后生成的yolov5s-sim.onnx 模型文件
Pytorch模型与ONNX模型
model.pt模型文件是pytorch框架中用于保存和加载权重和网络结构的一种格式。
ONNX(英语:Open Neural Network Exchange)是一种针对机器学习所设计的开放式的文件格式,用于存储训练好的模型。它使得不同的人工智能框架(如Pytorch、MXNet)可以采用相同格式存储模型数据并交互。 ONNX的规范及代码主要由微软,亚马逊,Facebook和IBM等公司共同开发,以开放源代码的方式托管在Github上。
查看ONNX模型
将生成的onnx模型上传至netron网站并查看模型结构:https://netron.app/
9.nncase神经网络加速器转换模型
我们现在已经将模型从pytorch格式转换为onnx格式,现在我们需要将模型继续转换为nncase格式的模型文件。
所以我们需要使用到nncase中Compiler部分,将ONNX模型转换为DongshanPI-Vision开发板(K510芯片)中使用的kmodel文件。
我们已经提前在Ubuntu中提供好了模型转换程序,程序位于
/home/ubuntu/yolov5s-modelTransformation
下
gen_yolov5s_320_with_sigmoid_bf16_with_preprocess_output_nhwc.py
①将刚刚生成的onnx拷贝到当前目录下,如下图所示:
②执行该程序转换模型:
python gen_yolov5s_320_with_sigmoid_bf16_with_preprocess_output_nhwc.py --target k510 --dump_dir ./tmp --onnx ./yolov5s-sim.onnx --kmodel ./yolov5s-sim.kmodel
阅读nncase开发手册,了解模型转换程序gen_yolov5s_320_with_sigmoid_bf16_with_preprocess_output_nhwc.py
对比提供的API与实际的模型转换程序。打开文档网址:https://canaan-docs.100ask.net/Application/AIApplicationDevelopment-Canaan/05-nncase_Developer_Guides.html
模型转换程序
10.K510端侧部署模型
10.1 DongshanPI-vision开发板操作
①转换完成后将生成的yolov5s-sim.kmodel文件拷贝到开发端执行。
②将文件拷贝到
/app/ai/kmodel/kmodel_release/object_detect/yolov5s_320/
目录下存放,如下所示:
cp yolov5s-sim.kmodel /app/ai/kmodel/kmodel_release/object_detect/yolov5s_320/
③在/app/ai/shell 目录下修改object_detect.sh脚本文件:
vi object_detect.sh
修改脚本文件中的执行程序内容:
cd ../exe && ./object_detect ../kmodel/kmodel_release/object_detect/yolov5s_320/yolov5s-sim.kmodel 320 240 320 0.5 0.45 ./video_object_detect_320.conf 1 0 None
④修改完成后执行object_detect.sh脚本
./object_detect.sh
10.2 脚本文件讲解
①运行AI显示任务时需要优先保证屏幕显示正常,即调整显示相关的QoS为高优先级。
devmem 0x970E00fc 32 0x0fffff00
devmem 0x970E0100 32 0x000000ff
devmem 0x970E00f4 32 0x00550000
上述是通过devmem直接去读写寄存器的值。
②进入可执行程序路径。
cd ../exe
③执行目标检测应用并传入对应参数。
./object_detect ../kmodel/kmodel_release/object_detect/yolov5s_320/yolov5s_320_sigmoid_bf16_with_preprocess_output_nhwc.kmodel 320 240 320 0.5 0.45 ./video_object_detect_320.conf 1 0 None
AI应用参数介绍:参数1:模型路径(kmodel)参数2:模型尺寸(320)参数3:视频宽度(240)参数4:视频高度(320)参数5:检测对象阈值(0.5)用于区分对象还是非对象object,检测框中是否含有目标。参数6:非极大值抑制(0.45)用于找出最佳的预测框。参数7:摄像头描述文件( video_object_detect_320.conf )参数8:图像输入格式(1) 其中1表示RGB,0表示BGR参数9:是否启用时间计数(0)参数10:中间图像文件夹路径(None)
11.训练自定义模型部署
11.1 嵌入式AI模型部署流程
嵌入式AI模型部署流程如下图所示:
11.2 yolov5训练自定义数据集
1.准备数据集
您可以通过相机设备准备图像数据集,如下所示:
2.数据预处理(可选)
数据集成是将来自多个不同源的数据通过一定的思维逻辑或物理逻辑集成到一个统一的数据集合中。
数据转换是将数据从一种表示形式变为另一种表现形式的过程。即将数据类型转换/数据语义转换数据粒度转换等。
数据清洗是对一些没有用的数据进行处理的过程。很多数据集存在数据缺失、数据格式错误、错误数据或重复数据的情况,如果要使数据分析更加准确,就需要对这些没有用的数据进行处理。
数据降维是一种维度缩减技术,指在某些限定条件下,降低随机变量个数,得到一组“不相关”主变量的过程。对数据进行降维一方面可以节省计算机的储存空间,另一方面可以剔除数据中的噪声并提高机器学习算法的性能。有时用于神经系统科学是信息量最大的维度它找到数据集的低维表示信息尽可能的保留原始数据。
3.数据标注
我们需要使用到开源的标注软件labelImg,仓库地址:https://github.com/HumanSignal/labelImg
①Windows端下载地址:https://github.com/tzutalin/labelImg/files/2638199/windows_v1.8.1.zip
②Ubuntu端使用方法:
1.pip3 install labelImg -i https://pypi.tuna.tsinghua.edu.cn/simple/
2.labelImg
注意需要额外安装两个库:
sudo apt-get install libxcb-xinerama0
sudo apt-get install libxcb-cursor0
打开labelImg软件后,进行图像标注操作。如下所示:
打开数据集文件夹:
修改存放label标签的文件夹:
修改标签格式为YOLO格式:
绘制一个新的框:
绘制完成后添加标签:
绘制后效果图:
绘制后,点击Save保存标签值到我们刚刚选择的保存目录中。
点击Next可以选择下一张图片继续标注
标注完成后会得到如下标签值数据集:
classes.txt文件中包含标注的标签名
4.拆分数据集
在模型训练中,需要有训练集和验证集。可以简单理解为网络使用训练集去训练,训练出来的网络使用验证集验证。在总数据集中训练集通常应占80%,验证集应占**20%**。所以将我们标注的数据集按比例进行分配。
创建训练文件夹train,文件夹中存放图像images和对应的labels。
创建验证文件夹val,文件夹中存放图像images和对应的labels。
训练数据集
验证数据集
在yolov5源码目录下新建数据集文件夹100ask_datasets,存放训练数据集合验证数据集。
注意:数据集文件夹需要放在未修改模型文件的yolov5-6.0项目目录下。
6.创建数据集配置文件
新建数据集配置文件,该文件包含训练数据集路径、验证数据集路径、类别数、标签值。
进入models目录下,拷贝yolov5s.yaml文件,粘贴并models目录下重命名为100ask_yolov5s.yaml
7.修改模型配置文件
注意:
修改100ask_yolov5s.yaml中类别的数目为自己训练模型的类别的数目。
假设我这里的数据集只有1个类别,就修改为1。如果您的类别为3或者更多,就修改为3或者更多。
8.修改训练程序train.py
打开yolov5-6.0项目文件夹中的train.py,修改数据配置文件路径,如下图红框所示中的内容:
修改后如下所示:
parser.add_argument('--cfg',type=str, default='models/100ask_yolov5s.yaml',help='model.yaml path')
parser.add_argument('--data',type=str, default=ROOT /'data/100ask-data.yaml',help='dataset.yaml path')
9.执行训练程序train.py
在终端输入:
python train.py
输入完成后,等待训练完成。默认会迭代300次,如果您的电脑配置过低,请修改迭代次数。
FAQ:
1.RuntimeError: result type Float can’t be cast to the desired output type long int
原因:原因是新版本的torch无法自动执行此数据类型转换,旧版本torch可以。
解决办法:
修改yolov5-6.0/utils/loss.py文件的173行,在yolov5项目目录下执行
vi utils/loss.py +173
将原来的代码修改为:
gain = torch.ones(7, device=targets.device).long()
FAQ:
2.home/ubuntu/.local/lib/python3.8/site-packages/torch/functional.py:504: UserWarning: torch.meshgrid: in an upcoming release, it will be required to pass the indexing argument.
原因:文件中用到了torch.meshgrid函数,而该函数严格调用需要显示地传递“indexing”参数
解决办法:
修改/home/ubuntu/.local/lib/python3.8/site-packages/torch/functional.py 文件的504行
vi /home/ubuntu/.local/lib/python3.8/site-packages/torch/functional.py +504
将原来的代码修改为:
return _VF.meshgrid(tensors, **kwargs, indexing = 'ij') # type: ignore[attr-defined]
训练所需的时间与你设置的迭代epoch值有关
等待训练完成后,会在yolov5-6.0项目runs/train/exp*目录下,看到对应的训练结果。
我们可以在weight目录下,看到最好的模型文件和最后训练的模型文件。
8.验证模型
修改val.py程序中的数据集配置文件和模型文件
修改结果为:
修改完成后,在yolov5-6.0项目目录终端下执行 :
python val.py
9.使用新模型预测图像
在yolov5-6.0项目目录中,进入data目录下,新建100ask-images文件夹,用于存放测试图片,如下图所示:
修改yolov5-6.0项目目录中detect.py程序中的模型路径和测试图像路径
修改结果为:
修改完成后,在yolov5-6.0项目目录终端下执行 :python detect.py
执行完成后,我们可以在runs/detect/exp*目录下查看,测试后的图片。
10.使用修改模型文件后的yolov5项目导出模型
将我们训练出来的best.pt,放入使用修改模型后的yolov5源码目录下。
修改export.py程序中的数据集描述文件路径和模型路径:
修改结果为:
注意:将数据集描述文件拷贝到该项目中的data目录下。
导出动态模型,在终端输入:
python export.py --include onnx --dynamic
导出新模型文件为ONNX格式:
固定模型的输入尺寸,在yolov5-6.0项目终端输入:
python -m onnxsim best.onnx best-sim.onnx --input-shape 1,3,320,320
简化完成后,我们可以在yolov5-6.0目录下看到生成的best-sim.onnx的文件。
11.查看新模型
使用netron查看模型文件
12.转换新模型为kmodel
将模型文件传入yolov5s模型文件转换文件夹,使用
gen_yolov5s_320_with_sigmoid_bf16_with_preprocess_output_nhwc.py
程序进行模型转换:
yolov5s模型文件转换文件夹,如下所示:
在终端输入:
python gen_yolov5s_320_with_sigmoid_bf16_with_preprocess_output_nhwc.py --target k510 --dump_dir ./tmp --onnx ./best-sim.onnx --kmodel ./best-sim.kmodel
转换完成后,我们可以在当前模型转换目录下看到生成的
best-sim.kmodel
的文件。
11.3 获取并编译AI应用程序
获取AI应用程序
①在Ubuntu家目录下输入:
git clone https://e.coding.net/weidongshan/dongsahnpi-vision/100ask_base-aiApplication-demo.git
②获取完成后,可以在家目录下,看到名为
100ask_base-aiApplication-demo
的文件夹。进入
100ask_base-aiApplication-demo
文件夹下可以看到里面存放有对应的代码。
ubuntu@ubuntu2004:~$ cd 100ask_base-aiApplication-demo
ubuntu@ubuntu2004:~/100ask_base-aiApplication-demo$ ls
③下载对应交叉编译工具链:https://dongshanpi.cowtransfer.com/s/55562905c0e245
并将其放在100ask_base-aiApplication-demo目录下。
④解压交叉编译工具链:
tar-xzvf riscv64-buildroot-linux-gnu_sdk-buildroot.tar.gz
编译AI应用程序
①进入AI应用源码目录:
ubuntu@ubuntu2004:~/100ask_base-aiApplication-demo$ cd code/
②激活环境变量:
ubuntu@ubuntu2004:~/100ask_base-aiApplication-demo/code$ source build.sh
③配置完成后,在终端输入make,开始编译应用程序。
ubuntu@ubuntu2004:~/100ask_base-aiApplication-demo/code$ make
④安装应用程序到tmp目录下:
ubuntu@ubuntu2004:~/100ask_base-aiApplication-demo/code$ makeinstall
1.1.4 修改目标检测应用源码
进入yolov5应用源码文件夹:
ubuntu@ubuntu2004:~/100ask_base-aiApplication-demo/code$ cd object_detect
进入文件夹后可以看到如下图中的文件,修改红框中
object_detect.h
文件
修改类别数CLASS_NUM为您自定义训练集中所使用的类别数。需要和您定义的模型描述文件100ask_yolov5s.yaml对应。
修改标签值为您自定义数据集中的标签值,需要和您自定义数据集描述文件100ask-data.yaml中的class names对应。
修改先验框Anchor为您自定义训练集中所使用的参数。需要和您定义的模型描述文件100ask_yolov5s.yaml对应。
重新编译AI应用:
ubuntu@ubuntu2004:~/100ask_base-aiApplication-demo/code/object_detect$ make clean
ubuntu@ubuntu2004:~/100ask_base-aiApplication-demo/code/object_detect$ make
ubuntu@ubuntu2004:~/100ask_base-aiApplication-demo/code/object_detect$ makeinstall
进入tmp/app/ai/exe目录下即可找到编译新生成的yolov5目标检测应用
object_detect
。
11.5 开发板运行验证
拷贝生成的模型文件
best.kmodel
和
object_detect
可执行程序到开发板端运行。
这里我使用TF卡的形式也可以使用ssh,拷贝文件到开发板中。
下图中有展示TF卡槽位置:
开始前,请连接摄像头和屏幕。
如下图所示:
开发板运行验证:
①将模型文件拷贝到/app/ai/kmodel/kmodel_release/object_detect/yolov5s_320/目录下:
[root@canaan ~/sd/p1 ]$ cp best-sim.kmodel /app/ai/kmodel/kmodel_release/object_detect/yolov5s_320/
②将object_detect 重命名并拷贝到/app/ai/exe/文件夹下
[root@canaan ~/sd/p1 ]$ mv object_detect object_detect_100ask
[root@canaan ~/sd/p1 ]$ ls
System Volume Information object_detect_100ask
best-sim.kmodel
[root@canaan ~/sd/p1 ]$ cp object_detect_100ask /app/ai/exe/
④将模型文件拷贝到
/app/ai/kmodel/kmodel_release/object_detect/yolov5s_320/
目录下:
[root@canaan ~/sd/p1 ]$ cp best-sim.kmodel /app/ai/kmodel/kmodel_release/object_detect/yolov5s_320/
⑤进入/app/ai/shell/目录下
[root@canaan ~/sd/p1 ]$ cd /app/ai/shell/
⑥修改目标检测应用脚本object_detect.sh
[root@canaan /app/ai/shell ]$ vi object_detect.sh
修改内容为:
注释原来的执行程序,新增下面的执行程序命令:
cd../exe && ./object_detect_100ask ../kmodel/kmodel_release/object_detect/yolov5s_320/best-sim.kmodel 3202403200.50.45 ./video_object_detect_320.conf 10 None
运行新的目标检测应用程序和新模型。
⑦执行脚本验证:
[root@canaan /app/ai/shell ]$ ./object_detect.sh
验证结果从上图可以看到,这就证明我们自定义训练出来开的模型文件已经成功部署到开发板端。
12.AI应用程序框架解析
12.1 AI应用程序数据流框图
12.2 阅读端侧推理模型示例
打开nncase开发文档网址:https://canaan-docs.100ask.net/Application/AIApplicationDevelopment-Canaan/05-nncase_Developer_Guides.html
12.3 YOLOV5目标检测程序框架解析
注意:
1.AI工作只作画框和绘制预测类别作用。
2.显示工作只作显示屏上显示工作。
12.4 YOLOV5目标检测程序解析-AI工作
1.创建类
// 创建一个目标检测类
objectDetect od(obj_thresh, nms_thresh, net_len, {valid_width, valid_height});
目标检测类属于Simulator类, 用于在PC上推理kmodel
目的:创建这个类,用于准备内存、加载模型,设置模型的输入输出、模型的推理、后处理等功能。在这个类的public中定义功能函数。
下面为目标检测部分定义截图:
// 目标检测类定义(部分)
class objectDetect
{
public:
objectDetect(float obj_thresh, float nms_thresh, int net_len, Framesize frame_size);
void prepare_memory();
void set_input(uint32_t index);
void set_output();
void load_model(char *path);
void run();
void get_output();
void post_process(std::vector<BoxInfo> &result);
~objectDetect();
2.加载模型
od.load_model(kmodel_path); // load kmodel(加载模型)
函数定义:
void objectDetect::load_model(char *path)
{
od_model = read_binary_file<unsigned char>(path); // 读取传入的地址中的模型文件
interp_od.load_model({ (const gsl::byte *)od_model.data(), od_model.size() }).expect("cannot load model.");
std::cout << "============> interp_od.load_model finished!" << std::endl;
}
3.准备内存
Od.prepare_memory(); // memory allocation(准备内存)
截取代码prepare_memory定义中的重要片段。
virtual_addr_output = (char *)mmap(NULL, allocAlignMemOdOutput.size, PROT_READ | PROT_WRITE, MAP_SHARED, mem_map, allocAlignMemOdOutput.phyAddr);
virtual_addr_input[i] = (char *)mmap(NULL, allocAlignMemOdInput[i].size, PROT_READ | PROT_WRITE, MAP_SHARED, mem_map, allocAlignMemOdInput[i].phyAddr);
代码理解:内核申请一块共享内存供应用程序使用,这块内存的地址称为虚拟地址。外部应用想使用这块内存仅需要去调用这块虚拟内存即可。虚拟地址virtual_addr_input中包含了指向共享内存的指针。
4.VideoCapture获得摄像头数据
mtx.lock(); //获得锁(获得独占式资源的能力)
cv::VideoCapture capture; //使用OpenCV创建一个capture类,用于调用摄像头
capture.open(5);//打开/dev/video5节点
// video setting
capture.set(cv::CAP_PROP_CONVERT_RGB, 0); //不将捕获的图像转换为RGB
capture.set(cv::CAP_PROP_FRAME_WIDTH, net_len); //设置捕获视频宽为模型宽
capture.set(cv::CAP_PROP_FRAME_HEIGHT, net_len); //设置捕获视频高为模型高
// RRRRRR....GGGGGGG....BBBBBB, CHW
capture.set(cv::CAP_PROP_FOURCC, V4L2_PIX_FMT_RGB24); //获取原来的格式,将原来的格式转换为RGB24图像
mtx.unlock(); //释放锁
cv::Mat rgb24_img_for_ai(net_len, net_len, CV_8UC3, od.virtual_addr_input[0] + (net_len - valid_width) / 2 + (net_len - valid_height) / 2 * net_len);//创建Mat数据类型,用于存储图像数据,存放位置位于虚拟地址(共享内存)中
ret = capture.read(rgb24_img_for_ai); //读取视频图像,并将图像数据存放在共享内存中
5.寻找3通道地址
//padding
uint8_t *r_addr, *g_addr, *b_addr;
g_addr = (uint8_t *)od.virtual_addr_input[0] + offset_channel;
r_addr = is_rgb ? g_addr - offset_channel : g_addr + offset_channel;
b_addr = is_rgb ? g_addr + offset_channel : g_addr - offset_channel;
od.virtual_addr_input[0]为图像数据的首地址,那么RGB图像或BGR图像,就可知道3通道中的中间通道G的地址
知道3通道BGR中的中间通道G的地址后,求剩下两通道的地址,下面为求解rgb三通道的各个地址。
6.填充图像
//gnne_input_width:模型宽度 320 gnne_valid_width:视频输入宽度 240
if (gnne_valid_width < gnne_input_width) {
uint32_t padding_r = (gnne_input_width - gnne_valid_width); //计算总共需要填充的大小
uint32_t padding_l = padding_r / 2; //计算左边需要填充的大小
uint32_t row_offset = (gnne_input_height - gnne_valid_height) / 2; //计算下一个需要填充的偏移值
padding_r -= padding_l; //计算右边需要填充的大小
for (int row = row_offset; row < row_offset + gnne_valid_height/*30+240*/; row++) {
uint32_t offset_l = row * gnne_input_width; //计算下一个左边需要填充的偏移值
uint32_t offset_r = offset_l + gnne_valid_width + padding_l; //计算下一个右边需要填充的偏移值
memset(r_addr + offset_l, PADDING_R, padding_l); //填充左边R通道,填充值为114(灰度值),填充范围
memset(g_addr + offset_l, PADDING_G, padding_l); //填充左边G通道,填充值为114(灰度值),填充范围
memset(b_addr + offset_l, PADDING_B, padding_l); //填充左边B通道,填充值为114(灰度值),填充范围
memset(r_addr + offset_r, PADDING_R, padding_r); //填充右边R通道,填充值为114(灰度值),填充范围
memset(g_addr + offset_r, PADDING_G, padding_r); //填充右边G通道,填充值为114(灰度值),填充范围
memset(b_addr + offset_r, PADDING_B, padding_r); //填充右边B通道,填充值为114(灰度值),填充范围
}
}
实际图像如下所所示:
7.设置输入矩阵
od.set_input(0); //设置输入矩阵
Object_detect程序中set_input定义:
void objectDetect::set_input(uint32_t index)
{
auto in_shape = interp_od.input_shape(0); //设置输入矩阵的shape auto input_tensor = host_runtime_tensor::create(dt_uint8, //设置数据类型
in_shape, //设置tensor的形状
//设置用户态数据(存放输入数据)
{ (gsl::byte *)virtual_addr_input[index], net_len * net_len * INPUT_CHANNELS},
false, //是否拷贝
hrt::pool_shared, //内存池类型,使用的是共享内存池
allocAlignMemOdInput[index].phyAddr).expect(“cannot create input tensor”); //共享内存的物理地址
interp_od.input_tensor(0, input_tensor).expect(“cannot set input tensor”); //设置输入的矩阵
}
8.设置输出矩阵
od.set_output();//设置输出矩阵
Object_detect程序中set_output定义:
void objectDetect::set_output()
{
for (size_t i = 0; i < interp_od.outputs_size(); i++)
{
auto out_shape = interp_od.output_shape(i); //设置输出矩阵的shape
auto output_tensor = host_runtime_tensor::create(dt_float32, //设置数据类型
out_shape, //设置tensor的形状
{(gsl::byte *)virtualAddrOdOutput[i], output_size[i]}, //设置用户态数据(存放输出数据)
false, //是否拷贝
hrt::pool_shared, output_pa_addr[i]).expect(“cannot create output tensor”); //共享内存的物理地址
interp_od.output_tensor(i, output_tensor).expect(“cannot set output tensor”); //设置输出的矩阵
}
}
9.运行模型推理
od.run(); //运行kmodel推理
Object_detect程序中run定义:
void objectDetect::run()
{
interp_od.run().expect("error occurred in running model"); //运行kmodel推理
}
注意:
在运行kmodel推理前,我们已经设置了输入矩阵和输出矩阵的存放地址,所以我们只需要访问存放地址,拷贝出来使用即可。
10.获取推理结果
od.get_output(); //获取推理后的输出结果
Object_detect程序中get_output定义:
void objectDetect::get_output()
{
output_0 = reinterpret_cast<float *>(virtualAddrOdOutput[0]);
output_1 = reinterpret_cast<float *>(virtualAddrOdOutput[1]);
output_2 = reinterpret_cast<float *>(virtualAddrOdOutput[2]);
}
提示: reinterpret_cast用于进行各种不同类型的指针之间、不同类型的引用之间以及指针和能容纳指针的整数类型之间的转换。转换时,执行的是逐个比特复制的操作。
注意:将输出矩阵拷贝到objectDetect类中的私有成员变量中
11.后处理
od.post_process(result); //后处理
Object_detect程序中get_output定义:
void objectDetect::post_process(std::vector<BoxInfo> &result)
{
auto boxes0 = decode_infer(output_0, net_len, 8, classes_num, frame_size, anchors_0, obj_thresh);
result.insert(result.begin(), boxes0.begin(), boxes0.end());
auto boxes1 = decode_infer(output_1, net_len, 16, classes_num, frame_size, anchors_1, obj_thresh);
result.insert(result.begin(), boxes1.begin(), boxes1.end());
auto boxes2 = decode_infer(output_2, net_len, 32, classes_num, frame_size, anchors_2, obj_thresh);
result.insert(result.begin(), boxes2.begin(), boxes2.end());
nms(result, nms_thresh);
}
decode_infer函数:
进行后处理操作,将输出的tensor结果转换为坐标的格式存储在vector容器中。
std::vector<BoxInfo> decode_infer(float *data, int net_size, int stride, int num_classes, Framesize frame_size, float anchors[][2], float threshold)
{
// 计算比例和增益,用于缩放坐标
float ratiow = (float)net_size / frame_size.width;
float ratioh = (float)net_size / frame_size.height;
float gain = ratiow < ratioh ? ratiow : ratioh;
// 存储解码后的边界框
std::vector<BoxInfo> result;
// 计算网格大小
int grid_size = net_size / stride;
int one_rsize = num_classes + 5; // 每个锚框有num_classes + 5个值
// 遍历网格
for (int shift_y = 0; shift_y < grid_size; shift_y++)
{
for (int shift_x = 0; shift_x < grid_size; shift_x++)
{
int loc = shift_x + shift_y * grid_size;
// 遍历锚框
for (int i = 0; i < 3; i++)
{
float *record = data + (loc * 3 + i) * one_rsize;
float *cls_ptr = record + 5;
// 遍历类别
for (int cls = 0; cls < num_classes; cls++)
{
float score = (cls_ptr[cls]) * (record[4]);
// 检查分数是否超过阈值
if (score > threshold)
{
// 解码边界框坐标
cx = ((record[0]) * 2.f - 0.5f + (float)shift_x) * (float)stride;
cy = ((record[1]) * 2.f - 0.5f + (float)shift_y) * (float)stride;
w = pow((record[2]) * 2.f, 2) * anchors[i][0];
h = pow((record[3]) * 2.f, 2) * anchors[i][1];
cx -= ((net_size - frame_size.width * gain) / 2);
cy -= ((net_size - frame_size.height * gain) / 2);
cx /= gain;
cy /= gain;
w /= gain;
h /= gain;
// 创建BoxInfo结构并将其添加到结果向量
BoxInfo box;
box.x1 = std::max(0, std::min(frame_size.width, int(cx - w / 2.f)));
box.y1 = std::max(0, std::min(frame_size.height, int(cy - h / 2.f)));
box.x2 = std::max(0, std::min(frame_size.width, int(cx + w / 2.f)));
box.y2 = std::max(0, std::min(frame_size.height, int(cy + h / 2.f)));
box.score = score;
box.label = cls;
result.push_back(box);
}
}
}
}
}
// 返回解码后的边界框向量
return result;
}
nms函数:
删除模型预测后冗余的预测框,保留最佳的结果。
12.类别名清理
/****fixed operation for display clear****/
/****显示屏清理的固定操作****/
cv::Mat img_argb;
uint64_t index;
{
buf_mgt_writer_get(&buf_mgt, (void **)&index); //获取DRM的写入数据的能力
ScopedTiming st("display clear", enable_profile);
struct drm_buffer *fbuf_argb = &drm_dev.drm_bufs_argb[index];
img_argb = cv::Mat(screen_height, screen_width, CV_8UC4, (uint8_t *)fbuf_argb->map); //将图像输出至map
for (uint32_t cc = 0; cc < points_to_clear[index].size(); cc++)
{
cv::putText(img_argb, strs_to_clear[index][cc], points_to_clear[index][cc], cv::FONT_HERSHEY_COMPLEX, 1.5, cv::Scalar(0, 0, 0, 0), 1, 8, 0); //预测的类别名 清理为黑色
}
}
如下图所示:将显示屏上的类别名称清理为黑色
13.绘制矩形框
for (auto r : result){
if (obj_cnt < 32){
struct vo_draw_frame frame; //创建画框和标注的图像
frame.crtc_id = drm_dev.crtc_id;
frame.draw_en = 1; //是否绘制
frame.frame_num = obj_cnt; //绘制个数
int x1 = r.x2 * screen_width / valid_width;
int x0 = r.x1 * screen_width / valid_width;
int y0 = r.y1 * screen_height / valid_height;
int y1 = r.y2 * screen_height / valid_height;
x1 = std::max(0, std::min(x1, (int)screen_width)); //如果x1值超出屏幕宽度,则取屏幕宽度
x0 = std::max(0, std::min(x0, (int)screen_width)); //如果x0值超出屏幕宽度,则取屏幕宽度
y0 = std::max(0, std::min(y0, (int)screen_height)); //如果y0值超出屏幕宽度,则取屏幕高度
y1 = std::max(0, std::min(y1, (int)screen_height)); //如果y1值超出屏幕宽度,则取屏幕高度
frame.line_x_start = x0; //设置x轴的起点
frame.line_x_end = x1; //设置x轴的终点
frame.line_y_start = y0; //设置y轴的起点
frame.line_y_end = y1; //设置y轴的终点 draw_frame(&frame); //绘制矩形框
cv::Point origin;
origin.x = (int)(r.x1 * screen_width / valid_width); //绘制展示标签值的x坐标
origin.y = (int)(r.y1 * screen_height / valid_height + 10); //绘制展示标签值的y坐标
//从result容器中获取标签值
std::string text = od.labels[r.label] + “:” + std::to_string(round(r.score * 100) / 100.0).substr(0,4);
//在指定的坐标处绘制标签值
cv::putText(img_argb, text, origin, cv::FONT_HERSHEY_COMPLEX, 1.5, cv::Scalar(0, 0, 255, 255), 1, 8, 0);
points_to_clear[index].push_back(origin); //将坐标值加入清空容器中
strs_to_clear[index].push_back(text); //将标签值加入清空容器中
绘制预测框和预测类别和预测概率,并将其输出至显示屏上。
14.屏幕清理和摄像头释放
/****显示屏清理的操作****/
for (uint32_t i = obj_cnt; i < 32; i++) {
struct vo_draw_frame frame;
frame.crtc_id = drm_dev.crtc_id;
frame.draw_en = 0;
frame.frame_num = i;
draw_frame(&frame);
}
}
frame_cnt += 1;
buf_mgt_writer_put(&buf_mgt, (void *)index);
}
/****fixed operation for capture release and display clear****/
/****固定摄像头捕获释放和显示清除的操作****/
printf("%s ==========release \n", __func__);
mtx.lock(); //获得锁
capture.release(); // 释放摄像头资源
mtx.unlock(); //释放锁
for(uint32_t i = 0; i < 32; i++)
{
struct vo_draw_frame frame;
frame.crtc_id = drm_dev.crtc_id;
frame.draw_en = 0;
frame.frame_num = i;
draw_frame(&frame);
}
}
AI工作的流程图如下所示:
12.5 YOLOV5目标检测程序解析- display_work
使用V4L2打开指定摄像头设备节点,并将调用DRM输出显示函数,将视频流buffer显示在显示屏上
mtx.lock(); //获得锁
vdev = v4l2_open(dev_info[0].video_name[1]); //使用v4l2打开指定摄像头设备节点
if (vdev == NULL) {
printf("error: unable to open video capture device %s\n",
dev_info[0].video_name[1]);
mtx.unlock();
goto display_cleanup;
}
memset(&format, 0, sizeof format);
format.pixelformat = dev_info[0].video_out_format[1] ? V4L2_PIX_FMT_NV12 : V4L2_PIX_FMT_NV16; //设置视频输出格式
format.width = dev_info[0].video_width[1]; //设置视频宽度
format.height = dev_info[0].video_height[1]; //设置视频高度
ret = v4l2_set_format(vdev, &format); //设置帧格式
if (ret < 0)
{
printf("%s:v4l2_set_format error\n",__func__);
mtx.unlock();
goto display_cleanup;
}
ret = v4l2_alloc_buffers(vdev, V4L2_MEMORY_USERPTR, DRM_BUFFERS_COUNT); //申请帧缓冲
if (ret < 0)
{
printf("%s:v4l2_alloc_buffers error\n",__func__);
mtx.unlock();
goto display_cleanup;
}
FD_ZERO(&fds); //对内存中保存的文件句柄进行清理操作
FD_SET(vdev->fd, &fds); //用来将一个给定的文件描述符加入集合之中
for (i = 0; i < vdev->nbufs; ++i) {
buffer.index = i;
fbuf_yuv = &drm_dev.drm_bufs[buffer.index];
buffer.mem = fbuf_yuv->map;
buffer.size = fbuf_yuv->size;
ret = v4l2_queue_buffer(vdev, &buffer); //buffer入队
if (ret < 0) {
printf("error: unable to queue buffer %u\n", i);
mtx.unlock();
goto display_cleanup;
}
}
ret = v4l2_stream_on(vdev); //开启视频流
if (ret < 0) {
printf("%s error: failed to start video stream: %s (%d)\n", __func__,
strerror(-ret), ret);
mtx.unlock();
goto display_cleanup;
}
mtx.unlock();
while(quit.load()) {
struct timeval timeout;
fd_set rfds;
timeout.tv_sec = SELECT_TIMEOUT / 1000;
timeout.tv_usec = (SELECT_TIMEOUT % 1000) * 1000;
rfds = fds;
ret = select(vdev->fd + 1, &rfds, NULL, NULL, &timeout); //定时器作用,判断是否获取视频超时
if (ret < 0) {
if (errno == EINTR)
continue;
printf("error: select failed with %d\n", errno);
goto display_cleanup;
}
if (ret == 0) {
printf("error: select timeout\n");
goto display_cleanup;
}
process_ds0_image(vdev, format.width, format.height); //使用DRM框架将视频设备数据显示在显示屏上
}
display_cleanup:
mtx.lock(); //获得锁
video_stop(vdev); //关闭视频流
video_cleanup(vdev); //关闭内存映射相关的内存块和关闭视频设备
mtx.unlock(); //释放锁
}
12.6 YOLOV5目标检测程序解析- DRM显示函数process_ds0_image
static int process_ds0_image(struct v4l2_device *vdev, unsigned int width, unsigned int height)
{
// 声明一个结构体用于存储视频缓冲区信息
struct v4l2_video_buffer buffer;
int ret;
// 声明静态结构体,用于存储上一帧的视频缓冲区信息
static struct v4l2_video_buffer old_buffer;
// 屏幕初始化标志,用于标识屏幕是否已初始化
static int screen_init_flag = 0;
mtx.lock(); //获得锁
ret = v4l2_dequeue_buffer(vdev, &buffer); //把数据放回缓存队列(出队),并获取到视频buf
if (ret < 0) {
printf("error: unable to dequeue buffer: %s (%d)\n",
strerror(-ret), ret);
mtx.unlock(); //释放锁
return ret;
}
mtx.unlock(); //释放锁
// 如果视频缓冲区存在错误,打印警告信息并跳过当前帧的处理
if (buffer.error) {
printf("warning: error in dequeued buffer, skipping\n");
return 0;
}
fbuf_yuv = &drm_dev.drm_bufs[buffer.index]; // 获取当前帧的视频缓冲区信息
// 如果屏幕已经初始化
if (screen_init_flag) {
if (drm_dev.req)
drm_wait_vsync(); //等待显示屏空闲时间,等待完成后才可传入新数据
uint64_t index;
if (buf_mgt_display_get(&buf_mgt, (void **)&index) != 0) //获取DRM显示能力
index = 0;
//获取用于显示的另一个缓冲区的信息
struct drm_buffer *fbuf_argb = &drm_dev.drm_bufs_argb[index];
//设置平面显示
if (drm_dmabuf_set_plane(fbuf_yuv, fbuf_argb)) //将视频buf传入DRM显示buf中
{
std::cerr << "Flush fail \n";
return 1;
}
}
// 如果屏幕已经初始化
if(screen_init_flag) {
fbuf_yuv = &drm_dev.drm_bufs[old_buffer.index]; // 获取上一帧的视频缓冲区信息
old_buffer.mem = fbuf_yuv->map; // 更新上一帧的视频缓冲区信息到 old_buffer 结构体
old_buffer.size = fbuf_yuv->size; //获取drm显示buf的大小
//使用互斥锁确保线程安全性
mtx.lock();
ret = v4l2_queue_buffer(vdev, &old_buffer); //把数据从缓存中读取出来(入队)
if (ret < 0) {
printf("error: unable to requeue buffer: %s (%d)\n",
strerror(-ret), ret);
mtx.unlock();
return ret;
}
mtx.unlock();
}
else {
screen_init_flag = 1;
}
old_buffer = buffer; //将buffer的数据赋值给旧buffer
return 0;
}
注意:old_buffer是静态变量保存着上一个buffer
DRM显示流程为:
1.Buffer获取新的V4l2数据,交给DRM去做显示操作。
2.等待上一个buffer传输完成后,再将新buffer传入DRM去做显示。
3.old_buffer保存着上一个buffer的数据和地址,由于需要等待DRM读取并显示,所以等待下一回合才去入队归还buffer给驱动
版权归原作者 韦东山 所有, 如有侵权,请联系我们删除。