3.8.cuda运行时API-使用cuda核函数加速yolov5后处理

news2024/11/14 22:05:40

目录

    • 前言
    • 1. Yolov5后处理
    • 2. 后处理案例
      • 2.1 cpu_decode
      • 2.2 gpu_decode
    • 总结

前言

杜老师推出的 tensorRT从零起步高性能部署 课程,之前有看过一遍,但是没有做笔记,很多东西也忘了。这次重新撸一遍,顺便记记笔记。

本次课程学习精简 CUDA 教程-使用 cuda 核函数加速 yolov5 的后处理

课程大纲可看下面的思维导图

在这里插入图片描述

1. Yolov5后处理

Yolov5 是目标检测中比较经典的模型,学习对其后处理进行解码是非常有必要的。在这里我们仅使用核函数对 Yolov5 推理的结果进行解码并恢复成框,掌握后处理所解决的问题,以及对于性能的考虑。

经验之谈

  1. 对于后处理的代码研究,可以把 PyTorch 的数据通过转换成 numpy 后,tobytes 再写到文件,然后再到 c++ 中读取的方式,能够快速进行问题研究和排查,此时不需要 tensorRT 推理也可以做后处理研究。这也叫变量控制法
  2. fast_nms_kernel 会在极端情况下少框,但是这个极端情况一般不会出现,实测几乎没有影响
  3. fast nms 在 cuda 实现上比较简单,高效,不用排序

2. 后处理案例

我们来看下 Yolov5 整个后处理过程:decode解码 + nms

由于整个后处理过程可能有点复杂,因此我们可以先在 CPU 上完成,然后再考虑 GPU 上的工作。

为了方便演示整个后处理过程,我们通过 PyTorch 去进行推理,把推理的结果利用 numpy 保存下来,然后利用 c++ 读取进行后处理,同时也可以看下 PyTorch 最终的结果和我们后处理的结果是否一致。

numpy 保存推理结果的代码如下:

with open("../workspace/predict.data", "wb") as f:
   f.write(pred.cpu().data.numpy().tobytes())

Yolov5 在 COCO 数据集上的输入是一个 [n,85] 为维度的 tensor,其中 85 是 [cx,cy,width,objectness,classfication * 80]

关于后处理原理和更多细节请查看 YOLOv5推理详解及预处理高性能实现

2.1 cpu_decode

我们先来看 cpu_decode,CPU 解码的重点有:

  1. 避免多余的计算,需要知道有些数学运算需要的事件远超过很多 if,减少他们的次数就是提高性能的关键
  2. nms 的实现是可以优化的,例如 remove_flags 并且预先分配内存,reserve 对输出分配内存

核心代码如下:

vector<Box> cpu_decode(float* predict, int rows, int cols, float confidence_threshold = 0.25f, float nms_threshold = 0.45f){
    
    vector<Box> boxes;
    int num_classes = cols - 5;
    for(int i = 0; i < rows; ++i){
        float* pitem = predict + i * cols;
        float objness = pitem[4];
        if(objness < confidence_threshold)
            continue;

        float* pclass = pitem + 5;
        int label     = std::max_element(pclass, pclass + num_classes) - pclass;
        float prob    = pclass[label];
        float confidence = prob * objness;
        if(confidence < confidence_threshold)
            continue;

        float cx     = pitem[0];
        float cy     = pitem[1];
        float width  = pitem[2];
        float height = pitem[3];
        float left   = cx - width * 0.5;
        float top    = cy - height * 0.5;
        float right  = cx + width * 0.5;
        float bottom = cy + height * 0.5;
        boxes.emplace_back(left, top, right, bottom, confidence, (float)label);
    }

    std::sort(boxes.begin(), boxes.end(), [](Box& a, Box& b){return a.confidence > b.confidence;});
    std::vector<bool> remove_flags(boxes.size());
    std::vector<Box> box_result;
    box_result.reserve(boxes.size());

    auto iou = [](const Box& a, const Box& b){
        float cross_left   = std::max(a.left, b.left);
        float cross_top    = std::max(a.top, b.top);
        float cross_right  = std::min(a.right, b.right);
        float cross_bottom = std::min(a.bottom, b.bottom);

        float cross_area = std::max(0.0f, cross_right - cross_left) * std::max(0.0f, cross_bottom - cross_top);
        float union_area = std::max(0.0f, a.right - a.left) * std::max(0.0f, a.bottom - a.top) 
                         + std::max(0.0f, b.right - b.left) * std::max(0.0f, b.bottom - b.top) - cross_area;
        if(cross_area == 0 || union_area == 0) return 0.0f;
        return cross_area / union_area;
    };

    for(int i = 0; i < boxes.size(); ++i){
        if(remove_flags[i]) continue;

        auto& ibox = boxes[i];
        box_result.emplace_back(ibox);
        for(int j = i + 1; j < boxes.size(); ++j){
            if(remove_flags[j]) continue;

            auto& jbox = boxes[j];
            if(ibox.label == jbox.label){
                // class matched
                if(iou(ibox, jbox) >= nms_threshold)
                    remove_flags[j] = true;
            }
        }
    }
    return box_result;
}

该代码主要可分为预处结果解码和非极大值抑制两部分

预测结果解码

首先遍历每个预测框,通过置信度阈值(confidence_threshold)对预测结果进行过滤。然后,计算预测框的类别,选择 80 个类别中最高概率的类别作为预测框的标签。接下来,将预测框的中心点和宽高转变成左上角和右下角坐标,并将预测框的信息保存到 boxes

非极大值抑制(NMS)

首先我们需要对 boxes 中的所有预测框按照置信度进行降序排序,方便后续 NMS 操作。NMS 的实现主要是通过 remove_flags 这个标志来实现的,将未标记为需要移除的预测框保存到 box_result

关键的性能优化点

  • 预测框过滤,在 decode 过程中先利用置信度阈值过滤,避免了不必要的后续计算和处理
  • 预测框排序,在 lambda 函数中传引用,同时对 box_result 利用 reverse 进行预分配提升性能
  • 使用标志位:在 NMS 过程中,使用 remove_flags 标志位来标记需要移除的预测框,相比于两两预测框比较提高了效率

2.2 gpu_decode

我们再来看 gpu_decode,GPU 解码的重点有:

  1. 表示输出数量不确定的数组,用 [count, box1, box2, box3] 的方式,此时需要有最大数量限制
  2. 通过 atomicAdd 实现数组元素的加入,并返回索引
  3. 和 cpu_decode 一样,不必要的计算尽量省掉

decode 核心代码如下:

static __global__ void decode_kernel(
    float* predict, int num_bboxes, int num_classes, float confidence_threshold, 
    float* invert_affine_matrix, float* parray, int max_objects, int NUM_BOX_ELEMENT
){  
    int position = blockDim.x * blockIdx.x + threadIdx.x;
    if (position >= num_bboxes) return;

    float* pitem     = predict + (5 + num_classes) * position;
    float objectness = pitem[4];
    if(objectness < confidence_threshold)
        return;

    float* class_confidence = pitem + 5;
    float confidence        = *class_confidence++;
    int label               = 0;
    for(int i = 1; i < num_classes; ++i, ++class_confidence){
        if(*class_confidence > confidence){
            confidence = *class_confidence;
            label      = i;
        }
    }

    confidence *= objectness;
    if(confidence < confidence_threshold)
        return;

    int index = atomicAdd(parray, 1);
    if(index >= max_objects)
        return;

    float cx         = *pitem++;
    float cy         = *pitem++;
    float width      = *pitem++;
    float height     = *pitem++;
    float left   = cx - width * 0.5f;
    float top    = cy - height * 0.5f;
    float right  = cx + width * 0.5f;
    float bottom = cy + height * 0.5f;
    // affine_project(invert_affine_matrix, left,  top,    &left,  &top);
    // affine_project(invert_affine_matrix, right, bottom, &right, &bottom);

    // left, top, right, bottom, confidence, class, keepflag
    float* pout_item = parray + 1 + index * NUM_BOX_ELEMENT;
    *pout_item++ = left;
    *pout_item++ = top;
    *pout_item++ = right;
    *pout_item++ = bottom;
    *pout_item++ = confidence;
    *pout_item++ = label;
    *pout_item++ = 1; // 1 = keep, 0 = ignore
}

