长沙市网站建设_网站建设公司_JavaScript_seo优化
2026/1/16 9:59:12 网站建设 项目流程

一、前言:YOLOv5后处理到底要做什么?

你复习的这份笔记核心是解决「模型输出→实际检测框」的转换问题:

  • YOLOv5模型的输出是一个[n, 85]的张量(n是预测框数量,85=中心坐标cx/cy + 宽高w/h + 目标置信度obj + 80类分类概率);
  • 后处理的核心任务:
    1. 解码:把[cx, cy, w, h]转换成实际的检测框坐标[left, top, right, bottom],并过滤低置信度的框;
    2. NMS(非极大值抑制):去掉同一目标的重复检测框(比如一个人被多个框框住,只留置信度最高的);
  • 加速目标:CPU版后处理在检测框数量大时速度慢,用CUDA核函数并行处理每个预测框,把后处理时间从“毫秒级”压到“微秒级”。

二、核心概念:先搞懂YOLOv5后处理的基础规则

用「招聘筛选」比喻帮你理解:

后处理步骤技术逻辑生活化比喻
解码1. 计算置信度(obj×类概率)
2. 过滤置信度低于阈值(比如0.25)的框
3. 把cx/cy/w/h转成左上/右下坐标
1. 看简历的“综合评分”(obj=基础分,类概率=岗位匹配分)
2. 淘汰综合分低于60分的候选人
3. 把候选人的“模糊地址”转成“精确地址”
NMS1. 按置信度排序
2. 去掉和高置信度框重叠度(IoU)超过阈值(比如0.45)的重复框
1. 按综合分从高到低排候选人
2. 淘汰和第一名重复的候选人(比如同一个人投了多个岗位)

三、后处理案例拆解:CPU版→GPU版

笔记里先讲CPU版(易理解、打基础),再讲GPU版(并行加速),我们逐段拆解。

3.1 先明确:YOLOv5输出的格式约定

模型输出的每个预测框是85个数值,顺序是:
[cx, cy, width, height, objectness, class1, class2, ..., class80]

  • objectness:这个框里“有目标”的概率(0~1);
  • class1~class80:这个框属于80类(COCO数据集)中某一类的概率;
  • 最终置信度 = objectness × 最高类概率(比如obj=0.8,类概率=0.9→置信度=0.72)。

3.2 CPU_decode:后处理的“手工筛选版”

CPU版是后处理的“基础实现”,核心是「逐框处理+有序去重」,先看代码结构,再拆解关键逻辑。

