☰
目标检测评估:FPPI曲线绘制原理与代码实现详解
2026/10/8 17:30:44 网站建设 项目流程

简介:面向目标检测与行人检测场景,这里提供一套绘制miss rate versus false positives per image(FPPI)曲线的MATLAB代码包,适合研究人员、算法工程师及学生用于评估检测器在不同误检率下的漏检性能。压缩包内含10840个文件,以txt格式的检测结果或标注文件为主(约1.07万个),另有137个vbb视频标注、7个m格式脚本和1个mat数据文件,整体约161.4MB。txt与vbb文件可支撑数据组织与结果解析,m脚本承担核心绘图逻辑,mat文件用于存储中间变量或基准数据,结构清晰,便于按需调用。目前已有1178人学习使用,适合需要快速生成FPPI曲线、对比不同检测器性能、完成论文实验或工程验证的开发者。配套参考说明可帮助理解数据组织方式与绘图流程,减少自行整理格式和调试的时间成本。 做目标检测评估,尤其是行人检测,你一定见过那张log-log坐标下的曲线:横轴是False Positives Per Image(FPPI,每张图像的平均误检数),纵轴是Miss Rate(漏检率)。我第一次看到这张图时,满脑子疑问:为什么不用mAP?为什么横轴不是precision/recall?直到自己动手实现绘制代码,才把背后的一套评估体系弄明白。这篇文章就把我从零到一绘制FPPI曲线的过程、代码和踩过的坑完整分享一下。无论你是刚入门目标检测的研究生,还是需要复现论文对比实验的工程师,弄清楚这条曲线怎么画、怎么看,都会对评估检测器有更直观的理解。

1. FPPI曲线是什么,为什么目标检测偏偏用它

1.1 从一次“尴尬”的评估经历说起

我之前在做一个行人检测项目,算法在公开数据集上的mAP有0.78,看着还挺漂亮。结果放到实际监控视频里一跑,画面里全是误检框,几乎没法用。后来导师扔给我一篇论文,让我对比里面的方法,我才注意到人家用的评估指标根本不是我熟悉的mAP,而是Miss Rate vs FPPI曲线。这件事给我上了一课:不同任务要用不同的评估方式,行人检测强调“在极低误检条件下还能不漏检”,而mAP把各种阈值下的性能平均了,反而掩盖了低误检区间的真实表现。

FPPI曲线的核心思想很简单:把检测器输出的所有框按置信度从高到低排序,然后从最高置信度开始,逐步放低阈值,统计当前阈值下的漏检率和每张图的平均误检数。这样,曲线上每个点对应一个检测阈值,左上角是“几乎不误检但漏检很多”,右下角是“误检很多但漏检很少”。评估检测器时,我们希望曲线尽量靠近左下角,也就是在低误检率时依然保持低的漏检率。

1.2 和PR曲线、mAP有什么本质区别

PR曲线横轴是Recall,纵轴是Precision,它描述的是“检出来的东西里有多少是对的”和“所有该检的东西检出了多少”之间的权衡。这里的问题在于:Precision的计算分母是检测器输出的所有框,它对每张图有多少个候选框非常敏感。如果一张图里有100个待检测目标,另一张图里只有1个,同样的FP数量在这两张图上对Precision的冲击完全不同。FPPI把误检数量除以图像总数,相当于固定了“每看一张图允许你错多少次”,这个尺度在实际部署中更直观。

mAP则是PR曲线下的面积再取平均,它把阈值从高到低的所有状态揉成一个数。做学术对比时,mAP确实方便,但当你关心“每张图允许0.1个误检时,检测器能检出多少目标”这样的现实问题时,mAP给不了答案。FPPI曲线可以直观看出来:在FPPI=0.1这个点,哪个方法的miss rate更低。这也是为什么Caltech Pedestrian、KITTI等数据集的评测报告里,FPPI曲线几乎成了标配。

1.3 FPPI、FPPW、每图误检数这些名词一次性理清

  • FP(False Positive):检测器输出了一个框,但这个框没有匹配到任何真实标注框,就是误检。
  • FPPI(False Positives Per Image):全部FP数量除以测试图像总数。注意是图像总数,不是包含目标的图像数量。
  • Miss Rate:1减去Recall。Recall被定义为“检出并正确匹配到的GT数量除以总GT数量”,所以Miss Rate等于漏检的GT数量除以总GT数量。
  • FPPW(False Positives Per Window):这是比较老的概念,在滑动窗口时代用的多,用“误检窗口数除以扫描过的窗口总数”。窗口数量会随图像内容变化,同一张图不同金字塔层产生的窗口数量差很多,导致FPPW在不同图片间不太可比,所以后来大家都改用FPPI。