上述 gpu_decode 代码和 cpu 处理非常像,其中核函数启动的线程数为预测框的数量,每个线程处理一个框的解码工作,position 代表当前线程的 Idx,*predict 为所有预测框的首地址,pitem 为当前线程要处理的预测框的起始地址,如下图所示:

在这里插入图片描述

图2-1 pitem

同时为了保存 decode 后的预测框,我们使用原子加(atomicAdd)操作来避免多个线程同时写入输出数组时的冲突问题,可以确保结果的准确性。具体来说,index = atomicAdd(parray, 1) 表示将 parray 指向的内存位置的值加上 1,并将加前的值赋给 index,而 index 表示当前所处理的边界框在所有边界框中的索引值。为了避免超过最大边界框数量,会在 index 超过 MAX_IMAGE_BOXES 时直接返回,不再处理该边界框。

将预测框完成解码后就需要将其解码后的框信息保存下来,保存的首地址是 *parrayparray 的第一个元素是保存下来的框的数量,后面才是一个个框的信息,如下图所示。

在这里插入图片描述

图2-2 pout_item

当然对于 nsm 你也可以采用 cuda 加入,代码如下:

static __global__ void fast_nms_kernel(float* bboxes, int max_objects, float threshold, int NUM_BOX_ELEMENT){

    int position = (blockDim.x * blockIdx.x + threadIdx.x);
    int count = min((int)*bboxes, max_objects);
    if (position >= count) 
        return;
    
    // left, top, right, bottom, confidence, class, keepflag
    float* pcurrent = bboxes + 1 + position * NUM_BOX_ELEMENT;
    for(int i = 0; i < count; ++i){
        float* pitem = bboxes + 1 + i * NUM_BOX_ELEMENT;
        if(i == position || pcurrent[5] != pitem[5]) continue;

        if(pitem[4] >= pcurrent[4]){
            if(pitem[4] == pcurrent[4] && i < position)
                continue;

            float iou = box_iou(
                pcurrent[0], pcurrent[1], pcurrent[2], pcurrent[3],
                pitem[0],    pitem[1],    pitem[2],    pitem[3]
            );

            if(iou > threshold){
                pcurrent[6] = 0;  // 1=keep, 0=ignore
                return;
            }
        }
    }
} 

fast_nms_kernel 在极端情况下会少框,比如当存在多个重叠框,并且它们具有相同的置信度时,由于核函数中的条件判断和并行计算的特性,可能会导致后面的框覆盖前面的框,从而使得前面的框被忽略。

值得注意的是在对 mAP 进行测试性能的时候,只能采用 CPU 版本的 nms,这是因为 mAP 测试需要精确计算每个框的重叠情况,并且需要按照特定的算法进行排序和抑制。而在 GPU 上进行并行计算的 nms 方法往往会牺牲一定的精确性,无法满足 mAP 测试的要求。

下图对比了 PyTorch 的效果和我们自己实现的后处理的效果,可以看到结果是没问题的

在这里插入图片描述

图2-3 PyTorch效果

在这里插入图片描述

图2-4 自定义实现后处理的效果

总结

本次课程学习了经典目标检测算法 Yolov5 的后处理,我们先在 cpu 上实现了整个 decode,cpu 版本的实现性能已经非常高了,适合在一些边缘嵌入式设备上运行,随后我们根据 cpu 版本的 decode 编写了核函数来加速整个 decode 解码过程,很多东西还是需要大家自己多去动手,多去尝试。

关于代码的更多探讨可参考 infer源码阅读之yolo.cu

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/738872.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【*2200线段树Pushup】CF1567 E