3.2.1 核心代码
// 先定义Box结构体:存检测框的坐标、置信度、类别structBox{floatleft,top,right,bottom,confidence;floatlabel;Box(floatl,floatt,floatr,floatb,floatc,floatlb):left(l),top(t),right(r),bottom(b),confidence(c),label(lb){}};vector<Box>cpu_decode(float*predict,introws,intcols,floatconfidence_threshold=0.25f,floatnms_threshold=0.45f){vector<Box>boxes;intnum_classes=cols-5;// 85-5=80类// 第一步:解码+过滤低置信度框for(inti=0;i<rows;++i){float*pitem=predict+i*cols;// 当前预测框的首地址floatobjness=pitem[4];// 目标置信度if(objness<confidence_threshold)// 过滤低obj框,减少后续计算continue;// 找概率最高的类别float*pclass=pitem+5;intlabel=std::max_element(pclass,pclass+num_classes)-pclass;floatprob=pclass[label];floatconfidence=prob*objness;// 最终置信度if(confidence<confidence_threshold)// 二次过滤continue;// 把cx/cy/w/h转成左上/右下坐标floatcx=pitem[0];floatcy=pitem[1];floatwidth=pitem[2];floatheight=pitem[3];floatleft=cx-width*0.5;floattop=cy-height*0.5;floatright=cx+width*0.5;floatbottom=cy+height*0.5;boxes.emplace_back(left,top,right,bottom,confidence,(float)label);}// 第二步:NMS去重// 1. 按置信度降序排序std::sort(boxes.begin(),boxes.end(),[](Box&a,Box&b){returna.confidence>b.confidence;});std::vector<bool>remove_flags(boxes.size(),false);// 标记是否要移除std::vector<Box>box_result;box_result.reserve(boxes.size());// 预分配内存,提升性能// 定义IoU计算函数:计算两个框的重叠度autoiou=[](constBox&a,constBox&b){floatcross_left=std::max(a.left,b.left);floatcross_top=std::max(a.top,b.top);floatcross_right=std::min(a.right,b.right);floatcross_bottom=std::min(a.bottom,b.bottom);floatcross_area=std::max(0.0f,cross_right-cross_left)*std::max(0.0f,cross_bottom-cross_top);// 重叠面积floata_area=(a.right-a.left)*(a.bottom-a.top);floatb_area=(b.right-b.left)*(b.bottom-b.top);floatunion_area=a_area+b_area-cross_area;// 并集面积returnunion_area==0?0:cross_area/union_area;// IoU=重叠面积/并集面积};// 2. 遍历框,标记重复框for(inti=0;i<boxes.size();++i){if(remove_flags[i])continue;// 已标记移除,跳过auto&ibox=boxes[i];box_result.emplace_back(ibox);// 保留当前最高置信度框for(intj=i+1;j<boxes.size();++j){if(remove_flags[j])continue;auto&jbox=boxes[j];if(ibox.label==jbox.label&&iou(ibox,jbox)>=nms_threshold){remove_flags[j]=true;// 同类+高IoU,标记移除}}}returnbox_result;}
3.2.2 关键解读(新手必懂)
  1. 性能优化点(笔记重点)
    • 提前过滤:先过滤objness低的框,避免后续计算类别概率、坐标转换(“先筛掉明显不合格的,再细看”);
    • 预分配内存:box_result.reserve(boxes.size())——避免vector频繁扩容,提升速度;
    • 标志位去重:用remove_flags标记要移除的框,而非直接删除(删除会导致数组移位,效率低)。
  2. 核心逻辑总结
    • 解码:逐框算置信度→过滤→转坐标;
    • NMS:排序→逐框对比IoU→标记重复框→保留未标记的框。

3.3 GPU_decode:后处理的“流水线工厂版”

GPU版的核心是「并行处理每个预测框」——启动和预测框数量相等的线程,每个线程处理一个框的解码,再用并行NMS去重。

3.3.1 第一步:decode_kernel(解码核函数)

每个线程负责一个预测框的解码,核心解决“并发写入结果”的问题(用原子操作atomicAdd)。

核心代码
// 定义常量:每个检测框保存的元素数(left,top,right,bottom,confidence,label,keepflag)constintNUM_BOX_ELEMENT=7;constintMAX_OBJECTS=1000;// 最大检测框数量static__global__voiddecode_kernel(float*predict,intnum_bboxes,intnum_classes,floatconfidence_threshold,float*invert_affine_matrix,float*parray,intmax_objects,intNUM_BOX_ELEMENT){// 1. 计算当前线程的全局索引(对应第几个预测框)intposition=blockDim.x*blockIdx.x+threadIdx.x;if(position>=num_bboxes)return;// 超出预测框数量,退出// 2. 取当前预测框的首地址float*pitem=predict+(5+num_classes)*position;floatobjectness=pitem[4];if(objectness<confidence_threshold)return;// 3. 找概率最高的类别float*class_confidence=pitem+5;floatconfidence=*class_confidence++;intlabel=0;for(inti=1;i<num_classes;++i,++class_confidence){if(*class_confidence>confidence){confidence=*class_confidence;label=i;}}// 4. 计算最终置信度,二次过滤confidence*=objectness;if(confidence<confidence_threshold)return;// 5. 原子操作:获取当前框的结果索引(解决并发写入冲突)intindex=atomicAdd(parray,1);// parray[0]存框的数量,原子加1返回旧值if(index>=max_objects)return;// 超过最大框数,退出// 6. 转换坐标floatcx=*pitem++;floatcy=*pitem++;floatwidth=*pitem++;floatheight=*pitem++;floatleft=cx-width*0.5f;floattop=cy-height*0.5f;floatright=cx+width*0.5f;floatbottom=cy+height*0.5f;// 7. 保存结果:parray[1 + index*7]开始存当前框的信息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=保留,0=移除}
关键解读(新手必懂)
  1. 线程索引position = blockDim.x * blockIdx.x + threadIdx.x——每个线程对应一个预测框,比如num_bboxes=2000,就启动2000个线程;
  2. 原子操作atomicAdd
    • parray是GPU上的数组,格式为[count, box1, box2, ...](第一个元素存有效框数量,后面存框信息);
    • atomicAdd(parray, 1):多个线程同时写parray[0]时,保证“加1”操作不冲突(比如线程A和B同时加1,结果是2而不是1);
    • index是当前框在结果数组中的位置,比如parray[0]初始是0,线程A执行后index=0,parray[0]变成1;线程B执行后index=1,parray[0]变成2;
  3. 和CPU版的区别
    • 不用逐框循环(CPU是for循环,GPU是并行线程);
    • 结果保存到连续数组,而非vector(GPU上用数组更高效);
    • 暂时不做NMS,先解码保存所有有效框。
3.3.2 第二步:fast_nms_kernel(并行NMS核函数)

GPU版NMS不用排序(CPU版要排序),直接并行对比每个框和其他框的IoU,核心是“快但极端情况可能少框”。

核心代码
// 先定义IoU计算函数(GPU版)__device__floatbox_iou(floatleft1,floattop1,floatright1,floatbottom1,floatleft2,floattop2,floatright2,floatbottom2){floatcross_left=max(left1,left2);floatcross_top=max(top1,top2);floatcross_right=min(right1,right2);floatcross_bottom=min(bottom1,bottom2);floatcross_area=max(0.0f,cross_right-cross_left)*max(0.0f,cross_bottom-cross_top);floatarea1=max(0.0f,right1-left1)*max(0.0f,bottom1-top1);floatarea2=max(0.0f,right2-left2)*max(0.0f,bottom2-top2);floatunion_area=area1+area2-cross_area;returnunion_area==0?0:cross_area/union_area;}static__global__voidfast_nms_kernel(float*bboxes,intmax_objects,floatthreshold,intNUM_BOX_ELEMENT){// 1. 计算当前线程索引(对应第几个有效框)intposition=blockDim.x*blockIdx.x+threadIdx.x;intcount=min((int)*bboxes,max_objects);// 有效框数量if(position>=count)return;// 2. 取当前框的信息float*pcurrent=bboxes+1+position*NUM_BOX_ELEMENT;for(inti=0;i<count;++i){float*pitem=bboxes+1+i*NUM_BOX_ELEMENT;if(i==position||pcurrent[5]!=pitem[5])continue;// 跳过自己/不同类的框// 3. 对比置信度:如果其他框置信度更高,计算IoUif(pitem[4]>=pcurrent[4]){if(pitem[4]==pcurrent[4]&&i<position)continue;// 置信度相同,保留索引小的floatiou=box_iou(pcurrent[0],pcurrent[1],pcurrent[2],pcurrent[3],pitem[0],pitem[1],pitem[2],pitem[3]);if(iou>threshold){pcurrent[6]=0;// 标记为移除return;}}}}
关键解读(新手必懂)
  1. fast NMS的核心逻辑
    • 每个线程处理一个有效框,对比它和所有其他框;
    • 如果存在“同类别+置信度更高+IoU超阈值”的框,就标记当前框为移除(pcurrent[6]=0);
    • 不用排序,直接并行对比,速度比CPU版快10倍以上;
  2. 为什么极端情况少框?
    • 比如有3个重叠框,置信度都是0.8,线程处理顺序不同可能导致全部被标记移除;
    • 实测中这种情况极少,不影响实际使用;
  3. mAP测试要用人CPU NMS的原因
    • mAP(平均精度)需要精确的框筛选结果,GPU版fast NMS的“少量漏框”会影响精度计算;
    • 实际部署时用GPU版(快),测试精度时用CPU版(准)。
