YOLOv5 的量化及部署 - RGB 专题
技术背景
YOLOv5 是一种高效的目标检测算法,尤其在实时目标检测任务中表现突出。YOLOv5 通过三种不同尺度的检测头分别处理大、中、小物体;检测头共包括三个关键任务:边界框回归、类别预测、置信度预测;每个检测头都会逐像素地使用三个 Anchor,以帮助算法更准确地预测物体边界。
YOLOv5 具有多种不同大小的模型(YOLOv5n、YOLOv5s、YOLOv5m、YOLOv5l、YOLOv5x)以适配不同的任务类型和硬件平台。本文以基于色选机数据集训练出的 YOLOv5n 模型为例,介绍如何使用 PTQ 进行量化编译并使用 C++进行全流程的板端部署。介绍重点在于输入数据为 RGB 和 NHWC 时的处理方式。
本文主要提供以下几方面的指导:
1、对 pytorch 模型做适当的调整并导出 onnx
2、以合适的方式准备校准数据并编译上板模型
3、板端部署时正确准备输入数据
4、进行反量化后处理融合以降低延时
5、提供正确的板端性能评测方法
模型输入输出说明
从 pytorch 导出的 onnx 模型,具体的输入输出信息如下图所示:
本示例使用的 yolov5n 模型,相较于公版在输入和输出上存在以下几点变动:
1、输入分辨率设定为 384x2048,对应输出分辨率也调整为了 48x256,24x128,12x64
2、类别数量设定为 17,因此输出 tensor 的通道数变为了(17+4+1)x3=66
3、模型输入端插入了一个 transpose 节点,这样模型可以直接读取 NHWC 的输入数据
4、为了优化整体耗时,模型尾部的 sigmoid 计算被放在了后处理
此外本文使用的 v5n 模型扩充了一定参数量,介于官方的 v5n 和 v5s 之间,因此推理耗时会比普通的 v5n 略高。
模型导出注意事项
因为 pytorch 模型训练时的 layout 通常都是 NCHW,而 opencv 读图之后的 layout 都是 NHWC,因此为了部署时的预处理更加简单高效,我们建议给 pytorch 模型的输入节点增加一个 transpose,这个 transpose 的作用是把 NHWC 转换成 NCHW(对应 0312 的参数),从而将提供给模型的输入数据从 NCHW 更改为 NHWC,以契合 opencv 读到的数据。可参考如下代码,更改后再导出 onnx。
Class MyModel(nn.Module):
def
__init__
(self):
super(MyModel, self).
__init__
()
# 网络结构定义
def forward(self, x):
x = x.permute(0, 3, 1, 2)
# 后续网络操作
return x
该模型的尾部也使用了一个 transpose,让模型以 NHWC 的方式输出。需要强调的是,虽然模型的首尾都有 transpose 节点,但在编译完成后,这些 transpose 节点都会消失。这是因为最适配芯片的 layout 是 NHWC,即便模型内部的算子都是 NCHW 排布,工具链也会转换为 NHWC,这样一来模型首尾的 transpose 相当于不起作用,就可以被工具链干掉了,模型编译后从输入到输出全部就是 NHWC。至于为什么需要手动插入 transpose,这是因为工具链不想改变用户行为(即不改变 onnx 输入输出的 layout)。如果你不加 transpose,工具链可能反而会给你加上,因此建议由用户来加上 transpose 节点。
工具链环境
全部工程基于 OE 1.2.8 进行。
horizon-nn 1.1.0
horizon_tc_ui 1.24.3
hbdk 3.49.15
PTQ 量化编译流程
准备校准数据
先准备 100 张如上图所示的色选机数据集图片存放在 seed100 文件夹,之后可借助 horizon_model_convert_sample 的 02_preprocess.sh 脚本帮助我们生成校准数据。
02_preprocess.sh
python3 ../../../data_preprocess.py \
--src_dir ./seed100 \
--dst_dir ./calibration_data_rgb_f32 \
--pic_ext .rgb \
--read_mode opencv \
--saved_data_type float32
preprocess.py
def calibration_transformers():
transformers = [
PadResizeTransformer(target_size=(384, 2048)),
BGR2RGBTransformer(data_format="HWC"),
]
return transformers
校准数据仅需 resize 成符合模型输入的尺寸,并按照 HWC 转换成 RGB 即可。
配置 yaml 文件
model_parameters:
onnx_model: 'yolov5n-rgb.onnx'
march: 'bayes-e'
working_dir: 'model_output'
output_model_file_prefix: 'yolov5n-rgb'
input_parameters:
input_type_rt: 'rgb'
input_layout_rt: 'NHWC'
input_type_train: 'rgb'
input_layout_train: 'NHWC'
norm_type: 'data_scale'
scale_value: 0.003921568627451
calibration_parameters:
cal_data_dir: './calibration_data_rgb_f32'
cal_data_type: 'float32'
calibration_type: 'default'
compiler_parameters:
optimize_level: 'O3'
input_type_rt 指模型在部署时输入的数据类型,这里使用 rgb
input_layout_rt 指模型在部署时输入的数据排布,由于 rgb 本身就是 NHWC,且为了简化预处理,这里使用 NHWC
input_type_train 指浮点模型训练时使用的数据类型,这里使用 rgb
input_layout_train 指浮点模型训练时使用的数据排布,虽然 pytorch 原生是 NCHW,但由于我们在输入处插入了 transpose 节点,因此这里使用 NHWC
norm_type 和 scale_value 根据浮点模型训练时使用的归一化参数设置,这里配置 scale 为 1/255(mean 和 scale 的具体计算方法可参考文章 https://developer.d-robotics.cc/forumDetail/71036815603174578)
编译上板模型
hb_mapper makertbin --config ./yolov5n_config.yaml --model-type onnx
执行以上命令后,即可编译出用于板端部署的 bin 模型。
根据编译日志可看出,yolov5n 模型的三个输出头,量化前后的余弦相似度均>0.99,符合精度要求。
Runtime 部署流程
在算法工具链的交付包中,ai benchmark 示例包含了读图、前处理、推理、后处理等完整流程的 C++源码,但考虑到 ai benchmark 代码耦合度较高,有不低的学习成本,不方便用户嵌入到自己的工程应用中,因此我们提供了基于 horizon_runtime_sample 示例修改的简易版本 C++代码,只包含 1 个头文件和 1 个 C++源码,用户仅需替换原有的 00_quick_start 示例即可编译运行。
通常来说,模型编译完成后,尾部会有 CPU 计算的反量化节点。一个常见的优化策略是将反量化计算和后处理一起运行,节约一遍数据遍历次数,从而降低耗时。本文同时提供了不删除反量化算子和删除反量化算子的后处理代码示例。反量化融合的具体原理和实践参考可以阅读这篇文章:
https://developer.d-robotics.cc/forumDetail/116476291842200072
头文件
该头文件内容主要来自于 ai benchmark 的 code/include/base/perception_common.h 头文件,包含了对 argmax 和计时功能的定义,以及目标检测任务相关结构体的定义。
#include
#define BSWAP_32(x) static_cast(__builtin_bswap32(x))
#define r_int32(x, big_endian) (big_endian) ? BSWAP_32((x)) : static_cast((x))
typedef std::chrono::steady_clock::time_point Time;
typedef std::chrono::duration<u_int64_t, std::micro> Micro;
template
inline size_t argmax(ForwardIterator first, ForwardIterator last) {
return std::distance(first, std::max_element(first, last));
}
typedef struct Bbox {
float xmin{0.0};
float ymin{0.0};
float xmax{0.0};
float ymax{0.0};
Bbox() {}
Bbox(float xmin, float ymin, float xmax, float ymax)
: xmin(xmin), ymin(ymin), xmax(xmax), ymax(ymax) {}
friend std::ostream &operator<<(std::ostream &os, const Bbox &bbox) {
const auto precision = os.precision();
const auto flags = os.flags();
os << "[" << std::fixed << std::setprecision(6) << bbox.xmin << ","
<< bbox.ymin << "," << bbox.xmax << "," << bbox.ymax << "]";
os.flags(flags);
os.precision(precision);
return os;
}
~Bbox() {}
} Bbox;
typedef struct Detection {
int id{0};
float score{0.0};
Bbox bbox;
const char *class_name{nullptr};
Detection() {}
Detection(int id, float score, Bbox bbox)
: id(id), score(score), bbox(bbox) {}
Detection(int id, float score, Bbox bbox, const char *class_name)
: id(id), score(score), bbox(bbox), class_name(class_name) {}
friend bool operator>(const Detection &lhs, const Detection &rhs) {
return (lhs.score > rhs.score);
}
friend std::ostream &operator<<(std::ostream &os, const Detection &det) {
const auto precision = os.precision();
const auto flags = os.flags();
os << "{"
<< R"("bbox")"
<< ":" << det.bbox << ","
<< R"("prob")"
<< ":" << std::fixed << std::setprecision(6) << det.score << ","
<< R"("label")"
<< ":" << det.id << ","
<< R"("class_name")"
<< ":\"" << det.class_name << "\"}";
os.flags(flags);
os.precision(precision);
return os;
}
~Detection() {}
} Detection;
struct Perception {
std::vector det;
enum {
DET = (1 << 0),
} type;
friend std::ostream &operator<<(std::ostream &os, Perception &perception) {
os << "[";
if (perception.type == Perception::DET) {
auto &detection = perception.det;
for (int i = 0; i < detection.size(); i++) {
if (i != 0) {
os << ",";
}
os << detection[i];
}
}
os << "]";
return os;
}
};
源码
为方便用户阅读,该源码使用全局变量定义了若干参数,请用户在实际的应用工程中,避免使用过多全局变量。代码中已在合适的位置添加中文注释。需要特意说明的是,置信度阈值使用了 score_threshold_objness 和 score_threshold 两个变量,第一个需要设置成经过 sigmoid 反函数的值,比如原来设置的 0.2,那么 sigmoid 经过反函数以后的值大概是 -1.38,这样设置可以大大降低后处理的时间,第二个变量则是正常的置信度阈值,通常二选一即可。
#include <iostream>
#include <iomanip>
#include <vector>
#include <memory>
#include <mutex>
#include <thread>
#include <algorithm>
#include "dnn/hb_dnn.h"
#include "opencv2/core/mat.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/imgproc.hpp"
#include "head.h"
// 上板模型的路径
auto modelFileName = "yolov5n-rgb.bin";
// 单张测试图片的路径
std::string imagePath = "seed.jpg";
// 测试图片的宽度
int image_width = 2048;
// 测试图片的高度
int image_height = 384;
// 置信度阈值
float score_threshold_objness = -1.38;
float score_threshold = 0.2;
// 分类目标数
int num_classes = 17;
// 模型输出的通道数
int num_pred = num_classes + 4 + 1;
// nms的topk
int nms_top_k = 5000;
// nms的iou阈值
float nms_iou_threshold = 0.5;
// 为模型推理准备输入输出内存空间
void prepare_tensor(int input_count,
int output_count,
hbDNNTensor *input_tensor,
hbDNNTensor *output_tensor,
hbDNNHandle_t dnn_handle) {
hbDNNTensor *input = input_tensor;
for (int i = 0; i < input_count; i++) {
hbDNNGetInputTensorProperties(&input[i].properties, dnn_handle, i);
int input_memSize = input[i].properties.alignedByteSize;
hbSysAllocCachedMem(&input[i].sysMem[0], input_memSize);
input[i].properties.alignedShape = input[i].properties.validShape;
}
hbDNNTensor *output = output_tensor;
for (int i = 0; i < output_count; i++) {
hbDNNGetOutputTensorProperties(&output[i].properties, dnn_handle, i);
int output_memSize = output[i].properties.alignedByteSize;
hbSysAllocCachedMem(&output[i].sysMem[0], output_memSize);
}
}
// 读取bgr图片并转换为特定格式再存储进输入内存
void read_image_2_tensor_as_rgb(std::string imagePath,
hbDNNTensor *input_tensor) {
hbDNNTensor *input = input_tensor;
hbDNNTensorProperties Properties = input->properties;
cv::Mat bgr_mat = cv::imread(imagePath, cv::IMREAD_COLOR);
cv::Mat rgb_mat;
cv::cvtColor(bgr_mat, rgb_mat, cv::COLOR_BGR2RGB);
auto input_data = input->sysMem[0].virAddr;
uint8_t *rgb_data = rgb_mat.ptr();
int image_length = 384 * 2048 * 3;
memcpy(input_data, rgb_data, image_length);
Time preprocess_start = std::chrono::steady_clock::now();
uint8_t *data_u8{reinterpret_cast(input_data)};
int8_t *data_s8{reinterpret_cast(input_data)};
for (int i = 0; i < image_length; i++)
data_s8[i] = data_u8[i] - 128;
Time preprocess_end = std::chrono::steady_clock::now();
Micro preprocess_latency = std::chrono::duration_cast(preprocess_end - preprocess_start);
std::cout << "preprocess -128 time: " << static_cast(preprocess_latency.count() / 1000.0) << " ms" << std::endl;
}
float dequantiScale(int32_t data, bool big_endian, float &scale_value) {
return static_cast(r_int32(data, big_endian)) * scale_value;
}
// 后处理的核心代码(不包括nms),初步筛选检测框
void process_tensor_core(hbDNNTensor *tensor,
int layer,
std::vector &dets){
hbSysFlushMem(&(tensor->sysMem[0]), HB_SYS_MEM_CACHE_INVALIDATE);
int height, width, stride;
std::vector<std::pair<double, double>> anchors;
if(layer == 0){
height = 48; width = 256; stride = 8; anchors = {{10, 13}, {16, 30}, {33, 23}};
} else if (layer == 1){
height = 24; width = 128; stride = 16; anchors = {{30, 61}, {62, 45}, {59, 119}};
} else if (layer == 2){
height = 12; width = 64; stride = 32; anchors = {{116, 90}, {156, 198}, {373, 326}};
}
int anchor_num = anchors.size();
auto quanti_type = tensor->properties.quantiType;
if(quanti_type == hbDNNQuantiType::NONE){
auto *data = reinterpret_cast(tensor->sysMem[0].virAddr);
for (uint32_t h = 0; h < height; h++) {
for (uint32_t w = 0; w < width; w++) {
for (int k = 0; k < anchor_num; k++) {
double anchor_x = anchors[k].first;
double anchor_y = anchors[k].second;
float *cur_data = data + k * num_pred;
float objness = cur_data[4];
if(objness < score_threshold_objness)
continue;
int id = argmax(cur_data + 5, cur_data + 5 + num_classes);
double x1 = 1 / (1 + std::exp(-objness)) * 1;
double x2 = 1 / (1 + std::exp(-cur_data[id + 5]));
double confidence = x1 * x2;
if (confidence < score_threshold)
continue;
float center_x = cur_data[0];
float center_y = cur_data[1];
float scale_x = cur_data[2];
float scale_y = cur_data[3];
double box_center_x =
((1.0 / (1.0 + std::exp(-center_x))) * 2 - 0.5 + w) * stride;
double box_center_y =
((1.0 / (1.0 + std::exp(-center_y))) * 2 - 0.5 + h) * stride;
double box_scale_x =
std::pow((1.0 / (1.0 + std::exp(-scale_x))) * 2, 2) * anchor_x;
double box_scale_y =
std::pow((1.0 / (1.0 + std::exp(-scale_y))) * 2, 2) * anchor_y;
double xmin = (box_center_x - box_scale_x / 2.0);
double ymin = (box_center_y - box_scale_y / 2.0);
double xmax = (box_center_x + box_scale_x / 2.0);
double ymax = (box_center_y + box_scale_y / 2.0);
double xmin_org = xmin;
double xmax_org = xmax;
double ymin_org = ymin;
double ymax_org = ymax;
if (xmax_org <= 0 || ymax_org <= 0)
continue;
if (xmin_org > xmax_org || ymin_org > ymax_org)
continue;
xmin_org = std::max(xmin_org, 0.0);
xmax_org = std::min(xmax_org, image_width - 1.0);
ymin_org = std::max(ymin_org, 0.0);
ymax_org = std::min(ymax_org, image_height - 1.0);
Bbox bbox(xmin_org, ymin_org, xmax_org, ymax_org);
dets.emplace_back((int)id, confidence, bbox);
}
data = data + num_pred * anchors.size();
}
}
} else if(quanti_type == hbDNNQuantiType::SCALE){
auto *data = reinterpret_cast(tensor->sysMem[0].virAddr);
auto dequantize_scale_ptr = tensor->properties.scale.scaleData;
bool big_endian = false;
for (uint32_t h = 0; h < height; h++) {
for (uint32_t w = 0; w < width; w++) {
for (int k = 0; k < anchor_num; k++) {
double anchor_x = anchors[k].first;
double anchor_y = anchors[k].second;
int32_t *cur_data = data + k * num_pred;
float objness = dequantiScale(cur_data[4], big_endian, *(dequantize_scale_ptr));
if (objness < score_threshold_objness)
continue;
int offset = num_pred * k;
double max_cls_data = std::numeric_limits::lowest();
int id{0};
for (int cls = 0; cls < num_classes; cls += 4) {
for (int j = 0; j < 4; j++) {
float score = cur_data[cls + 5 + j] * dequantize_scale_ptr[offset+cls+5+j];
if (score > max_cls_data) {
max_cls_data = score;
id = cls + j;
}
}
}
double x1 = 1 / (1 + std::exp(-objness)) * 1;
double x2 = 1 / (1 + std::exp(-max_cls_data));
double confidence = x1 * x2;
float center_x = dequantiScale(
cur_data[0], big_endian, *(dequantize_scale_ptr + offset));
float center_y = dequantiScale(
cur_data[1], big_endian, *(dequantize_scale_ptr + offset + 1));
float scale_x = dequantiScale(
cur_data[2], big_endian, *(dequantize_scale_ptr + offset + 2));
float scale_y = dequantiScale(
cur_data[3], big_endian, *(dequantize_scale_ptr + offset + 3));
double box_center_x =
((1.0 / (1.0 + std::exp(-center_x))) * 2 - 0.5 + w) * stride;
double box_center_y =
((1.0 / (1.0 + std::exp(-center_y))) * 2 - 0.5 + h) * stride;
double box_scale_x =
std::pow((1.0 / (1.0 + std::exp(-scale_x))) * 2, 2) * anchor_x;
double box_scale_y =
std::pow((1.0 / (1.0 + std::exp(-scale_y))) * 2, 2) * anchor_y;
double xmin = (box_center_x - box_scale_x / 2.0);
double ymin = (box_center_y - box_scale_y / 2.0);
double xmax = (box_center_x + box_scale_x / 2.0);
double ymax = (box_center_y + box_scale_y / 2.0);
double xmin_org = xmin;
double xmax_org = xmax;
double ymin_org = ymin;
double ymax_org = ymax;
if (xmax_org <= 0 || ymax_org <= 0)
continue;
if (xmin_org > xmax_org || ymin_org > ymax_org)
continue;
xmin_org = std::max(xmin_org, 0.0);
xmax_org = std::min(xmax_org, image_width - 1.0);
ymin_org = std::max(ymin_org, 0.0);
ymax_org = std::min(ymax_org, image_height - 1.0);
Bbox bbox(xmin_org, ymin_org, xmax_org, ymax_org);
dets.emplace_back((int)id, confidence, bbox);
}
data = data + num_pred * anchors.size() + 2;
}
}
} else {
std::cout << "x5 unsupport shift dequantize!" << std::endl;
}
}
// nms处理,精挑细选出合适的检测框
void yolo5_nms(std::vector &input,
std::vector &result,
bool suppress) {
std::stable_sort(input.begin(), input.end(), std::greater());
std::vector skip(input.size(), false);
std::vector areas;
areas.reserve(input.size());
for (size_t i = 0; i < input.size(); i++) {
float width = input[i].bbox.xmax - input[i].bbox.xmin;
float height = input[i].bbox.ymax - input[i].bbox.ymin;
areas.push_back(width * height);
}
int count = 0;
for (size_t i = 0; count < nms_top_k && i < skip.size(); i++) {
if (skip[i]) {
continue;
}
skip[i] = true;
++count;
for (size_t j = i + 1; j < skip.size(); ++j) {
if (skip[j]) {
continue;
}
if (suppress == false) {
if (input[i].id != input[j].id) {
continue;
}
}
float xx1 = std::max(input[i].bbox.xmin, input[j].bbox.xmin);
float yy1 = std::max(input[i].bbox.ymin, input[j].bbox.ymin);
float xx2 = std::min(input[i].bbox.xmax, input[j].bbox.xmax);
float yy2 = std::min(input[i].bbox.ymax, input[j].bbox.ymax);
if (xx2 > xx1 && yy2 > yy1) {
float area_intersection = (xx2 - xx1) * (yy2 - yy1);
float iou_ratio =
area_intersection / (areas[j] + areas[i] - area_intersection);
if (iou_ratio > nms_iou_threshold) {
skip[j] = true;
}
}
}
result.push_back(input[i]);
// 打印最终筛选出的检测框的置信度和位置信息
std::cout << "score " << input[i].score;
std::cout << " xmin " << input[i].bbox.xmin;
std::cout << " ymin " << input[i].bbox.ymin;
std::cout << " xmax " << input[i].bbox.xmax;
std::cout << " ymax " << input[i].bbox.ymax << std::endl;
}
}
// 多线程加速后处理计算
std::mutex dets_mutex;
void process_tensor_thread(hbDNNTensor *tensor, int layer, std::vector &dets){
std::vector local_dets;
process_tensor_core(tensor, layer, local_dets);
std::lock_guard lock(dets_mutex);
dets.insert(dets.end(), local_dets.begin(), local_dets.end());
}
void post_process(std::vector &tensors,
Perception *perception){
perception->type = Perception::DET;
std::vector dets;
std::vector threads;
for (int i = 0; i < tensors.size(); ++i) {
threads.emplace_back(
&tensors, i, &dets
{
process_tensor_thread(&tensors[i], i, dets);
});
}
for (auto &thread : threads)
thread.join();
yolo5_nms(dets, perception->det, false);
}
int main(int argc, char **argv) {
//初始化模型
hbPackedDNNHandle_t packed_dnn_handle;
hbDNNHandle_t dnn_handle;
const char **model_name_list;
int model_count = 0;
hbDNNInitializeFromFiles(&packed_dnn_handle, &modelFileName, 1);
hbDNNGetModelNameList(&model_name_list, &model_count, packed_dnn_handle);
hbDNNGetModelHandle(&dnn_handle, packed_dnn_handle, model_name_list[0]);
std::cout<< "yolov5 demo begin!" << std::endl;
std::cout<< "load model success" <
这里强调一下read_image_2_tensor_as_rgb函数:
由于input_type_rt配置为rgb/bgr时,板端模型的输入是int8,而不是uint8,
因此需要用户在预处理中遍历所有输入数据手动做-128处理,
未来的工具链版本会针对RGB输入场景做更多的适配,以避免这种额外操作。
运行说明
用户可将头文件和源码放入horizon_runtime_sample/code/00_quick_start/src路径,并执行build_x5.sh编译工程,
再将horizon_runtime_sample/x5文件夹复制到开发板的/userdata目录,
并在/userdata/x5/script/00_quick_start/路径下存放上板模型、测试图片等文件,并编写板端运行脚本:
bin=../aarch64/bin/run_mobileNetV1_224x224
lib=../aarch64/lib
export LD_LIBRARY_PATH=
${lib}:$
{LD_LIBRARY_PATH}
export BMEM_CACHEABLE=true
${bin}
运行结果如下(以未删除反量化的模型为例):
yolov5 demo begin!
load model success
prepare intput and output tensor success
preprocess -128 time: 0.769 ms
read image to tensor as rgb success
model infer time: 9.669 ms
model infer success
score 0.585894 xmin 1448.85 ymin 148.522 xmax 1516.65 ymax 276.543
postprocess time: 1.574 ms
postprocess success
release resources success
yolov5 demo end!
对于这次推理,我们的输入图像为下图:
可以看到,推理程序成功识别到了目标物体,并且给出了正确的坐标信息:
score 0.585894 xmin 1448.85 ymin 148.522 xmax 1516.65 ymax 276.543
模型推理耗时说明
需要强调的是,应用程序在推理第一帧的时候,会产生加载推理框架导致的额外耗时,因此运行该程序测出的模型推理耗时是偏高的。
准确的模型推理时间应当以hrt_model_exec工具实测结果为准,参考命令:
hrt_model_exec perf --model-file ./yolov5n.bin --thread-num 1(测试单线程单帧延时,关注latency)
hrt_model_exec perf --model-file ./yolov5n.bin --thread-num 8(测试多线程极限吞吐量,关注FPS)
耗时优化对比
85 ymin 148.522 xmax 1516.65 ymax 276.543
postprocess time: 1.574 ms
postprocess success
release resources success
yolov5 demo end!
对于这次推理,我们的输入图像为下图:
[外链图片转存中...(img-8GFgwSsT-1740301301013)]
可以看到,推理程序成功识别到了目标物体,并且给出了正确的坐标信息:
```Plain
score 0.585894 xmin 1448.85 ymin 148.522 xmax 1516.65 ymax 276.543
模型推理耗时说明
需要强调的是,应用程序在推理第一帧的时候,会产生加载推理框架导致的额外耗时,因此运行该程序测出的模型推理耗时是偏高的。
准确的模型推理时间应当以hrt_model_exec工具实测结果为准,参考命令:
hrt_model_exec perf --model-file ./yolov5n.bin --thread-num 1(测试单线程单帧延时,关注latency)
hrt_model_exec perf --model-file ./yolov5n.bin --thread-num 8(测试多线程极限吞吐量,关注FPS)