FEATURED · 精选文章

基于VisDrone的无人机小目标检测:YOLOv3与SSD实战

发布时间 / 2026/9/11 22:44:35
来源 / 创域科博编辑部
栏目 / 资讯中心
基于VisDrone的无人机小目标检测:YOLOv3与SSD实战 简介一项面向无人机视角的目标检测课程设计资源基于Python开发适合正在完成人工智能、计算机视觉类课程作业的学生以及希望快速搭建检测实验的初学者。项目以VisDrone数据集为输入同时实践YOLO与SSD两种框架完整覆盖从数据准备、模型配置到训练测试的流程并提供实时检测演示程序。整个资源压缩包共232个文件约2.33MB主要包括Python源码、编译生成的pyc文件、模型配置文件、数据集标签与样例图片yaml、cfg、data等文件对应不同检测框架的参数与数据组织cpp、cu和h源码则涉及NMS后处理等底层实现便于深入理解算法细节。目前已有305人学习。借助该资源可系统梳理无人机目标检测实验的完整链路包括VisDrone数据集处理方式、YOLO与SSD模型训练差异、测试及demo部署技巧以及NMS等关键模块的代码实现对完成课程作业或作为同类项目起点都有实际帮助。1. 为什么通用检测器在无人机航拍图上失灵VisDrone 的小目标难题无人机俯视视角下一辆汽车在 1280×720 图像里可能只占 20×10 像素直接用 COCO 预训练的 YOLOv3 检测漏检率会高得让人怀疑模型被改坏了。真正的问题不在检测头而在训练数据分布COCO 里目标平均占图像面积 10% 以上VisDrone 里平均不到 1%之前学到的 anchor 和特征尺度完全错位。这个课程设计项目做的就是一次“模型迁移校准”基于 Python 把 VisDrone 原始标注转换成 YOLO 和 SSD 都能用的格式调整自定义 cfg 中的 anchor 和 filters分别训练两套检测器最后写一个能实时跑无人机画面的 demo。代码包里出现的 yolov3.cfg、nms_cpu.cpp、vision.cpp 这些文件恰好构成了从训练配置到后处理加速的完整链路。适合正在做目标检测课设、无人机视觉毕设或者被小目标检测折磨到想砸键盘的从业者。2. VisDrone 标注格式剖析与 YOLO/SSD 训练数据转换拿到 VisDrone 数据集后第一件事不是跑模型而是看标注长什么样。VisDrone 的标注是纯文本每行一个目标字段用逗号分隔和 COCO 的 JSON、VOC 的 XML 完全不同。不转换数据Darknet 和 SSD 的训练入口根本读不进去。2.1 VisDrone 原始 txt 字段含义VisDrone 每个 txt 文件名与对应图像一致每行格式如下字段顺序名称说明1bbox_left目标框左上角 x 坐标像素2bbox_top目标框左上角 y 坐标3bbox_width框宽度4bbox_height框高度5score检测难度/置信度0 表示忽略6category类别编号从 0 到 117truncation截断程度 0/1/28occlusion遮挡程度 0/1/2/3需要特别注意的是 category 字段0 是 ignore 区域1 到 11 分别对应行人、人、自行车、汽车、面包车、卡车、三轮车、遮阳三轮车、公交车、摩托车、其他。转换到 YOLO 或 SSD 时通常把 0 类目标过滤掉同时把 1-11 映射成 0-10保证类别 id 从 0 开始。否则 Darknet 会认为第一个类别的数据是背景导致 loss 曲线看起来正常但 mAP 全是 0。2.2 转成 YOLO 训练需要的 txtDarknet 读取的标注文件是class_id x_center y_center width height中心点和宽高都归一化到 0-1。下面这个 Python 脚本是我在课设里用的转换器支持按图像宽高归一化import os def visdrone2yolo(src_txt, dst_txt, img_w, img_h, ignore_zeroTrue): with open(src_txt, r, encodingutf-8) as f: lines f.readlines() with open(dst_txt, w, encodingutf-8) as f: for line in lines: parts line.strip().split(,) if len(parts) 8: continue x, y, w, h, score, cat map(int, parts[:6]) if score 0: continue # score0 表示该目标被标注为“不适合检测” if ignore_zero and cat 0: continue # 去掉 ignore 类别 if w 0 or h 0: continue # 类别 1-11 映射到 0-10 cls_id cat - 1 cx (x w / 2) / img_w cy (y h / 2) / img_h nw w / img_w nh h / img_h f.write(f{cls_id} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}\n)调用时你需要提供每张图像的宽高VisDrone 原始图片多数是 1360×765 或 1280×720统一读图获取会更稳。这个脚本有两个关键点一是忽略 score0 的目标这些目标在数据集中被标注者标记为“拿不准”训练进去只会增加噪声二是 cls_id 要减一因为 Darknet 的类别索引从 0 开始不转换会让所有框的类别错位一个字节模型在验证时几乎无法正确匹配 GT。2.3 转成 SSD 能用的 VOC XMLSSD 训练不像 YOLO 那样直接读 txt经典实现如 PyTorch 的 ssd.pytorch一般吃 VOC 格式的 XML。VOC 格式里每个目标是一个object节点包含name和bndbox坐标。我一般这样做import xml.etree.ElementTree as ET def visdrone2voc(src_txt, xml_path, img_name, img_w, img_h, class_names): root ET.Element(annotation) ET.SubElement(root, filename).text img_name size ET.SubElement(root, size) ET.SubElement(size, width).text str(img_w) ET.SubElement(size, height).text str(img_h) with open(src_txt, r) as f: for line in f: parts line.strip().split(,) if len(parts) 8: continue x, y, w, h, score, cat map(int, parts[:6]) if score 0 or cat 0: continue obj ET.SubElement(root, object) ET.SubElement(obj, name).text class_names[cat - 1] box ET.SubElement(obj, bndbox) ET.SubElement(box, xmin).text str(x) ET.SubElement(box, ymin).text str(y) ET.SubElement(box, xmax).text str(x w) ET.SubElement(box, ymax).text str(y h) tree ET.ElementTree(root) tree.write(xml_path, encodingutf-8, xml_declarationTrue)class_names需要你按类别顺序定义一个列表比如[pedestrian, people, bicycle, car, van, truck, tricycle, awning-tricycle, bus, motor, others]。这里的坐标是像素整数不需要归一化VOC 格式的坐标直接落到图像尺寸上SSD 的 data loader 会自己做归一化。转换完成后再按 VOC 的标准目录结构组织成Annotations/和JPEGImages/SSD 项目里的xml_parse.py才能识别。2.4 类别映射与样本过滤策略VisDrone 有 11 个有效类别但无人机俯视下“汽车”和“货车”在很小的分辨率里长相几乎一样硬拆反而让模型犹豫。我在课设里做了两件事一是把car、van、truck合并成一个vehicle类二是过滤掉所有小于 8×8 像素的 GT 框。前者降低分类难度后者避免训练时对极小目标输出一带而过的小 loss导致整体精度被这类大量低质量样本拖垮。合并类别的操作需要在上面两个脚本里同步修改映射表否则 YOLO 的 txt 和 SSD 的 XML 会出现类别号不一致训练时模型就会学乱。这种过滤策略更适合小目标检测任务尤其无人机图像里大量目标都在 20 像素以下保留全部标注只会让模型更关注容易学的中大型目标。3. Darknet 下 YOLOv3 自定义模型训练cfg 修改与 NMS 的坑VisDrone 数据准备好之后接下来就是让 YOLOv3 跑起来。项目代码里有三个 cfg 文件很多人直接把预训练模型下载下来换张图测试却忽略了 cfg 里的类别数和 kernel 数量根本不匹配训练第一步就报维度错误。这一章把 cfg 选型和后处理 NMS 的实现一起说清楚。3.1 三个 cfg 文件怎么选配置文件适合场景特点yolov3.cfg原版 COCO 80 类输入尺寸 608速度慢但精度上限高yolov3-tiny.cfg移动端、实时性优先只有 15 层卷积速度快但小目标能力弱yolov3-custom.cfg自定义类别数在原始结构上精简用于本项目训练我自己用的是 yolov3-custom.cfg 作为起点因为它允许自由调整训练尺寸和类别数而且去掉了对 COCO 数据集的过拟合倾向。如果你在 Jetson 这类设备上跑可以选 yolov3-tiny.cfg但注意 tiny 版本下采样很快对 VisDrone 这种大量小目标的数据集很不友好精度会掉 15 个百分点以上。3.2 修改 filters 和 anchorsYOLOv3 每个检测头需要三类预测框坐标、目标置信度、类别概率。对应输出通道数为(classes 5) * 3。我用一个 Python 脚本来自动计算并替换 cfg 中的 filters 和 anchorsimport numpy as np from sklearn.cluster import KMeans # 读取 YOLO 格式的 txt 标注得到所有 GT 框的宽高归一化 def load_wh(label_dir, img_w, img_h): wh [] for f in os.listdir(label_dir): with open(os.path.join(label_dir, f)) as fh: for line in fh: parts line.strip().split() if len(parts) 5: w float(parts[3]) * img_w h float(parts[4]) * img_h if w 8 and h 8: wh.append([w, h]) return np.array(wh) # 用 KMeans 聚类出 9 个 anchor按面积从大到小排序 kmeans KMeans(n_clusters9, random_state42).fit(wh) anchors kmeans.cluster_centers_.astype(int) anchors anchors[np.argsort(-anchors[:, 0] * anchors[:, 1])]聚类之后得到的 anchors 可以直接替换yolov3-custom.cfg里第 610、696、783 行附近的anchors参数。替换后不要忘记把每个 yolo 层前面的卷积层 filters 改成(你的类别数 5) * 3。VisDrone 若设定 8 类则 filters 为 39类别数改错时 Darknet 会报Filter count and number of output channels of previous layer do not match这是训练前最常踩的坑。3.3 NMS 后处理在 Darknet 里的存在感训练结束后做验证或写 demo都避不开 NMS。项目里的nms_cpu.cpp和nms.cu分别在 CPU 和 GPU 上实现非极大值抑制。它们在 Darknet 源码的src/目录里编译时通过开关控制走哪套实现。如果你直接在 Python 里用 OpenCV 的dnn模块加载权重NMS 是 OpenCV 自己实现的和 Darknet 里跑的结果会有细微差别主要体现在重叠框阈值nms_thresh的处理上。Darknet 默认用 0.45OpenCV 的NMSBoxes也用 0.45但两者的置信度过滤顺序不完全一致实测同一张图可能差几个框。我在训练时把 GPU 用的 NMS 改成了跨类别 NMS即所有类别一起抑制而不是每个类别单独抑制。无人机俯拍时行人和骑自行车的人经常重叠单独抑制会让一个行人框和一辆自行车框同时保留视觉上非常乱。改法是去掉nms.cu里对class_id的判断让所有 box 参与全局抑制。3.4 训练命令与参数解析假设你的 Darknet 编译完成数据配置custom.data里已经写好类别数、train/valid 路径和 names 文件训练命令如下./darknet detector train custom.data yolov3-custom.cfg darknet53.conv.74 -gpus 0,1 -map参数解析detector train训练检测器custom.data数据配置每行一个路径yolov3-custom.cfg网络结构配置darknet53.conv.74预训练 backbone 权重必须使用 Darknet 官方提供的这个文件它只包含卷积层不包含检测头-gpus 0,1指定双卡训练显存不足可以去掉-map每个 epoch 结束在验证集上计算 mAP方便观察过拟合。训练时建议把batch64、subdivisions16效果等价于 batch size 为 4显存小的机器也能跑。如果发现 loss 在前几千次迭代不降检查 learning rate 是否大于 0.001以及burn_in参数是否设置合理。4. SSD 路线实战依赖配置与训练对比YOLO 路线跑通后我把同样的数据用 SSD 复现了一遍主要是想对比两套框架在小目标上的表现。SSD 和 YOLO 最大的不同在于它从多个 feature map 同时预测浅层特征图保留更多小目标的细节所以理论上比 YOLOv3 更有优势。实际结果却取决于 anchor 设计。4.1 为什么还要在 SSD 上再做一遍SSD 的默认 anchor 比例是固定的 [1, 2, 3, 0.5, 1/3] 等这些比例是来自 PASCAL VOC 的日常物体。VisDrone 的俯视车辆几乎都是扁平形状宽高比集中在 1.5 到 3 之间直接拿默认配置训练模型对细长形目标召回堪忧。做一遍 SSD 训练不是为了证明哪个框架更强而是为了对比 anchor 策略和数据增强对最终效果的影响。例如 SSD 的hard negative mining在无人机密集场景中能把误检率压得比 YOLO 低这是值得借鉴的。4.2 使用 mmdetection 微调 SSD这里选用 mmdetection 作为 SSD 的训练工具因为它的配置修改比原版 Caffe 实现简单得多而且纯 Python 接口更适合延续项目主线。先准备 VOC 格式的数据集然后写一个简化配置# ssd_visdrone.py _base_ [../ssd/ssd300_coco.py] model dict( bbox_headdict( num_classes8, anchor_generatordict( typeSSDAnchorGenerator, scales[0.1, 0.2, 0.4, 0.6, 0.8, 1.0], ratios[[1, 2.5, 5], [1, 2.5, 5], [1, 2.5, 5], [1, 2.5, 5], [1, 2.5], [1, 2.5]] ) ) ) dataset dict( traindict( typeVOCDataset, ann_filedata/visdrone/voc/ImageSets/Main/trainval.txt, img_prefixdata/visdrone/voc/ ) )上面的 ratios 在默认基础上加了 2.5 和 5是为了匹配俯拍车辆的细长形状。scales中前几个较小的值对应更高的特征分辨率用来捕捉小目标。修改 anchor 后需要同步检查 SSD 的输出通道数是否匹配否则会报 mismatch 错误。训练命令python tools/train.py configs/ssd/ssd_visdrone.py --work-dir work_dirs/ssd_visdrone4.3 训练、评估和易错点训练时需要注意 batch size 最好不要小于 16否则 BN 层的统计量不稳定。VisDrone 图片尺寸较大SSD 300 输入会等比例缩放但全部 resize 到 300×300 会导致小目标进一步缩小。我加了两个额外的增强随机裁剪和缩放让目标以更大的尺寸出现在训练图中。评估指标用 mmdetection 自带的 mAP 脚本即可但要注意它的评估协议会把difficult标注忽略如果转换时没去掉 ignore 目标结果会虚高。python tools/test.py configs/ssd/ssd_visdrone.py work_dirs/ssd_visdrone/epoch_80.pth --eval mAP常见问题是显卡显存不够导致 OOM可以把samples_per_gpu降到 2同时开启persistent_workers加快数据读取。SSD 的 loss 收敛速度比 YOLO 快但最终 mAP 通常比 YOLOv3 低原因还是深层 feature map 对小目标的分辨率不足。这里要明确一个结论SSD 的工程复杂度比 YOLO 低适合快速验证如果要做实时高精度YOLOv3 加 tiling 更强。4.4 与 YOLO 实测对比要点维度YOLOv3SSD300小目标召回中低检测速度高GPU 上 ~45 FPS高~60 FPS配置复杂度需要改 cfg 和 NMS修改 Python 配置密集场景误检稍高较低这张表不是绝对结论取决于你的 anchors 和数据增强。我的建议是如果你只需要 3-5 个宽泛类别如车、人、自行车YOLO 一把梭如果你关注误检率SSD 的 hard negative mining 更省心。5. 实时 demo 的优雅实现tiling 多尺度推理技巧最后一步是把训练好的模型装进实时检测 demo。项目里的vision.cpp是 Darknet 对外提供的接口我在 Python 里直接用 ctypes 调用libdarknet.so省去重新编译 OpenCV 的麻烦。这里分享一个能提高无人机检测精度的采坑技巧把原图切块推理而不是直接缩小。5.1 Python 实时检测脚本import cv2 import time from darknet import load_network, detect_image net load_network(yolov3-custom.cfg, yolov3-custom.weights, 0.5, 0.45) cap cv2.VideoCapture(drone.mp4) while cap.isOpened(): ret, frame cap.read() if not ret: break # Darknet 需要RGB输入且尺寸与训练时一致 rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) detections detect_image(net, rgb, thresh0.5) for name, prob, (cx, cy, w, h) in detections: x1, y1 int(cx - w/2), int(cy - h/2) x2, y2 int(cx w/2), int(cy h/2) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, f{name} {prob:.2f}, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imshow(drone det, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release()load_network中的 0.5 和 0.45 分别对应置信度阈值和 NMS IoU 阈值实际调参时把置信度降到 0.25 可以找回更多小目标但误检也会变多。5.2 Tiling 切块策略直接对整帧缩放会让小目标损失细节。常见做法是把 1280×720 的原图切成四块 640×360 的 patch每块独立送进网络再把检测框坐标映射回原图。这种方法能让目标像素在输入中放大一倍小目标召回提升明显。代价是推理时间变成原来的 4 倍但无人机场景通常对实时性要求没那么苛刻倾斜 20-30 FPS 可以接受。def tile_detect(frame, net, tile_size640): h, w frame.shape[:2] dets_all [] for y in range(0, h, tile_size): for x in range(0, w, tile_size): patch frame[y:ytile_size, x:xtile_size] rgb cv2.cvtColor(patch, cv2.COLOR_BGR2RGB) dets detect_image(net, rgb, thresh0.3) for name, prob, (cx, cy, pw, ph) in dets: # 映射到全局坐标 dets_all.append((name, prob, (x cx, y cy, pw, ph))) return dets_all最后这个 tiling 策略是处理无人机小目标性价比最高的方式配合 YOLOv3-custom 的 anchor 优化能让原本只有 20×20 像素的小目标在 patch 中变成 40×40 以上模型识别难度直降一个量级。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