我第一次在DOTA数据集上测mAP的时候,想法很简单:把模型跑一遍,拿预测结果算个均值平均精度,完事。结果折腾了整整两天,期间反复怀疑自己是不是把模型加载错了。后来才明白,DOTA评测的绝大部分坑,根本不在模型结构里,而在数据准备、坐标变换、旋转框度量和评测脚本的一致性上。如果你也想在DOTA上测mAP,或者在复现遥感旋转框检测论文时被这个指标卡住,这篇文章就是给你准备的。
DOTA是航空大场景目标检测里绕不开的数据集,图像动辄两千乘两千起步,有些甚至超过四千乘四千,里面的目标包括飞机、船舶、储油罐、操场、港口、桥梁、车辆等一系列常见物体。它的标注不是水平框,而是带旋转角度的任意四边形,所以评测逻辑比COCO、VOC那种常规数据集复杂不少。同样的模型、同样的权重,换一种切图方式或评测脚本,mAP可能波动好几个点。这也是很多人“复现不出论文指标”的隐性原因之一。
这篇文章我会从DOTA评测的特殊性讲起,把标注格式、切图预处理、mAP计算原理、完整测试流程,以及我在实际测试中踩过的一堆坑全部梳理出来。不管你是第一次接触旋转框检测,还是已经跑过几轮但mAP数值总对不上,这篇文章应该都能帮上忙。
1. 为什么“拿DOTA测mAP”和别的数据集不是一回事
1.1 DOTA数据集的特殊性
DOTA的全称是Detection in Aerial Images,核心是遥感影像中的目标检测。它的第一个版本v1.0包含15个类别:飞机、船舶、储油罐、棒球场、网球场、篮球场、田径场、港口、桥梁、大型车辆、小型车辆、直升机、环形交叉路口、足球场、游泳池。光看类别就知道,这里面既有体育场、港口这种占地巨大的目标,也有车辆、储油罐这种小而密集的目标,尺度差异非常极端。
图像分辨率方面,DOTA原始图像通常在800×800到4000×4000之间,甚至更高。绝大多数检测模型直接吃这么大的图是不现实的,显卡放不下,所以必须切图。这一“切”,就切出了和普通数据集测评完全不同的流程。COCO里你只需要把图像缩放到固定尺寸再推理,DOTA不行——你需要在原分辨率下把大图切成patch,逐块推理,再把结果拼回整图。
还有一个关键点是标注格式。DOTA的每个目标不是用矩形框表示的,而是用四个顶点坐标表示,也就是一个任意四边形,通常接近旋转矩形。这就意味着评测时不能直接用轴对齐的IoU,而要算旋转框之间的重叠面积。旋转IoU的计算复杂度和出错率都比水平框高一个量级,很多mAP虚高的案例就是在这里翻车的。
1.2 评测管线与普通数据集的差异
普通目标检测数据集的评测链路很直接:图像缩放 → 模型推理 → 格式转换 → 计算mAP。DOTA多出两个关键环节:切图预处理和跨patch结果合并。这两个环节对最终数值的影响,有时比模型本身还大。
举个例子,我测试某个模型时换了切图步长,从512改成800,切图的重叠区域变大了一倍,结果mAP涨了1.8个百分点。不是因为模型变强了,而是因为更大范围的重叠让每个目标被完整覆盖到的概率提高了,跨patch合并时能正确匹配的检测框变多了。
所以在DOTA上测mAP,从严格意义上讲,测的不是“模型”,而是“模型+预处理+后处理”整个系统。理解这一点非常重要,否则你会把预处理差异带来的指标波动误判成模型性能变化,后续调试方向就全偏了。
2. 数据准备与切图预处理:评测前的第一道坎
2.1 标注格式与类别映射
DOTA的标注文件是txt格式,每行代表一个目标,字段依次是:四个顶点的x-y坐标、类别名称、difficult标记。四对坐标的顺序是固定的,但起点在哪一个角不固定,有的从左上开始,有的从右下开始,整体构成一个四边形。解析时要保证四个点依次连接后形成的多边形是凸的且方向一致,否则后续的旋转IoU计算会出问题。
类别名称用的是连字符风格,比如large-vehicle、storage-tank、baseball-diamond。训练模型时一般会把类别名称映射成0到14的数字索引,但评测的时候,提交结果里必须写回原始的类别字符串。这个映射表一旦搞错,评测脚本会把所有预测框都当成未知类别,mAP直接归零。
DOTA还有difficult属性,表示这个目标是否属于“极难”样本。官方评测在计算mAP时会忽略difficult的GT,也就是说,你预测到difficult目标上既不算TP也不算FP,等于这部分目标不参与评估。但有些第三方脚本处理得不严谨,会把这些目标当成普通GT参与匹配,导致结果和官方口径不一致。
2.2 滑窗切图与目标归属规则
切图是DOTA评测里最核心的预处理步骤。常用的切图尺寸是1024×1024或800×800,步长则根据重叠区域需求来定。我的习惯是切图尺寸1024×1024、步长824,这样相邻patch之间会有200像素左右的重叠,既能覆盖跨patch的目标,又不会产生太多重复检测。
切图时最关键的是目标归属规则。主流做法有两种。第一种是“中心点归属法”,即目标四边形的中心点落在哪个patch内,就把这个完整目标分配给该patch,坐标整体偏移到patch局部坐标系。第二种是“相交截断法”,即目标与patch只要有交集就保留,并把目标多边形裁剪到patch边界内。
实测下来,中心点归属法更适合评测,因为每个GT只会出现在一个patch里,不会重复计GT;截断法保留了更多可见部分,但同一个GT可能在多个patch里各出现一部分,评测时容易造成GT重复或检测框匹配混乱。训练时用截断法可以增加样本充实度,但测试时我强烈建议用中心点归属,然后统一映射回原图坐标再评测。
切图还要注意一个细节:如果目标原本很大,中心点落在某个patch里,但目标大部分延伸到patch之外,切出来的局部图上可能只剩一小块目标区域。这种情况要设置一个最小可见面积比例,比如目标在patch内的可见面积低于原面积的30%就丢弃该patch中的这个GT,否则模型学到的是一堆零星碎片,干扰评测结果。
2.3 训练/测试预处理保持一致
很多人训练和测试用的预处理不一致,这是mAP对不上的常见原因。比如训练时做了随机缩放(0.5到1.5倍),测试时为了省事直接固定缩放成1024×1024,这会让目标尺寸分布和训练时完全不同,尤其是小目标,mAP会明显下滑。
我的建议是,DOTA测试时尽量用原分辨率推理,不缩放或只做固定倍数的等比例缩放,并且切图参数、目标过滤规则都要和训练时的设置保持一致。你甚至可以写一个配置文件,把切图尺寸、步长、重叠率、最小可见比例、中心点归属规则全部记录下来,每次评测前确认一遍,避免隔了一周自己都忘了当初用的什么设置。
别忘了不同DOTA图像的GSD(地面采样距离)可能有差异,也就是每像素代表的实际地面尺寸不同。这意味着同一辆车,在不同图像里的像素尺寸可能差好几倍。全局resize在这种场景下会进一步放大目标的尺度差异,除非你有明确的理由,否则别轻易做全局缩放。
3. mAP计算原理与评测脚本选型
3.1 AP/mAP到底怎么算
mAP是mean Average Precision的缩写,核心是每个类别分别计算AP,再对所有类别取平均。AP反映的是模型在这个类别上“不同置信度阈值下”的综合精度-召回表现。
计算过程可以拆成这么几步:把某个类别的所有预测框按置信度从高到低排序,从最高置信度开始逐一判定为TP还是FP。判定标准是预测框与某个GT的IoU是否超过阈值(DOTA官方常用0.5),且一个GT只能被匹配一次。随着判定推进,累计TP数量除以已判定框数量就是Precision,累计TP数量除以该类别GT总数就是Recall。把所有置信度阈值下得到的(Recall, Precision)点连成PR曲线,曲线下的面积就是AP。
VOC 2010之后的插值方式比较常用,它要求在每个Recall区间上取右侧最大的Precision作为该区间的代表值,然后按梯形面积积分。这样PR曲线就不会因为个别低分预测框而出现锯齿状的剧烈摆动,AP计算更稳定。
DOTA的mAP基本沿用这个思路。但要注意,类别不均衡时单一mAP值会掩盖很多信息。某几个大类的AP很高,几个小类AP很低,平均之后看起来还行,实际细节目标检测能力可能很弱。所以评测时我会额外打印每个类别的AP列表,而不是只看一个总mAP。
3.2 旋转框的IoU计算
旋转框评测绕不开旋转IoU。两个水平矩形的IoU可以用面积交集除以面积并集,但两个带角度的四边形形状各异,必须先求交叠多边形,再算交叠面积。
求交叠多边形的常用方法是Sutherland–Hodgman裁剪算法:把一个多边形按另一个多边形的每条边逐一裁剪,保留落在内侧的点,最终得到交叠区域的顶点序列。这个过程在几何上不复杂,但实现细节很容易出错。比如两个多边形的顶点顺序必须一致,一个顺时针一个逆时针就会算出负面积;坐标要不要取整也会影响最终IoU数值,官方脚本通常保留浮点精度,自己实现时不要提前取整。
很多人图省事,用轴对齐外接矩形近似旋转框的IoU,结果算出来的mAP虚高。看起来模型效果不错,换到真实场景部署时发现检测框偏转就匹配不上。原因是近似IoU放大了匹配范围,给了模型“差不多也能算对”的错误信号。在旋转框检测评测中,这就是最大的隐性坑。
3.3 官方devkit与MMRotate评测的取舍
评测脚本选型上,主流有两条路线:一是DOTA官方配套的devkit,二是MMRotate这类开源工具箱里自带的评测模块。两者都能跑出mAP,但口径有差异。
官方devkit的优点是与论文原始定义完全一致,复现论文指标时最可信。缺点是用起来麻烦,要编译C++代码、准备特定目录结构、处理中间文件,对新手不太友好。MMRotate的优点是配置化程度高,改一下配置文件就能跑,适合快速验证实验;缺点是它的评测流程里内置的切图和合并策略不一定和官方完全一致,跑出来的绝对数值可能有偏差。
| 对比项 | 官方devkit | MMRotate评测模块 |
|---|---|---|
| 指标口径 | 与论文提交一致 | 基本一致,但存在细微差异 |
| 上手难度 | 需要编译,目录结构繁琐 | 配置即用,适合日常实验 |
| 旋转IoU实现 | 官方维护,可靠性高 | 依赖具体后端实现 |
| 适合场景 | 论文复现、最终指标提交 | 日常实验快速对比 |
我的做法是:实验内部快速迭代用MMRotate,最终汇报或写对比结论前,再用官方devkit跑一遍验证。两套脚本的结果差异如果超过一个百分点,优先查预处理和类别映射,不要急着怀疑模型。
4. 完整测试流程:从模型输出到最终数值
4.1 推理输出与坐标变换
模型在patch上推理输出的坐标,通常是中心点格式加宽高和角度,比如(cx, cy, w, h, angle),也可能是四角点坐标。无论哪种,评测前都要转换回DOTA要求的四边形坐标格式,并把patch坐标变换回原图坐标。
坐标变换的核心是加偏移量。如果我把原图切成了若干个1024×1024的patch,第i行第j列的patch在原图中的起始坐标是(start_x, start_y),那么推理得到的每个坐标都要加上这个偏移量。如果你在推理时还对patch做了缩放,比如缩放到800×800,还要先除以缩放系数再偏移。
这里有一个我反复犯过的错误:坐标变换时忘记考虑patch内部已经做了letterbox或padding。训练时给输入图加了灰色边,测试时却直接拿模型输出的坐标去映射,导致所有检测框往某个方向偏移一段固定距离。这种错误的表现是mAP低得离谱,但可视化时又能看到框大致对准了目标,就是全部偏了一截。遇到这种情况,先检查输入图像的预处理是否和训练时完全一致。
类别索引的映射也要放在这个环节处理。模型输出的类别编号是训练时自己定义的,DOTA的标注类别顺序是固定的。提交评测前,把索引映射回形如large-vehicle的原始类别名,拼进结果文件的每一行。
4.2 结果合并与跨patch NMS
大目标横跨多个patch时,同一个目标很可能在多个patch里都被检测出来。如果不做合并,这些重复框在评测时会被算成FP,mAP直接受损。
标准的做法是把所有patch的检测框(已经映射回原图坐标)汇总到一起,然后按类别做NMS。具体步骤是:先按类别分组,组内按置信度降序排列,保留最高置信度的框,删除与其IoU超过阈值的其他框,然后重复这个过程直到处理完所有框。
NMS的IoU阈值怎么定是个经验活。阈值太低,比如0.3,会把同一个目标的多个实例误删,密集场景下漏检严重;阈值太高,比如0.8,重复框大量保留,FP飙升。我实测在DOTA上0.5是一个比较稳的起点,目标密集或者小目标多时,可以适当调到0.6。
还有一种情况需要特别注意:一个大目标被切成几块后,模型可能在每个patch里都预测出一个置信度不低的局部框。跨patch NMS会把这些局部框当作不同目标保留下来,因为它们的IoU可能并不高。这时光靠NMS不够,还要考虑目标跨patch合并策略:比如检测框的中心是否落在另一个更大框的邻近区域,或者两个框的相邻边距离是否小于某个像素阈值。这个问题在DOTA的plane和large-vehicle类别上尤其明显。
4.3 格式化提交与指标汇总
DOTA评测提交结果的标准格式,每行看起来像这样:
图片名 置信度 x1 y1 x2 y2 x3 y3 x4 y4 类别名类别名必须和GT完全一致,不能有大小写、连字符或下划线的偏差。我之前把large-vehicle写成了large_vehicle,评测脚本一个目标都匹配不上,所有预测框全部变成FP,该类别AP直接归零。这种错误不打印每个类别的AP根本发现不了。
跑完评测后,建议把每个类别的AP单独记录,不要只看mAP。DOTA的15个类别里,像plane、ship这类目标形状规整的,AP通常偏高;storage-tank这类密集圆形的、ground-track-field这类超大型目标的,AP波动很大,和切图策略关系密切。
如果你用了测试时增强(TTA),务必固定随机种子并保持所有测试图像的增强参数一致,否则同一模型跑两次可能差出零点几个点。我习惯在评测脚本里把中间结果转存一份,方便后续画PR曲线排查:一个类别AP低,到底是预测框位置不准,还是漏检严重,还是重复框太多,从PR曲线的形状能明显区分出来。
5. 实测过程中的坑与排查记录
5.1 类别名称多一个下划线,mAP整体崩盘
这是我第一次在DOTA上跑mAP时踩的坑,印象极其深刻。模型训练时用0到14的数字索引,转评测格式时我用字符串的replace操作把类别索引映射回名称,结果把连字符-误替换成了下划线_。官方评测脚本在GT文件中找不到large_vehicle,于是所有large-vehicle的预测框全部无法匹配,该类别AP直接是0。
这个类别在15个类别里占比不小,最终mAP被拉低了将近4个点。我当时还以为是模型训练出了问题,排查了半天才发现是类别名映射错误。从那之后我养成了一个习惯:评测之前先随机挑一个结果文件,检查前几行的类别名称,和GT标注文件里的类别名称做精确比对,用命令直接diff一眼就能看到差异。
5.2 角度定义不一致导致的旋转框错位
旋转框检测里,角度定义是最容易被忽视的暗坑。DOTA的标注是四角点坐标,本身没有歧义,但模型输出的角度定义五花八门:有-90到0的,有0到90的,长边定义还是短边定义也会影响角和边长的解释。
我第一次把某个模型输出转换成四边形坐标时,用的是训练代码里默认的角度解析方式,但没有细看它的定义范围。结果提交评测后mAP比预期低了七八个点,可视化检测结果发现框全转了大约45度。这个角度偏差不像90度那样明显错位,肉眼看还能勉强贴合目标,但IoU已经远低于0.5阈值了。
排查方法很简单:挑几张图,把模型输出的框直接画出来和GT叠在一起,逐个检查旋转方向是否一致。再选三五个目标,手工用几何库算一下预测框和GT的旋转IoU,确认数值在0.7以上再跑全量评测。这个步骤不需要覆盖所有图,抽几张就能暴露角度定义问题。
5.3 切图后目标截断与面积阈值过滤
切图时目标被截断是正常现象,问题的关键在于截断后要不要保留这个GT。如果目标原始面积是5000像素,但切到某个patch里只剩800像素,这个局部目标对模型来说几乎是不可判别的,强行保留只会给训练或评测增加噪声。
我踩过的坑是训练和测试用了不同的过滤条件。训练时我设置了“可见面积低于50%就丢弃”,测试时忘了同步这个配置,把切出来的所有GT都保留参与评测。结果是测试集中出现了大量残缺目标,模型在这些目标上表现不稳定,mAP出现异常波动。
更隐蔽的坑来自置信度过滤。有些人测mAP时为了减少计算量,只保留置信度高于0.3的预测框,然后交给评测脚本计算。这个操作会让PR曲线在高召回区域缺失样本,导致AP偏低;而如果你只保留高置信度框,PR曲线看起来“很干净”,AP虚高。评测mAP的正确姿势是保留全部预测框,让评测脚本自己对所有置信度阈值积分,不要在环节外提前截断。
5.4 数据分布与评测结果的解读误区
DOTA的官方划分里,val和test的数据分布存在明显差异。我实测过同一个模型在val和test上的mAP差出3个点以上。原因主要是test图像包含更多超大尺寸场景和更密集的小目标,模型在切块推理时对这些目标的漏检更严重。
所以看到论文里报的mAP时,先确认它是在哪个划分上测的,用什么评测脚本,什么切图参数。很多复现不出来的情况不是代码写错,而是对方的评测口径你无法完全还原。对比实验时,尽量固定同一个测试子集、同一套预处理、同一套评测脚本,只改变模型这一个变量。
mAP在DOTA这种复杂场景下,只是一个横向对比的标准刻度,不代表模型部署后的真实表现。我在实际项目中还额外统计了每个类别的漏检率和误检率分布,哪些小目标聚集区域容易漏检,哪些大目标会被切成碎片误检,这些信息比一个总mAP有用得多。
最后说一点心得体会。做DOTA评测,真正花时间的不是跑模型,而是保证评测链路的一致性。我现在的做法是:每次评测前把切图参数、类别映射、角度定义、NMS阈值、评测脚本版本全部记录在一个文本里,和结果一起存档。这样哪怕两周后想复查,也能完全复现当时的评测环境。先抽几张图把坐标变换和旋转IoU检查一遍,再跑全量评测,看起来多花了十分钟,实际上帮你避开的坑,很可能要花几个小时才能排查出来。