FPPI就相当于“平均每看一张图,允许你错几次”。FPPI=1意味着平均每张图错一次,很多实际系统里这个误检密度已经完全不可接受;FPPI=0.01则意味着平均100张图才错一次,非常严格。

2. 画曲线之前,先搞懂输入数据和匹配逻辑

2.1 检测结果和GT数据长什么样

绘制FPPI曲线不能只靠检测框坐标,还要有对应的GT标注。我一般把检测结果整理成这样的结构:每行代表一个检测框,包含图像ID、置信度分数、四个坐标,以及一个“是否匹配到GT”的标记。匹配标记不是一开始就有的,需要通过计算IoU得到。

GT标注也需要类似的结构:图像ID、四个坐标,另外往往还有“ignore”标记。以Caltech数据集为例,有些目标太小或者严重遮挡,训练时不算正样本,评估时也不计入总GT数量。如果把这些忽略目标参与计算,miss rate会被严重拉低,得出一个看起来很优秀但没有意义的曲线。所以画图前第一步,是确认自己的GT清洗逻辑和数据集官方评测方案一致。

2.2 匹配规则:IoU阈值、多检去重、忽略区域

匹配检测框和GT框,最常用的方法是计算IoU(交并比)。给定一个检测框和一个GT框,IoU大于某个阈值就认为是匹配。行人检测一般默认IoU=0.5,但某些严格评测,比如部分KITTI子任务,会用0.7。这个阈值会直接影响曲线形态,阈值越高,检测框必须和GT贴合得越紧,miss rate通常会上升。

还有一个容易踩坑的地方:多个检测框匹配到同一个GT时,只能算一个TP,其余全部算FP。具体做法是,在匹配前先按置信度排序,然后从高到低依次匹配。一个GT一旦被某个高置信度检测框占用,后面的检测框即使IoU达标,也只能记为FP。忽略区域的处理类似:检测框落在忽略区域内,不参与TP或FP统计,直接跳过。如果自己的代码里漏掉这一步,画出来的曲线在低FPPI段会异常乐观。

2.3 按置信度排序后的累加统计

匹配完成后,我们得到一串检测框,每个框要么是TP,要么是FP。接下来把所有检测框按置信度降序排列,然后从高到低逐个累加:每遇到一个TP就tp加一,每遇到一个FP就fp加一。在某个累加时刻,当前“虚拟阈值”就是该检测框的置信度,高于该置信度的所有框都被认为是检测器的输出。此时:

  • Recall = tp / 总GT数
  • Miss Rate = 1 - Recall
  • FPPI = fp / 总图像数

这样,每处理一个检测框,就能产生一个点(FPPI, Miss Rate)。整条曲线上的点数量等于检测框总数。需要注意:因为新加入的框可能是FP,所以FPPI会随着阈值放宽而单调不减;而Miss Rate会因为新加入TP而降低,如果新加入的是FP则Miss Rate保持不变。因此曲线总体是随着FPPI增大而下降的阶梯状。

3. 完整绘制代码:从计算结果到log-log图

3.1 计算FPPI和Miss Rate的函数

基于上面的累加逻辑,我写了一版简洁易用的计算函数。输入是匹配好的检测框列表,每个元素是一个字典,至少包含score和match两个字段。match=True表示该框匹配到GT,是TP候选;match=False表示FP。同时传入总GT数量和总图像数。

import numpy as np def compute_fppi_missrate(detections, gt_count, image_count): """ detections: list[dict],每个dict包含'score'和'match' gt_count: 参与评估的GT总数 image_count: 测试图像总数 返回: fppi数组, miss_rate数组,均已按检测阈值累加 """ # 按置信度降序排序,置信度最高的框最先被选中 dets = sorted(detections, key=lambda d: d['score'], reverse=True) tp = 0 fp = 0 fppi_list = [] miss_list = [] for det in dets: if det['match']: tp += 1 else: fp += 1 recall = tp / gt_count if gt_count > 0 else 0.0 miss_rate = 1.0 - recall fppi = fp / image_count if image_count > 0 else 0.0 fppi_list.append(fppi) miss_list.append(miss_rate) return np.array(fppi_list), np.array(miss_list)

这个函数会输出两个一维数组,长度等于检测框数量。注意:排序前一定要确认score是数值类型,不是字符串;另外gt_count不能为0,否则除零。实际中如果总GT数为0,那评测就没有意义,直接跳过这张图/这个序列。

3.2 使用Matplotlib绘制标准FPPI曲线