Problem - E - Codeforces 题意&#xff1a; 思路&#xff1a; 维护这些信息即可 Code&#xff1a; #include <bits/stdc.h>#define int long longusing namespace std;const int mxn2e510; const int mxe2e510; const int mod1e97; const int Inf1e18;struct info{in…

【C语言】gcc编译时报错 fatal error: stdio.h: 没有那个文件或目录

零、问题 在Ubuntu20.04.6中使用GCC编译一个HelloWorld代码时遇到如下问题&#xff1a; 首先确认了&#xff0c;自己单词没有拼写错。 然后再检查GCC的版本&#xff0c;确实没问题&#xff1a; 我用的是Ubuntu20.04.6的版本。 壹、解决 没有标准的头文件需要安装build-es…

和鲸社区数据分析每周挑战【第九十七期:技术博客文本分析】

和鲸社区数据分析每周挑战【第九十七期&#xff1a;技术博客文本分析】 文章目录 和鲸社区数据分析每周挑战【第九十七期&#xff1a;技术博客文本分析】一、背景描述二、数据说明三、问题描述四、数据导入五、数据探索性分析六、对文章标题进行文本分类预测1、数据预处理2、逻…

C++万字自学笔记

[TOC] 一、 C基础 C的IDE有CLion、Visual Studio、DEV C、eclipse等等&#xff0c;这里使用CLion进行学习。 0. C初识 0.1 第一个C程序 编写一个C程序总共分为4个步骤 创建项目创建文件编写代码运行程序 #include <iostream>int main() {using namespace std;cout…

新手如何快速安装电脑监控软件?

越来越多的管理者选择使用电脑监控软件&#xff0c;许多新手不知道具体怎样安装&#xff0c;本期将为大家介绍下具体的安装流程。 电脑监控软件购买之后&#xff0c;会提供网址和账号密码&#xff0c;登录后需要先添加员工信息&#xff0c;有三种方法&#xff1a; &#xff0…

Android性能优化(bin启动优化)

我们平时会在android里面写个bin程序来干点活&#xff0c;但是有时候我们会发现很奇怪的现象&#xff0c;我明明很早就启动这个bin了&#xff0c;但是过了很久bin程序的main函数才被调用~。这个是为啥呢&#xff1f;主要有2个原因&#xff1a; 一.bin程序依赖的so库太多&#…

steam搬砖项目,csgo游戏搬砖熟练操作后,可以月入过万~

科思创业汇 大家好&#xff0c;这里是科思创业汇&#xff0c;一个轻资产创业孵化平台。赚钱的方式有很多种&#xff0c;我希望在科思创业汇能够给你带来最快乐的那一种&#xff01; 网上创业创造了一批赚钱的人&#xff0c;年收入从几十万到几百万不等&#xff0c;营业额从几…

基于springboot房屋租赁管理系统

开发工具&#xff1a;IDEA&#xff0c;jdk1.8 服务器&#xff1a;tomcat9.0 数据库&#xff1a;mysql5.7 前端&#xff1a;jsp、bootstrap 技术&#xff1a; springbootmybatis-plus 系统主要分前台和后台&#xff0c;分租客、房东、管理员三个角色 系统功能介绍说明&…

nodejs 高级编程-通信

一、通信基本原理 通信必要条件 主机之间需要有传输介质主机上必须有网卡设备主机之间需要协商网络速率 二、网络通讯方式 常见的通讯方式 交换机通讯路由器通讯 如何建立多台主机互连&#xff1f; 如何定位局域网中的其他主机&#xff1f; 通过Mac地址来唯一标识一台主机…

hcip笔记---ospf的LSA限制和不规则区域

有关ACL&#xff1a;例如&#xff1a;1.1.1.0 0.0.0.255这个网段以及后面跟随的通配符&#xff0c;通配符和反掩码长得很像&#xff0c;同时都是用0标识不可变&#xff0c;1标识可变&#xff0c;但反掩码里的1和0必须连续出现&#xff0c;而通配符则不需要遵循这个规则&#xf…

