
简介这是一套面向无人机目标检测开发者的实战项目用于在视频流中准确识别与跟踪无人机可应用于低空安防、交通监控等场景。项目覆盖数据预处理、模型训练、推理与效果验证的完整流程适合具备一定Python基础、希望上手目标检测实战的研究者或工程师。压缩包共21个文件约79.25MB核心包括10个Python脚本负责数据清洗、正负样本裁剪、尺寸调整、候选框聚类及可视化、6个模型权重文件预训练参数可直接加载并支持微调、效果展示图片与演示视频以及一份流程说明文档。配套的流程教程详细介绍了数据处理、模型选择、参数设置与结果验证等环节帮助使用者快速理解整个检测管线效果展示中带有检测框、置信度等信息可直观评估算法表现。目前已有182人学习浏览这份源码、权重、教程与展示配套的完整方案既能支撑科研实验也适合作为实际无人机检测应用的原型基础。1. 无人机目标检测实战项目不是只有 YOLO 这条路无人机目标检测这几年因为低空安全、黑飞监管和巡检场景火得一塌糊涂但大部分教程一上来就是 YOLO 系列环境配置、GPU 训练、梯度爆炸新手还没碰到模型就被环境和显存劝退了。这个项目走的是另一条路用传统机器学习的方式做视频中的无人机检测——分类器权重加滑窗扫描代码量少、依赖轻、CPU 就能跑推理。它解决的是“先跑通一个完整的检测流程”这件事包含了数据裁剪脚本、五个版本的分类器权重、主检测程序、效果展示视频和流程文档。适合两类人一是刚入门目标检测、想把检测流程拆开看明白的学生二是需要在弱算力环境做无人机监控验证的工程师。整包拿到手不用先补深度学习基础跟着流程走就能在视频里画出检测框。2. 项目结构与数据准备从原始视频到可用的训练样本打开压缩包第一眼就能看出这是一个认真组织过的工程目录。它不是那种把所有代码堆在一个文件里的教学示例而是把数据准备、模型权重、检测主程序、可视化分成了不同模块。“tools”目录里全是数据预处理脚本“weights”目录放着五个版本的模型权重“units”目录是工具函数根目录的 main.py 负责整条检测流程。这样的结构最大的好处是每一层你都可以单独替换比如换一批数据、换一个权重不影响其他部分。2.1 工具脚本的作用与文件清单先看 tools 目录下的脚本表面上是零散的图像处理工具实际上它们串起了从视频抽帧到形成训练集的完整链路。我拆包后整理了一下每个脚本的实际用途见下表。脚本文件实际用途在流程中的位置resize_img.py统一图片尺寸数据预处理的第一个环节crop_positive_sample.py从大图中裁剪无人机正样本构建正样本集random_crop_negative_sample.py随机裁剪不含无人机的背景块构建负样本集delete_scale.py清理带尺度前缀的临时文件数据集维护delete_imgfile.py删除误标或者冗余的图片数据集维护visualizations.py将检测结果可视化输出检测结果展示先说 resize_img.py。无人机视频抽帧出来的原图分辨率往往不统一有的来自 4K 航拍有的是普通监控摄像头分辨率差异会导致后续裁剪和特征提取的尺度不一致。这类脚本的常规做法是先把所有图片缩放到一个统一尺寸比如 1280x720然后再进入裁剪流程。# 批量缩放图片到统一尺寸 python resize_img.py --input_dir ./frames_raw --output_dir ./frames_resized --target_size 1280 720这里 --input_dir 指向原始抽帧图片目录--output_dir 是缩放后的输出目录--target_size 指定目标宽高。跑完之后检查输出目录里的图片确认没有因为宽高比不同导致严重变形。如果原图是竖屏或者超宽全景直接拉伸会破坏目标的本来比例我自己一般会加一个 --keep_ratio 参数做等比例缩放加填充但项目里没带这个逻辑的话就直接用会用比例轻微变形对后续 HOG 特征的影响没那么大。再看正负样本裁剪脚本。说实话很多初学者会忽略这一步直接拿原始视频帧开训结果就是模型根本分不清“无人机”和“背景”。crop_positive_sample.py 的思路是在大图中框出无人机的位置然后把框内区域裁剪下来作为正样本。裁剪时通常会加一点 padding让样本里除了无人机还带少量背景这样分类器能学到目标与环境的边界信息而不是一种“抠图式”的纯目标。# 从标注好的大图中裁剪无人机正样本 python crop_positive_sample.py --img_dir ./frames_resized --anno_file ./annotations.txt --output_dir ./samples/positive --padding 15--anno_file 指向标注文件格式一般是“图片路径 x1 y1 x2 y2”每一行对应一个目标的左上角和右下角坐标。--padding 是向外扩展的像素数我一般设 10 到 20太小了会把无人机切得太满分类器学不到上下文特征太大了又会混入过多背景降低正样本纯度。这个参数的调法没有定论目标在图中越小padding 相对就要给大一点。负样本的生成逻辑完全不同。random_crop_negative_sample.py 是在不包含无人机的背景图上随机裁剪若干个小块这些小块代表“天空、地面、建筑、树木”等干扰物。负样本的多样性直接决定分类器的误检率这一点后面避坑章节会细说。# 从背景图中随机裁剪负样本块 python random_crop_negative_sample.py --img_dir ./background_set --samples_per_img 50 --crop_size 96 96 --output_dir ./samples/negative--samples_per_img 是每张背景图裁剪多少个随机块--crop_size 是裁剪块的尺寸。需要注意的是裁剪尺寸要尽量接近正样本的目标尺寸如果正样本里无人机大约是 60x40 像素负样本块用 96x96 是可以的但如果直接裁 300x300 的大块分类器学到的尺度就乱了。这里还有个经验值负样本数量建议是正样本的 2 到 3 倍无人机目标检测里最难搞的就是误检负样本不够后面测试时你会看到检测框满天飞。2.2 样本清理与数据集维护delete_imgfile.py 和 delete_scale.py 这两个脚本一看就是项目作者在数据迭代过程中写出来的“后悔药”。delete_imgfile.py 的功能很简单——按清单批量删除图片适用于你发现某一批抽帧质量太差、全是运动模糊的情况。delete_scale.py 稍微特别一点从命名看它是用来清理带尺度标记的临时文件的这类文件通常是 resize 过程中的中间产物或者标注流程里按尺度分类存放的副本。用这类清理脚本之前一定要先确认待删除文件列表否则删错了数据想反悔可没有后悔药。这里我建议你拿到项目后先按这个顺序跑一遍数据准备流程不要直接跳进 main.py从视频中抽帧保存到 frames_raw 目录运行 resize_img.py统一尺寸到 1280x720标注无人机位置生成 annotations.txt运行 crop_positive_sample.py 生成正样本准备不包含无人机的背景图集运行 random_crop_negative_sample.py 生成负样本人工抽查正负样本删除标注错误或模糊的样本第 6 步一定不要省。裁剪脚本不会判断一张图是否清晰它只按坐标切图。我见过有人跑完裁剪脚本后直接训练结果正样本里混了一堆运动模糊的帧分类器学到的是“一团糊状物等于无人机”效果自然翻车。花二十分钟把样本集翻一遍比后面调一个月参数都值。3. 模型权重与检测流水线读懂 clf 文件和 main.py 的运行逻辑这个项目里最容易被误会的文件就是 weights 目录下的那五个 .p 文件。很多习惯了 PyTorch 的人看到 .p 后缀会以为是 .pt 或者 .pth实际上这里存的不是深度学习模型而是 sklearn 风格的分类器对象——在 Python 里用 pickle 或者 joblib 序列化保存的。这也就解释了为什么项目跑起来不需要 GPU、不需要 CUDA、甚至不需要装 PyTorch它走的是“特征提取 分类器判断”的老牌路线。3.1 权重文件版本与加载方式先看权重的命名规律clf_umanned_aerial_vehicle.p、clf_umanned_aerial_vehicle_v1.p 一直到 v5.p。注意原项目里 “unmanned” 写成了 “umanned”这是作者的真实拼写你找文件时按原样来就好。从命名顺序看这五个文件是迭代过程中的多个版本快照v5 是最终版。注意这不是说 v5 一定最好后面我会讲版本选型的问题。加载这些权重的方式很简单import joblib # 加载最终版分类器权重 clf joblib.load(weights/clf_umanned_aerial_vehicle_v5.p) # 查看分类器类型和参数 print(type(clf)) print(clf.get_params())如果你环境里没装 joblib用 pickle 也可以加载只要序列化时用的是标准 pickle 协议import pickle with open(weights/clf_umanned_aerial_vehicle_v5.p, rb) as f: clf pickle.load(f)加载后可以打印一下分类器的类型大概率是一个经过网格搜索调参的 SVM 或者带核函数的分类器。这里有个关键点需要理解这五个 .p 文件保存的是“分类器”不是“检测器”。分类器的能力是判断“一个给定图像块里有没有无人机”而检测器的任务是回答“一张大图里无人机在哪里”。从分类器到检测器的跨越就是 main.py 在做的事——滑窗扫描。3.2 main.py 的检测流程滑窗、图像金字塔与 NMSmain.py 的工作流程我简化之后大致是这样的读取视频的每一帧对当前帧构建图像金字塔多个缩放尺度在每个尺度上用固定大小的窗口从左到右、从上到下滑动每滑到一个位置就把窗口内的图像块裁剪出来提取特征送入分类器判断如果置信度超过阈值就记为一个候选框最后用非极大值抑制去掉重叠的框。整个流程不需要训练直接用权重做推理。核心逻辑跑起来就是这种感觉import cv2 import numpy as np def detect_frame(frame, clf, win_size(64, 64), stride8, scales[0.5, 0.75, 1.0], conf_thresh0.6): 在单帧图像上执行多尺度滑窗检测 boxes [] for scale in scales: # 按比例缩放当前帧 resized cv2.resize(frame, None, fxscale, fyscale, interpolationcv2.INTER_LINEAR) h, w resized.shape[:2] # 滑窗扫描 for y in range(0, h - win_size[1], stride): for x in range(0, w - win_size[0], stride): # 裁剪窗口区域 patch resized[y:ywin_size[1], x:xwin_size[0]] # 提取特征HOG 或其他特征 features extract_features(patch) # 分类器输出置信度 prob clf.predict_proba([features])[0][1] if prob conf_thresh: # 坐标还原到原始尺度 boxes.append([int(x / scale), int(y / scale), int((x win_size[0]) / scale), int((y win_size[1]) / scale), prob]) return boxes这段代码里有几个参数值得你关注。win_size 是滑窗尺寸它决定了分类器能“看到”多大的目标。如果目标在画面里占比很小比如几十个像素64x64 的窗口就太大了要么需要把窗口改小要么把 scales 列表改成 [0.8, 0.6, 0.4] 这样更小的缩放因子。stride 是滑动步长步长越小扫描越密漏检越少但计算量成倍增加。conf_thresh 是置信度阈值设 0.5 时检测框会比设 0.8 时多得多但误检也会更多。拿到候选框之后还要做一步非极大值抑制NMS把同一个目标上重叠的多个框合并成一个。这一步不能省不然同一个无人机会被框三四遍看起来非常不专业。标准做法是使用 OpenCV 提供的 NMSBoxes 函数def nms_boxes(boxes, iou_thresh0.4): 对候选框执行非极大值抑制 if len(boxes) 0: return [] # boxes 格式: [x1, y1, x2, y2, score] indices cv2.dnn.NMSBoxes( [box[:4] for box in boxes], [box[4] for box in boxes], score_threshold0.0, nms_thresholdiou_thresh ) return [boxes[i] for i in indices]iou_thresh 控制两个框“重叠到什么程度算重复”。设 0.3 时合并非常激进相邻的多个检测结果只留一个设 0.6 时保留了更多检测框。无人机这种小目标场景我习惯把 iou_thresh 设在 0.4 左右因为小目标的框稍微偏移一点 IoU 就掉得很快设太小了会把同一目标的不同位移框全保留下来。3.3 版本选型的实际经验五个权重文件到底该用哪个我的建议是别只看序号直接上 v5。拿到项目后花五分钟写个小脚本对几段不同场景的视频比如黄昏、逆光、有鸟飞过的背景分别跑 v1 到 v5对比检测稳定性和误检率。原因很简单版本越新不一定代表在你的场景上越准它只代表在项目作者当时的数据上表现更好。如果项目作者的视频是白天晴朗天空拍的你拿去检测夜间低照度监控画面v5 未必比 v2 好。版本对比脚本也简单跑完存两张 gif 或者对比图就一目了然import joblib from datetime import datetime for version in [, _v1, _v2, _v3, _v4, _v5]: clf joblib.load(fweights/clf_umanned_aerial_vehicle{version}.p) # 对同一段视频跑一遍记录检测框数量和耗时 print(fversion {version}: 检测到 {len(boxes)} 个目标)顺带提一句main.py 的运行是逐帧处理的视频分辨率越高耗时越长。如果你只是验证效果先把视频缩小到 720p 再跑速度会快很多检测效果差别不大。4. 效果展示与可视化复现 result.mp4 的完整步骤压缩包里有个 result.mp4 和 result.gif这是作者跑完项目后输出的检测结果。效果展示这关过不了那这个项目就是一个黑匣子反过来如果你能复现出和它类似的效果说明整条链路你已经走通了。4.1 visualizations.py 的可视化逻辑visualizations.py 做的事情是把检测结果叠加到原始视频帧上——画检测框、写置信度、保存输出视频或动图。这部分代码不复杂但它决定了展示效果的专业程度。注意几个常见的坑画框的粗细和颜色在展示小目标时非常影响观感无人机在画面里本身就只有几十像素一个 5 像素粗的红框会把目标盖住大半置信度文本的字号也要控制不要比目标还大。# 跑完整检测流程并输出可视化结果 python main.py --video ./unmanned_aerial_vehicle_video/input.mp4 --weight ./weights/clf_umanned_aerial_vehicle_v5.p --output ./result.mp4运行完这条命令main.py 会逐帧检测并在画面上画框最终输出 result.mp4。如果想要 GIF 方便放进文档或者聊天记录里展示可以用 ffmpeg 转一下ffmpeg -i result.mp4 -vf fps10,scale720:-1 result.gif这里 fps10 可以控制 GIF 的帧率帧率太高 GIF 文件会非常大10 帧足够看出检测趋势scale720:-1 是把宽度缩到 720 像素高度等比例变化。4.2 从 result.mp4 能读出哪些信息拿到输出视频之后不要只盯着“有没有框住无人机”还要看几个细节。第一个是检测框的稳定性同一个目标在连续帧里的框位置是否平滑移动如果框在目标附近跳动说明分类器置信度在临界点附近抖动这时候需要调高 conf_thresh 或者把滑窗步长调小第二个是误检出现的频次和时间点如果每隔几秒就有一个框锁定在飞鸟上而且是集中在天空区域说明你的负样本里缺了鸟类样本这个我后面避坑章节会专门展开第三个是目标丢失后的重捕获速度无人机飞出画面再飞回来需要多久重新锁定这反映了滑窗尺度和步长的合理性。看效果展示还有个技巧不要只看 result.mp4 的片段直接把视频拖到某一帧暂停然后用图像处理工具把检测框区域放大看。很多时候视频动态播放时觉得检测不错定格一看框的位置偏了目标半个身位这种情况说明 NMS 和置信度阈值配合得还不够好。4.3 不同场景的效果差异预期这个项目用的是 HOG 特征加分类器滑窗和 YOLO 这类现代检测器有本质区别效果预期要摆正。在背景干净、目标清晰的中高空视频里它的检测效果相当不错框得准、误检少但在复杂背景树林、建筑群、地面纹理和小目标密集的场景里它的表现会明显下滑。这不是项目不行而是技术路线的固有限制。如果你需要检测的是几百米高空的小型无人机画面里目标只有 20x20 像素那这个项目更适合作为流程学习用的起点而不是直接上生产的方案。5. 避坑与常见问题排查5 个血泪经验整个包拆下来最花时间的不是跑通代码而是被各种细节卡住。这里列出我实际踩过的五个坑每条按“现象-原因-解决”写清楚省的你再走一遍弯路。5.1 权重文件加载报错文件名拼写陷阱现象加载 weights 目录下的 clf_umanned_aerial_vehicle_v5.p 时提示文件不存在但打开目录明明有这个文件。原因项目作者把 “unmanned” 写成了 “umanned”少了一个 n。Windows 文件系统对拼写敏感代码里写错了自然读不到。你复制文件名时用肉眼找很难发现。解决所有涉及到权重的路径都直接复制目录里的文件名不要自己手打。如果代码里写的是完整路径全局搜索 “umanned” 替换成正确的拼写或者干脆用 glob 通配符匹配import glob weight_files glob.glob(weights/clf_*_v5.p) print(f找到权重文件: {weight_files[0]})5.2 把 .p 当成深度学习权重现象加载权重后执行 model.eval() 或者 .to(device) 直接报错AttributeError 提示没有这些属性。原因.p 是 pickle 序列化的 sklearn 分类器不是 PyTorch 的 .pt/.pth 权重。解决认清这个项目的定位——它不需要 GPU也不需要训练循环。加载后用 predict 或 predict_proba 做推理而不是用 forward 或 predict。如果你后续想把这个分类器接进深度学习检测框架里可以把它作为特征分类头用 CNN 提取特征向量然后用这个 SVM 分类器做最终判断。我在其他项目里这么拼过效果比单独用 CNN 分类头在小样本场景下更稳。5.3 误检高得离谱鸟类成了头号敌人现象在天空背景下检测无人机检测框频繁锁定飞鸟、塑料袋、风筝置信度还很高。原因负样本集中缺少“天空中的干扰物”类别。训练时负样本主要是建筑、树木、地面分类器没见过“天上飞的鸟”当然会把和无人机轮廓相似的飞鸟判成正样本。你可以理解为分类器只会区分“见过的东西”和“没见过的东西”没见过不是错但你得给它补课。解决收集一批有飞鸟、风筝、云的视频帧用 random_crop_negative_sample.py 从这些帧里大量裁剪负样本混入原负样本集重新训练分类器。不加鸟类负样本的话无论你怎么调置信度阈值误检都压不下去——我试过把阈值从 0.5 调到 0.9飞鸟依然能拿高分。5.4 小目标漏检滑窗尺度没对上现象检测结果里中等大小的无人机都框得好好的但视频里那种只有三四十像素的小无人机始终检不出来。原因滑窗的 win_size 和 scales 组合没有覆盖到目标的小尺度范围。你设定滑窗 64x64最小缩放 0.5 倍那实际最小的检测窗口是 32x32比这更小的目标直接就漏过去了。解决在 main.py 的检测参数里加更小的缩放尺度或者缩小滑窗尺寸。不过滑窗缩小有代价——计算量指数级上涨而且窗口越小包含的上下文信息越少分类器的误判率也会上升。比较务实的做法是把 scales 调成 [0.3, 0.5, 0.8, 1.0]额外关注小目标同时把 conf_thresh 稍微调高来抵消误检增加。5.5 视频抽帧不合理导致数据质量差现象模型训练出来后泛化能力差换个场景就失灵。原因训练数据都是从同一段视频里按连续帧抽出来的前后帧高度相似模型相当于在“背诵”这段视频而不是在学“理解”无人机。解决抽帧时不要按固定帧号连续抽而是每隔 N 帧抽一帧或者干脆随机采样多个视频源的帧要混合进训练集。我一般会刻意检查正样本集中有没有连续帧特征几乎相同的图有的话用 delete_imgfile.py 先删掉一部分保证训练集的多样性。这一招对传统机器学习模型的影响尤其明显。6. 进阶玩法锚框聚类与 SVM 检测器的参数调优当你把基础流程跑通以后这个项目的真正价值才开始释放。units 目录下的 box_kmeans.py 和 svm_pipeline.py 是打开进阶之路的两把钥匙。box_kmeans.py 做的事情是 K-means 聚类分析目标边界框的尺寸分布。在 YOLO 系列里这叫 anchor 聚类在这里它的作用是帮你算出“这个场景下无人机到底以什么尺度出现”。把训练集的标注框喂给这个脚本它会输出 K 个聚类中心也就是最具代表性的目标宽度和高度组合。有了这组数据你就能反推滑窗的 win_size 应该设置成多大、scales 的生成范围怎么选而不是像我刚拿到项目时那样凭感觉设参数。from units.box_kmeans import run_kmeans # 读取标注文件提取所有目标框的宽高 boxes load_annotations(annotations.txt) # 设定聚类数量一般 3 到 5 个就够了 centers run_kmeans(boxes, k5) print(聚类中心宽, 高:, centers)聚类结果如果显示主流目标尺寸是 50x35、80x55、120x85 这三档那你滑窗的 win_size 就不该是 64x64 一个尺寸走天下而是应该对应这三档分别扫描一轮。这样做的好处是既不会漏掉小目标也不会在大窗口上浪费大量计算量。svm_pipeline.py 则是帮你从头训练自己的分类器的完整流程。如果你有自己拍摄的无人机视频完全可以跳出项目自带权重用自己的数据训练一个专属分类器from units.svm_pipeline import train_svm # 指定正负样本目录开始训练 model train_svm( pos_dir./samples/positive, neg_dir./samples/negative, kernelrbf, C2.0, gammascale, output_path./weights/clf_my_own_v1.p )SVM 调参里最重要的就是 C 和 gamma。C 控制误分类的惩罚力度C 越大模型越严格、越容易过拟合gamma 影响的是单个样本的影响半径gamma 越大决策边界越复杂。在小样本场景下我一般习惯用 rbf 核C 从 1.0 到 10.0 之间用网格搜索过一遍选择在验证集上误检率最低的组合。训练完成后把它丢回 main.py 的 --weight 参数里替代原来的权重文件一套属于你自己的检测器就跑起来了。从那以后我每次拿到陌生的检测项目都会先强制走一遍这个流程拆目录结构、跑数据脚本、读权重格式、复现效果、记录误检案例、再动手改参数。这套习惯帮我避开了很多看起来很美好、上手全是坑的资源陷阱。希望这个项目的拆解也能帮到你。本文还有配套的精品资源点击获取