得到数组后,用matplotlib画图。FPPI曲线必须在log-log坐标下看,因为FPPI通常在0.01到1之间变化,线性坐标会把低误检区域的细节全部压扁。常见坐标轴范围是横轴0.01到1,纵轴0.01到1。这个范围也是Caltech官方评测图表的默认范围,方便和论文里的结果对比。

import matplotlib.pyplot as plt def plot_fppi_curve(fppi, miss_rate, label='MyDetector', color='r'): # 过滤掉0值,否则log坐标会得到-inf mask = (fppi > 0) & (miss_rate > 0) fppi = fppi[mask] miss_rate = miss_rate[mask] plt.loglog(fppi, miss_rate, color=color, linewidth=2, label=label, drawstyle='steps-post') plt.xlim([0.01, 1.0]) plt.ylim([0.01, 1.0]) plt.xlabel('False positives per image (FPPI)') plt.ylabel('Miss rate') plt.grid(True, which='both', linestyle='--', alpha=0.4) plt.legend(loc='lower right')

这里有个细节:drawstyle='steps-post'会让曲线呈现阶梯状,符合累加统计的本质。很多论文里的曲线看上去平滑,是因为他们在log空间做了插值重采样。直接画阶梯线其实更诚实,同时也避免插值带来的假象。

3.3 多方法对比与图例美化

实际对比实验时,往往要在同一张图上画多条曲线,比如自己方法和几个Baseline。这时需要保持坐标范围一致,并且用不同的颜色、线型区分。我习惯把画图函数封装成可以多次调用的形式,每次传入不同的曲线数据。

def plot_multiple_fppi_curves(curves): """ curves: list of (fppi, miss_rate, label, color, linestyle) """ plt.figure(figsize=(7, 6)) for fppi, miss_rate, label, color, linestyle in curves: mask = (fppi > 0) & (miss_rate > 0) plt.loglog(fppi[mask], miss_rate[mask], color=color, linestyle=linestyle, linewidth=2, label=label, drawstyle='steps-post') plt.xlim([0.01, 1.0]) plt.ylim([0.01, 1.0]) plt.xlabel('False positives per image (FPPI)') plt.ylabel('Miss rate') plt.grid(True, which='both', linestyle='--', alpha=0.4) plt.legend(loc='lower right') plt.tight_layout() return plt.gca()

另外,如果某个检测器特别强,曲线会贴近左下角,可能超出坐标范围。这时不要盲目放大范围,而应该检查是不是漏了忽略区域,或者GT数量统计错了。坐标轴范围设定为0.01到1,是行业惯例,强行扩大反而难以和已有论文公平比较。

4. 从曲线到MR:平均漏检率的计算与插值技巧

4.1 为什么要做log空间插值

论文表格里经常出现一个叫做“MR”的数字,比如“MR-2.00%”,中文常翻译为平均漏检率。这个指标不是把所有曲线上的点平均,而是在固定的FPPI区间上做均匀抽样,再求漏检率的平均值。由于FPPI是log坐标,抽样点也应该在log空间均匀分布,否则大部分点会集中在FPPI接近1的位置,低估了低误检区域的性能。

标准做法是:在横轴FPPI区间[0.01, 1]上取9个点(也有取11个点的),这些点在log尺度下均匀分布。然后利用已经画出的阶梯曲线,插值出这9个点对应的miss rate,再求平均值。由于每个检测器产生的点数量和分布不同,只有通过统一采样点,才能公平地比较MR数值。

4.2 MR指标的计算代码

我封装了一个MR计算函数,输入是fppi数组和miss_rate数组,输出是一个浮点数。核心思路是把fppi和miss_rate都转换到log10空间,然后在logFPPI轴上用np.interp插值。插值前需要保证fppi是单调递增的,并且没有重复值。由于累加得到的fppi本身单调不减,重复的fppi值需要做去重处理。

def compute_mr(fppi, miss_rate, fppi_range=(0.01, 1.0), num_points=9): # 转为log空间 log_fppi = np.log10(fppi) log_miss = np.log10(miss_rate) # 过滤掉inf和nan,并保证单调递增 valid = np.isfinite(log_fppi) & np.isfinite(log_miss) log_fppi = log_fppi[valid] log_miss = log_miss[valid] # 去重:FPPI相同只保留最后一个(对应更低的miss rate) _, unique_idx = np.unique(log_fppi, return_index=True) # np.unique返回排序后的索引,需要取最后一个等于插值时的边界 unique_idx = np.concatenate([unique_idx[1:] - 1, [len(log_fppi) - 1]]) log_fppi = log_fppi[unique_idx] log_miss = log_miss[unique_idx] # 在指定范围生成log均匀采样点 sample_fppi = np.logspace(np.log10(fppi_range[0]), np.log10(fppi_range[1]), num=num_points) sample_miss_log = np.interp(np.log10(sample_fppi), log_fppi, log_miss) # 转回线性空间并求平均 sample_miss = np.power(10, sample_miss_log) mr = np.mean(sample_miss) return mr

