第一章 指针篇
第二章 CUDA原理篇
第三章 CUDA编译器环境配置篇
第四章 kernel函数基础篇
第五章 kernel索引(index)篇
第六章 kenel矩阵计算实战篇
第七章 kenel实战强化篇
第八章 CUDA内存应用与性能优化篇
第九章 CUDA原子(atomic)实战篇
第十章 CUDA流(stream)实战篇
第十一章 CUDA的NMS算子实战篇
第十二章 YOLO的部署实战篇
第十三章 基于CUDA的YOLO部署实战篇
随着人工智能的发展与人才的内卷,很多企业已将深度学习算法的C++部署能力作为基本技能之一。面对诸多arm相关且资源有限的设备,往往想更好的提速,满足更高时效性,必将更多类似矩阵相关运算交给CUDA处理。同时,面对市场诸多教程与诸多博客岑子不起的教程或高昂教程费用,使读者(特别是小白)容易迷糊,无法快速入手CUDA编程,实现工程化。
因此,我将结合我的工程实战经验,我将在本专栏实现CUDA系列教程,帮助读者(或小白)实现CUDA工程化,掌握CUDA编程能力。学习我的教程专栏,你将绝对能实现CUDA工程化,完全从环境安装到CUDA核函数编程,从核函数到使用相关内存优化,从内存优化到深度学习算子开发(如:nms),从算子优化到模型(以yolo系列为基准)部署。最重要的是,我的教程将简单明了直切主题,CUDA理论与实战实例应用,并附相关代码,可直接上手实战。我的想法是掌握必要CUDA相关理论,去除非必须繁杂理论,实现CUDA算法应用开发,待进一步提高,将进一步理解更高深理论。
第一章到第三章探索指针在cuda函数中的作用与cuda相关原理及环境配置;
第四章初步探索cuda相关函数编写(global、device、__host__等),实现简单入门;
第五章探索不同grid与block配置,如何计算kernel函数的index,以便后续通过index实现各种运算;
第六、七章由浅入深探索核函数矩阵计算,深入探索grid、block与thread索引对kernel函数编写作用与影响,并实战多个应用列子(如:kernel函数实现图像颜色空间转换);
第八章探索cuda内存纹理内存、常量内存、全局内存等分配机制与内存实战应用(附代码),通过不同内存的使用来优化cuda计算性能;
第九章探索cuda原子(atomic)相关操作,并实战应用(如:获得某些自加索引等);
第十章探索cuda流stream相关应用,并给出相关实战列子(如:多流操作等);
第十一到十三章探索基于tensorrt部署yolo算法,我们首先将给出通用tensorrt的yolo算法部署,该部署的前后处理基于C++语言的host端实现,然后给出基于cuda的前后处理的算子核函数编写,最后数据无需在gpu与host间复制操作,实现gpu处理,提升算法性能。
目前,以上为我们的cuda教学全部内容,若后续读者有想了解知识,可留言,我们将根据实际情况,更新相关教学内容。
大神忽略
源码链接地址点击这里
学到这里,想必已掌握cuda编程精髓,基本可熟练使用cuda编写自己逻辑的算子。然,从事人工智能行业,更多企业需要我们使用cuda实现模型部署的速度提升,则如何将深度学习相关处理使用cuda加速实现是一个核心问题。我们将使用cuda编码金典算子-NMS。但我想有些刚入门读者不理解NMS,而难懂本篇cuda算子的代码和逻辑。为此,本节内容主要为nms原理、基于cpu实现nms算子方法、基于cuda实现nms算子方法,且将前2个内容作为上篇,后一个内容作为下篇,并附源码。
基于cpu的nms可优化,基于cuda的nms源码(不开源)在下篇文章中可找到!!!
NMS:中文称非极大值抑制,多个候选框使用nms算法,排除重叠多余候选框,找到少数最可能候选框。一般在目标检测算法中从一张图片中找出很多个可能是物体的矩形框。
非极大值抑制:依靠分类器得到多个候选框,以及关于候选框中属于类别的概率值,根据分类器得到的类别分类概率做排序,具体算法流程如下:
(1)将所有框的得分排序,选中最高分及其对应的框
(2)遍历其余的框,如果和当前最高分框的重叠面积(IOU)大于一定阈值,我们就将框删除,其原因为超过设定阈值,两个框的表示同一目标,选择分类概率最高的框表示该目标类别。
(3)从未处理的框中继续选一个得分最高的,重复上述过程。
iou也称交并比:一般通过iou公式判断2个框交集大小,iou值越大表示2个框交集更大,在目标检测中更可能表示同一个目标,否则表示2个不同目标。
IOU=Area(A∩B)/Area(A∪B)
以上内容已经说明nms原理与计算步骤,本小节将直接介绍cpu编写nms代码。
较为简单,利用2个box坐标计算交并比,不过多说明,其iou代码如下:
float iou_cpu(Bbox box1, Bbox box2) {
int x1 = max(box1.x, box2.x);
int y1 = max(box1.y, box2.y);
int x2 = min(box1.x + box1.w, box2.x + box2.w);
int y2 = min(box1.y + box1.h, box2.y + box2.h);
int w = max(0, x2 - x1);
int h = max(0, y2 - y1);
float over_area = w * h;
return over_area / (box1.w * box1.h + box2.w * box2.h - over_area);
}
其中Bbox定义为解构体如下:
struct Bbox {
int x; //左上角x
int y;
int w;
int h;
};
这部分代码较为麻烦,主要借助vector保存定义结果(一般模型预测结果)结构体(我定义为Predect_result),在nms函数中获得被保留的索引,并将其索引使用vector保存,代码使用keep_index表示,我已有注释于代码,我将不在具体介绍,其代码如下:
int get_max_index(vector<Predect_result> pre_detection) {
//获得最佳置信度的值,并返回对应的索引值
int index;
float conf;
if (pre_detection.size() > 0) {
index = 0;
conf = pre_detection.at(0).conf;
for (int i = 0; i < pre_detection.size(); i++) {
if (conf < pre_detection.at(i).conf) {
index = i;
conf = pre_detection.at(i).conf;
}
}
return index;
}
else {
return -1;
}
}
bool judge_in_lst(int index, vector<int> index_lst) {
//若index在列表index_lst中则返回true,否则返回false
if (index_lst.size() > 0) {
for (int i = 0; i < index_lst.size(); i++) {
if (index == index_lst.at(i)) {
return true;
}
}
}
return false;
}
vector<int> nms_cpu(vector<Predect_result> pre_detection, float iou_thr)
{
/*
返回需保存box的pre_detection对应位置索引值
*/
int index;
vector<Predect_result> pre_detection_new;
//Detection det_best;
Bbox box_best, box;
float iou_value;
vector<int> keep_index;
vector<int> del_index;
bool keep_bool;
bool del_bool;
if (pre_detection.size() > 0) {
pre_detection_new.clear();
// 循环将预测结果建立索引
for (int i = 0; i < pre_detection.size(); i++) {
pre_detection.at(i).index = i;
pre_detection_new.push_back(pre_detection.at(i));
}
//循环便利获得保留box位置索引-相对输入pre_detection位置
while (pre_detection_new.size() > 0) {
index = get_max_index(pre_detection_new);
if (index >= 0) {
keep_index.push_back(pre_detection_new.at(index).index); //保留索引位置
// 更新最佳保留box
box_best.x = pre_detection_new.at(index).bbox[0];
box_best.y = pre_detection_new.at(index).bbox[1];
box_best.w = pre_detection_new.at(index).bbox[2];
box_best.h = pre_detection_new.at(index).bbox[3];
for (int j = 0; j < pre_detection.size(); j++) {
keep_bool = judge_in_lst(pre_detection.at(j).index, keep_index);
del_bool = judge_in_lst(pre_detection.at(j).index, del_index);
if ((!keep_bool) && (!del_bool)) { //不在keep_index与del_index才计算iou
box.x = pre_detection.at(j).bbox[0];
box.y = pre_detection.at(j).bbox[1];
box.w = pre_detection.at(j).bbox[2];
box.h = pre_detection.at(j).bbox[3];
iou_value = iou_cpu(box_best, box);
if (iou_value > iou_thr) {
del_index.push_back(j); //记录大于阈值将删除对应的位置
}
}
}
//更新pre_detection_new
pre_detection_new.clear();
for (int j = 0; j < pre_detection.size(); j++) {
keep_bool = judge_in_lst(pre_detection.at(j).index, keep_index);
del_bool = judge_in_lst(pre_detection.at(j).index, del_index);
if ((!keep_bool) && (!del_bool)) {
pre_detection_new.push_back(pre_detection.at(j));
}
}
}
}
}
del_index.clear();
del_index.shrink_to_fit();
pre_detection_new.clear();
pre_detection_new.shrink_to_fit();
return keep_index;
}
其中Predect_result 为需要使用结果的结构体,也可以自定义添加,如添加feature[512]的跟踪特征保留等,其结构如下:
struct Predect_result {
//center_x center_y w h
//cv::Rect_ box; //tlwh
float bbox[4];
float conf; // bbox_conf * cls_conf
int index; //类别中的位置索引[0,cls_num-1]
};
主函数首先初始化预测结构体,共假设6个目标框,如下代码,
vector< Predect_result> predect= {
{522.0 ,178.0, 310.0 ,280.0 ,0.9,0},
{616.5, 178.0, 229.0, 280.0, 0.8,1 },
{567.0, 724.5, 528.0, 693.0, 0.8,2},
{512.0, 683.0, 638.0, 496.0, 0.7,3},
{1255.0, 352.0, 432.0, 410.0, 0.6,4},
{1455.0, 105.0, 232.0, 208.0, 0.5,5}};
随后调用nms_cpu函数,保留可能目标索引于vector中,如下代码,
vector<int> nms_keep_index = nms_cpu(predect, 0.9)
最后将其保留索引提取并画框。
整体主函数代码如下:
int main()
{
// 创建一组矩形框
vector< Predect_result> predect= {
{522.0 ,178.0, 310.0 ,280.0 ,0.9,0},
{616.5, 178.0, 229.0, 280.0, 0.8,1 },
{567.0, 724.5, 528.0, 693.0, 0.8,2},
{512.0, 683.0, 638.0, 496.0, 0.7,3},
{1255.0, 352.0, 432.0, 410.0, 0.6,4},
{1455.0, 105.0, 232.0, 208.0, 0.5,5}};
vector<int> nms_keep_index = nms_cpu(predect, 0.9)
输出结果
cv::Mat image = cv::imread("image.jpg");
for (int i = 0; i < nms_keep_index.size(); i++) {
int idx = nms_keep_index.at(i);
float x = predect.at(idx).bbox[0];
float y = predect.at(idx).bbox[1];
float w = predect.at(idx).bbox[2];
float h = predect.at(idx).bbox[3];
cv::Point p1(x-w/2, y-h/2);
cv::Point p2(x + w / 2, y + h / 2);
cv::rectangle(image, p1, p2, cv::Scalar(0, 255, 0), 4, 1, 0);//矩形的两个顶点,两个顶点都包括在矩形内部
}
cv::resize(image, image, cv::Size(600, 400), 0, 0, cv::INTER_NEAREST);
cv::imshow("www", image);
cv::waitKey(100000);
cv::destroyAllWindows();
return 0;
}
iou阈值为0.2,表示重叠高于0.2需排除,效果如下:
iou阈值为0.9,表示重叠高于0.9需排除,效果如下:
注:介于基于cpu的nms代码较多,我将不在完全展现,需要可点击我的github链接,已在本文上面--->源码链接地址
以上为nms算法原理和基于cpu的实现方法,然基于cpu实现的nms还有进一步优化空间,读者可在此基础上继续优化。但我希望,读者能查看下篇cuda实现方法,老实说cuda实现速度非常快。