尧图精选

YOLOv8打架行为检测实战:从训练到ONNX部署与GUI开发

🕒 发布时间:2026/10/2 3:00:46 📁 来源:尧图网络
简介基于YOLOv8的打架行为检测系统是一套可直接运行的PythonPyQt5工程面向安防监控和智能视频分析开发者用于自动识别画面中的“打架”与“非打架”行为。压缩包共90个文件、约17.45MB包含检测源码、ONNX模型、PyQt5精美GUI界面、评估指标曲线、66张测试图片以及xml标注、txt说明、pyc缓存等辅助文件目录结构清晰便于复现与二次开发。目前已有482人学习下载。核心代码包括Yolov8Detector.py检测脚本、main.py启动入口、模型权重及class_names.txt配合yolov8n.onnx可快速部署测试评估曲线直观展示准确率、召回率等指标便于验证模型性能。测试环境为Windows10、Python3.8、torch1.9与ultralytics8.2.70适合希望动手实践目标检测或搭建行为识别原型的学习者。1. 打架行为检测为什么绕不开YOLOv8一个安防场景的技术选型实录做安防监控的人都有体会摄像头能录下一切但真正需要的是在打架发生的那几秒内把告警推给值班室。基于YOLOv8的打架行为检测系统是这类需求的典型落地形态——Python源码负责训练与推理ONNX模型承担跨平台部署评估指标曲线用于验证精度精美GUI界面则把模型包装成非技术人员也能操作的工具。我当初接到智慧校园的打架检测需求时对比了SlowFast、YOLOv5、YOLOv8几条路线最终落地还是选了YOLOv8单阶段检测的速度优势明显Anchor-Free的设计对密集人群中的重叠目标更友好而且从训练到导出ONNX再到边缘部署链路最短、生态最全。这篇文章把整条路讲清楚模型在做什么、数据怎么准备、训练参数怎么调、导出ONNX有哪些坑、GUI怎么接。2. YOLOv8打架行为检测的原理与选型先搞清楚模型在干什么2.1 打架行为检测的任务边界目标检测能做行为识别吗打架是一种动态行为严格来说属于行为识别Action Recognition的范畴主流学术方案会用SlowFast、STM、PoseC3D这类带时序建模的模型。但落地工程和做学术不一样监控场景要求的是实时告警不能容忍等待十几帧才能出结果。实际项目里最常见的做法是把打架当作一个目标类别来检测让YOLOv8直接输出这个人/这群人正在打架的边界框。这个做法成立的前提是打架行为在单帧图像中存在明显的视觉特征肢体大幅度伸展、多人身体重叠、姿态形变剧烈、动作幅度远超日常交互。YOLOv8检测的是打架状态而不是打架动作的起始瞬间对于安防告警来说足够用。代价是模型会学到一堆和打架相关的上下文特征——背景中的围观众人、特定角度下的肢体交错这些都会成为误检来源后面第5章我会专门说怎么控误检。从数据标注角度看把行为检测降维成目标检测也显著降低了标注成本。做时序行为识别要逐帧标注动作段而做目标检测只需要在关键帧上画框一张图一个标签标注效率差一个数量级。所以这个zip包里只有目标检测的标签体系没有动作序列标注本质上是工程对学术的妥协方向没错。2.2 Anchor-Free与C2fYOLOv8的网络结构与推理流程YOLOv8的网络结构分为三块Backbone负责提取特征Neck负责多尺度融合Head负责输出检测结果。Backbone用的是CSPDarknet的改进版本核心模块是C2f——把CSPNet的梯度流设计保留下来同时用更多的Split操作让梯度回传路径更丰富。C2f的细节是输入先经过一个卷积然后分两条路径其中一条再split成多个分支最后concat合并。和YOLOv5的C3模块相比C2f的浅层特征保留得更好对打架这种目标尺度变化大的场景有帮助。Head是YOLOv8改动最大的地方。和YOLOv5的Coupled Head分类和回归共用一个卷积不同YOLOv8用的是Decoupled Head分类分支和回归分支各自独立的卷积层。打架检测里分类和回归任务的冲突是客观存在的——分类要关注语义特征这个目标像不像打架回归要关注几何特征框定得准不准解耦之后两个任务各学各的收敛更稳定。另一个关键设计是Anchor-Free。YOLOv5还需要通过聚类预设一组Anchor框YOLOv8直接预测目标的中心点与宽高把匹配问题交给TaskAlignedAssigner去解决。打架场景里人体互相遮挡严重人和人的IoU经常超过0.5Anchor-Based的匹配策略在这种密集目标下容易出现一个Anchor适配多个GT的冲突而Anchor-Free配合动态匹配策略对重叠目标的容忍度更高。推理流程可以压缩成四步输入图像缩放并pad到640x640Backbone输出三层不同尺度的特征图80x80、40x40、20x20Neck用PAN-FPN把高层语义和低层细节融合Head在每层特征图上预测边界框参数 类别概率。最终输出经过NMS去重留下置信度最高的框。这四步里任何一步出了问题都会在后面的ONNX部署阶段暴露出来。2.3 与YOLOv5、SlowFast的选型对比实时性与精度的平衡点我在选型时把YOLOv5、YOLOv7、SlowFast都跑了一遍结论是在打架检测这个具体任务上YOLOv8不是各项指标最强但综合落地成本最低。YOLOv5的问题在于Anchor-Based的匹配机制。打架场景中目标高度重叠预设Anchor的尺寸通常覆盖不到两个人扭打成一团这种极端宽高比的框。虽然理论上通过优化Anchor尺寸可以缓解但每次换数据集都要重新聚类工程上很烦。YOLOv8的Anchor-Free直接把这个环节省掉了。YOLOv7的精度确实比YOLOv8略高但代价是模型结构更复杂E-ELAN结构、辅助训练头、带重参数化的卷积这些组件在PyTorch里跑得好好的一到导出ONNX就问题频出——算子是自定义的ONNX Runtime不支持要么改结构要么写算子插件引入额外的部署风险。YOLOv8的算子池相对干净ONNX导出基本不用改网络结构。SlowFast这类行为识别模型的精度上限更高能区分拥抱和打架这种单帧模糊的行为但它的输入是连续多帧视频片段推理延迟天然包含帧累积时间。实测在1080P摄像头流上SlowFast的处理速度只有个位数FPS而打架告警场景要求的是秒级响应。所以我的判断是先用YOLOv8做实时检测如果后续确实需要区分细微动作再在YOLOv8检测到打架嫌疑后对片段做时序分析两级级联。这个方案既保住实时性又留了精度升级空间。选型对比可以用一张表说明模型实时性密集目标表现ONNX部署友好度工程链路完整度YOLOv5高一般Anchor需重新聚类好好YOLOv7中较好差自定义算子多中YOLOv8高好Anchor-Free好完善官方支持导出SlowFast低需多帧累积不适用差差我最终选择YOLOv8的理由还可以补充一条ultralytics官方把训练、验证、导出、预测这条链路做成了统一的CLIPython API也设计得干净这让拿到源码包就能跑通成为可能而不是要自己拼装一套训练脚本。3. 用YOLOv8训练打架行为检测模型从Labelme标注到损失曲线3.1 用Labelme标注打架数据JSON转YOLO格式的Python脚本做打架检测的数据准备核心难点的排序是这样的数据来源——打架视频相对敏感公开数据集少UCF-Crime、RWF-2000这类数据集的动作定义和实际安防场景差距大最靠谱的做法是拿监控视频截帧后自己标注标注工具——我习惯用Labelme因为它的多边形标注精度高而且JSON格式便于后处理。Labelme导出的格式是JSON里面存的是多边形的点坐标而YOLO训练需要的格式是每行一个目标的类别ID和归一化后的中心点x、y、宽w、高h所以转换脚本是绕不开的第一段代码。下面这个脚本处理Labelme导出的JSON目录转换成YOLO格式的txt标注文件同时生成train.txt和val.txt的划分。import json import os import shutil from pathlib import Path def labelme_to_yolo(json_path, output_dir, class_names): 将Labelme的JSON标注转换为YOLO格式txt json_path: 单张图片对应的JSON文件路径 output_dir: 输出txt文件的目录 class_names: 类别名列表如 [fight] with open(json_path, r, encodingutf-8) as f: data json.load(f) img_w data[imageWidth] img_h data[imageHeight] txt_name Path(json_path).stem .txt txt_path os.path.join(output_dir, txt_name) lines [] for shape in data[shapes]: label shape[label] if label not in class_names: continue points shape[points] # 计算多边形的最小外接矩形的左上角与右下角坐标 x_min min(p[0] for p in points) y_min min(p[1] for p in points) x_max max(p[0] for p in points) y_max max(p[1] for p in points) # 转为归一化的中心点坐标与宽高YOLO要求0~1 x_center (x_min x_max) / 2.0 / img_w y_center (y_min y_max) / 2.0 / img_h box_w (x_max - x_min) / img_w box_h (y_max - y_min) / img_h # 防止边界值为0或超过1做裁剪 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) box_w min(max(box_w, 0.0), 1.0) box_h min(max(box_h, 0.0), 1.0) class_id class_names.index(label) lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) with open(txt_path, w) as f: f.write(\n.join(lines)) # 使用示例 class_names [fight] json_root labelme_annotations yolo_label_dir yolo_labels os.makedirs(yolo_label_dir, exist_okTrue) for json_file in Path(json_root).glob(*.json): labelme_to_yolo(str(json_file), yolo_label_dir, class_names)这个脚本有两处是大家容易忽略的一是Labelme标注的坐标基于原始图像尺寸如果你的图片在标注前做过缩放转换前必须对齐否则框会偏二是YOLO格式要求归一化到0~1我习惯在最后加一层min/max裁剪防止多边形顶点恰好压在图像边缘时归一化出现1.000001这种越界值——训练时YOLO不会报错但这张图的Loss会异常跳变。标注时的类别定义一定要统一。我踩过的坑是标注的人把打架斗殴推搡分成三个类别导致模型学到的类间差异极小精度很低。实际做打架检测类别越少越好先只标fight一个类后续误检严重了再考虑加负样本类。3.2 训练配置data.yaml、超参与命令行启动数据准备完成后进入训练阶段。ultralytics的训练入口是yolo命令先要准备data.yaml描述数据路径和类别信息。# data.yaml # 训练集和验证集的图片目录路径可以是绝对路径也可以是相对路径 path: ./dataset train: images/train val: images/val # 类别数量与类别名nc必须与names的长度一致 nc: 1 names: 0: fight训练命令和参数说明# 从yolov8n.pt预训练权重开始训练适合数据量少的场景 yolo train \ datadata.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ device0 \ patience10参数说明modelyolov8n.ptn代表nano是最小的YOLOv8模型。打架检测属于单类别检测不需要用yolov8x这种大模型n或s在精度和速度之间最平衡。epochs100初始设100轮配合patience10做早停。正常情况30~50轮就收敛设100是保底。imgsz640输入分辨率。打架场景的监控画面通常摄像头离得远目标小如果算力允许设到768或832能明显提升小目标召回但训练和推理都会变慢。batch16显存够就往上加。打架数据集一般就几百到几千张图batch太大会提前过拟合。lr00.01初始学习率。迁移学习场景0.01是安全的起点如果你是从零训练建议降到0.001。训练结束后模型会保存到runs/detect/train/weights/目录best.pt是验证集mAP最高的权重last.pt是最后一轮权重。我一般只保留best.ptlast.pt主要用于中断后恢复训练。这里多说一句环境的坑。很多新手卡在ultralytics装不上其实主要就是Python版本和PyTorch版本不匹配。Python安装教程里经常提到的是3.8~3.11版本可用我实测3.10配合PyTorch 2.1最稳。建议用专门的虚拟环境不要动系统Python否则后面装onnxruntime时容易互相污染。3.3 监控评估指标损失函数曲线与mAP的判读方法训练每完成一轮ultralytics会在runs/detect/train/results.csv写一行训练指标包含box_loss、cls_loss、dfl_loss、precision、recall、mAP50、mAP50-95等。命令行看数字是一回事画成曲线判读趋势是另一回事。下面这段代码用pandas和matplotlib直接读results.csv画图。import pandas as pd import matplotlib.pyplot as plt # 读取训练日志 df pd.read_csv(runs/detect/train/results.csv) # 后面几列是epoch、时间等前面才是指标按列名筛选 fig, axes plt.subplots(2, 3, figsize(15, 8)) # 训练与验证损失 axes[0, 0].plot(df[train/box_loss], labeltrain box_loss) axes[0, 0].plot(df[val/box_loss], labelval box_loss) axes[0, 0].set_title(Box Loss) axes[0, 1].plot(df[train/cls_loss], labeltrain cls_loss) axes[0, 1].plot(df[val/cls_loss], labelval cls_loss) axes[0, 1].set_title(Cls Loss) axes[0, 2].plot(df[metrics/precision], labelprecision) axes[0, 2].plot(df[metrics/recall], labelrecall) axes[0, 2].set_title(Precision / Recall) axes[0, 2].legend() axes[1, 0].plot(df[metrics/mAP50], labelmAP50) axes[1, 0].set_title(mAP50) axes[1, 1].plot(df[metrics/mAP50-95], labelmAP50-95) axes[1, 1].set_title(mAP50-95) plt.tight_layout() plt.savefig(training_curves.png, dpi150)判读的重点是看验证损失和训练损失之间的剪刀差。如果train_loss一路下降但val_loss在某个epoch后掉头向上说明过拟合已经开始这时候回看patience早停机制有没有生效。mAP50是打架检测最核心的指标因为打架框的定位不需要像素级精确框稍微偏一点不影响告警判断但mAP50-95对定位精度更敏感如果它明显低于mAP50说明很多框偏移量偏大可以检查是不是标注框本身画得不紧。单类检测还有一个容易误判的点mAP50很高但对单张图的检测效果差。mAP是测试集上的统计均值而打架检测真正在乎的是别漏报、别乱报。所以我在评估阶段不只看mAP曲线还会拿验证集里置信度低于0.3的漏检图、置信度高于0.8但框错位置的误检图单独挑出来看找出模型没学到的“打架姿态”。4. 模型导出与跨平台部署PyTorch转ONNX到ONNX Runtime推理4.1 PyTorch转ONNX导出脚本、opset与动态轴的设置训练好的best.pt只能在PyTorch环境里跑要脱离Python训练环境部署第一步就是导出ONNX。YOLOv8官方提供了一键导出命令yolo export modelbest.pt formatonnx opset12 dynamicTrue simplifyTrue这条命令背后做的工作被我拆解成手动流程方便理解各项参数的意义。下面这段代码是手动torch.onnx.export的写法import torch from ultralytics import YOLO # 加载训练好的模型 model YOLO(best.pt) # 获取原生的torch模型 torch_model model.model torch_model.eval() # 构造一个虚拟输入shape要与训练时的imgsz一致 dummy_input torch.randn(1, 3, 640, 640).to(next(torch_model.parameters()).device) # 导出ONNX torch.onnx.export( torch_model, dummy_input, best.onnx, opset_version12, input_names[images], output_names[output], dynamic_axes{ images: {0: batch}, output: {0: batch} } )关键参数说明opset_version12ONNX算子集版本。opset太低会缺一些新算子太高会导致ONNX Runtime旧版本不兼容。我统一用12兼容性和算子覆盖率最平衡。dynamic_axes把batch维设为动态这样同一个模型可以支持1张图推理和多张图批量推理。输入图像尺寸固定640x640不做动态输入原因是尺寸变化会触发TensorRT或RKNN的重建优化流程速度反而慢。output_names[output]YOLOv8导出ONNX后只有一个输出节点shape是(batch, 4nc, 8400)8400是三层特征图80x80、40x40、20x20的预测总数之和。导出后用onnxsim做图优化能去掉一些冗余的Transpose和Reshape算子。我有一次导出后ONNX体积从45MB缩到41MB推理速度还提升了10%。命令是python -m onnxsim best.onnx best_sim.onnx如果装了onnxsim的话。4.2 ONNX Runtime推理CPU推理与INT8量化对比ONNX Runtime是目前跑ONNX最省心的推理引擎。下面这段代码是完整的推理流程读取图像、预处理、推理、后处理NMS。预处理和PyTorch里做的一定要保持一致否则会出现第5章说的“导出后结果不一致”问题。import cv2 import numpy as np import onnxruntime as ort class YOLOv8ONNX: def __init__(self, onnx_path): so ort.SessionOptions() so.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_ALL self.session ort.InferenceSession(onnx_path, so, providers[CPUExecutionProvider]) self.input_name self.session.get_inputs()[0].name self.output_name self.session.get_outputs()[0].name def letterbox(self, img, new_shape(640, 640)): # 保持宽高比缩放四周pad灰色边 h, w img.shape[:2] r min(new_shape[0] / h, new_shape[1] / w) new_unpad (int(round(w * r)), int(round(h * r))) img cv2.resize(img, new_unpad, interpolationcv2.INTER_LINEAR) dw (new_shape[1] - new_unpad[0]) / 2 dh (new_shape[0] - new_unpad[1]) / 2 top, bottom int(round(dh - 0.1)), int(round(dh 0.1)) left, right int(round(dw - 0.1)), int(round(dw 0.1)) img cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value(114, 114, 114)) return img, r, (dw, dh) def preprocess(self, img): img, r, pad self.letterbox(img) img img[:, :, ::-1].transpose(2, 0, 1) # BGR转RGB并改为CHW img np.ascontiguousarray(img, dtypenp.float32) img / 255.0 # 归一化到0~1 img np.expand_dims(img, axis0) return img, r, pad def postprocess(self, output, conf_thres0.25, iou_thres0.45): # output shape: (1, 4nc, 8400)先转成(8400, 4nc) preds output[0].transpose(1, 0) boxes, scores preds[:, :4], preds[:, 4] # 对单类别只需取第一个score conf scores[:, 0] keep conf conf_thres boxes, conf boxes[keep], conf[keep] # NMS需要方框坐标YOLOv8输出是中心点宽高转成左上角右下角 x1 boxes[:, 0] - boxes[:, 2] / 2 y1 boxes[:, 1] - boxes[:, 3] / 2 x2 boxes[:, 0] boxes[:, 2] / 2 y2 boxes[:, 1] boxes[:, 3] / 2 boxes np.stack([x1, y1, x2, y2], axis1) # 调用opencv的NMS idx cv2.dnn.NMSBoxes(boxes.tolist(), conf.tolist(), conf_thres, iou_thres) if len(idx) 0: return [], [], [] idx idx.flatten() return boxes[idx], conf[idx], idx def detect(self, img): input_data, r, pad self.preprocess(img) outputs self.session.run(None, {self.input_name: input_data}) boxes, confs, _ self.postprocess(outputs[0]) # 将坐标映射回原图尺寸 orig_boxes [] for box in boxes: x1 (box[0] - pad[0]) / r y1 (box[1] - pad[1]) / r x2 (box[2] - pad[0]) / r y2 (box[3] - pad[1]) / r orig_boxes.append([int(x1), int(y1), int(x2), int(y2)]) return orig_boxes, confs # 推理测试 detector YOLOv8ONNX(best_sim.onnx) img cv2.imread(fight_scene.jpg) boxes, confs detector.detect(img) for box, conf in zip(boxes, confs): cv2.rectangle(img, (box[0], box[1]), (box[2], box[3]), (0, 0, 255), 2) cv2.putText(img, ffight {conf:.2f}, (box[0], box[1]-5), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.imwrite(result.jpg, img)这里最容易出错的是坐标映射letterbox时在原图四周pad了一圈灰边后处理得到的框坐标是在padding之后的图像坐标系里的必须减去pad偏移并除以缩放比例才能映射回原图。很多人忘了减pad[0]和pad[1]导致框整体往右下偏移——这个坑在监控场景很致命因为打架的人往往在画面边缘框偏移之后可能直接落在墙上了。onnxruntime和onnx的区别这里也值得澄清一句onnx是模型格式onnxruntime是运行这个格式的推理引擎。你在网上搜onnx怎么运行实际拿到的答案几乎都是基于onnxruntime的。INT8量化是在CPU上提速的主要手段。onnxruntime提供了quantization模块动态量化最简单from onnxruntime.quantization import quantize_dynamic, QuantType # 动态量化最简单不需要校准数据 quantize_dynamic(best_sim.onnx, best_int8.onnx, weight_typeQuantType.QInt8)动态量化只量化权重激活值还是浮点推理速度大概提升20%~40%。如果想进一步压到半精度甚至全INT8需要静态量化得准备几百张校准图统计激活值分布流程复杂一些。我实测在i5-1240P这种普通CPU上FP32的YOLOv8n推理单帧约50msINT8动态量化后降到35ms左右在RK3588上差距会更明显FP32跑不到实时INT8刚好能到25FPS上下。4.3 边缘设备部署RK3588上跑YOLOv8的转换链路如果你要在RK3588这类边缘盒子部署链路变成pt - onnx - rknn。中间多了一层坑也多一层。常见做法是使用rknn-toolkit2在PC端完成转换再把rknn模型部署到板端推理。第一步还是导出ONNX但这里的导出要特别注意RKNN Toolkit对动态shape支持不好我建议导出时把batch固定为1不要开dynamic_axes否则转RKNN阶段会报错。导出命令yolo export modelbest.pt formatonnx opset12 dynamicFalse simplifyTrue然后在PC端安装rknn-toolkit2执行转换脚本from rknn.api import RKNN rknn RKNN() # 配置目标平台为rk3588 rknn.config(target_platformrk3588, mean_values[[0, 0, 0]], std_values[[255, 255, 255]]) # 加载ONNX模型 ret rknn.load_onnx(modelbest_sim.onnx) if ret ! 0: print(load onnx failed) # 构建rknn模型 ret rknn.build(do_quantizationTrue, datasetcalib.txt) if ret ! 0: print(build failed) # 导出rknn文件 ret rknn.export_rknn(best.rknn)注意这里mean_values和std_values要和训练时的预处理对齐。YOLOv8训练时的预处理是除以255然后把像素值从0~1输入网络所以对应的归一化配置是mean[[0,0,0]], std[[255,255,255]]。如果你用的是ImageNet预训练那种mean[0.485,0.456,0.406], std[0.229,0.224,0.225]的配置模型输出会完全不同这是跨平台部署最常见的预处理不一致问题。板端推理时rknn的输入输出格式和onnxruntime略有差异需要自己写NMS。rknn-toolkit2的examples里有yolov8的参考代码按官方demo改即可。这里的经验是板端NMS用cv2.dnn.NMSBoxes也能跑但如果帧率要求高建议用rknn提供的npu加速API。5. 打架行为检测避坑指南翻车记录与排查方法5.1 Loss还在降mAP却不涨先查数据而不是调参现象训练到第20轮train_loss和val_loss都在稳步下降但mAP50始终在0.6上下徘徊上不去。原因打架数据集的类别极其不平衡——绝大多数标注的打架框都集中在画面中心因为摄像头装在墙上打架事件基本发生在画面中间区域。模型学到的不是打架的样子而是画面中心的物体是打架导致边缘区域的打架样本mAP贡献为0。另一个常见原因是标注框太松把周围人群也圈进去了模型学到的特征混入了大量围观者信息。解决先统计所有标注框的中心点分布和宽高分布画出来看是不是畸形的。如果是中心点聚集把训练集里画面边缘的样本过采样或者用Mosaic增强强制模型在拼接图像的不同位置学到目标的特征。如果是框太松重新标注那些把多人框在一个大框里的样本尽量每人单独一个框框住的主要是发生肢体接触的部位。5.2 ONNX导出后检测结果与PyTorch不一致查这三个地方现象best.pt在PyTorch里检测正常导出ONNX后用onnxruntime推理很多框的位置偏了、置信度变了甚至部分目标漏检。原因这是导出环节最高频的事故90%出在预处理不一致。PyTorch推理时YOLO走的是模型内部的预处理逻辑而onnxruntime的推理脚本要自己写预处理像素归一化方式、BGR/RGB顺序、letterbox的pad填充值任何一项对不上输入给网络的数据分布就不同。第二个原因是输出后处理写错了——YOLOv8输出是中心点加宽高格式如果按照YOLOv5的格式去解码框的中心和宽高全错。第三个原因是opset版本过低导致某些算子被拆成多个子图精度损失累积。解决先检查RGB/BGR顺序训练时用的是BGR读图然后内部转RGB你的onnx推理脚本是否也做了这一步。再检查归一化训练用的是像素值除以255映射到0~1你没有做这一步就是输入数据整体偏大。最后检查NMS参数PyTorch里的NMS阈值和onnx推理脚本里设置的是否一致。如果这三项都确认无误还是不一致把opset升到15试一次。5.3 拥抱和握手都被误判成打架负样本不够现象模型在测试集上mAP50有0.85但一接到真实监控视频两个人拥抱、握手、拍肩膀甚至弯腰捡东西都会被误报。原因训练集里只有fight这一类正样本没有专门收集干扰场景做负样本。YOLO的损失函数只对正样本区域计算分类损失模型对这个区域看起来像打架但不是打架的场景完全没有分辨能力。拥抱这个动作在单帧里和打架的视觉相似度极高——两人距离近、肢体重叠、有交互动作模型没有见过足够多的拥抱样本自然会输出高置信度的误检。解决分两步走。第一步收集大量假打架负样本拥抱、握手、搀扶这三种场景各找几百张把它们作为背景图放进训练集训练时这些图里没有目标框模型会学到这些特征不该激活打架类。第二步在GUI界面加置信度阈值调节功能部署时根据现场反馈把阈值从默认的0.25调到0.5甚至0.6。这里没有完美的阈值只能根据误报率实测数据去调现场环境不同阈值不同。5.4 GUI界面假死视频推理卡顿线程设计错了现象用PySide2或PyQt做GUI界面打开视频后窗口拖不动、按钮点了没反应关掉窗口要等好几秒推理FPS只有个位数。原因把视频帧读取、模型推理这些耗时操作直接放在了主线程的循环里。GUI框架的主线程负责处理界面事件你在主线程里做一次推理要几十毫秒这段时间界面事件全部被阻塞表现就是假死。视频帧读取和推理相互等待读帧占用了推理时间推理占用了读帧时间FPS自然上不去。解决用QThread把推理逻辑放到独立线程主线程只负责接收结果并刷新画面。更完整的方案是双队列一个队列存原始帧一个队列存带检测结果的帧读帧线程只放帧不消费推理线程只管消费和处理GUI只从结果队列取数据。后面第6章我会给出具体架构代码。5.5 训练集太小导致过拟合冻结backbone不一定能救现象数据集只有300张图训练到第40轮时val_loss开始上升train_loss还在下降mAP50出现剧烈波动泛化能力明显不足。原因打架数据集标注成本高很多项目起步阶段只有几百张图。YOLOv8n有几百万个参数几百张图远远不够让模型从零学到通用特征过拟合是必然的。解决处理顺序是这样——先冻结backbone只训练head用预训练权重把backbone的特征提取能力保留住同时开启大强度数据增强ultralytics的Mosaic、MixUp、HSV增强全部打开再用早停机制patience15或20保住最佳模型。如果还不行就该考虑扩充数据而不是继续调参了。常见做法是从公开数据集里找包含暴力场景的片段截帧或者把已有数据做随机裁剪、旋转、亮度扰动生成扩充样本10倍左右的扩增在打架检测上是有效的。6. GUI工程的最后一公里用PySide2把模型封装成演示工具6.1 架构设计QThread、信号槽与双队列缓冲一个能给别人演示的打架检测GUI核心不在于界面多花哨而在于视频画面不卡、告警及时、阈值可调。我用PySide2实现的结构是三线程模型主线程管UIVideoThread管视频读取和推送帧DetectThread管模型推理。import cv2 import sys import queue from PySide2.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton, QSlider from PySide2.QtCore import QThread, Signal, QTimer from PySide2.QtGui import QImage, QPixmap class VideoThread(QThread): frame_ready Signal(object) # 原始帧就绪 def __init__(self, video_source): super().__init__() self.video_source video_source self.cap None self.running True def run(self): self.cap cv2.VideoCapture(self.video_source) while self.running: ret, frame self.cap.read() if not ret: break self.frame_ready.emit(frame) # 只发原始帧 QThread.msleep(5) # 控制读帧节流 def stop(self): self.running False if self.cap: self.cap.release() class DetectThread(QThread): result_ready Signal(object, object) # (带检测结果的帧, 检测信息) def __init__(self, onnx_path, conf_thres0.4): super().__init__() self.frame_queue queue.Queue(maxsize4) self.detector YOLOv8ONNX(onnx_path) self.conf_thres conf_thres self.running True def run(self): while self.running: try: frame self.frame_queue.get(timeout1) except queue.Empty: continue boxes, confs self.detector.detect(frame, self.conf_thres) # 在帧上绘制检测框 for box, conf in zip(boxes, confs): cv2.rectangle(frame, (box[0], box[1]), (box[2], box[3]), (0, 0, 255), 2) cv2.putText(frame, ffight {conf:.2f}, (box[0], box[1]-5), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) self.result_ready.emit(frame, (len(boxes), confs)) def stop(self): self.running False class MainWindow(QMainWindow): def __init__(self): super().__init__() self.video_thread VideoThread(0) # 0表示摄像头 self.detect_thread DetectThread(best_sim.onnx) # 信号槽连接视频线程发帧给检测线程的队列检测线程发结果给UI刷新 self.video_thread.frame_ready.connect(self.on_new_frame) self.detect_thread.result_ready.connect(self.update_frame) self.conf_slider QSlider() # 阈值滑条让用户现场调 def on_new_frame(self, frame): # 队列满则丢弃最旧的帧保证UI响应不积压 if self.detect_thread.frame_queue.full(): try: self.detect_thread.frame_queue.get_nowait() except queue.Empty: pass self.detect_thread.frame_queue.put(frame) def update_frame(self, frame, detection_info): # 把OpenCV的BGR帧转为QImage并显示 rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qimg QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.video_label.setPixmap(QPixmap.fromImage(qimg))这个架构的关键点读帧和推理之间用有界队列缓冲队列满了直接丢旧帧防止推理跟不上时内存无限涨也防止UI显示滞后越来越严重。帧率不稳时宁可跳帧也不要卡界面这是GUI类检测工具通用原则。6.2 阈值调节与告警策略的个人习惯GUI界面做出来后我习惯把置信度阈值和告警延迟做成可调参数而不写死在代码里。现场部署时不同的摄像头角度、光线条件对最佳阈值影响很大前端值班人员又不愿意改代码所以界面上一定要有滑条。我的个人习惯是套告警策略而不是单帧告警单帧检测到fight后连续3帧内至少出现2帧才算真告警减少闪烁和偶发误报。这个策略在打架事件里不会错因为打架过程至少持续几秒而单帧误检通常无法连续命中。这个策略用帧计数实现代码量很小但对演示效果和值班体验的提升非常大。这类项目的最后一公里往往是反直觉的模型训练只占三成工作量数据标注和GUI体验各占三成剩下一成才是环境部署。我做过好几个类似的检测项目之后学到的教训是不要在mAP从0.84调到0.87上花太多时间把精力放在漏报率和误报率的权衡上放在界面能不能让客户自己调阈值上现场满意度反而更高。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →