3D车辆检测AP评价指标代码的理解

article/2025/8/8 9:55:48

课题研究的是单目3D车辆的识别,采用的目标检测网络是SMOKE,为了可以更好的定量评测训练模型的性能,需要使用到合理的评测指标,目前比较流行的评测指标是得到多组precision和recall值画出PR曲线,然后计算PR曲线下的面积得到AP。

关于更详细的原理的介绍已经有很多优秀的文章,本篇博客主要是结合Kitti数据集的官网代码以及自己的理解做的一些总结,如有理解不当请斧正。

基本概念的理解

IOU:IOU指的是两个检测框的交并比,也就是两个框的重合程度,数值越大代表两个框重合度越高。在计算AP的过程中,IOU的一个作用是作为判断目标是否被检测出来的阈值,如果detection框与groundtruth框的IOU大于一定的阈值(比如说0.7),那么就说明目标别检测出来了。

注意:对于绘制PR曲线来说,判断目标是否被检测出来有两个重要指标,一个是上面说的IOU,还有一个就是置信度,所以我理解IOU的作用就是做一个固定值的过滤,后续画PR曲线的precision和recall值对其实是根据置信度的不断调整获得的。

TP,FP,FN:T和F代表true和false,P和N代表positive和negative,所以

  • TP:检出目标是positive,groundtruth确实是true,说明目标被正确的检出
  • FP:检出目标是positive,groundtruth却是false,说明误检出了不存在的目标,误检
  • FN:检出目标是negative,groundtruth是true,说明本该被检出的没有检出,漏检

precision和recall:precision和recall的定义如下

那么在程序中我们应该如何计算TP、FP、FN呢?

 

TP、FP:

遍历detection的每张image中的每个lable,比如image1中的label_b和lable_c,然后对于每一个lable,再遍历groundtruth中image1的所有lable,查看是否匹配,如果成功匹配到一个,那么TP+1,如果groundtruth中都没有匹配的,那么说明detection中此lable为误捡,则FP+1.

FN:

遍历groundtruth的每张image中的每个lable,比如image1中的label_A和lable_B,再去除刚才TP+1的时候匹配上的,如果还有剩下的,那么代表此lable漏检了,FN+1.

 可以看到precision是针对detection,也就是所有detection检测的目标有多少是正确的,也叫作查准率;recall则是针对groundtruth,也就是groundtruth中所有的目标有多少是被检测出来的,也叫作查全率。

从定义可以看出,这两个是指标是矛盾的,要保证其中一个效果好,另一个必然保证不了效果。比如:如果希望precision(查准率)高,那么我就提高检测的阈值,我不用管没检测出来的,我只要保证我检测出来的就是真的目标,那么precision值肯定高,但是因为好多并没有检测出来,漏检很多,recall值肯定不高;如果希望recall(查全率)高,那么我就降低检测阈值,不用管误捡的状况,只要保证尽可能的把目标检测出来,那么recall值肯定高,但是因为误捡很多,precision得不到保证。

所以针对这一对天然矛盾的指标,取任何一方都不合理,因此使用更加合理的AP,也就是取不同的置信度得到若干对precision和recall,将其画在y轴为precision和x轴为recall的坐标系中构成PR曲线,围成的面积就是AP值。

为了保证AP值的统一,也需要统一其他的参数:IOU阈值和置信度数量,在SMOKE论文中

可以看到使用的IOU阈值是0.7 计算AP采用40个点 

经过以上相关概念的分析,我们可以想一下,如果我们面对着groundtruth的labels和detection的labels,我们得到AP大体的思路是如何的?

如果我们想要得到AP,那我们必须根据40个置信度(假设采用40个点)来计算precision和recall,计算precision和recall可以依次比较两帧groundtruth和detection的结果得到TP,FP,FN,那么如何得到40个置信度呢? 也就是说我们还需要提前遍历一遍所有的labels得到所有的TP结果和它们的置信度,从这些置信度里面选择40个,经过这么一分析,我们可以大体得到代码的总体思路:

注意点:这个需要注意一个概念,就是如何定义“匹配”,这个需要考虑两个指标,一个是IOU,另一个就是置信度,在AP流程中,需要计算两次TP,第一次定义匹配是找满足最小IOU后找置信度最大的,也就是获取置信度列表的时候;第二次计算TP定义匹配则是寻找最大IOU的,也就是最终画曲线所需要的TP。所以整个流程计算了两次TP,一次是为了获取置信度列表,另一次是为了画AP曲线。

 下面主要结合代码来分析

  • 数据预处理

数据预处理的目的是将labels中的数据读入到程序的vector中,当然在读入的过程中可以针对自己的需求进行操作,比如我只比较Car类别,那么在读入的过程中我可以舍弃掉其他的类别以及dont care数据这一部分是在loadGroundtruth和loadDetections两个函数中

if (fscanf(fp, "%s %lf %d %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf",str, &g.truncation, &g.occlusion, &g.box.alpha,&g.box.x1,   &g.box.y1,     &g.box.x2,    &g.box.y2,&g.h,      &g.w,        &g.l,       &g.t1,&g.t2,      &g.t3,        &g.ry )==15 && !strcasecmp(str, "Car")) {   // 只需要Car类型

其中groundtruth和detection的label结果都存放在事先定义好的结构体tGroundtruth和tDetection中,下一个预处理的过程是在 cleanData这个函数里,这个函数所做的操作是是这样的:将单帧图像groundtruth中的所有label和detection中的所有label进行遍历来确定是不是应该ignore,而是否ignore的判断标准有class,occlusion,truncation和height,这里要说一下,kitti评价的时候会按照遮挡和截断情况将场景分为简单,中等,困难三个场景,考虑到自己的场景比较简单,因此修改代码为只评测Car这个类别,并且不对场景难度进行区分,同时因为训练集没有标定2D框,所以不考虑height这个类别(这些的修改都在阈值定义的数据结构里),进行判断的代码如下:

bool ignore = false;if(gt[i].occlusion>MAX_OCCLUSION[difficulty] || gt[i].truncation>MAX_TRUNCATION[difficulty] || height<=MIN_HEIGHT[difficulty])ignore = true;
if(!strcasecmp(det[i].box.type.c_str(), CLASS_NAMES[current_class].c_str()))valid_class = 1;elsevalid_class = -1;int32_t height = fabs(det[i].box.y1 - det[i].box.y2);// set ignored vector for detections   height不低于一定范围并且class正确if(height<MIN_HEIGHT[difficulty])ignored_det.push_back(1);else if(valid_class==1)ignored_det.push_back(0);elseignored_det.push_back(-1);
  • 置信度列表

这里再说明一下,为什么需要置信度列表?

因为为了更合理的评价结果,需要画AP曲线,也就是需要计算不同的precision和recall点对,而每一对不同的precision和recall,则对应着一个置信度阈值,比如说置信度阈值提高,那么groundtruth和detection中匹配的lable肯定会变少;如果我们把置信度阈值放宽,那么匹配的lable就会变多,也就是将目标检出的概率变大了。

这里获得的置信度的总数理想情况下应该是40,因为这是事先定好的,那么如何选择40个置信度呢?当然可以从0%到100%进行40等分,然后计算得到40个precision和recall点对,但是这种方法计算起来比较“繁琐”,这是因为对于每一个置信度,都需要去计算一遍TP,FP,FN,虽然理论上是这样的,不过代码中的方式更简洁一点而且也更有意思。

正常的思路是这样的:选择一个置信度---计算recall和precision---画图

而代码中思路是这样的:先确定recall值为i/40(i从1取到40)---找到每个recall对应的置信度---用这个置信度去计算precision---画图

计算先确定recall值再去确定置信度,比如说当前recall值为7/40,那么对应的置信度是多少呢?这就需要找到recall值与TP、FN与置信度之间的联系了,这也是代码中置信度列表选取的核心!

recall的计算公式是 recall = TP/(TP + FN),分母是groundtruth中非ignore的目标的个数,可以理解为所有待检测的目标,这个值在预处理数据的时候就被计算出来了,在程序中是n_gt,这样recall的值就取决于TP的值,而TP的值则取决于置信度的取值,置信度取值越高,TP值越小(这就是前面说的目标是否被检测出来一个取决于IOU阈值,另一个取决于置信度)。

TP值和置信度取值的关系可以看下图:

  图解:当置信度取0.98以上的时候,由于所有detection的lable的置信度都达不到,那么其实就是所有的目标没有检测出来,因此给置信度过滤掉了,当我们把置信度降为0.98的时候,一个匹配上了,那么TP值就是1;当置信度再降为0.95的时候,有两个lable被判定为检出并匹配上,所以TP值就是2和3对应的置信度都是0.95;为什么TP值只能到60呢?这是因为在判断检出的时候还需要考虑IOU,及时置信度为0就算检出,但是因为有最低IOU的限制,也不能匹配上。

这样我们就得到TP值和置信度的关系了,那么如何获取到置信度列表呢?就是第一次计算TP的时候,记录每一个判定为“匹配”的lable的置信度,最后降序排序。注意这时候判定groundtruth和detection“匹配”,也就是TP+1的标准是:符合IOU最低标准的条件下,置信度最高的那个。

这时候有了置信度列表了,就可以按照刚才的思路进行了:加入recall值取到7/40,根据recall值的公式,得到对应的TP值,然后根据上面的表找到最接近的TP值对应的那一列的置信度记录下来,这里还要注意,recall值的取值是从1/40到40/40的,但是由于TP值取不到n_gt值,所以recall值列表后面可能对应的置信度都是0,表现在AP曲线上就是曲线与recall轴重合了,其实也很好理解,TP值不够大,说明模型效果不太好,AP曲线快速与recall轴重合,面积是0,AP值就比较小,恰好说明模型效果不太好。

下面是根据降序排序的所有置信度取置信度列表:

for(int32_t i=0; i<v.size(); i++){   //此时的置信度已经按照降序排序了// check if right-hand-side recall with respect to current recall is close than left-hand-side one// in this case, skip the current detection scoredouble l_recall, r_recall, recall;l_recall = (double)(i+1)/n_groundtruth;if(i<(v.size()-1))r_recall = (double)(i+2)/n_groundtruth;elser_recall = l_recall;// 也就是说只要current_call走的比较快,那么就不添加v了,直到v的索引跟上之后就把当前的v[i]值加入进去if( (r_recall-current_recall) < (current_recall-l_recall) && i<(v.size()-1))continue;// left recall is the best approximation, so use this and goto next recall step for approximationrecall = l_recall;// the next recall step was reachedt.push_back(v[i]);current_recall += 1.0/(N_SAMPLE_PTS-1.0);}
  • 计算P R

通过前面得到了40个置信度作为置信度列表(其实后面因为取不到,所以40个置信度后面好多都是0值)这一步就是取置信度列表中的值,计算模型对于测试样本的precision和recall(而且recall值不用计算了,因为recall值在一开始被人为定义为i/40了,是不是很方便),实现过程在computeStatistics函数中

首先对于计算FN和TP,我们需要遍历groundtruth所有的label,找在对应帧中label的状况,如果找到符合条件的,那么就tp++,如果没有找到,就说明漏检了,则fn++

if(  !compute_fp && overlap>MIN_OVERLAP[metric][current_class] && det[j].thresh>valid_detection){det_idx         = j;valid_detection = det[j].thresh;
}
else if(compute_fp && overlap>MIN_OVERLAP[metric][current_class] && (overlap>max_overlap || assigned_ignored_det) && ignored_det[j]==0){max_overlap     = overlap;det_idx         = j;valid_detection = 1;assigned_ignored_det = false;
}
else if(compute_fp && overlap>MIN_OVERLAP[metric][current_class] && valid_detection==NO_DETECTION && ignored_det[j]==1){det_idx              = j;valid_detection      = 1;assigned_ignored_det = true;
}

上面是寻找匹配detection label的判断标准,其中compute_fp代表是否计算fp,第一次调用这个函数也就是为了得到置信度列表的时候,我们不需要计算fp,因为我们的目的是得到所有TP样本的置信度,而第二次调用的时候为了计算precision和recall,就需要置compute_fp为true了。

可以看到如果不计算fp的话,寻找最佳的匹配label在满足最低overlap阈值的情况下主要是寻找置信度最大的,而在计算fp的时候则主要考虑的是overlap。

下面主要是计算fn和tp

if(valid_detection==NO_DETECTION && ignored_gt[i]==0) {stat.fn++;
}else if(valid_detection!=NO_DETECTION && (ignored_gt[i]==1 || ignored_det[det_idx]==1)){assigned_detection[det_idx] = true;
}else if(valid_detection!=NO_DETECTION){stat.tp++;stat.v.push_back(det[det_idx].thresh);// clean upassigned_detection[det_idx] = true;
}

然后下面计算fn,fn代表漏检,因此需要先遍历detection,然后在groundtruth里面寻找

 for(int32_t i=0; i<det.size(); i++){  //遍历detection labels// count false positives if required (height smaller than required is ignored (ignored_det==1)// FP的意思是对于groundtruth没有的label,detection却检测出来了if(!(assigned_detection[i] || ignored_det[i]==-1 || ignored_det[i]==1 || ignored_threshold[i]))stat.fp++;}
  • PR曲线和计算AP

其实到上一步大部分的工作已经做完了,下面就是利用计算的TP,FP,FN画PR曲线和计算AP,画曲线就不说了,不过在计算AP的时候,需要事先对precisions做一个预处理,因为在我看来,之前recall选定固定值之后,AP的计算其实就是precision×(1/40)并求和,而为了画PR曲线,我想将precision按照降序排序,然后画图计算,可是源代码中给定的预处理方式是:

for (int32_t i=0; i<thresholds.size(); i++){precision[i] = *max_element(precision.begin()+i, precision.end());if(compute_aos)aos[i] = *max_element(aos.begin()+i, aos.end());
}

对于此我的理解是这种方法是为了避免曲线出现摆动,保证曲线是下降的,而AP是一个评价指标,只要所有模型所采用的评价指标的计算方法是一样的就可以,不必拘泥于中间的一些细节问题。


http://chatgpt.dhexx.cn/article/fpoSnFi8.shtml

相关文章

根据车辆型号自动生成车辆编号

开发工具与关键技术&#xff1a;Visual Studio 2015 linq 正则 作者&#xff1a;孙水兵 撰写时间&#xff1a;2019年6月26一、 功能 根据不同的类型的车辆型号&#xff0c;生成以车辆型号开头的车辆编号。 二、 达到的效果 用户选择了车辆型号之后&#xff0c;将对应的车辆编…

【路径规划】基于遗传算法求解多车多类型车辆的车辆路径优化问题附matlab代码

1 内容介绍 多车辆多路线的交通路线优化涉及到排序问题,是一个N-P难题,高效精确的算法存在的可能性不大.提出了基于遗传算法的求解方法,给出了实例来证明如何利用遗传算法解决多车辆多路线的优化问题.结果证明,一般情况下利用遗传算法对于多车辆多路线的行车路线优化能得到一组…

机动车登记信息代码

原链接&#xff1a;机动车登记信息代 搜索结果本栏目用于收集和整理行业相关标准&#xff0c;如机械行业&#xff0c;化工行业等。http://www.gb99.cn/e/search/result/?searchid76243针对其中第七项车辆信息牌照代码如下&#xff1a;

利用低代码平台进行车辆管理,为交通行业添砖加瓦

概要&#xff1a;本文介绍了交通行业车辆管理的重要性&#xff0c;并详细阐述了基于低代码平台设计的车辆管理系统的优势。通过快速开发、易于维护、增加灵活性、提高数据可靠性、降低成本以及实时监控等多个方面&#xff0c;这种车辆管理系统可以帮助企业提高效率和降低成本&a…

(c++课程设计)简单车辆管理系统(有五种类型的车辆)代码+报告

关于这个课程设计 &#xff0c;差点没把我头发愁没。 好了其实本质还是东拼西凑&#xff0c;编程能力没怎么长进&#xff0c;花里胡哨的东西却学了不少&#xff08;不是&#xff09; 万恶的学院&#xff0c;虽然要求三人一组&#xff0c;但是却分一二三类&#xff0c;三个人代…

什么是车辆识别代码(VIN)

车辆识别代码(VIN),VIN是英文Vehicle Identification Number(车辆识别码)的缩写。因为ASE标准规定:VIN码由17位字符组成,所以俗称十七位码。正确解读VIN码,对于我们正确地识别车型,以致进行正确地诊断和维修都是十分重要的。车辆识别代码根据国家车辆管理标准确定,包…

Eigen学习笔记1:在VS2015下Eigen(矩阵变换)的配置

一、Eigen简介 Eigen是一个高层次的C 库&#xff0c;有效支持线性代数&#xff0c;矩阵和矢量运算&#xff0c;数值分析及其相关的算法。 Eigen适用范围广&#xff0c;支持包括固定大小、任意大小的所有矩阵操作&#xff0c;甚至是稀疏矩阵&#xff1b;支持所有标准的数值类型&…

鲁鹏老师三维重建课程之单视图重建

配置Json环境 使用Jsoncpp包中的.cpp和 .h文件 解压上面下载的 Jsoncpp 文件&#xff0c;把 jsoncpp-src-0.5.0文件拷贝到工程目录下&#xff0c; 将 jsoncpp-src-0.5.0\jsoncpp-src-0.5.0\include\json 和 jsoncpp-src-0.5.0\jsoncpp-src-0.5.0\src\lib_json 目录里的文…

常用 Linux 软件汇总!很全,但不敢说最全

点击下方公众号「关注」和「星标」 回复“1024”获取独家整理的学习资料&#xff01; 音频 Airtime - Airtime 是一款用于调度和远程站点管理的开放广播软件 Ardour - 在 Linux 上录音&#xff0c;编辑&#xff0c;和混音 Audacious - 开源音频播放器&#xff0c;按你想要的方式…

RoadMap:面向自动驾驶视觉定位的轻量级语义地图(ICRA2021)

点击上方“3D视觉工坊”&#xff0c;选择“星标” 干货第一时间送达 标题&#xff1a;RoadMap: A Light-Weight Semantic Map for Visual Localization towards Autonomous Driving 作者&#xff1a;Tong Qin, Yuxin Zheng, Tongqing Chen, Yilun Chen, and Qing Su 来源&#…

计算机图形学作业( 三):使用openGL画一个立方体,并实现平移、旋转和放缩变换

计算机图形学作业( 三):使用openGL画一个立方体,并实现平移、旋转和放缩变换 题目引入GLM库画立方体模型、观察和投影修改着色器立方体的顶点深度测试立方体变换平移旋转放缩渲染管线的理解代码题目 引入GLM库 利用 openGL 进行 3D 绘图需要用到大量的数学矩阵运算,而 Op…

一文掌握基于深度学习的自动驾驶小车开发(Pytorch实现,含完整数据和源码,树莓派+神经计算棒)

目录 一 . 基本介绍 二、模拟平台安装和基本使用 三、基于OpenCV的自动驾驶控制 3.1基于HSV空间的特定颜色区域提取 3.2基于canny算子的边缘轮廓提取 3.3感兴趣区域定位 3.4基于霍夫变换的线段检测 3.5动作控制&#xff1a;转向角 四、基于深度学习的自动驾驶控制 4.…

OpenGL坐标变换及其数学原理,两种摄像机交互模型(附源程序)

&#xfeff;&#xfeff; 实验平台&#xff1a;win7&#xff0c;VS2010 先上结果截图&#xff08;文章最后下载程序&#xff0c;解压后直接运行BIN文件夹下的EXE程序&#xff09;&#xff1a; a.鼠标拖拽旋转物体&#xff0c;类似于OGRE中的“OgreBites::CameraStyle::CS_ORB…

Python-WingIde各种调试方法

一、 本地从IDE启动文件调试 主要步骤:设置断点,F5开始调试 二、 本地从IDE外启动文件调试 1.) 从WingIDE的安装目录(默认C:\Program Files (x86)\Wing IDE 6.0)复制wingdbstub.py到被调试代码所在目录 2.) 代码中添加importwingdbstub 3.) IDE左下角设置如图 4.) 在…