这里需要注意的是,np.unique默认返回从小到大排序的索引,但我需要的是原始单调序列中每个相同值对应的最后一个点。如果你自己实现,也可以简单遍历一遍,遇到相同的log_fppi就把前一个点删掉。插值函数要求x轴数据严格递增,否则结果会错乱。

4.3 坐标范围和锯齿问题的处理

用drawstyle='steps-post'画出来的曲线会有很多水平段和竖直段,在log-log坐标下会出现比较明显的锯齿。这在原理上没问题,但只看图的话不够美观。有些论文会先把曲线在log空间插值成均匀网格,再画成实线,这样看起来更平滑。插值本身不会改变曲线的性能含义,但要注意:插值只能用于视觉展示,不能把插值结果当作新的曲线点去计算MR,否则会引入人为偏差。

还有一个常见问题是曲线在低FPPI端突然断开。原因是FPPI数组里前几个值可能都是0,loglog无法处理0。我通常会在画图之前对fppi做一个下限截断:

fppi_for_plot = np.maximum(fppi, 1e-5)

但截断后曲线上会出现一条水平段,表示“从某个很小的FPPI开始记录”。如果你希望曲线从坐标轴左边界开始,也可以不截断,而是把最左侧的点直接设置为xlim的下限值,这样在视觉上更干净。

5. 实操中绕不开的坑和我的个人建议

5.1 调试时最容易被坑的三个环节

第一个坑:匹配逻辑里漏掉“忽略区域”。我最初自己写的匹配器没有考虑ignore区域,导致很多检测框落在大面积忽略区域里,被算成FP,曲线左边严重上抬。后来对照官方评测代码才发现,检测框与ignore框的IoU大于0.5时,这个检测应被直接忽略,不算FP也不算TP。

第二个坑:GT数量统计不一致。有些评估工具会把一张图中同一目标的不同标注方式都算进去,比如部分遮挡和严重遮挡分别标注两个框,导致总GT数量虚高。计算gt_count时必须使用和官方评测一致的GT集合,否则miss rate整体偏低,曲线和别人的对比结果对不上。

第三个坑:多个检测框匹配同一个GT的去重顺序。如果不先按置信度排序再匹配,而是遍历GT去匹配检测框,很可能让一个低置信度的检测框抢先占用GT,高置信度的检测框反而变成FP,把曲线推向更差。正确做法是:按置信度从高到低遍历检测框,为每个框寻找IoU最大的GT,并检查该GT是否已被占用。

5.2 可用的现成工具箱和参考实现

如果是跑常见数据集,建议直接用官方评测工具,省去自己造轮子的麻烦。Caltech行人检测官方提供了一套Matlab工具,里面已经包含了FPPI曲线绘制、MR计算等功能。Python生态里,illegal(一个香港中文大学开源的目标检测评估包)也能生成FPPI曲线。如果你在用MMDetection,它自带的eval_map主要算mAP,但配合自定义评估代码也可以输出FPPI数据。自己写代码的好处在于灵活,特别是你想画某个特定阈值下的曲线,或者修改插值点数时,官方工具往往做不到。

不过我个人建议,入门阶段先自己实现一遍匹配和累加逻辑,再对照官方工具的结果。这个过程能帮你彻底理解FPPI曲线的每个点是怎么来的。如果直接调用工具箱,遇到曲线异常时你根本不知道从哪排查。

5.3 后续可以扩展的玩法

当你把FPPI曲线画熟练之后,可以试着扩展几个方向:一是把曲线和只输出框坐标的检测格式解耦,凡是能转成检测框序列的方法,都能用这套流程评估;二是画不同类别的子曲线,分析检测器对难例的灵敏度;三是把置信度校准引入评估,看看误检大多来自低置信度还是高置信度。还有一个很实用的操作,是把FPPI曲线和速度指标放一起做折线图,横轴是推理时间,纵轴是某个固定FPPI下的miss rate,用来体现“速度-精度”平衡。我在做边缘设备部署时,就用这种方式筛选过模型,比单纯看mAP直观得多。

最后再分享一个经验:画完图不要只盯着MR数值。MR把9个采样点平均后,可能掩盖了某个区间的异常。我见过一个检测器MR很好看,但FPPI=0.01时miss rate特别差,这在部署场景里其实更致命。所以每一步都要多看曲线本身,尤其是曲线的左段——那个区域才是行人检测这种“低误检容忍度”任务真正比拼的地方。

本文还有配套的精品资源,点击获取

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

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

立即咨询