3.3.3 GPU_decode的完整流程
// 伪代码:GPU后处理的整体调用vector<Box>gpu_decode(float*predict_cpu,introws,intcols){// 1. CPU→GPU拷贝模型输出float*predict_gpu=nullptr;checkRuntime(cudaMalloc(&predict_gpu,rows*cols*sizeof(float)));checkRuntime(cudaMemcpy(predict_gpu,predict_cpu,rows*cols*sizeof(float),cudaMemcpyHostToDevice));// 2. GPU分配结果数组([count, box1, box2,...])float*parray_gpu=nullptr;intparray_size=1+MAX_OBJECTS*NUM_BOX_ELEMENT;checkRuntime(cudaMalloc(&parray_gpu,parray_size*sizeof(float)));checkRuntime(cudaMemset(parray_gpu,0,parray_size*sizeof(float)));// 初始化count=0// 3. 启动解码核函数dim3block_size(256);// 每个block256个线程dim3grid_size((rows+255)/256);// 向上取整decode_kernel<<<grid_size,block_size>>>(predict_gpu,rows,cols-5,0.25f,nullptr,parray_gpu,MAX_OBJECTS,NUM_BOX_ELEMENT);// 4. 启动fast NMS核函数intcount=min((int)*parray_gpu,MAX_OBJECTS);grid_size=(count+255)/256;fast_nms_kernel<<<grid_size,block_size>>>(parray_gpu,MAX_OBJECTS,0.45f,NUM_BOX_ELEMENT);// 5. GPU→CPU拷贝结果,解析成Box结构体float*parray_cpu=newfloat[parray_size];checkRuntime(cudaMemcpy(parray_cpu,parray_gpu,parray_size*sizeof(float),cudaMemcpyDeviceToHost));vector<Box>result;count=min((int)parray_cpu[0],MAX_OBJECTS);for(inti=0;i<count;++i){float*pbox=parray_cpu+1+i*NUM_BOX_ELEMENT;if(pbox[6]==1){// 保留的框result.emplace_back(pbox[0],pbox[1],pbox[2],pbox[3],pbox[4],pbox[5]);}}// 6. 释放内存checkRuntime(cudaFree(predict_gpu));checkRuntime(cudaFree(parray_gpu));delete[]parray_cpu;returnresult;}

四、补充知识:笔记里的实战技巧

  1. 变量控制法(调试神器)
    • 用PyTorch推理后,把输出转成numpy→tobytes→保存到文件;
    • C++读取文件数据做后处理,不用搭TensorRT环境就能调试,快速对比CPU/GPU结果是否一致;
  2. GPU后处理的性能优化
    • 线程块大小设为32的倍数(比如256),符合GPU warp(32线程)的执行规则;
    • 尽量减少GPU和CPU之间的数据拷贝(解码、NMS都在GPU上做,最后只拷贝结果);
  3. fast NMS的取舍
    • 部署优先用GPU fast NMS(快),精度测试用CPU标准NMS(准);
    • 极端情况漏框的问题,可通过调整NMS阈值(比如从0.45降到0.4)缓解。

      图2-3 PyTorch效果

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

五、总结:核心要点回顾

  1. YOLOv5后处理分两步:解码(过滤+转坐标)+ NMS(去重),CPU版逐框处理,GPU版并行处理每个框;
  2. GPU解码的核心是atomicAdd:解决多线程并发写入结果的冲突问题;
  3. GPU NMS用fast NMS:不用排序,并行对比IoU,速度快但极端情况可能少框,部署时性价比极高。

这份案例是YOLOv5 TensorRT部署的核心环节——把预处理(之前的warpAffine)和后处理都放到GPU上,实现“模型推理+前后处理”全GPU加速,这也是高性能部署的关键。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询