图形处理单元(GPU)的演进

CPU 和 GPU 好久没有更新了&#xff0c;最近在阅读 CUDA 相关的一些论文&#xff0c;因为都是碎片化阅读&#xff0c;容易导致读过后&#xff0c;可能过一段时间又忘记掉&#xff0c;所以决定抽时间翻译翻译阅读的论文&#xff0c;一方面增强自己记忆&#xff0c;一方面与大家共…

图形学 光栅化 matlab 源代码

实验二&#xff1a;直线的光栅化算法 DDA Bresenham 实验三&#xff1a;圆的光栅化算法 编程实现两种中点画圆算法&#xff0c;第2种算法利用二阶差分方法&#xff1b; 实验四&#xff1a;多边形扫描转换算法 4.1对多边形扫描线填充算法进行简要描述&#xff0c;并给出多边形扫…

图形学入门合集1

Games101作业0 1虚拟机的使用 1.1虚拟机的安装 这里我们使用 Oracle VM VirtualBox 虚拟机。如果你使用 Windows 系统&#xff0c;你可以直接下载[链接](https://download.virtualbo%20%09%09x.org/virtualbox/6.1.4/VirtualBox-6.1.4-136177-Win.exe)&#xff0c;下载完成后…

Recorder︱图像特征检测及提取算法、基本属性、匹配方法

