ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

YOLOv5打架行为检测:从数据集、模型训练到PyQt界面封装全流程

YOLOv5打架行为检测:从数据集、模型训练到PyQt界面封装全流程

简介:面向安防监控、校园管理、工地安全等需要自动识别打架冲突行为的场景,这是一套基于YOLOv5的检测方案,内置已训练好的模型权重,同时提供推理脚本和图形界面,可对图像或视频帧进行实时检测并将结果直观标注出来,方便快速验证模型效果或部署演示。数据资源整合了上万张打架行为样本的标注信息,训练集、验证集、测试集划分完善,配置文件中已写明各集合对应的图片目录,类别分为正常与打架两类,便于直接开始训练或微调多种主流算法,也有助于完整理解目标检测项目中的数据准备流程,目录结构清晰,迁移到其他项目中也很方便。压缩包共两千个文件,主体是xml标注文件,另有三份PDF说明文档和几个Python脚本;其中环境配置文档与图形界面使用说明,可以协助解决依赖安装、界面操作等常见问题。整体大小约五百零八兆字节,已有160人学习下载;适合毕业设计、智能安防项目演示及算法效果对比,拿到后既可直接调用权重完成推理,也可利用现成数据划分继续迭代训练,显著减少从零整理数据与配置环境的工作量。

1. 打架行为检测不是单纯的“目标检测”,而是一整套工程

拿到“YOLOv5算法打架行为检测模型+数据集+pyqt界面”这个标题的从业者,一般带着很具体的现场诉求:园区监控、校园宿舍、工地或者看守所,需要让摄像头在两个人发生肢体冲突时自动弹窗报警,而不是等值班员盯着屏幕看。这套东西的核心是三个部分——用YOLOv5训练出来的行为检测模型、一份能支撑训练的数据集、以及一个能接摄像头实时展示结果的PyQt界面。它适合做安防预研、毕业设计、竞赛,也适合小团队快速给客户出demo。

这里必须先说一个反直觉的结论:打架行为在单张静态图像上非常难直接检测。“打架”本质是动态事件,两个人抱在一起和两个人扭打在一起,单帧画面上的视觉差异可能极小。多数成熟方案里YOLOv5只负责把人找出来,真正判定打架要靠两帧之间的交叠率、运动幅度这些时序特征。指望训一个“fight”类就万事大吉,后面大概率要为误报付出代价。下面从技术选型、数据集、训练参数到界面封装,把整条落地路径拆开讲。

2. 两条技术路线怎么选:直接训“fight类”还是检测+时序判定

2.1 端到端的“fight”单类:几分钟跑通,误报是后患

最快上手的做法,是把打架视频按帧抽出来,在画面上直接标一个“fight”类别,然后用YOLOv5训练一个单类目标检测器。这个路线的好处是链路极短:标注脚本写一写,训练命令一跑,demo就能出。不少初学者和比赛团队都是这么交差的,也确实能在测试集上拿到不错的mAP。

但单帧分类有一个绕不开的毛病:静态图像上“打架”和“拥抱”“拉扯”“摔倒”在视觉上高度相似。模型真正学到的是“两个人靠得近、姿态比较乱、肢体大面积重叠”这一组特征,而不是“打架”这个概念。换到现场场景,两个人在聊天时勾肩搭背、一个人扶另一个人、甚至一群人在拥挤的电梯口,都会把模型激活。我见过一个在公开打架数据集上mAP接近0.9的模型,放到宿舍走廊里一个拥抱就报警,值班员一晚上被骚扰好几次。所以这条路线只适合三类情况:演示、比赛、或者机位固定且画面场景极单一的环境。真往生产走,得做好接受高误报的准备。

2.2 更耐用的组合:YOLOv5行人检测配合交叠率与运动幅度

从业者普遍采用的稳妥方案,是让YOLOv5只检测“person”类,把打架判定交给一个独立的时序逻辑层。这个方案的前提是“人”这个类别非常好训,公开数据集多,泛化能力强;而打架判定层做在YOLOv5之外,用可解释的规则去完成,现场出了问题可以直接改规则,而不是重新训模型。

