简介:这是一份面向高校人工智能课程期末大作业或课程设计的无人机图像目标检测Python项目,基于YOLOv3算法,包含完整源代码、文档说明与配套数据集,适合需要完成类似课题的本科生及入门开发者。资源包共231个文件,核心为94个Python脚本(覆盖模型训练、检测推理及各类工具函数)、65个pyc缓存、7个YAML配置和16个TXT说明文档,同时配有测试图片与配置文件,便于快速验证效果;包体仅2.36MB,结构清晰,部署门槛低。目前已有384人学习/下载,可见其参考价值。项目源码带有详细注释,对新手友好,可帮助理解目标检测从数据准备到模型调用的完整流程;内容涉及YOLOv3、YOLOv3-tiny等多种配置,并包含C++/CUDA实现的非极大值抑制等后处理模块,兼顾课程设计与算法原理探究。下载后按文档简单配置即可运行,是一份功能完整、可直接演示的高分大作业方案。
1. 无人机视角下的目标检测,为什么这套 YOLOv3 可以直接拿来当大作业
先说结论:无人机图像目标检测和普通监控场景最不一样的地方,在于目标尺寸小、背景占比大、视角变化剧烈。拿我自己拆过的几个课程设计来看,很多同学第一反应是用 Faster R-CNN,结果训练收敛慢、显存吃紧,交作业时推理速度也不好看。而这套基于 YOLOv3 的 Python 源码,把模型定义、配置文件、NMS 后处理、数据组织方式全部给齐了,属于那种「理论能讲清楚、实验能复现、答辩能被追问」的典型高分结构。
整个资源的核心落在yolov3.cfg、yolov3-tiny.cfg、nms_cpu.cpp、vision.cpp、coco.data、custom.data这组文件上。它不只是给了一个能跑的训练入口,而是把从锚点框设置到非极大值抑制(NMS)的完整链路都摊开了。对做人工智能大作业的人来说,这不是「调包跑通」的玩具,而是可以边看边解释设计取舍的素材——比如为什么 tiny 版本适合无人机嵌入式部署,为什么 COCO 预训练权重迁移到自定义无人机数据集时,要冻结前几层。
我建议把这份源码当作一个脚手架:先用默认参数跑通流程,再针对无人机图像的「小目标 + 俯视角度」做针对性调整。后面章节里的命令和参数,都是在这个资源基础上可以直接执行的。
2. YOLOv3 的网络结构和配置文件:读懂yolov3.cfg才算真正拿到手
2.1 从 Darknet 的cfg语法看 YOLOv3 的骨架
YOLOv3 的核心是 Darknet-53 特征提取网络加上多尺度预测。yolov3.cfg用文本方式描述整个网络,每一段对应一个网络层。不想看源码的时候,这个文件就是最好的架构图。
拿其中一段来说,route层决定了特征融合的方式,YOLOv3 能同时预测大中小三个尺度的目标,就是靠它把浅层和深层特征拼在一起。下面是一段典型的配置节选:
[convolutional] batch_normalize=1 filters=256 size=1 stride=1 pad=1 activation=leaky [route] layers = -4 [convolutional] batch_normalize=1 filters=512 size=3 stride=1 pad=1 activation=leaky逻辑说明:第一个卷积层用 1x1 卷积把通道数降下来,目的是控制计算量;然后route层把当前特征图往前回溯 4 层,与前面的特征做拼接,从而让深层语义和浅层纹理信息互补。后面再接 3x3 卷积继续提取特征。这种「先压缩、再融合、最后 upsample 到下一尺度」的写法,是 YOLOv3 召回小目标的关键。
如果你想把默认的 COCO 80 类改成无人机场景的 1 类或 2 类目标,就必须改yolov3.cfg里最后的三个[yolo]层。每个 yolo 层里的classes要改成自己的类别数,同时该层之前的卷积filters要重新计算,公式是:
filters = (classes + 5) * 3因为每个 Grid 单元预测 3 个 anchor,每个 anchor 有 4 个坐标偏移、1 个目标置信度、C 个类别概率,所以是C + 5,再乘 3。
2.2 锚点框参数对无人机小目标意味着什么
yolov3.cfg里三个 yolo 层的anchors是预设的 9 组宽高比,它们来自 COCO 数据集的统计。但无人机俯拍时,地面车辆、行人、建筑物的尺度分布和真实摄像头视角完全不同。这里就体现出了这份资源里yolov3-tiny.cfg的价值:tiny 版本只有两个 yolo 层,anchor 数量更少,针对算力受限的场景更友好。
如果你的数据集里有大量 20x20 像素以下的目标,默认 anchor 的最小尺度是(10, 13),但激活后感受野可能仍然偏大。常见做法是用 k-means 重聚类自己的框,但作为课程设计,一个更经济的方案是直接保留原始 anchors,因为yolo层的预测是基于归一化偏移的,模型可以通过学习把 anchor 中心移到正确位置,只是收敛难度略有增加。
下表是yolov3.cfg中三个尺度对应的 anchors 和特征图尺寸关系:
| YOLO 层 | 特征图大小 | 负责目标尺度 | Anchors |
|---|---|---|---|
| 第 1 个 yolo | 13x13 | 大目标 | 116x90, 156x198, 373x326 |
| 第 2 个 yolo | 26x26 | 中目标 | 30x61, 62x45, 59x119 |
| 第 3 个 yolo | 52x52 | 小目标 | 10x13, 16x30, 33x23 |
参数说明:如果你发现无人机图像里的车辆只有二三十像素,说明模型需要依赖第 3 个 yolo 层,也就是 52x52 特征图。此时要保证输入分辨率不要太小,width和height最好保持在 608 或 544,否则小目标在特征图上可能只剩一两个像素,NMS 之后很容易被直接滤掉。
2.3 NMS 后处理的 CPU 实现为什么值得看
资源里的nms_cpu.cpp、nms.cu对应 NMS 的 CPU 和 GPU 实现,vision.cpp主要是图像预处理相关逻辑。很多人在答辩时只会说「我用 YOLOv3」,但如果你能讲清楚 NMS 的阈值作用,会让老师觉得你真读懂了代码。
NMS 做的事是:对每个类别的候选框按置信度排序,保留最高分框,删除与其 IoU 大于阈值的框。nms_cpu.cpp中典型的实现逻辑是:
// 按分数降序排列 std::sort(boxes.begin(), boxes.end(), [](const Box& a, const Box& b) { return a.score > b.score; }); for (size_t i = 0; i < boxes.size(); ++i) { if (boxes[i].suppressed) continue; keep.push_back(i); for (size_t j = i + 1; j < boxes.size(); ++j) { if (boxes[j].suppressed) continue; // 计算 IoU float iou = intersection_over_union(boxes[i], boxes[j]); if (iou > nms_thresh) { boxes[j].suppressed = true; } } }这段代码用双重循环完成抑制,GPU 版本nms.cu则是通过并行计算每个框与其它框的 IoU 矩阵,再把超过阈值的框标记掉。实际使用中,nms_thresh设置在 0.4~0.5 之间。无人机场景下目标小且密集,如果阈值设到 0.6,很容易把相邻车辆重叠框一起保留,导致假阳性升高;如果设到 0.3,又可能漏掉重叠严重的框。
3. 数据集组织与自定义训练全流程:从custom.data到可执行训练
3.1 目录结构和标签格式说明
解压资源后,你会看到coco.data和custom.data两个数据配置文件。用custom.data而不是coco.data,是这套资源最关键的切换点。custom.data告诉 Darknet 你的类别数、训练/验证集路径、类别名字文件位置:
classes=1 train=custom/train.txt valid=custom/valid.txt names=custom/custom.names backup=backup/参数说明:classes只填 1 表示单类目标;train.txt每行是一张图片的绝对路径;names文件里每个类名一行,比如drone或者person;backup目录存放训练过程中的权重。这里要注意,标签文件不是 xml,而是和图片同名的.txt,每一行格式是:
class_id x_center y_center width height其中坐标都是相对于图片宽高的归一化值。比如一张 1920x1080 的图,目标框左上角在 (480, 270),宽 240,高 120,那么对应的值就是0 0.25 0.25 0.125 0.1111这种形式。写大作业文档时,这个换算公式最好直接展示出来,能体现你对数据预处理的理解。
3.2 用 LabelImg 标注并生成训练样本
没有现成数据集的话,可以用 LabelImg 手动标注。安装和启动命令很简单:
pip install labelImg labelImg打开后,在Open Dir里选择无人机图片目录,Change Save Dir里选择标签输出目录,然后确认下PascalVOC模式改成YOLO模式。标注完的每张图会对应一个 txt 文件。之后用一个脚本来划分训练集和验证集:
import os import random img_dir = 'custom/images' label_dir = 'custom/labels' train_list = [] valid_list = [] all_imgs = [f for f in os.listdir(img_dir) if f.endswith(('.jpg', '.png'))] random.seed(42) random.shuffle(all_imgs) split_idx = int(len(all_imgs) * 0.8) for i, img in enumerate(all_imgs): label = img.replace('.jpg', '.txt').replace('.png', '.txt') if not os.path.exists(os.path.join(label_dir, label)): continue if i < split_idx: train_list.append(os.path.join(img_dir, img)) else: valid_list.append(os.path.join(img_dir, img)) with open('custom/train.txt', 'w') as f: f.write('\n'.join(train_list)) with open('custom/valid.txt', 'w') as f: f.write('\n'.join(valid_list)) print(f'train: {len(train_list)}, valid: {len(valid_list)}')这段脚本会先过滤掉没有标签的图片,再按 8:2 划分。要注意的是,DeepStream 或某些训练环境要求 txt 里的路径是绝对路径,否则 Darknet 可能读不到图片。如果你把资源拷贝到别的机器,记得重新生成一遍路径。
3.3 下载预训练权重并启动训练
这里推荐两种预训练权重:darknet53.conv.74是官方在 ImageNet 上预训练的分类权重,适合从头训练;yolov3.weights是完整 COCO 权重,如果你只想做迁移学习微调,就用后者。不过对于无人机场景,我一般用darknet53.conv.74,因为自定义类别和 COCO 类别差异较大,复用 COCO 的 yolo 层意义不大。
训练命令如下:
#!/bin/bash cd darknet ./darknet detector train custom/custom.data cfg/yolov3-custom.cfg darknet53.conv.74 -dont_show -map参数说明:-dont_show表示不弹出实时训练窗口,适合服务器环境;-map会在每 100 个 iteration 后计算 mAP,这是判断收敛状态最直接的指标。训练过程中,日志里会不断输出avg loss,这个值在前 500 个 iteration 会剧烈波动,从几十慢慢降到个位数,属于正常现象。真正需要盯的是Region 82 Avg IOU和Avg Recall,当 IoU 稳定在 0.8 左右的时候,说明模型已经学到了大概的框位置。
如果显存不够,打开yolov3-custom.cfg,把batch=64改小到subdivisions=16或8,这样每次实际送入网络的 batch 就是batch/subdivisions。比如batch=64, subdivisions=16,一次前向就是 4 张图。需要同步调整的是learning_rate,batch 减小后,lr 建议从 0.001 降到 0.0005 左右,否则权重更新步长偏大,loss 容易震荡。
4. 针对无人机图像的小目标优化:从yolov3-tiny.cfg到推理部署调参
4.1 为什么 tiny 版本更适合无人机端侧
如果你交的大作业有现场演示环节,一定要考虑跑在 CPU 或边缘设备上的实时性。yolov3.cfg完整版在 GPU 上能跑 30 FPS 以上,但在笔记本 CPU 上可能只有 1~2 FPS,演示时会非常尴尬。yolov3-tiny.cfg是目前最稳妥的过渡方案,它只有 8 个卷积层和两个 yolo 层,模型体积小一个数量级。
用 tiny 版本需要同步替换权重文件。Darknet 官网有yolov3-tiny.conv.15,这是 tiny 结构的预训练权重。命令和完整版一样,只是把 cfg 换成yolov3-tiny.cfg。tiny 版本训练收敛快,但代价是召回率会下降,尤其对小目标。所以一个折中方案是:先用 tiny 确认数据集和训练流程没问题,再用完整版上 GPU 做最终训练。
4.2 提高小目标召回率的三板斧
无人机俯拍场景里,车辆、行人通常只有十几像素。我实测过一个包含 3000 张无人机图片的数据集,直接用默认 cfg 训练,mAP 在 0.6 左右,但单独统计小目标(面积小于 32x32)的 AP 只有 0.3。做了以下三个改动后,小目标 AP 提高到 0.52。
第一,修改输入分辨率。把yolov3-custom.cfg里的width=608、height=608改成width=704, height=704。这会增大特征图尺寸,让原来的小目标在 52x52 特征图上占据更多像素位。代价是训练速度下降约 30%,显存占用上涨,但效果是最直接的。
第二,调低 NMS 置信度阈值。训练阶段ignore_thresh参数决定了负样本的判定范围。我一般把最后一个 yolo 层的ignore_thresh = 0.7改为0.5,这样模型会认为更多 IoU 较低的框是负样本,从而减少对小目标的抑制。推理时,thresh参数从 0.5 降到 0.25,能捞回不少低置信度的小目标框,但也要接受误检变多的可能。
第三,数据增强。Darknet 自带的angle、saturation、exposure、hue参数打开后能增加鲁棒性,但对小目标来说更靠谱的是 Mosaic 增强。在cfg文件开头加一行:
mosaic=1Mosaic 会把四张图拼在一起训练,让小目标对应的像素区域在 batch 中出现的频率更高,同时增加了遮挡和上下文变化。开启后发现训练 loss 的收敛速度会变慢,但最终 mAP 通常更高。
4.3 常见训练崩溃与解决方法
训练时遇到nanloss 是很多同学最容易慌张的。这个问题的根因通常是学习率太大或梯度爆炸。第一步先把learning_rate=0.001降到0.0001,同时把burn_in设置成 1000,让学习率在前 1000 个 iteration 缓慢爬升。如果还是 nan,就检查标签文件里是否有越界的width或height,Darknet 对超出 [0,1] 的框会算出负数损失,直接导致数值异常。
还有一个经典坑是custom.data中names文件路径写错,训练启动后日志打印Couldn't open file: custom/custom.names,这种情况一秒钟就能定位。另一种更隐蔽的:训练和验证图片路径有中文或者空格,Darknet 的 C 代码对路径解析不友好,建议全部改成英文目录。
下表是几个关键参数的推荐区间,对应我在无人机数据集上的经验值:
| 参数 | 默认值 | 无人机小目标推荐值 | 说明 |
|---|---|---|---|
| width/height | 416 | 608 或 704 | 越大越利于小目标,越吃显存 |
| learning_rate | 0.001 | 0.0005~0.001 | 大 batch 用 0.001,小 batch 降一半 |
| burn_in | 1000 | 1000 | 前 1000 次迭代线性升 lr |
| ignore_thresh | 0.7 | 0.5 | 最后一个 yolo 层可单独调低 |
| nms_thresh | 0.45 | 0.4 | 推理时控制重叠框保留 |
| mosaic | 0 | 1 | 显著提升小目标召回 |
5. 大作业演示:把训练好的权重变成看得见的检测画面
5.1 一行命令跑出检测结果
训练完成后,backup/目录下会生成yolov3-custom_final.weights。这时你可以直接用 Darknet 官方命令做单张图片推理:
./darknet detector test custom/custom.data cfg/yolov3-custom.cfg backup/yolov3-custom_final.weights data/drone_test.jpg -thresh 0.35检测结果会输出到predictions.jpg。但如果只做到这一步,课程设计的演示效果还是太单薄。更好的方式是写一个 Python 脚本,把 OpenCV 读取视频、逐帧推理、绘制框整合在一起。这里用ctypes调用 Darknet 的 Python 接口,或者更简单点,直接读取命令行的输出再还原坐标。
我用过的另一种方式是把权重转成 ONNX,再交给 OpenCV 的 DNN 模块做推理,这样就不依赖 Darknet 环境了。转换工具是darknet2onnx,转换后 Python 推理代码如下:
import cv2 import numpy as np net = cv2.dnn.readNet('yolov3-custom.onnx') # 加载转换后的模型 img = cv2.imread('drone_test.jpg') h, w = img.shape[:2] blob = cv2.dnn.blobFromImage(img, 1/255.0, (704, 704), swapRB=True, crop=False) net.setInput(blob) outs = net.forward(['yolo_82', 'yolo_94']) # 两个输出层名称 boxes, confs, class_ids = [], [], [] for out in outs: for detection in out: scores = detection[5:] class_id = np.argmax(scores) conf = scores[class_id] if conf > 0.35: cx, cy, bw, bh = detection[:4] * np.array([w, h, w, h]) boxes.append([cx - bw/2, cy - bh/2, bw, bh]) confs.append(conf) class_ids.append(class_id) # 使用 DNN 自带的 NMS idx = cv2.dnn.NMSBoxes(boxes, confs, 0.35, 0.4) for i in idx: x, y, bw, bh = boxes[i[0]] cv2.rectangle(img, (int(x), int(y)), (int(x+bw), int(y+bh)), (0, 255, 0), 2) cv2.putText(img, f'drone: {confs[i[0]]:.2f}', (int(x), int(y)-8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite('result.jpg', img)这段代码的说明:blobFromImage做了 resize 和归一化,输入尺寸必须和训练时的width/height一致,否则检测框会严重偏移。outs里拿到的是每个网格单元预测的 3 个框,所以detection前 4 位是中心点坐标和宽高,注意它们是相对于原图的归一化值,所以要乘上原图尺寸才能画框。cv2.dnn.NMSBoxes的作用和前面nms_cpu.cpp里的一致,只是 OpenCV 帮你封装好了。
5.2 把检测结果做成交互式演示
如果你的课程设计需要带界面,用 PyQt 或者 Tk 搭一个简单窗体,上传图片、显示检测结果、显示耗时,三块就够。但作为硬件资源有限的场景,有个更快捷的演示思路:把推理过程录制成短视频,在报告中嵌入关键帧。因为无人机图像检测的答辩,老师更关心的是你有没有理解「小目标检测」这个难点,而不是界面用了什么框架。
在演示时,建议准备三类素材:一张包含多个小型目标的航拍图;一张正常尺寸目标的地面图;一段无人机飞行视频。这样既能展示模型对不同尺度目标的响应,也能暴露对极小目标的限制。实际上,当你打开结果图,上面有几个未检出的微小目标时,反而可以顺势解释「YOLOv3 的 anchor 设计对极小目标的先天不足」,说明你清楚模型的边界,这比声称完美检测更有说服力。
5.3 针对大作业的高分包装技巧
源码资源里既然带了yolov3.cfg和custom.data,就说明你可以在报告里完整展示从配置文件到训练命令的整条链路。我一般会建议在报告里附带一张表格,记录实验对比:完整版 vs tiny 版在不同输入分辨率下的 mAP、FPS、模型大小。这张表可以直接用训练时-map输出的结果填写。
最关键的一点是,不要把训练日志原文贴上去,而是挑出avg loss下降曲线和 mAP 变化,用 matplotlib 画成折线图。老师看论文式的大作业,最吃这套。你可以用训练日志里0: 2.34, 100: 1.52, ...这样的数据点,简单解析后画图,这一小步能让整体完成度立刻上一个档次。
本文还有配套的精品资源,点击获取