简介面向本科毕业设计的YOLOv5异常行为检测改进方案资源包适用于计算机视觉、深度学习方向的毕业生及研究者。方案聚焦异常驾驶等动态行为识别场景系统给出四种针对性改进引入Ghostconv轻量化卷积以大幅降低模型参数量引入BiFPN特征融合并增强中小目标检测能力加入CA注意力机制以提升目标框定位精度将CIoU替换为Alpha-EIoU在不引入额外参数的同时有效提升检测精度。压缩包共214个文件以105个yaml模型配置、45个Python训练/推理脚本、20个jpg图片样例为主配合txt、md、sh、Dockerfile等环境与说明文件整体仅2.8MB结构紧凑、易于查阅。资源附带Jupyter教程、Docker化运行环境以及数据增强前后对比图方便从零搭建环境、理解改进点并复现训练流程。目前已有121人学习适合作为毕业设计代码框架或算法优化的参考基线。1. 解析基于YOLOv5的异常行为检测先把“异常”定义在单帧之外设想一个落地场景机房的单路摄像头对着门禁走廊系统要做的不是把画面里的目标分类成“人”而是在一段持续时间内判断出摔倒、打架、突然聚集等异常行为。把这个问题交给 YOLOv5通常会走两条路线把行为当成一个单帧类别来训练或者用 YOLOv5 检测出人之后再用时间窗口中的边界框变化规律去推断行为属性。对于本科毕业设计以及大部分工程初版后者更容易做出稳定效果也更容易解释。完整链路包含解包检查数据集、训练目标检测模型、设计行为判定规则、接入视频流、验证误报率和报警延迟这条主线。本文按这条主线把每一步涉及的参数和踩坑点拆开讲适合已经跑通过基础目标检测、想在行为语义上更进一步的人。2. 解开zip先看数据YOLOv5数据集的目录结构、标注格式与训练命令2.1 不急着解压先用 unzip -l 检查工程内容项目标题是一个 zip 压缩包常见做法是先查看归档内容而不是立刻解到当前目录。理由很实际压缩包里可能放着几十 GB 的原始视频或者训练图片加权重体积超出预期直接解压可能填满磁盘。先列出清单可以确认工程里到底有哪些东西再决定是完整解压还是只挑需要的文件出来。unzip -l 本科毕业设计-基于YOLOv5的异常行为检测.zip | head -50-l参数只列出 zip 内的文件列表而不解压输出信息包含每项的文件大小、日期和压缩包内的路径。没有 unzip 命令时同样可以用7z l查看。我看列表时重点找三类目录dataset/、weights/和runs/train/。dataset/说明训练数据随包提供weights/里有没有best.pt或last.pt决定后面是直接做推理还是先复现训练runs/train/exp*/weights/best.pt则说明项目自带训练产出。确认目录结构没问题后再执行解压unzip 本科毕业设计-基于YOLOv5的异常行为检测.zip -d desktop_final解压到desktop_final而不是当前目录避免多个文件散落在工作区根目录。常见的坑是中文文件名在部分 Linux 系统下显示乱码这是因为 zip 内部编码和系统编码不一致不影响文件内容但会影响后续脚本里的路径引用最好先把目录改成纯英文名再继续。2.2 解包后核对的最小文件清单一个能复现训练的 YOLOv5 异常行为项目解压后至少应该包含下面这些内容文件或目录作用缺失时的后果dataset/images/train/训练图像无法开始训练dataset/labels/train/YOLO 格式标注文件训练时找不到标签dataset/dataset.yaml数据路径、类别数量、类别名--data参数无从指向weights/*.pt预训练或训练好的权重只能重新训练runs/train/训练日志和曲线无法追溯训练效果如果解压后发现weights/是空的不用慌这通常是为了控制压缩包体积刻意不放权重。按 2.4 的命令从零训练或从 COCO 预训练权重继续训练即可。如果labels/不存在说明数据只有图像没有标注需要先用标注工具生成 YOLO 格式标签这往往是整个题目里最耗时的一部分。2.3 YOLO 标注格式txt 里的四个归一化数值YOLOv5 的标注文件不是直接存像素坐标而是每个目标一行0 0.4561 0.5298 0.2347 0.6123 1 0.7123 0.4312 0.1988 0.3566四个数字依次是class_id x_center y_center width height其中x_center y_center width height全部除以了原始图像的宽和高归一到 0 到 1 之间。比如一张宽 1280 高 720 的图像某个人的框是x1300, y1200, x2700, y2600则中心点(500, 400)归一化后是(500/1280, 400/720)框宽高同理。这也是常见报错来源有人直接把像素坐标写进 txt训练 loss 一开始就异常大或者干脆报AssertionError: labels require float。检查标注是否合理可以用下面的代码读一个样本把坐标乘回图像尺寸后画框验证。import cv2 img cv2.imread(dataset/images/train/000001.jpg) h, w img.shape[:2] with open(dataset/labels/train/000001.txt) as f: for line in f: cid, xc, yc, bw, bh map(float, line.split()) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, str(int(cid)), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(label_check.jpg, img)这段逻辑把归一化坐标还原成图像坐标再画框。逐行读取txt每行用split()切出五个字段前一个是类别编号后四个是归一化的框参数。如果画出来的框明显偏移、大小不对问题往往不在脚本本身而是标注文件坐标系和原始图像尺寸不匹配。2.4 用 YOLOv5 训练自己的数据集dataset.yaml 与最小命令准备目录和标注后写一个dataset.yaml内容大致如下path: ./dataset train: images/train val: images/val nc: 1 names: [person]path指 yaml 文件视角下的数据集根目录。train和val用相对路径YOLOv5 会自动拼接。nc是类别数量names是类别名顺序必须和标注文件里的class_id一一对应。这里有一个值得明确的选择。很多人会把names直接写成[normal, fall, fight]让 YOLOv5 本身去区分行为。实际上行为是强时间相关属性单帧图像里的“摔倒”和“蹲下”视觉差异很小直接训练单帧分类经常出现抖动和误检。更常见的工程做法是nc1只让 YOLOv5 检测人行为交给后面第三层的时序规则去判断。本科毕业设计如果强调“做出来能演示”后者在有限数据量下更容易收敛。训练命令是整套流程的核心python train.py \ --data dataset.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 120 \ --device 0--weights yolov5s.pt表示加载 COCO 预训练权重做迁移学习对少量自定义数据很必要。--img 640是输入尺寸监控画面人物较小时可以升到 1280但显存占用和推理耗时都会增加。--batch根据显卡显存调整8GB 显存跑yolov5s建议 16 左右跑不动时优先减半而不是改小--img。--epochs对行为检测场景建议 100 到 150太多会造成过拟合太少则框的位置不够稳定。2.5 从 runs/train 里读训练状态训练结束后runs/train/exp目录下会生成results.png、confusion_matrix.png和若干验证集预测图。看results.png时重点不是 mAP 数字高低而是训练 loss 和验证 loss 两条曲线的关系两者一起下降说明还有空间验证 loss 在后期反弹则是过拟合信号。遇到反弹第一反应是降低 epoch、增加数据增强或减少模型规模而不是继续训练。把runs/train/exp/weights/best.pt复制到项目根目录的weights/下作为后续行为判定的检测权重。3. 单帧目标检测管不到时间YOLOv5的输出如何变成异常行为判定3.1 为什么要做行为判定后处理YOLOv5 的输出本质上是每个目标的类别、置信度和边界框它没有时间上下文。一个人在某一帧里到底是蹲下系鞋带还是摔倒模型单凭一张 RGB 图很难判断因为语义边界依赖前后帧的变化趋势。这就是为什么“异常行为检测”不能仅仅被看作一个目标检测任务。常见的落地架构是YOLOv5 负责检测人输出[x1, y1, x2, y2, conf, cls]后端用跟踪器把连续帧里的同一个目标关联起来再针对每个人的轨迹序列用规则或小型时序模型判断行为类型。这条链路的好处是每个环节都能独立验证检测器准不准、行为规则合不合理可以分开调这也让毕业设计的章节划分非常自然。3.2 从边界框里提取的六个特征和参考阈值实际项目中我从 bbox 序列里提取的特征主要有六个特征计算方式参考阈值倾向对应的行为框高宽比(y2-y1)/(x2-x1)小于 0.9 持续多帧摔倒、横卧中心点位移速度相邻帧中心点距离单帧位移超过图像宽度的 5%打架、奔跑框面积变化率当前面积/上一帧面积突变超过 2 倍快速靠近摄像机两框交并比两个人的框做 IoU大于 0.5打架、聚集置信度骤降当前 conf 减上一帧 conf下降超过 0.3遮挡、遮挡后的跌倒目标驻留时长中心点持续停留在小范围停留超过 10 秒徘徊、滞留这些特征不是互相独立的。比如摔倒通常伴随高宽比先大于 1.2再在一两秒内跌破 0.9同时中心点发生一次明显位移后趋于静止。单独拿一个特征判断很容易误报实际使用时要把它们组合成条件规则。3.3 一个可直接使用的行为判定窗口类给出一段简单的 Python 实现核心是用固定长度队列保存最近 N 帧的单目标框再在队列上计算特征。from collections import deque import numpy as np class BehaviorWindow: def __init__(self, win_frames15, aspect_threshold0.9): self.buffer deque(maxlenwin_frames) self.aspect_threshold aspect_threshold def push(self, bbox): self.buffer.append(bbox) def check_fall(self): if len(self.buffer) self.buffer.maxlen: return False arr np.array(self.buffer) heights arr[:, 3] - arr[:, 1] widths arr[:, 2] - arr[:, 0] aspects heights / (widths 1e-6) centers (arr[:, :2] arr[:, 2:4]) / 2.0 step np.linalg.norm(centers[1:] - centers[:-1], axis1) aspect_drop aspects[0] 1.2 and aspects[-1] self.aspect_threshold sudden_move step.max() 0.05 return aspect_drop and sudden_movepush每帧接收一个检测框bbox是[x1, y1, x2, y2]的像素坐标。check_fall在队列缓存满 15 帧后开始计算15 帧在 25fps 下约等于 0.6 秒。aspects记录每帧的高宽比step计算相邻帧中心点移动距离。摔倒的典型特征是检测框从竖直变成近似横卧且中间某一帧有明显位移。widths 1e-6是为了避免除零。这段代码只处理单个目标。监控画面里有多个人的时候不能把所有框塞进同一个队列而是一人一个BehaviorWindow按跟踪 ID 区分。3.4 多目标和跟踪ID跳变的坑如果没有现成跟踪器最简做法是用贪婪匹配把上一帧所有框和当前帧所有框做 IoU 矩阵优先匹配 IoU 最大且超过 0.3 的组合。这样能维持短时间内的 ID但对快速运动的人两帧之间位移可能很大IoU 直接变成 0ID 就会跳变。更稳妥的是接入 ByteTrack 或 DeepSORT它们输出稳定的track_id。拿到track_id后按 ID 分组每个 ID 对应一个BehaviorWindow这样就不会把两个人的框混在一起算高宽比。如果项目里没有跟踪器至少要在行为判定时限制“每个窗口处理同一个目标”否则报警会非常不稳定。4. 接入视频流YOLOv5推理参数、时间窗与工程排错4.1 命令行快速验证与会话内推理的选型YOLOv5 官方detect.py可直接对视频、摄像头和输入图片做推理适合先验证权重效果。python detect.py \ --weights weights/best.pt \ --source data/videos/fall_test.mp4 \ --img 640 \ --conf-thres 0.25 \ --iou-thres 0.45 \ --save-txt --save-conf \ --project runs/detect --name demo这条命令会把标注后的帧图片输出到runs/detect/demo/目录。--save-txt同时导出每帧的检测结果 txt--save-conf会在 txt 每一行末尾追加置信度字段。需要特别注意--project和--name不加的话会落到默认的runs/detect/exp下连续执行多次会生成exp2、exp3文件路径容易搞混。但detect.py只负责“看到什么”不负责“发生了什么”。要做行为判定更多时候是把它当模型加载工具在主循环里逐帧处理。用torch.hub加载权重更灵活import torch model torch.hub.load(ultralytics/yolov5, custom, pathweights/best.pt, force_reloadFalse) model.conf 0.25 model.iou 0.45 model.max_det 100torch.hub.load的第一个参数是仓库名第二个参数custom表示加载本地权重path指向best.pt。加载后直接设置成员变量conf、iou、max_det等价于命令行参数省去每次推理时传参。4.2 必调的 5 个检测参数行为检测场景里YOLOv5 推理参数不宜全部用默认值下面五个最值得调参数建议范围场景说明conf0.15 到 0.35阈值越低召回越高误检越多打架场景会被遮住可适当调低iou0.3 到 0.5多人重叠时降为 0.3防止两个打架的人被 NMS 合并成一个框max_det50 到 200监控画面人少100 足够设太高会拖慢后处理imgsz640 到 1280画面中人物较小就调大注意显存和推理耗时agnostic_nmsFalse只检测 person 一类时不重要多类别场景保持 False这些参数需要和第三层的时序规则联动调整。比如conf0.15会导致大量低置信度框行为窗口的噪声变大误报增加此时规则里的“连续触发帧数”就要适当提高。4.3 基于 OpenCV 的行为判定引擎骨架下面是接入视频流的最小骨架把目标检测、行为窗口和报警输出串起来import cv2 import torch model torch.hub.load(ultralytics/yolov5, custom, pathweights/best.pt) cap cv2.VideoCapture(0) # 0 表示本机摄像头 fall_monitor BehaviorWindow(win_frames15) while cap.isOpened(): ok, frame cap.read() if not ok: break results model(frame) boxes results.xyxy[0].numpy() # 生产环境需要配合跟踪器这里演示单目标 if len(boxes) 0: bbox boxes[0][:4] fall_monitor.push(bbox) if fall_monitor.check_fall(): frame_id int(cap.get(cv2.CAP_PROP_POS_FRAMES)) print(fall alarm at frame, frame_id) fall_monitor.buffer.clear() cv2.imshow(monitor, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()results.xyxy[0]拿到的是当前帧所有检测框每一行格式为[x1, y1, x2, y2, conf, cls]坐标是原始图像的像素值。.numpy()把张量转成 ndarray方便后续进入规则计算。报警后调用buffer.clear()清空窗口否则同一个摔倒事件会在连续多帧里反复触发。这里有个容易忽略的点model(frame)内部会把 BGR 转换为 RGB所以直接用 OpenCV 读到的frame传给模型即可不要先转换再传。4.4 三个运行期问题显存、坏帧、视频流断线第一个是显存不足。yolov5s在 640 下占用约 2GB 显存和老显卡共用显存时容易超限。先砍--img到 480再看 batch还不行就退回 CPU 推理验证逻辑。CPU 推理一帧大约几百毫秒只适合调试不适合实时演示。第二个是视频文件损坏。监控视频来自不同设备OpenCV 读入后某些帧会返回空frame。常见做法是遇到not ok时跳过当前帧而不是直接 breakframe_id int(cap.get(cv2.CAP_PROP_POS_FRAMES)) cap.set(cv2.CAP_PROP_POS_FRAMES, frame_id 1) continue这样等于强制跳到下一帧避免单个坏帧中断整个流程。第三个是网络摄像头断流。RTSP 源断流后cap.isOpened()会变成 FalseRead 循环会在中途卡死。简单处理是每 1000 帧检查一次连接状态断开后重新构造VideoCapture同时对连续读取失败次数做计数超过阈值就退出并告警。提示行为判定不要对每一帧都输出报警结果建议把报警状态挂在时间窗口上连续触发指定帧数才确认能显著降低单帧波动造成的误报。5. 从指标到可展示结果验证方法和一个环形缓冲区技巧5.1 把验证切成视频片段级而不是只看 mAP异常行为数据天然不平衡正常帧可能占 99%整体 mAP 再高也不能说明“摔倒能报警”。我会把测试视频切成片段每个片段要么包含某个异常事件要么是完全正常时段。评估时记录三件事异常事件发生后 T 秒内是否触发了报警、正常时段每小时的误报次数、从异常发生帧到首次报警帧的延迟。这个维度比训练时的 loss 曲线更能说明问题也适合直接放进毕业设计的结论部分。5.2 detect.py 导出结果并用 ffmpeg 合成演示视频对答辩或项目汇报来说只有数字不够直观。detect.py已经在runs/detect/demo/下输出了带框的逐帧图片用 ffmpeg 合成视频ffmpeg -y -framerate 25 \ -i runs/detect/demo/fall_test_%06d.jpg \ -c:v libx264 -pix_fmt yuv420p -crf 23 \ fall_test_annotated.mp4%06d匹配fall_test_000001.jpg这种六位帧号命名。-pix_fmt yuv420p保证视频在大部分播放器里能正常打开。如果画面上还想叠加报警时间可以在生成帧图片后用 OpenCV 的putText把报警标记写进图片再合成这样演示视频里能直接看到报警触发帧的位置。5.3 报警前也留证据环形缓冲区技巧行为报警发生时异常动作可能已经过了 1 到 2 秒只保存报警后的画面会丢失关键过程。常见做法是在内存里维护一个环形缓冲存最近 N 帧原始图像报警触发时先回放缓冲再拼接后续实时帧。from collections import deque class RingBuffer: def __init__(self, maxlen75): frames deque(maxlenmaxlen) self.data frames def write(self, frame): self.data.append(frame.copy()) def flush(self): result list(self.data) self.data.clear() return resultmaxlen75在 25fps 下对应 3 秒。write放在逐帧循环顶部每次读帧就写入flush在报警确认时取出全部缓存并清空。需要注意1080p 的 BGR 帧约占 6MB 内存75 帧接近 450MB连续运行时内存占用偏高。实际项目建议在写入前把帧缩小到 640 宽度或改成只保存 JPEG 编码后的字节兼顾清晰度和内存。把flush()得到的帧序列和报警后的实时帧拼接就能形成“异常前 3 秒 报警后 2 秒”的完整证据片段这也是异常行为检测系统交付时最容易被低估但最有价值的一部分。本文还有配套的精品资源点击获取