模型流程
1. NPU初始化
NpuUint npu_uint;
int ret = npu_uint.npu_init();
2.根据传入的模型文件,创建网络
NetworkItem yolov5;
status = yolov5.network_create(model_file, network_id);
vip_create_network()
network_create_io_buffer()//创建输入buffer
3. 准备和验证网络的配置,进行运行前的准备
status = yolov5.network_prepare();
4. 获取输入的buffer,buffer在步骤2中创建的的。
yolov5.get_network_input_buff_info(0, &input_buffer_ptr, &input_buffer_size);
yolov5_preprocess_no_copy(input_file, input_buffer_ptr, input_buffer_size);
get_input_data //对输入图像进行预处理,调整合适的尺寸,获取一个输入指针。
5.创建一个输出buffer,实际已经预分配好了,output_data数组中存储的是指针。
int output_cnt = yolov5.get_output_cnt();
float **output_data = new float*[output_cnt]();
for (int i = 0; i < output_cnt; i++)
output_data[i] = new float[yolov5.m_output_data_len[i]];
//模型跑多少次?loop_count
while(count <loop_count) {
count ++;
5. 配置网络的输入和输出缓存区
status = yolov5.network_input_output_set()
vip_set_input
vip_set_output
6. 提交运行网络的任务,开始运行网络
status = yolov5.network_run();
7. 获取输出,输出是什么样的格式?
yolov5.get_output(output_data);
8.后处理
yolov5_postprocess(input_file, output_data);
}
模型输入预处理
输入数据预处理
void get_input_data(const char* image_file, unsigned char* input_data, int letterbox_rows, int letterbox_cols)
{
cv::Mat sample = cv::imread(image_file, 1);
if (sample.empty()) {
fprintf(stderr, "cv::imread %s failed\n", image_file);
return;
}
cv::Mat img;
cv::cvtColor(sample, img, cv::COLOR_BGR2RGB);
/* letterbox process to support different letterbox size */
//按比例缩放输入图片
float scale_letterbox;
if ((letterbox_rows * 1.0 / img.rows) < (letterbox_cols * 1.0 / img.cols)){
scale_letterbox = letterbox_rows * 1.0 / img.rows;
} else {
scale_letterbox = letterbox_cols * 1.0 / img.cols;
}
int resize_cols = int(scale_letterbox * img.cols);
int resize_rows = int(scale_letterbox * img.rows);
cv::resize(img, img, cv::Size(resize_cols, resize_rows));
// 创建Mat,但并不会进行内存拷贝而是创建了一个 头部指针(header)
//来引用 input_data 指向的内存。
cv::Mat img_new(letterbox_cols, letterbox_rows, CV_8UC3, input_data);
//保持原始图像长宽比的同时缩放到目标尺寸,不足部分用灰色(114)填充
int top = (letterbox_rows - resize_rows) / 2;
int bot = (letterbox_rows - resize_rows + 1) / 2;
int left = (letterbox_cols - resize_cols) / 2;
int right = (letterbox_cols - resize_cols + 1) / 2;
// Letterbox filling
cv::copyMakeBorder(img, img_new, top, bot, left, right, cv::BORDER_CONSTANT, cv::Scalar(114, 114, 114));
}
模型后处理
int yolov5_postprocess(const char *imagepath, float **output)
{
cv::Mat m = cv::imread(imagepath, 1);
std::vector<Object> objects;
detect_yolov5_rt(m, objects, output);
draw_objects(m, objects);
return 0;
}
static int detect_yolov5_rt(const cv::Mat& bgr, std::vector<Object>& objects, float **output)
{
std::chrono::steady_clock::time_point Tbegin, Tend;
1.使用 std::chrono::steady_clock 进行时间计时,计算函数执行时间。
Tbegin 记录开始时间,Tend 用于记录结束时间。
Tbegin = std::chrono::steady_clock::now();
2. output 是一个三维数组,通常包含3个特征图,每个特征图对应不同尺度的输出数据。
每个特征图中的数据会被逐步解析,以得到目标物体的位置和类别信息。
const float *p8_data_ptr = output[0];
const float *p16_data_ptr = output[1];
const float *p32_data_ptr = output[2];
// set default letterbox size
int letterbox_rows = LETTERBOX_ROWS;
int letterbox_cols = LETTERBOX_COLS;
/* postprocess */
const float prob_threshold = SCORE_THRESHOLD;
//物体的置信度阈值,低于该值的目标将被丢弃。
const float nms_threshold = NMS_THRESHOLD;
//用于非最大抑制的阈值,用于去除重复的检测框,保留具有最高置信度的框。
std::vector<Object> proposals;
std::vector<Object> objects8;
std::vector<Object> objects16;
std::vector<Object> objects32;
3.generate_proposals_rt是一个用于从每个尺度的输出特征图中生成候选框(proposals)
函数。每个尺度(8x8、16x16 和 32x32)都会调用该函数,并将检测到的物体存储在对应
的objects向量中。
generate_proposals_rt(32, p32_data_ptr, prob_threshold, objects32, letterbox_cols, letterbox_rows);
proposals.insert(proposals.end(), objects32.begin(), objects32.end());
generate_proposals_rt(16, p16_data_ptr, prob_threshold, objects16, letterbox_cols, letterbox_rows);
proposals.insert(proposals.end(), objects16.begin(), objects16.end());
generate_proposals_rt( 8, p8_data_ptr, prob_threshold, objects8, letterbox_cols, letterbox_rows);
proposals.insert(proposals.end(), objects8.begin(), objects8.end());
4.将候选框按照置信度排序,从高到低。
qsort_descent_inplace(proposals);
std::vector<int> picked;
5.进行非最大抑制,去除冗余的候选框,保留置信度较高且没有重叠过多的框。
picked 向量存储最终选择的框的索引。
nms_sorted_bboxes(proposals, picked, nms_threshold);
6. 调整框的位置(从 letterbox 大小到原始图像大小)
计算输入图像与 letterbox 大小之间的缩放比率。letterbox 是指图像填充到
固定尺寸时,保持原图宽高比的矩形区域。计算缩放后的图像尺寸,并根据缩放比例
调整框的位置。
/* yolov5 draw the result */
float scale_letterbox;
int resize_rows;
int resize_cols;
if ((letterbox_rows * 1.0 / bgr.rows) < (letterbox_cols * 1.0 / bgr.cols))
{
scale_letterbox = letterbox_rows * 1.0 / bgr.rows;
}
else
{
scale_letterbox = letterbox_cols * 1.0 / bgr.cols;
}
resize_cols = int(scale_letterbox * bgr.cols);
resize_rows = int(scale_letterbox * bgr.rows);
int tmp_h = (letterbox_rows - resize_rows) / 2;
int tmp_w = (letterbox_cols - resize_cols) / 2;
float ratio_x = (float)bgr.rows / resize_rows;
float ratio_y = (float)bgr.cols / resize_cols;
7. 修正目标框的位置.picked.size() 表示最终选中的目标框数量。对于每个选
中的目标框(通过 picked 向量索引),调整其坐标,从 letterbox 坐标系转换回
原图坐标系。通过计算得到的 ratio_x 和 ratio_y 来调整坐标,确保目标框与原始
图像大小一致。
int count = picked.size();
objects.resize(count);
for (int i = 0; i < count; i++)
{
objects[i] = proposals[picked[i]];
float x0 = (objects[i].rect.x);
float y0 = (objects[i].rect.y);
float x1 = (objects[i].rect.x + objects[i].rect.width);
float y1 = (objects[i].rect.y + objects[i].rect.height);
x0 = (x0 - tmp_w) * ratio_x;
y0 = (y0 - tmp_h) * ratio_y;
x1 = (x1 - tmp_w) * ratio_x;
y1 = (y1 - tmp_h) * ratio_y;
x0 = std::max(std::min(x0, (float)(bgr.cols - 1)), 0.f);
y0 = std::max(std::min(y0, (float)(bgr.rows - 1)), 0.f);
x1 = std::max(std::min(x1, (float)(bgr.cols - 1)), 0.f);
y1 = std::max(std::min(y1, (float)(bgr.rows - 1)), 0.f);
objects[i].rect.x = x0;
objects[i].rect.y = y0;
objects[i].rect.width = x1 - x0;
objects[i].rect.height = y1 - y0;
}
8. 计算函数执行时间,并输出检测到的目标数目
Tend = std::chrono::steady_clock::now();
float f = std::chrono::duration_cast <std::chrono::milliseconds> (Tend - Tbegin).count();
fprintf(stderr, "detection num: %d\n", count);
return 0;
}
该函数主要用于处理 YOLOv5 模型的输出结果,通过三个不同尺度的输出特征图来生成候选框,然后使用非最大抑制(NMS)去除冗余框,最后将目标框的坐标从 letterbox 坐标系转换回原始图像的坐标系。该函数的核心任务是目标检测的后处理,包括筛选高置信度框、去除重复框、调整框位置等步骤。
计算先验框矩形坐标
static void generate_proposals_rt(int stride, const float* feat, float prob_threshold, std::vector<Object>& objects,
int letterbox_cols, int letterbox_rows)
{
static float anchors[18] = {10, 13, 16, 30, 33, 23, 30, 61, 62, 45, 59, 119, 116, 90, 156, 198, 373, 326};
//YOLO预定义的anchor尺寸,共9组(每组宽高),18个值
int anchor_num = 3; //3个先验框
int feat_w = letterbox_cols / stride; //计算特征图大小,如果是下采样8倍
int feat_h = letterbox_rows / stride; //那就是80 * 80
int cls_num = CLASS_NUM; //分类数量,默认是80
int anchor_group; //对先验框进行分组
if (stride == 8)
anchor_group = 1;
if (stride == 16)
anchor_group = 2;
if (stride == 32)
anchor_group = 3;
//反Sigmoid函数,将0~1的输入转化为原始值
float deprob_threshold = desigmoid(prob_threshold);
//特征图的尺寸大小,如果是8倍下采样就是80*80=6400份
int feat_size = feat_w * feat_h;
//计算单个anchor的特征数据总量,用于定位内存中不同anchor的偏移位置
//通过feat_ptr + feat_size_cls_5 * n可精确访问第n个anchor的预测数据
//feat_size_cls_5 = 80*80*85 = 544000
int feat_size_cls_5 = feat_size * (cls_num + 5);
//YOLOv5的输出特征图按(cls_num+5)*anchor_num*feat_size组织
float *feat_ptr = (float*)feat;
//将原始一维特征数组feat_ptr转换为3个二维矩阵anchor_0/1/2,每个矩阵维度为(cls_num+5)×feat_size
cv::Mat anchor_0 = cv::Mat((cls_num + 5), feat_size, CV_32FC1, feat_ptr);
cv::Mat anchor_1 = cv::Mat((cls_num + 5), feat_size, CV_32FC1, feat_ptr + feat_size_cls_5);
cv::Mat anchor_2 = cv::Mat((cls_num + 5), feat_size, CV_32FC1, feat_ptr + feat_size_cls_5 * 2);
//anchor_0直接引用feat_ptr起始地址,对应第一个anchor的预测数据。
//anchor_1通过feat_ptr + feat_size_cls_5偏移,定位到第二个anchor的数据块。
//anchor_2使用feat_size_cls_5*2偏移量访问第三个anchor数据。
cv::transpose(anchor_0, anchor_0);
cv::transpose(anchor_1, anchor_1);
cv::transpose(anchor_2, anchor_2);
float *anchor_data[3] = {(float*)anchor_0.data, (float*)anchor_1.data, (float*)anchor_2.data};
//创建float*类型的数组anchor_data[3],存储3个锚点矩阵的数据指针。
//三重循环分别遍历特征图高度(feat_h)、宽度(feat_w)和锚点数量(anchor_num)
for (int h = 0; h <= feat_h - 1; h++)
{
int h_feat_w_cls_5 = h * feat_w * (cls_num + 5);
//h_feat_w_cls_5计算当前行在特征图中的内存偏移量,
for (int w = 0; w <= feat_w - 1; w++)
{
int w_cls_5 = w * (cls_num + 5);
//w_cls_5计算当前列在行内的偏移量
for (int a = 0; a <= anchor_num - 1; a++)
{
//process cls score
int class_index = 0;
float class_score = -FLT_MAX;
int a_idx = h_feat_w_cls_5 + w_cls_5;
//定位到当前锚点的类别分数起始位置(+4跳过前4个坐标参数)
const float *feat_ptr = anchor_data[a] + a_idx + 4;
//遍历所有类别(cls_num),通过比较*(feat_ptr + s + 1)获
//遍历类别概率索引,计算类别中概率最高得分,用于计算先验框分数
for (int s = 0; s <= cls_num - 1; s++)
{
if (/*score*/ *(feat_ptr + s + 1) > class_score)
{
class_index = s;
class_score = *(feat_ptr + s + 1); //score;
}
}
//process box score
float box_score = *feat_ptr;
//box_score直接读取特征图数据(*feat_ptr),表示当前锚点包含目标的置信度
float final_score = 0.0f;
//采用双阈值过滤机制(deprob_threshold),仅当框分数和类别分数均达标时才计算最终分数
if (box_score >= deprob_threshold && class_score >= deprob_threshold)
final_score = sigmoid(box_score) * sigmoid(class_score);
//先验框得分 = 置信度值 x 类别概率中最大的值
//下面是YOLOv5特有的解码格式计算矩形框(x0,y0,x1,y1)
if (final_score >= prob_threshold)
{
int loc_idx = a_idx;
float dx = sigmoid(*(anchor_data[a] + loc_idx + 0));
float dy = sigmoid(*(anchor_data[a] + loc_idx + 1));
float dw = sigmoid(*(anchor_data[a] + loc_idx + 2));
float dh = sigmoid(*(anchor_data[a] + loc_idx + 3));
float pred_cx = (dx * 2.0f - 0.5f + w) * stride;
float pred_cy = (dy * 2.0f - 0.5f + h) * stride;
float anchor_w = anchors[(anchor_group - 1) * 6 + a * 2 + 0];
float anchor_h = anchors[(anchor_group - 1) * 6 + a * 2 + 1];
float pred_w = dw * dw * 4.0f * anchor_w;
float pred_h = dh * dh * 4.0f * anchor_h;
float x0 = pred_cx - pred_w * 0.5f;
float y0 = pred_cy - pred_h * 0.5f;
float x1 = pred_cx + pred_w * 0.5f;
float y1 = pred_cy + pred_h * 0.5f;
Object obj;
obj.rect.x = x0;
obj.rect.y = y0;
obj.rect.width = x1 - x0;
obj.rect.height = y1 - y0;
obj.label = class_index;
//先验框最后得分 = 置信度值 x 类别概率中最大的值
obj.prob = final_score;
//预测框被封装为Object结构体,存入objects向量供后续NMS处理
objects.push_back(obj);
}
}
}
}
}
generate_proposals_rt函数的作用是把所有先验框的信息(坐标信息、置信度、分类概率)计算处理放到objects中。一共是25200个先验框数量。
- 8倍下采样先验框数量: 3 x 80 x 80 = 19200
- 16倍下采样先验框数量: 3 x 40 x 40 = 4800
- 32倍下采样先验框数量: 3 x 20 x 20 = 1200
非极大值抑制
static void qsort_descent_inplace(std::vector<Object>& objects, int left, int right)
{
int i = left;
int j = right;
float p = objects[(left + right) / 2].prob;
while (i <= j)
{
while (objects[i].prob > p)
i++;
while (objects[j].prob < p)
j--;
if (i <= j)
{
// swap
std::swap(objects[i], objects[j]);
i++;
j--;
}
}
#pragma omp parallel sections
{
#pragma omp section
{
if (left < j) qsort_descent_inplace(objects, left, j);
}
#pragma omp section
{
if (i < right) qsort_descent_inplace(objects, i, right);
}
}
}
static void qsort_descent_inplace(std::vector<Object>& objects)
{
if (objects.empty())
return;
qsort_descent_inplace(objects, 0, objects.size() - 1);
}
按照先验框得分使用快排进行排序,先验框得分为先验框得分 = 置信度值 x 类别概率中最大的值,在generate_proposals中计算而来。
static void nms_sorted_bboxes(const std::vector<Object>& objects, std::vector<int>& picked, float nms_threshold, bool agnostic = false)
{
//清空输出容器picked, 预计算所有检测框面积存入areas数组,避免重复计算。
picked.clear();
const int n = objects.size();
std::vector<float> areas(n);
//调用OpenCV的area()方法计算每个矩形框面积
for (int i = 0; i < n; i++) {
areas[i] = objects[i].rect.area();
}
//遍历所有检测框,初始化当前框的保留标志keep
for (int i = 0; i < n; i++) {
const Object& a = objects[i];
int keep = 1;
//循环遍历已选中的检测框进行IoU计算
for (int j = 0; j < (int)picked.size(); j++)
{
const Object& b = objects[picked[j]];
// intersection over union
float inter_area = intersection_area(a, b);
float union_area = areas[i] + areas[picked[j]] - inter_area;
//计算交并比:inter_area / union_area,超过阈值则标记当前框为抑制状态
if (inter_area / union_area > nms_threshold)
keep = 0;
}
//未被抑制的框索引加入结果集
if (keep)
picked.push_back(i);
}
}
绘制目标框
static void draw_objects(const cv::Mat& bgr, const std::vector<Object>& objects)
{
std::chrono::steady_clock::time_point Tbegin, Tend;
1.使用 std::chrono::steady_clock 进行时间计时。Tbegin 记录开始时间,Tend
用于记录结束时间。目的是计算绘制物体框和标签的时间开销。
Tbegin = std::chrono::steady_clock::now();
2.将传入的原始图像 bgr 克隆到 image 中。这是因为原始图像不应该被修改,绘制操
作会在image上进行,而原图bgr保持不变。
cv::Mat image = bgr.clone();
3.遍历 objects 向量,这个向量包含了所有检测到的物体(每个物体通过 Object 类
型表示)。obj 是当前遍历到的物体。
for (size_t i = 0; i < objects.size(); i++)
{
const Object& obj = objects[i];
4.打印输出物体的详细信息:物体的标签 (obj.label)、置信度
(obj.prob * 100)、物体框的坐标(左上角和右下角),以及物体的类别
名称(g_classes_name[obj.label])。这对于调试非常有用。
fprintf(stderr, "%2d: %3.0f%%, [%4.0f, %4.0f, %4.0f, %4.0f], %s\n", obj.label, obj.prob * 100, obj.rect.x,
obj.rect.y, obj.rect.x + obj.rect.width, obj.rect.y + obj.rect.height, g_classes_name[obj.label].c_str());
5.使用 OpenCV 的 cv::rectangle 函数在图像上绘制矩形框,表示检测到的物体位置。
obj.rect 是物体的边界框,cv::Scalar(255, 0, 0) 表示框的颜色为蓝色(BGR格式)。
cv::rectangle(image, obj.rect, cv::Scalar(255, 0, 0));
6. 使用 sprintf 函数构造文本,显示物体的类别名称和置信度(格式为:类别名称 + 置信度百分比)。
g_classes_name[obj.label] 获取物体的类别名称,obj.prob * 100 表示物体的置信度。
char text[256];
sprintf(text, "%s %.1f%%", g_classes_name[obj.label].c_str(), obj.prob * 100);
int baseLine = 0;
cv::Size label_size = cv::getTextSize(text, cv::FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);
int x = obj.rect.x;
int y = obj.rect.y - label_size.height - baseLine;
if (y < 0)
y = 0;
if (x + label_size.width > image.cols)
x = image.cols - label_size.width;
//绘制文本的白色背景矩形框
cv::rectangle(image, cv::Rect(cv::Point(x, y), cv::Size(label_size.width, label_size.height + baseLine)),
cv::Scalar(255, 255, 255), -1);
//绘制文本信息
cv::putText(image, text, cv::Point(x, y + label_size.height), cv::FONT_HERSHEY_SIMPLEX, 0.5,
cv::Scalar(0, 0, 0));
}
cv::imwrite("output_yolov5.png", image);
Tend = std::chrono::steady_clock::now();
float f = std::chrono::duration_cast <std::chrono::milliseconds> (Tend - Tbegin).count();
std::cout << "draw objects time : " << f << " ms" << std::endl;
}

