项目标题里写的是"yolov13"的无人机检测系统,我拿到手第一反应是版本号有点超前,毕竟目标检测社区目前主线还没排到v13。但做工程的人都知道,项目文件夹里的权重文件叫什么版本不重要,重要的是能不能把模型加载起来、推理结果准不准、和Web框架配合顺不顺。我最近帮某高校课题组做过一套类似的无人机检测系统,后端用Python加Django,模型用YOLO系列,支持图片上传检测、视频文件检测、摄像头实时检测,从零到跑通大概花了两周时间,配套技术文档也一并整理完毕。本文把这套系统的完整做法整理成一份可落地的参考记录,内容包括模型选型、数据集处理、训练参数、Django接口设计、实时视频流方案、技术文档框架以及常见坑点,适合课程设计、课题演示,也给打算把YOLO接到Web服务里的同学一条清楚的路线。
使用这套系统的人不需要懂深度学习,浏览器打开页面就能操作。上传一张天空照片,几秒后返回带边界框的标注图;上传一段监控或航拍视频,系统会自动逐帧标注;如果现场部署了网络摄像头,页面上的实时画面会叠着检测框。所谓技术文档,也不是贴一堆日志,而是把环境搭建、接口调用、模型替换、部署上线这几件事讲明白,让别人照着文档能从零搭起来。
1. 项目定位与整体方案
1.1 场景确定:检测无人机还是用无人机检测
动手之前必须先把需求收敛。做这套系统的人通常有两种诉求:一种是地面端识别空中无人机,比如监控画面里有没有不明飞行物靠近,属于安防和低空监管场景;另一种是摄像头装在无人机上,让它自动识别地面行人、车辆、电线塔,属于巡检场景。标题"无人机检测系统"更偏向把无人机作为被检测对象,所以我下面的方案以识别空中无人机为主。如果你的需求刚好反过来,是无人机视角的下视检测,那也不用推翻架构,只需要把数据集换成对应的地面目标数据,再调整训练时的输入分辨率和anchor尺寸就行。
这点我建议拿到需求先确认清楚,否则做到一半返工很痛苦。当时我们开需求会时,有人以为是无人机挂载摄像头检测地面,有人以为是反无人机预警,两边聊了半小时才发现说的不是同一件事。后来我们把使用场景、摄像头安装位置、目标尺寸范围写成一页需求确认单,所有人才对齐。
1.2 为什么选YOLO家族加Django
目标检测算法现在可选范围很广,有双阶段的、有单阶段的。无人机检测看重实时性,单阶段的YOLO系列天然有优势。YOLO把目标检测当成回归问题,输入图片经过卷积神经网络,直接输出目标框的中心坐标、宽高、置信度和类别概率。对比双阶段方法,它在速度上快很多,在普通CPU服务器上也能做优化部署。加上社区资料非常密集,训练、导出、部署各个环节踩过的坑基本都能搜到解决方案,很适合做这种要交付的Web系统。
Django的选择理由也很直接。它自带Admin后台、ORM、模板引擎,表单处理和静态资源处理都很方便,适合快速搭出一个能上传、能展示、能看实时画面的Web应用。对比Flask,Django的项目结构更规整,模块边界清楚,多人协作和后面接数据库、用户权限、Session都不需要自己造轮子。写技术文档的时候也更好写,每个模块职责清晰,别人阅读代码时不容易迷路。
这里不是贬低Flask,如果你的系统只需要一个算法演示接口,Flask确实更轻。但我们这套系统最终要包含用户上传、结果管理、实时预览、文档说明这些完整功能,Django的自带能力能省下不少重复工作。
1.3 系统架构与工作流
整体架构分三层。最外层是浏览器页面,分成图片检测、视频检测、实时检测三个功能入口;中间层是Django的urls、views,负责接收请求、处理上传文件、调用检测服务;底层是模型推理模块,封装成一个Detector类,对上层屏蔽模型细节。数据层保存用户上传的原始文件、检测结果和标注后的文件。
一个典型请求流程是这样的:浏览器上传图片,views拿到文件后调用Detector,Detector加载权重推理得到目标框列表,再调用绘制函数把边界框画到原图上,保存成带时间戳的新文件,最后把结果图片URL和JSON数据返回给前端显示。视频检测和实时检测的流程在图片检测基础上扩展,后面会详细讲。
一个很重要的建议是:模型推理逻辑不要直接写在views里,单独拆成detector模块。这看起来是多建了一个文件,但好处后面会越来越明显。Django开发服务器可能同时处理多个请求,如果模型推理和请求逻辑耦合在一起,很容易出现线程安全和重复加载的问题。单独拆出来,后面换成TensorRT、换成其他模型,底层逻辑都不需要动。
2. 模型准备:从数据集到权重文件
2.1 数据集与标注细节
要在实际场景里识别无人机,数据质量和数量非常关键。网上可以找到一些公开的无人机检测数据,场景多是天空、楼宇、树林背景,包含无人机和飞鸟这类难样本。如果找不到完全匹配的数据集,一个稳妥做法是先用通用目标检测数据集做预训练,再用自采数据做增量训练。自采数据时尽量覆盖不同高度、不同天气、不同分辨率,画面里无人机目标的大小分布也要均匀,避免全是近距离的大目标,否则模型对远处小目标完全没有泛化能力。
标注格式建议直接用YOLO的txt格式,每行内容为class x_center y_center width height,坐标统一归一化到0到1。把无人机类别编号设为0。标注工具有多种选择,常见的图形化标注工具都支持YOLO格式输出。一个值得注意的细节是,标注框不要只框无人机机身,要把桨叶旋转区域大致圈进去,因为模型学到的特征包含外形轮廓和运动模糊,框太小反而学不到上下文,推理时遇到稍微远一点的目标就框得不准。
数据集的划分也要动脑子。一般按8比1比1分成训练集、验证集、测试集,建议写脚本按目录划分,而不是简单的随机打乱所有图片。尤其是无人机视频抽帧得到的数据,同一个视频里相邻帧非常相似,如果同时混进训练集和验证集,验证集指标会虚高,真实场景表现立刻露馅。
2.2 训练关键参数说明
训练时的输入分辨率建议至少640,如果无人机在画面里占比非常小,可以尝试1280。分辨率提高之后,模型对小目标的召回率会有明显提升,代价是显存占用和推理时间同步上升。训练轮数大概在100到300之间,我习惯从100轮起步,观察验证集损失曲线不再下降再考虑继续。batch size要根据显卡显存来调,8G显存跑640分辨率时设到8或16问题不大,再往上容易出现显存溢出。优化器可以选SGD或AdamW,初始学习率SGD设0.01、AdamW设0.001,调度器配合余弦退火。
训练过程中要关注两个核心指标:mAP50和mAP50-95。做无人机项目时还需要额外关注Recall,因为漏掉一个真实目标比多框一个虚警目标更严重,毕竟这是偏向预警的系统,漏报意味着防线被穿透。如果召回率上不去,可以先把置信度阈值调低,再看小目标分支的anchor尺寸设置是否合理。
2.3 模型导出与推理封装
训练得到的权重文件往往依赖对应版本框架的完整环境,放到Django服务器上之前最好统一导出成ONNX格式。ONNX Runtime部署时不需要完整训练环境,只需要装onnxruntime和OpenCV,依赖干净很多。导出时注意开启动态输入尺寸,这样部署时可以自由切换640、960、1280分辨率,不用重新导出。
推理封装类我命名为Detector,对外只暴露一个detect方法。初始化时加载ONNX模型,读取输入尺寸和类别数,内部做前处理、推理、后处理,最后返回标准格式的检测结果。前处理要做BGR转RGB,按letterbox方式缩放到模型输入尺寸,再除以255归一化。后处理要做置信度过滤和非极大值抑制。这部分逻辑可以借助目标检测框架自带的工具函数,也可以自己写一个几十行的小工具,后者更可控。
import cv2 import numpy as np import onnxruntime as ort class Detector: def __init__(self, model_path): self.session = ort.InferenceSession(model_path) self.input_name = self.session.get_inputs()[0].name def preprocess(self, img, size=640): h, w = img.shape[:2] scale = min(size / h, size / w) new_h, new_w = int(h * scale + 0.5), int(w * scale + 0.5) resized = cv2.resize(img, (new_w, new_h)) canvas = np.full((size, size, 3), 114, dtype=np.float32) canvas[:new_h, :new_w] = resized / 255.0 return canvas.transpose(2, 0, 1)[None], scale def postprocess(self, outputs, conf_thres=0.25, iou_thres=0.45): # 略:解析坐标、过滤低置信度、执行NMS return boxes def detect(self, img): input_tensor, scale = self.preprocess(img) outputs = self.session.run(None, {self.input_name: input_tensor}) return self.postprocess(outputs)模型导出和推理封装这两个环节,看起来是纯工程操作,实际上决定系统后面好不好维护。我见过很多项目模型能跑但没法交给别人用,就是因为推理代码和Web代码缠在一起,换一台机器配置半天,最后只能由写代码的人亲自部署。封装成独立模块之后,别人只需要替换模型路径,其他都不用动。
3. Django整合:图像、视频、摄像头三种检测
3.1 工程初始化与模型单例
Django部分我逐步说明。新建项目后创建一个app,比如命名为detector。在settings.py里配置MEDIA_URL和MEDIA_ROOT,用于存放上传文件和结果。在项目跑起来之前,先处理Detector的单例问题,保证整个Django进程只加载一次模型。实现方式不复杂,用模块级全局变量加threading.Lock保护初始化过程。
import threading from .inference import Detector _detector = None _lock = threading.Lock() def get_detector(): global _detector if _detector is None: with _lock: if _detector is None: _detector = Detector("weights/model.onnx") return _detector这里要给出明确警告:不要在Django view里每次请求都重新实例化Detector,否则内存会快速上涨,几个并发请求就能把服务器拖垮。单例模式是Web推理服务最基础的要求,先做到这一点再谈优化并发。
3.2 图像检测接口实现
图像检测接口逻辑上最简单。前端表单提交图片文件,views通过get_detector拿到模型,读入图片,调用detect得到结果,绘制到原图上保存。响应使用JSON格式返回目标框坐标、置信度和标注图URL,前端再把结果渲染到页面。
def image_detect(request): if request.method != "POST": return JsonResponse({"code": 1, "message": "only POST"}, status=405) f = request.FILES.get("image") if not f: return JsonResponse({"code": 1, "message": "no image"}, status=400) img = load_image_from_bytes(f.read()) detector = get_detector() boxes = detector.detect(img) result_img = draw_boxes(img, boxes) save_path = save_result(result_img) return JsonResponse({ "code": 0, "boxes": boxes, "image_url": "/media/" + save_path })这里必须提醒一个非常容易踩的坑:OpenCV的imread方法不支持中文路径,所以不要直接把上传文件存成带中文文件名后调用cv2.imread读取。要么把文件名统一改成uuid格式,要么像上面那样用字节流解码。我第一次做这个项目时就是被中文文件名卡了半下午,上传一张"无人机测试.jpg"直接报错,换成随机文件名后才正常。
3.3 视频检测与异步进度
视频检测比图片复杂,因为视频处理耗时长,一个30秒的视频可能需要处理几十秒甚至几分钟,浏览器HTTP请求同步等待完全不现实。我采用的方案是:上传视频后立刻返回一个task_id,后台用全局字典记录任务状态,前端每1秒轮询一次查询接口,拿到任务结束后再跳转到结果视频页面。
具体实现是先把上传视频保存到MEDIA目录,然后启动一个daemon线程去处理。线程用OpenCV逐帧读取,每隔若干帧做一次检测,不需要每一帧都推理,可以显著节省时间。所有检测框按帧号记录下来,处理结束后用VideoWriter把标注后的帧写成mp4文件。任务状态字典记录pending、running、done三种状态以及处理进度百分比。
视频处理线程需要设计成可响应停止标记,用户取消任务时能把临时文件清理掉,否则MEDIA目录会越堆越多。还有一个容易被忽略的点:OpenCV的VideoWriter在写mp4时要选对编码参数,常见用avc1或mp4v,否则输出的文件可能在播放器里打不开。如果是放到浏览器里直接预览,建议输出H.264编码的mp4,兼容性最好。
3.4 摄像头实时检测的两种方案
摄像头实时检测我提供两种方案,按部署环境选择。
第一种是浏览器端采集视频帧,适合局域网演示。前端调用getUserMedia获取摄像头流,用canvas定时采样,比如每秒取3到5帧,把jpg图片通过POST发给后端检测接口。后端返回目标框,前端再把框绘制在canvas上。这种方案的优点是后端不需要接触视频流,开发简单,占用带宽小;缺点是帧率受HTTP轮询限制,做不到非常顺滑的实时,但做演示已经完全够用。
第二种方案是后端直接读取RTSP或USB摄像头,适合接入监控摄像头。用OpenCV VideoCapture打开摄像头地址,后台线程循环读取帧并送入Detector,检测结果叠加后通过流式响应推给前端。我常用MJPEG流的方式实现,浏览器用img标签嵌入即可显示。缺点是延迟会累积,需要主动跳帧,例如只处理5FPS,处理不过来就丢弃旧帧。
不管是哪种方案,核心原则一样:实时检测不要每帧都做全分辨率推理,一定要控制帧率。输入分辨率降到640,单帧推理延迟控制在80毫秒左右,配合跳帧策略,用户体验才会稳定。一上来就追求30FPS全分辨率,最后往往视频卡到没法看。
4. 技术文档怎么写
4.1 文档整体结构规划
这套系统的技术文档,我建议按六章组织:项目说明、系统设计、环境搭建、接口与使用说明、模型训练、部署与运维。这个顺序符合开发者的阅读习惯,拿过来可以按顺序一步步操作,而不是翻半天找不到入口。项目说明部分写清楚系统能做什么、支持哪三种检测、功能模块有哪些。系统设计部分画清楚三层架构以及请求流程,配合目录结构说明每个文件和目录的作用。
我见过很多技术文档把环境搭建放在最前面,读者还没搞懂项目是干什么的就开始装包,很容易劝退。先花半页说清楚项目价值和架构,再看环境就顺理成章。文档里不要写太多宣传性词句,保持中性描述即可,重点是可操作性。
4.2 环境准备与快速开始
环境部分最容易出问题,我习惯在文档里放三块内容:开发环境、生产环境、依赖清单。开发环境写清楚操作系统、Python版本、CUDA版本;生产环境分CPU和GPU两种场景,分别给出对应的requirements.txt。快速开始这一节要尽量简短,最好控制在十步以内,让没接触过项目的新同学能在30分钟内把系统跑起来。通常列出数据库迁移、创建超管账号、启动开发服务器、访问测试这四个必做步骤就够,其他内容先不展开。
一个特别常见的坑是CUDA和PyTorch版本不匹配,在安装依赖时直接报错。文档里不要只写"安装PyTorch",要明确到版本号和安装命令,最好直接给出一条适用于当前操作系统的安装命令。这个细节能省大量技术支持的沟通时间。
4.3 API接口文档与使用说明
接口文档使用表格列最清晰,包含请求方法、URL、参数说明、返回字段、状态码、错误说明。这套系统的核心接口是图片检测、视频检测、摄像头实时流三个,每个接口下面补充两个例子,一个使用curl命令,一个使用前端fetch代码,方便联调。返回的JSON字段要定义成稳定格式,比如code、message、data,data里放检测框坐标和结果文件URL。前端页面和后端开发独立并行时,有了接口文档就不用反复口头对字段。
使用说明书是给不懂代码的人看的,要写清楚浏览器版本要求、摄像头权限在哪开、局域网内怎么访问、模型文件放在哪个位置。真实项目里往往不是算法不够好,而是别人不知道环境前置条件。文档把这些细节写清楚,能省下大量答疑时间。
5. 常见问题与优化实录
5.1 推理慢的优化路线
如果你把系统部署到没有显卡的机房或办公室内网,只能用CPU推理,那么必须做优化。优化路线从高到低排序:先导出ONNX并用ONNX Runtime推理,比直接用PyTorch CPU推理快不少;接着考虑int8量化,模型体积和延迟都可能降到原来一半;如果CPU支持AVX512指令集,ONNX Runtime会自动利用这些特性,不用额外配置。GPU环境下可以尝试TensorRT导出,结合批量推理和多级流水线,吞吐量还能继续提升。
视频检测场景尤其怕慢,按帧串行处理很容易让人失去耐心。一个简单做法是抽帧检测,每3帧取1帧做检测,输出时对未检测帧直接复用前一帧的结果,视觉上几乎没有差异,处理速度提升接近3倍。这个技巧在视频检测功能里几乎是必备优化。
5.2 小目标漏检怎么处理
无人机在远距离时目标小,漏检率会明显上升。针对小目标,我实际验证过比较有效的方向有三个:切图推理、提高分辨率、多尺度测试。切图推理是开源社区里解决小目标问题的常用手段,把大图切成重叠小块分别推理再合并结果,可以缓解目标在深层特征图上下采样丢失的问题,代价是推理次数变多。另一个方向是把输入分辨率提高到1280,配合letterbox,对只有几十个像素的目标效果显著。
容易被忽略的一点是置信度阈值不要设太高。小目标本身特征弱,预测置信度天然偏低,设置0.5可能把所有弱特征目标全部过滤掉,压到0.25甚至0.15再看召回率。预警场景宁可多出现几个误检,也不要漏掉真实目标,误检可以后续用规则或者多帧确认机制再过滤。
5.3 Django线程安全和模型并发
Django开发服务器默认可以处理多个请求,但模型推理不是天然线程安全的,尤其是ONNX Runtime的session如果配置了多线程,同一session并发推理可能报内存错误。我采用单例加锁的方式,用threading.Lock把推理过程包住。这种方式同时只允许一个请求推理,其他请求排队等待。对于课程设计和课题演示来说完全够用,如果后续要提升并发能力,建议把模型推理抽成独立服务,再用队列缓冲请求。
还有一点容易踩坑:模型输入张量不能在多个线程里复用,每次推理要重新构造输入缓冲。这个问题很隐蔽,之前视频检测线程和图片请求同时出现时我遇到过内存越界报错,排查了很久才发现是共享了同一份输入数据。现在代码里每次detect都重新创建输入张量,再也没出过类似问题。
5.4 视频流卡顿与延迟
摄像头实时检测的延迟来自采集、推理、网络传输三个环节。采集延迟需要调小OpenCV的缓冲区设置,把RTSP相关延迟参数调低;推理延迟用跳帧和量化解决;网络传输延迟在MJPEG方案里比较难控制,可以降低分辨率,把实际帧率控制在10帧以内,肉眼感知反而更稳定。如果只是本地演示,优先推荐浏览器getUserMedia加HTTP轮询,不折腾RTSP推流,延迟最小,还能绕开浏览器对MJPEG格式的兼容性限制。
5.5 文件路径与编码问题
最后说一个非常基础但总被忽略的问题:OpenCV的imread方法读不了中文路径和中文文件名,Windows系统上尤其明显。上传文件如果带中文名,保存时先转成服务器生成的uuid,读取时用字节流解码而不是直接传路径。视频输出路径同样使用英文,数据库里只存相对路径,展示时由Django用MEDIA_URL拼接,不存绝对路径,否则换机器所有路径都会失效。
这套系统做到最后,真正花时间的反而不是模型精度调优,而是把Django请求、视频流、文件管理、线程并发这些工程细节理顺。我的体会是先把端到端跑通,再回头优化模型,顺序一定不要反。先让一张图从浏览器传到后端,走完检测、结果返回、页面画框这一条完整链路,视频检测和实时检测就都成了在这个链路上加轮询和推流。如果一直停留在notebook里画图,离一个能交付的Web系统还很远。