深入思考Sui的独特性如何构建出跨时代的产品

近日&#xff0c;我们与Mysten Labs产品总监Janet Wu面对面探讨了Web3的产品开发过程&#xff0c;了解了她对Sui上最激动人心的产品用例的看法&#xff0c;以及她对该行业未来的展望。 您能简单介绍一下在Mysten Labs担任产品总监意味着什么吗&#xff1f; 对我而言&#xff…

0基础学习VR全景平台篇 第59篇:专业版功能-跨账号复制

功能位置示意 一、本功能将用在哪里&#xff1f; 跨账号复制&#xff0c;是指将本账号中已发布的VR漫游作品一键复制给其他账号使用。 复制成功后&#xff0c;其他账号中也会生成同样的作品以及获得相关的全景、音频、图片、视频等素材。 并且原作品和复制品可以独立编辑&am…

K8s为什么需要calico? calico 原理深入理解.

文章目录 为什么需要calico&#xff1f;-网络插件”千千万”&#xff0c;为何k8s要用calicocalico的架构calico Pod 跨node通信tunl0 的作用&#xff1f;为什么所有pod的默认网关都是169.254.1.1 &#xff1f;什么是ARP 代理&#xff1f;jksj BGP模式的calico工作原理calico BG…

vue3 报错解决:找不到模块‘xxx.vue’或其相应的类型声明。(Vue 3 can not find module)

src下面建立一个xx.d.ts的文件 declare module *.vue {import { ComponentOptions } from vueconst componentOptions: ComponentOptionsexport default componentOptions }

使用3DEXPERIENCE平台有效管理设计变更,随时处理问题

临时处理设计变更&#xff0c;电脑却不在身边怎么办?借助3DEXPERIENCE平台&#xff0c;我们可以轻松的用手机打开模型&#xff0c;还能够随时随地查看其他人的工作进展并进行审批。 在工作过程中&#xff0c;工程师小A发现&#xff0c;装配体的零件强度有点弱&#xff0c;小A…

HTML 第二部分 (前端学习)

由于&#xff0c;HTML的部分实在是太多了&#xff0c;第一部分&#xff0c;还没学一半&#xff0c;就已经抄了1w字。而且可能&#xff0c;真正用上的也比较少&#xff0c;更何况&#xff0c;一直坚持&#xff0c;一个不落的学下去&#xff0c;也存在一点注意力分散的困难&#…

ATM自助取款系统(Java)

文章目录 完整程序1. 课程设计目的2. 课程设计任务与要求3. 课程设计说明书3.1 需求分析3.1.1 功能分析3.1.2 性能要求分析 3.2 概要设计3.2.1 功能模块图&#xff0c;如图1。 3.3 详细设计3.3.1 实体类的设计3.3.2 实现数据库处理 3.4 主要程序功能流程图 4. 课程设计成果4.1 …

Linux——进程信号的发送

目录 一.信号发送的概念 首先来讲几个发送术语&#xff1a; 它有三种情况&#xff1a; 注意&#xff1a; 二.信号在内核中的表示示意图 三.信号捕捉 所以总结一下&#xff1a; 此时&#xff0c;会出现这样一个疑问&#xff1a;操作系统是如何得知现在被执行的进程是用户态…

DuDuTalk:4G录音工牌如何帮企业实现,线下服务的远程实时监督?

数字化时代&#xff0c;企业越来越重视客户体验。而线下服务是企业的员工跟客户直接互动最重要的环节&#xff0c;这直接关乎到客户对品牌、产品、服务的最直观评价和反馈&#xff0c;最终影响客户的购买和成交。 但企业的线下服务场景往往又是企业数字化最薄弱的一环&#xf…

Qt,day4

闹钟 #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent) :QWidget(parent),ui(new Ui::Widget) {ui->setupUi(this);this->setWindowTitle("闹钟");this->setWindowIcon(QIcon("D:\\HQYJRJ\\QT\\day1\\…