opencv参考函数
void cv::rectangle(
cv::Mat& img, // 输入输出图像
cv::Rect rec, // 矩形的位置和大小
const cv::Scalar& color, // 矩形的颜色(BGR格式)
int thickness = 1, // 矩形边框的厚度,默认是 1
int lineType = 8, // 线条类型,默认为 8(抗锯齿线)
int shift = 0 // 对坐标进行的位移,默认为 0
);
cv::Mat img = cv::imread("image.jpg"); // 读取图像
cv::Rect rect(50, 50, 200, 100); // 创建一个矩形,左上角为(50, 50),宽200,高100
cv::Scalar color(0, 255, 0); // 设置矩形的颜色为绿色(BGR格式)
cv::rectangle(img, rect, color, 2); // 在图像上绘制绿色的矩形,边框厚度为2
cv::imwrite("output.jpg", img); // 保存图像
- img: 输入和输出图像,矩形将被绘制在这张图像上。
- rec: 这个矩形的坐标和尺寸,用 cv::Rect(x, y, width, height) 来指定矩形的位置(左上角 (x, y))和尺寸(宽度和高度)。
- color: 矩形边框的颜色,采用 cv::Scalar 类型表示颜色(BGR 格式),例如 cv::Scalar(255, 0, 0) 表示蓝色。
- thickness: 边框的厚度。如果设置为负值(例如 -1),则矩形将会填充颜色,而不是只绘制边框。
- lineType: 线条类型,决定了绘制线条的质量,常用的值有 8(8-connected lines)、4(4-connected lines),以及 CV_AA(抗锯齿线)。
- shift: 表示坐标的位移,通常在进行精度处理时使用。
void cv::putText(
cv::Mat& img, // 输入输出图像
const std::string& text, // 要绘制的文本
cv::Point org, // 文本的起始位置(左下角)
int fontFace, // 字体类型
double fontScale, // 字体大小的缩放因子
const cv::Scalar& color, // 文本颜色(BGR格式)
int thickness = 1, // 字体线条的粗细,默认为 1
int lineType = 8, // 线条类型,默认为 8
bool bottomLeftOrigin = false // 是否以左下角为原点,默认为 false
);
cv::Mat img = cv::imread("image.jpg"); // 读取图像
std::string text = "Hello, OpenCV!"; // 要绘制的文本
cv::Point org(50, 50); // 文本的起始位置(左上角)
cv::Scalar color(0, 0, 255); // 文本颜色为红色(BGR格式)
int fontFace = cv::FONT_HERSHEY_SIMPLEX; // 使用无衬线字体
double fontScale = 1.0; // 字体大小
int thickness = 2; // 字体粗细
cv::putText(img, text, org, fontFace, fontScale, color, thickness); // 在图像上绘制文本
cv::imwrite("output_text.jpg", img); // 保存图像
- img: 输入和输出图像,文本将被绘制在这张图像上。
- text: 需要绘制的文本内容,使用 std::string 类型传递。
- org: 文本的起始位置(左下角的坐标),使用 cv::Point(x, y) 形式来指定。
- fontFace: 字体类型,OpenCV 提供了几种常见的字体类型:
cv::FONT_HERSHEY_SIMPLEX:简单无衬线字体; cv::FONT_HERSHEY_PLAIN:简单的无衬线字体,字形更小; cv::FONT_HERSHEY_COMPLEX:复杂的无衬线字体; cv::FONT_HERSHEY_SCRIPT_SIMPLEX:手写体; cv::FONT_HERSHEY_SCRIPT_COMPLEX:复杂的手写体等。
- fontScale: 字体的缩放因子,用于调整字体大小。1.0 表示默认大小,2.0 表示更大。 color: 文本颜色,采用 cv::Scalar 类型表示颜色(BGR 格式),例如 cv::Scalar(255, 0, 0) 表示蓝色。
- thickness: 字体的线条粗细,默认为 1。
- lineType: 线条类型,决定了绘制文本时的线条质量,常见的有 8(8-connected lines)和 CV_AA(抗锯齿线)。
- bottomLeftOrigin: 是否以左下角为原点。默认是 false,意味着文本是从左上角开始绘制的。如果设置为 true,则文本从左下角开始绘制
评论