判定层一般看两个量。第一个是两个人检测框的交叠率(IoU),打架过程中两个人的框会在连续多帧里保持高重叠;第二个是检测框高度和宽度的突变,挥拳、弯腰、倒地都会导致人体框的宽高变化率明显高于正常行走。实战里还有一个比全框IoU更稳定的特征——头部区域交叠率。两个人如果只是普通靠近,身体可能重叠但头不会长时间贴在一起;真打起来头部区域的重叠时间和比例会显著不同。帧与帧之间的关联可以用最简单的IOU tracker,不必一上来就上DeepSORT,少一个重识别模型就少一个坑,等规则判定扛不住了再换多目标跟踪不迟。

规则阈值我一般从IoU大于0.3且连续持续10帧以上、同时人体框高度变化超过20%开始调,具体数值要按摄像头安装高度和俯视角度重新标定。参数的意义不是让模型更准,而是让报警逻辑适配你的具体机位。

2.3 PyQt界面在这套系统里的定位:别让它背推理的锅

PyQt在整套系统里只干四件事:拉视频流、调用模型推理、把结果画到界面上、记录报警日志和截图。很多项目把界面写得又复杂又卡,问题往往不在代码逻辑,而在架构。最常见的新手错误是把模型推理直接写进界面的定时器里,结果一跑视频,界面卡成PPT,然后怪“模型太慢”。

正确的做法是把采集、推理、界面显示拆成三个独立的线程,线程之间用队列传帧。采集线程只管从摄像头或者视频文件里取帧,推理线程从队列里拿帧跑模型和判定逻辑,UI线程只负责把结果帧转成QImage画到QLabel上。这样模型哪怕一秒钟只跑5帧,界面也不会被拖死,最多是画面有明显跳帧。这个架构放在后面章节给出最小实现。

3. 数据集怎么做:公开数据抽帧、自建标注标准与正负样本配比

3.1 公开打架数据集怎么选,怎么转成YOLO格式

公开的打架行为数据集数量不算多,常见的是Hockey Fight、UBI-Fights、Movie Fights这一类。它们普遍是视频片段,而且大部分只带视频级标签,也就是“这一整段是打架”或“这一整段不是打架”,几乎没有逐帧的边界框标注。直接拿来训YOLOv5是不行的,你得自己抽帧、自己从零标框。所以拿到这些数据后的第一步不是训练,而是做一次“视频抽帧+帧级去重+人工标注”的流水线。

还有一个经常被忽略的操作:不要把公开打架数据集当成唯一来源。把COCO的person类、CrowdHuman这类大规模行人检测数据当作负样本池,效果出奇地好。打架数据作正样本,行人检测数据里那些“人群中两人靠近但不冲突”的画面作负样本,混合之后模型的误报会明显下降。

视频抽帧通常用OpenCV就能写干净,下面这段脚本是按固定间隔抽帧并保存的最小实现,我自己做数据集时一直用这个逻辑,只是把间隔和路径换一下。

import cv2 import os from pathlib import Path def extract_frames(video_path, out_dir, sample_interval=3): Path(out_dir).mkdir(parents=True, exist_ok=True) cap = cv2.VideoCapture(str(video_path)) if not cap.isOpened(): print(f"failed to open video: {video_path}") return name = Path(video_path).stem frame_id = 0 saved_id = 0 while True: ret, frame = cap.read() if not ret: break # 每隔 sample_interval 帧取一帧 # 打架视频中动作变化快,间隔太大会漏掉关键瞬间 if frame_id % sample_interval == 0: out_file = f"{out_dir}/{name}_frame_{saved_id:06d}.jpg" cv2.imwrite(out_file, frame) saved_id += 1 frame_id += 1 cap.release() print(f"{name}: total {frame_id} frames, saved {saved_id}") if __name__ == "__main__": extract_frames("fight_001.mp4", "./extracted_frames", sample_interval=3)

