简介:基于DOTA数据集的YOLO训练资源包,面向目标检测与计算机视觉方向的课程设计、期末大作业及毕业设计。整套资源涵盖Python源码、网络配置文件、数据映射文件、训练脚本及说明文档,共18个文件,压缩包约517KB,包括YOLO数据转换、图片切分、测试等脚本,以及DOTA数据集的类别与数据配置,便于直接复现和二次开发。资源代码思路清晰、注释详细,并已通过运行测试,内置预训练参数与示例输出,能帮助理解遥感旋转目标检测的训练流程。已有1323人学习下载,适合具备一定深度学习基础的学生或算法入门者参考。
1. 为什么航拍目标检测绕不开 DOTA 与 YOLO 的组合
做航拍图像目标检测的人,几乎都躲不开 DOTA 数据集。它是目前航拍场景下最常用的基准之一,图像分辨率高、目标尺度差异极大,小到车辆大到港口,而且目标排列密集且带有任意朝向。直接用普通水平框的检测器去套,很容易出现两个问题:框内混入大量背景,导致分类置信度虚高;密集场景下 NMS 抑制掉本该保留的相邻目标。很多人第一反应是上旋转框检测器,但工程上如果只是做落地部署、数据标注是水平框,YOLO 系列依然是最省成本的选择。这套资源就是用 YOLOv3 在 DOTA 上完整跑通训练、预训练加载、验证的全流程,并附带了源代码、cfg 配置、训练脚本以及文档说明,适合计算机视觉方向的课程设计、毕业设计,也适合刚接触航拍检测的工程师用来做基线。它能让你跳过大段时间消耗在环境配置和数据格式转换上的过程,直接看到从 DOTA 原始图像到 YOLO 可训练数据再到模型权重这一条链路是怎么走通的。
2. DOTA 数据集的齿轮:分块切割与旋转框拆分
DOTA 原始图像通常是几千乘几千像素的大图,直接塞进 YOLO 训练不仅显存放不下,而且小目标会被严重压缩。预处理阶段的关键手段是切图(split),同时把旋转框标注转换成 YOLO 所需的水平框格式。
2.1 DOTA 原始标注与 YOLO 标注的歧义
DOTA 数据集提供的是四个顶点坐标,表示一个任意方向的四边形。而 YOLOv3 只认cx, cy, w, h形式的水平边界框。常见做法是取四点的外接矩形,但要注意:如果目标本身很长并倾斜,外接矩形的面积会远大于目标实际面积,导致正样本中背景比例剧增。更稳妥的做法是先按角度聚类分析,如果目标角度分布集中(比如车辆多沿道路方向),可以考虑保留角度分支,否则就用外接矩形做近似。
# 四顶点坐标 (x1,y1,x2,y2,x3,y3,x4,y4) 转外接矩形 xs = [x1, x2, x3, x4] ys = [y1, y2, y3, y4] cx = (min(xs) + max(xs)) / 2 cy = (min(ys) + max(ys)) / 2 w = max(xs) - min(xs) h = max(ys) - min(ys)上面的 Python 代码给出了最简单的转换逻辑,直接取四顶点中最小的和最大的坐标值组成矩形。转换后需要注意归一化:在切图时,每个子图的尺寸是固定的,坐标需要除以子图宽高,而不是原始大图的宽高。这是最容易出错的地方,很多初学者的 mAP 突然变成 0,原因就是归一化分母用错了。
2.2 split.py 的切图策略与重叠参数
源码中的split.py负责把大图切成小图,同时把标注文件跟着切。切图窗口大小一般选 1024×1024 或 608×608,和训练输入尺寸匹配。重叠率(overlap)建议设为 0.2 到 0.5 之间。为什么不设 0?因为航拍目标可能正好横跨切图边界,完全切掉就丢失了一个有效样本。重叠率高会让相邻子图共享很多目标,相当于隐式提升了这些目标的采样频率,但也增加了冗余推理量。训练阶段用 0.25 的重叠率比较均衡。
python split.py --src_dir ../DOTA/images --label_dir ../DOTA/labels \ --out_dir ../DOTA_yolo/images --out_label_dir ../DOTA_yolo/labels \ --size 1024 --overlap 0.25上面命令把原始图像和标注文件夹同时处理,--size决定子图边长,--overlap控制相邻图重叠比例。切图后还需要过滤掉目标过小或完全在边界之外的标注,否则 YOLO 会大量预测出面积只有几个像素的噪声框,拖慢收敛速度。一般把宽高小于 10 像素的目标直接丢弃,保留大于阈值的。
# split.py 中的过滤逻辑示意 if w < 10 or h < 10: continue这里的 10 像素是经验值,如果你的验证集里小目标占比高,可以直接降到 4 或 5,但需要接受更多误检的代价。过滤逻辑不要放在切图之前,因为切图之后原本的大目标可能被切成小目标,这个“小”和原图的小不是一个概念。
2.3 imagetrans.py 与数据增强:不只是翻转
源码里的imagetrans.py实现了图像变换,包括随机翻转、90 度旋转和马赛克增强。你可能会疑惑:DOTA 是航拍图,旋转 90 度不会改变目标语义吗?实际上航拍图无所谓绝对方向,旋转后目标类别不变,模型学到的是特征本身,而不是相机朝向。所以旋转增强对 DOTA 非常有效,能显著提升模型的泛化能力。
# 90度旋转增强示例 def rotate_image_90(image, boxes): h, w = image.shape[:2] new_image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE) new_boxes = [] for box in boxes: x, y, bw, bh = box # 坐标旋转映射 new_x = (h - y - bh) / h new_y = x / w new_w = bh / h new_h = bw / w new_boxes.append([new_x, new_y, new_w, new_h]) return new_image, new_boxes注意上面的归一化坐标计算,第一行new_x用了h - y - bh,这是在坐标已经归一化到 [0,1] 的前提下对 x 方向反转。这组公式很容易写错,错误的结果是边界框全部偏到左上角或右下角。如果不想折腾旋转后坐标映射,就直接采用左右翻转和上下翻转,这两种操作对 YOLO 的格式最友好,只需要把x变成1 - x - w,y保持不变。航拍数据集的增强优先级建议是:随机翻转 > 亮度对比度扰动 > 马赛克 > 旋转。马赛克增强能显著丰富背景,但要注意 DOTA 图像本身已经有很多小目标,马赛克四图拼接后目标会更小,适当降低增强概率。
3. cfg 配置对 DOTA 场景的针对性调整
YOLOv3 的 cfg 文件dota-yolov3-416.cfg和dota-yolov3-tiny.cfg是整个训练的灵魂。416 表示输入尺寸,tiny 是为了在低算力设备上跑。很多人直接下载一个通用 yolov3.cfg 就去训练 DOTA,结果精度不尽如人意,其实原因都出在 anchor 尺寸和数据加载策略上。
3.1 anchor 重聚类的必要性与方法
DOTA 的目标尺寸分布和 VOC、COCO 截然不同。COCO 上小目标多,anchor 从 10×10 到 60×60 不等;而 DOTA 中操场、桥梁这类目标动辄几百上千像素,同时车辆等小目标又可能只有十几个像素。直接用默认 anchor 会让大目标的初始回归很难,小目标又被平均给拖累。常见做法是使用 k-means 在切图后的标注框上重新聚类 anchor。
# 使用 darknet 自带的 k-means 工具(或使用源码中的脚本) python kmeans_anchor.py --label_dir ../DOTA_yolo/labels --num_clusters 9k-means 的距离度量是 IoU 的补数:d(box, centroid) = 1 - IoU(box, centroid),目的是让 anchor 和真实框的 IoU 尽可能大。聚类完成后,把 9 组宽高填到三个 yolo 层的anchors =字段里。如果你用的是 tiny 版本,则只需 6 组。还要注意按面积排序,大的 anchor 放到最深的 yolo 层。
3.2 修改 classes、filters 与 max_batches
DOTA 原始数据集有 15 个类别(飞机、轮船、储罐、棒球场、网球场、篮球场、地面跑道、港口、桥梁、大型车辆、小型车辆、直升机、环形交叉口、足球场、游泳池)。资源中的dota.names应该也是 15 个,所以每个 yolo 层前面的卷积filters必须改成(15 + 5) * 3 = 60,否则训练时维度不匹配。classes=15在 yolo 层也要同步修改。
| 参数 | 默认值 | DOTA 建议值 | 说明 |
|---|---|---|---|
| batch | 64 | 16~32 | 航拍图目标密集,batch 太大容易显存溢出 |
| subdivisions | 1 | 8~16 | 实际送入显存的 batch 是 batch/subdivisions |
| width / height | 416 | 608 或 416 | 608 对小目标更友好,但训练时间增加约 1 倍 |
| max_batches | 50200 | 60000 | DOTA 类多,训练更久更稳 |
| steps | 40000,45000 | 48000,54000 | 学习率衰减点提前会导致后期收敛不足 |
| learning_rate | 0.001 | 0.001 | 加载预训练权重后可保持不变 |
上面是参数对比表,核心原则:如果显存不够,先增大subdivisions,不要减小width/height;减少 batch 会导致 BN 统计量抖动较大,配合小幅提高learning_rate到 0.002 可能更好。但稳妥的做法是 batch=32、subdivisions=8,对大多数 11GB 显存卡适用。steps一般设为max_batches * 0.8和0.9,在这个项目里也就是 48000 和 54000。
3.3 dota.data 与数据路径配置
dota.data文件指向train.txt、valid.txt和dota.names。路径最好写成绝对路径,否则训练时经常报Couldn't open file。别为了省事写相对路径,因为你可能在不同目录下启动训练命令,相对路径会瞬间失效。
classes=15 train=/home/user/DOTA_yolo/train.txt valid=/home/user/DOTA_yolo/valid.txt names=/home/user/DOTA_yolo/dota.names backup=/home/user/DOTA_yolo/backupbackup目录要手动创建,darknet 不会自动新建。训练中断时,backup里的.weights文件就是你恢复的救命稻草。train.txt的内容格式是每行一张切好的子图图片绝对路径,路径后缀建议 .jpg,与图片实际格式保持一致。
4. 预训练参数加载与训练脚本的确定性
这个资源的关键词里有“预训练的参数”,指的就是 darknet 在 ImageNet 上预训练的主干网络权重。YOLOv3 用darknet53.conv.74,tiny 版本用yolov3-tiny.conv.15。加载预训练权重后,前 50 层主干被初始化得比较好,目标检测层是随机初始化的,因此训练初期 loss 会快速下降但仍有较大波动,这属于正常现象。
4.1 train.sh 脚本解读与启动顺序
项目中给出了train.sh,标准运行逻辑是先加载预训练,再断点续训。下面是类似的标准命令:
#!/bin/bash ./darknet detector train dota.data dota-yolov3-416.cfg \ darknet53.conv.74 -gpus 0,1 2>train.log使用两张卡时,cfg 里batch会被自动按卡数切分,实际每张卡的 batch 等于 batch/GPU 数,所以如果之前设 batch=64,双卡则变成 32。这会影响 BN 的统计量,必要时要手动调整 batch。2>train.log是把日志重定向到文件,方便训练后排查 loss 曲线。如果只是想测试环境,先加载预训练跑 100 步看 loss 是否下降是最快的校验方式。
./darknet detector train dota.data dota-yolov3-416.cfg darknet53.conv.74 -gpus 0 -dont_show -map-map参数会在训练过程中定期计算 mAP,输出每条验证集的结果。默认每 4 epoch 计算一次,如果你想更频繁,需要在dota.data里加一行eval_period=2。这个参数对调参非常有用,能让你及时看到哪个 anchor 尺寸匹配度低。
4.2 断点续训:auto-resume-training.sh 的逻辑
项目里的auto-resume-training.sh脚本做的事情很简单,但很实用:查找backup目录下最新的.weights文件,然后接着训练。由于训练意外中断后,你手动找最新权重很容易找错,比如把 30000 步的权重当成 15000 步的加载,导致学习率重新计算,精度反而下降。
# auto-resume-training.sh 核心逻辑 latest_weight=$(ls -t backup/*.weights | head -1) echo "Resume from $latest_weight" ./darknet detector train dota.data dota-yolov3-416.cfg "$latest_weight" -gpus 0这里要特别说明:用backup里的权重恢复训练,和学习率衰减的步数统计是独立的。darknet 会从权重文件名里恢复已经迭代的次数,因此只有确保文件名包含_step标记的权重(比如backup/dota-yolov3-416_50000.weights),才会自动接续学习率。如果你改过 cfg 里的max_batches,那么恢复训练时会以新参数为准,但已走的步数不会回退,此时如果步数已经超过新的steps阈值,学习率会被压低到最低档,这是一个没人提醒过的隐坑。
4.3 训练日志与 loss 判定
训练期间 loss 数值前 100 步会从 20 以上快速降到 5 以内,之后下降缓慢。航拍目标密集的 DOTA 上,loss 很难降到低于 1.0,因为小目标回归难度大。更好的观测指标是avg Recall,darknet 在每批量结束后会打印avg Recall,当模型开始对目标有稳定预测时,这个值会从 0 开始上升到 0.3 以上。
v3 (iou loss, Normalizer: (iou: 0.07, obj: 1.00, cls: 1.00) Region 82 Avg IOU: 0.50, Class: 0.85, Obj: 0.70, No Obj: 0.01, .5R: 0.70, .75R: 0.42, count: 17看上面日志,.5R表示 IoU 阈值 0.5 的召回率,count 表示这批图中匹配到的目标数。如果count一直很小(小于 10),而你的训练集里每个子图差不多有几十个目标,那大概率是坐标归一化错误或ignore_thresh设置太高,导致大部分目标没有匹配到任何 anchor。此时需要检查data_transform后的 label 文件是否和图片对应,打印其中一个 label 文件即可判断。
5. 验证、可视化与小目标性能优化技巧
训练完的权重文件在backup目录下,直接用test.py跑验证集,会输出标注好的检测图。output.jpg和test.png应该是项目附带的结果样例,说明作者已经跑通过一次完整流程。
5.1 test.py 的推理调用与阈值调整
test.py的调用方式和 darknet 的 C API 绑定在一起,核心部分通常是加载网络、设置阈值、跑 forward。下面是伪代码骨架:
# test.py 中的推理逻辑 from darknet import load_network, detect_image net, class_names, colors = load_network( "dota-yolov3.cfg", "dota.names", "backup/dota-yolov3_final.weights" ) detections = detect_image(net, class_names, "test.png", thresh=0.35) for label, confidence, bbox in detections: x, y, w, h = bbox print(f"{label}: {confidence:.3f} at ({x:.1f}, {y:.1f}) {w:.1f}x{h:.1f}")thresh参数在可视化时设为 0.35 比较合理,因为训练结束的模型在验证集上往往偏向保守,置信度偏低。如果实测大量真实目标落在 0.3 以下,说明训练尚不充分,或者 anchor 与目标尺寸匹配不佳,此时盲调阈值没有意义。如果你需要高精度的推理结果,建议对同一图片跑两次不同输入尺度(416 和 608),然后在原图上做 NMS 合并,能有效提升小目标检测率。
5.2 针对 DOTA 小目标的最后优化手段
如果你用这套流程训练后 mAP 不理想,最优先检查的不是网络结构,而是预处理:DOTA 原图切 1024 后输入到 416 训练,小目标(小于 32 像素)占比过大时,应该直接改用 608 输入。其次,修改ignore_thresh从默认 0.7 降到 0.5,让 IoU 低于 0.5 的预测框也被视为正样本参与损失计算,注意这会略微增加训练噪声,但能提升小目标的召回。最后,对大目标类别(如港口、桥梁)的样本进行过采样,或者在dota.data里额外加一份仅包含大目标的训练列表让模型多看几轮。序列密度上,不要盲目调整 NMS 阈值,先观察测试图上同一目标是否产生大量重复框,如果是,再去调nms_iou_thresh到 0.4。
5.3 一个快速验证训练是否正常的小技巧
训练第 100 步时,把backup里刚保存的权重拿出来跑test.png,如果输出图上能在目标位置看到高亮但低置信度的框,说明梯度传导已经激活。如果完全没有任何框(连空白区域都没反应),检查dota.names里的类别数量是否和 cfg 里一致,不一致会导致检测层输出维度错位,这类错误在 darknet 的日志里不会直接报错,只会有莫名其妙的 NaN loss。还有一个容易忽视的点:训练时如果开了random=1,每个 batch 会随机改变输入尺度,这会让预训练权重的感受野被打乱,建议在 DOTA 这种目标尺度跨度极大的数据集上保持random=1,但前 1000 步可以临时设为 0,等模型稳定后再改回来。
本文还有配套的精品资源,点击获取