在做图像的研究&#xff0c;发现对图像本质、内核以及可以提取的特征方式一点儿都不懂&#xff0c;赶紧补补课。 . 一、图像常用属性 本节指的是一般来说&#xff0c;图像处理的一些角度&#xff0c;也是根据一些美图软件最为关注的一些图像属性&#xff1a; 基本属性&#…

视觉SLAM十四讲学习笔记-第三讲-相似、仿射、射影变换和eigen程序、可视化演示

专栏系列文章如下&#xff1a; 视觉SLAM十四讲学习笔记-第一讲_goldqiu的博客-CSDN博客 视觉SLAM十四讲学习笔记-第二讲-初识SLAM_goldqiu的博客-CSDN博客 视觉SLAM十四讲学习笔记-第二讲-开发环境搭建_goldqiu的博客-CSDN博客 视觉SLAM十四讲学习笔记-第三讲-旋转矩阵和E…

多视图几何三维重建实战系列之MVSNet

点击上方“3D视觉工坊”&#xff0c;选择“星标” 干货第一时间送达 1. 概述 MVS是一种从具有一定重叠度的多视图视角中恢复场景的稠密结构的技术&#xff0c;传统方法利用几何、光学一致性构造匹配代价&#xff0c;进行匹配代价累积&#xff0c;再估计深度值。虽然传统方法有较…