这段代码里最关键的是sample_interval这个参数。打架动作持续时间短,挥拳可能只有0.3秒,如果按每秒1帧来抽,一段10秒的冲突只能留下10帧,其中真正体现“造成伤害动作”的帧可能只有两帧;按每3帧抽1帧,在25fps的视频里等于每秒取8帧左右,足够覆盖动作变化。代价是正样本之间高度相似,需要靠后面的帧级去重去解决。你如果录制的视频本身就慢,可以把间隔调到5,但不要超过5,否则关键动作容易丢。

3.2 自建数据的标注标准:冲突主体、负样本与帧级抽帧

自建数据时,标注标准不统一是最大的质量杀手,多人协作时尤其明显。这里先说标准:打架场景中,把参与冲突的主要人员作为一个整体来标注,框取两人的外接矩形后向外扩10%到15%。不要一个打架画面里同时标了两个框一个标全身一个标上半身,模型会无所适从。单人的正常行为就只标全身框,不做额外的行为标签。

负样本的设计要花一半以上的时间。打架的误报源非常固定:拥抱、握手、拉扯、摔倒、奔跑、人群密集、两个人靠近交谈。这些场景必须全部进入训练集,每类至少占负样本总量的10%。如果负样本里只有普通的街景行人,模型会认为“两个人靠近”这件事本身就是异常,那现场必炸。

抽帧规则上,打架视频里冲突片段每秒连续抽3到5帧,非冲突片段随机抽,不要每帧都存。正样本如果全是同一段视频的相似帧,模型会过拟合到这段视频的特定光照和背景上,换个场景精度立刻滑坡。

3.3 建议的数据分布与增强参数

一个能支撑YOLOv5s训练到可用状态的数据集,规模我一般按下表来配:

数据类别建议规模说明
打架正样本帧3000张以上覆盖不少于10个不同场景,角度和光照要分散
常见误报负样本8000张以上拥抱、握手、拉扯、摔倒、奔跑、人群密集
普通行人负样本5000张以上用公开行人检测数据抽帧即可
总规模16000张起步低于这个量,泛化能力会明显受限

数据增强参数方面,Mosaic和MixUp建议开启,这两个增强对提升小目标鲁棒性帮助明显。HSV增强里H通道的值不要调大,场景的色调是重要现场线索,建议H只在±5%以内,S和V可以放到±15%左右。随机裁剪要当心,别把人物主体裁出画面。标注工具主流用labelImg或者labelme,前者直接出YOLO的txt格式,后者出coco格式需要再转,二选一我推荐labelImg。

提示:划分训练集和验证集时,一定要按视频片段分组,同一段视频的帧不能同时出现在训练集和验证集里。有人偷懒随机打乱,验证集里全是训练集同源帧,mAP虚高到0.95以上,一上现场立刻现原形。

4. 训练打架检测模型:yaml配置、3个必调超参数和验证指标

4.1 数据配置文件与最小训练命令

YOLOv5训练的第一步是把数据配置写清楚。一个标准的fight.yaml文件内容如下:

# 数据集配置:打架行为检测 path: ./datasets/fight # 数据集根目录 train: images/train # 训练集图片目录 val: images/val # 验证集图片目录 nc: 1 # 类别数,这里走单类别路线 names: ['fight'] # 类别名,端到端方案就这一个类

这里要注意,path指的一定是数据集根目录,train和val是相对于根目录的相对路径。想少出莫名其妙的问题,就把所有训练图片放在images/train和images/val下,同级的标签txt放在labels/train和labels/val下。YOLOv5会按目录名字自动找标签,不要自己改路径映射。

配置文件就绪后,最小训练命令是:

# 在yolov5仓库根目录下执行 python train.py \ --data fight.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 300 \ --patience 50 \ --project runs/train_fight \ --name exp_v1

--img 640是输入分辨率,打架场景摄像头视角下人物普遍偏大,不需要上1280,640在速度和精度之间最平衡。--batch 16按显存来,8G显存跑yolov5s刚好够用。--patience 50表示连续50轮验证集指标不提升就自动早停,打架数据量不大,早停能省不少时间。如果中断了,用--resume加--weights runs/train_fight/exp_v1/weights/last.pt续训,不用从头再来。

4.2 必调的三个超参数与一组“玄学”参数

YOLOv5的超参数写在hyp.scratch.yaml里,训练时可以传--hyp指定自己的文件。默认值本身是COCO数据上调出来的,打架数据集规模小、场景单一,照抄会有点水土不服。

参数建议值说明
lr00.003~0.005默认0.01,小数据集上偏大,前期loss容易震荡
box0.05~0.07框损失权重,打架场景人物重叠严重,适当提高让框更准
cls_pw1.0(单类)/ 1.5(双类)正负样本不均衡时按比例提权重
momentum0.937默认即可,改它收益极低
weight_decay0.0005默认即可
obj1.0默认即可

box参数的调整逻辑是:打架检测里误报的判定后层很依赖检测框的安稳程度,框抖得厉害,后面的时序规则就没法用,所以框的质量比分类置信度更重要。cls_pw只在双类(person和fight)时起作用,如果你走的是之前的“检测+规则”路线,只训person类,那这个参数不用动。

lr0是最值得花时间试的参数。我一般先用0.005跑50个epoch,如果前20轮loss几乎不下降,就降到0.003;如果训练集loss下降快而验证集波动大,说明学习率偏高,也得降。这里多少带点玄学成分,但方向是明确的:小数据集偏向小学习率。

4.3 训练过程中该看哪些指标,以及如何导出部署模型

训练期间主要看PR曲线、混淆矩阵、验证集结果图。打架场景对mAP0.5:0.95不必过分纠结,因为目标的语义本身模糊,重点应该放在召回率(Recall)和误报率上。漏报一次打架可能酿成事故,误报一次顶多被值班员骂一顿,这两个指标的优先级完全不同。

训练结束后导出部署模型,命令如下:

# 把best.pt导出为torchscript和onnx python export.py \ --weights runs/train_fight/exp_v1/weights/best.pt \ --include torchscript onnx \ --img 640

PyQt界面调用推荐用torchscript格式,加载稳定、不依赖原训练环境;如果后续要上服务端就用onnx,配合onnxruntime跑推理。导出完先用官方detect.py验证一遍,确认视频上输出正常再往界面里接。别把best.pt直接丢给PyQt去load,torchscript格式在工程里更稳妥,这也是不少人在部署阶段踩过的坑。

5. 避坑:训练与PyQt中的5个血泪经验

5.1 现场误报把值班员搞烦了:拥抱、摔倒全报警

现象:训练集mAP 0.85以上,现场一跑,两个人在走廊拥抱触发报警,一个人蹲下系鞋带也触发报警,值班员一天点几十次“误报确认”。

原因:单帧图像里“打架”和“拥抱”“摔倒”在视觉特征上高度重叠,模型只看单帧根本没有区分能力;训练时又没有把这类场景放负样本,模型学到的就是“两个人重叠就报警”。

解决:两层一起补。数据上把拥抱、摔倒、拉扯、奔跑全部抽帧加入负样本库重新训练;逻辑上增加时序确认——必须连续超过N帧(比如10帧)都满足判定条件才真正输出一次报警,而不是单帧命中就弹窗。这个时序确认放在规则层,不占训练成本,压误报效果立竿见影。

5.2 数据看着不少,模型却一直不收敛

现象:正负样本加起来一万多张,训练到100个epoch,loss还在高位横盘,验证集指标迟迟上不去。

原因:数据里同一段视频的连续帧太多,正样本其实高度重复,模型反复看的是几乎一样的画面;另一个可能是lr0设得太高,loss在训练初期就震荡坏了。

解决:按视频原始片段统计抽样后的帧数,对重复度高的片段重新抽帧,用感知哈希去重工具把相似帧删掉一批。学习率降到0.003再训一轮。检查一下数据目录里的图片数量和你以为的数量是否一致,很多人花了大量时间在清洗数据上,却忽略了同一段的帧占了正样本总数的六成。

5.3 PyQt界面跑几小时就卡死或内存持续增长

现象:界面上视频越来越卡,任务管理器里内存从启动时的400MB慢慢涨到2GB以上,最后界面无响应。

原因:帧队列无限增长。采集线程push的速度大于推理线程pop的速度,队列越积越长,内存就被吃光了;或者每一帧的QPixmap对象没有及时释放,以及线程关闭时VideoCapture没有被正常release。

解决:给帧队列设置最大长度,比如maxlen=3,新帧进来自动丢掉最旧的帧,保证延迟永远可控;QImage和QPixmap作为局部变量,用完即弃;程序退出时先停止推理线程和采集线程,再释放VideoCapture,顺序反了就是经典的崩溃现场。

5.4 打架框标注不统一,模型越训越“学废”

现象:训练过程中发现模型的框忽大忽小,有时候只框住上半身,有时候把两人框成一体,验证集的结果图看起来非常杂乱。

原因:多人协作标注时没有统一标准。有人习惯框全身,有人觉得打架动作里上半身才是关键,于是只框躯干;一个画面里两人冲突有人标了一个框,有人标了两个框。

解决:在标注规范里写死:单人场景框全身,多人冲突场景框“冲突整体”,并在外接矩形上外扩10%到15%。开工前先找5张典型图统一试标,验收时按类别抽样统计框的边长方差,异常的先修正再训练。这个环节省不得,返工成本比一次标对的成本高得多。

5.5 换个摄像头,模型就像失忆一样猛掉精度

现象:训练数据来自教室平视角,换到走廊顶视角摄像头,检测率直接掉了一半,误报反而变多。

原因:模型只见过训练集里的视角和光照,分布外的数据一来就崩。YOLOv5的增强再强,也只能在训练分布附近做变换,换机位等于换了一个分布,这是老生常谈的泛化问题。

解决:整理数据集时强制覆盖不同机位高度、俯视角度和光照条件,正样本里至少保证三种视角。数据增强里把S和V扰动放开到±20%,H保持小幅扰动。如果项目时间允许,去目标点位录10分钟真实场景视频加入训练集,比在网上再找一万张图片有用得多。

6. 收官:PyQt界面封装的正确姿势,把推理关进QThread

最后一个技巧,也是让PyQt界面不卡的最核心一条:模型推理绝对不能出现在UI线程里。下面是一个最小可用的推理线程封装,队列限长、信号回传结果帧、报警截图落盘。

import queue import threading import cv2 import torch from PyQt5.QtCore import QThread, pyqtSignal class InferThread(QThread): # 回传一张带检测结果的可显示帧 frame_ready = pyqtSignal(object) def __init__(self, model_path, input_queue, maxlen=3): super().__init__() # 这里直接用torchscript模型加载,避免依赖原仓库环境 self.model = torch.jit.load(model_path) self.input_queue = input_queue self.running = True def run(self): while self.running: try: # 队列限长,超时后丢弃旧帧,保证延迟不累积 frame = self.input_queue.get(timeout=0.1) except queue.Empty: continue # 模型前置处理:640x640归一化 img = cv2.resize(frame, (640, 640)) img = img[:, :, ::-1].transpose(2, 0, 1) img = torch.from_numpy(img.copy()).float() / 255.0 img = img.unsqueeze(0) with torch.no_grad(): preds = self.model(img) # 后处理只取置信度最高的前几个框 # 真实项目里还要把检测结果叠加到当前帧上再发出去 self.frame_ready.emit(frame) def stop(self): self.running = False self.wait()

这段代码里的关键参数是maxlen=3,它决定了推理延迟的上限。采集线程不管推多快,队列最多保留3帧,模型处理不过来就主动丢帧,界面显示永远跟手。frame_ready信号只传对象引用,不要在信号里传大数组拷贝,那样反而会引入新的卡顿。stop()方法里先置位再wait,避免程序退出时线程还在跑导致崩溃。

验证这套封装是否可靠,我习惯做一次8小时长稳测试:开着摄像头循环跑,用watch -n 60 free -m盯内存,用ps -o %cpu,%mem盯推理进程,如果内存曲线不持续上涨、队列积压稳定在3以内,这个界面才算可以交付。我早期直接把模型塞进UI定时器,界面卡成PPT,后来所有项目一律走线程+队列,再也没有因为界面问题返工。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进