简介:面向监控场景打架检测项目,这份资源提供3000张真实监控视角下的高质量打架图片,覆盖街道、酒吧、商店、公交车、监狱及空旷地等多元场景,包含两人冲突与多人斗殴情况,标签统一为fight类别。数据均经labelimg标注,同时给出VOC、COCO、YOLO三种常用格式,可直接接入YOLO等算法训练。附赠的YOLO11一键训练脚本还兼顾GPU、CPU及Mac芯片平台,并附有博主训练日志供参考。资源包为单个PDF文件,容量约5.63MB,内有数据集完整介绍及百度网盘获取方式,目前已有854人学习下载,适合从事安防监控、行为分析及目标检测实战的开发者快速部署使用。
1. 为什么打架检测数据集比模型结构更卡脖子
做目标检测的都知道,用 COCO 预训练权重去微调一个车辆识别模型,往往几十个 epoch 就有效果。但把同样的套路搬到打架检测上,很多人第一次就翻车了——mAP 低得离谱,拿测试视频一跑,别说检测打架了,两个人推搡都被框成“人”然后丢掉。问题基本不在模型,而在数据:打架是典型的小样本、强语义、长尾行为检测场景,公开数据集少,标注成本高,背景又极其杂乱(教室、走廊、工地、地铁站),模型稍微换个环境就失灵。
这套“打架检测数据集-3000张图+对应VOC/COCO/YOLO三种格式标签+支持GPU/CPU/Mac三平台YOLO11一键训练脚本”的方案,就是把最浪费时间的两个环节一次性解决了:一是数据已经标好并且同时给了三种工业级格式,不管你是用 Ultralytics 还是 Detectron2 还是老式 VOC 流程都能直接吃;二是配套的训练脚本把环境适配和训练参数收敛到一个命令里,GPU 机器、纯 CPU 服务器、Mac 上都跑得起来。适合的人很清楚:接安防项目要快速出 demo 的、做智慧校园/工地监控想验证打架识别可行性的、以及刚入门行为检测想省掉数据整理功夫的研究生。
2. 3000张图与三种格式:先搞懂打架检测在“喂”什么
2.1 为什么打架检测不能拿通用检测数据硬训
通用目标检测数据集(比如 COCO 的 person 类)只解决“人在哪里”,而打架检测要解决的是“人的交互状态是什么”。这两个任务的难度差了一个量级。COCO 里的人可能是站着的、坐着的、走着的,姿态相对独立;打架场景里两个人通常是高度重叠、肢体互相遮挡、运动模糊严重,而且人体关键点扭曲程度远超日常姿态。用 COCO 预训练权重直接做迁移,模型第一轮学到的全是“一个完整的人”,一旦两人身体重叠超过 30%,检测框就开始互相吞并。
所以在自建打架检测数据时,标注策略不能只画人框,而是要把“正在参与肢体冲突的人”整体圈起来作为一个类,通常命名 fight。正样本是成对的、有物理接触并且动作幅度剧烈的人,负样本则是并肩走路、拥抱、握手、搀扶这类容易混淆的场景。3000 张图听着不多,但如果正负样本比控制在 1:2 左右、背景场景覆盖足够,其实已经够训练出一个能出 demo 的模型了。关键不在于张数,在于每张图里有没有“难例”。
2.2 VOC、COCO、YOLO 三种格式到底差在哪
很多新手拿到数据集先问:三种格式不是重复了吗?其实每种格式背后对应一套工具链,你手里的训练脚本是哪个生态的,就得喂哪种标签。VOC 是早期最通用的标注格式,一个图片对应一个 XML 文件,人可读性强,自研脚本时最好调试;COCO 是 JSON 单文件格式,MMDetection、Detectron2 这类框架原生吃它;YOLO 是归一化坐标的 txt 格式,一行一个框,Ultralytics 生态直接读它。
三种格式的对应关系可以看这张表:
| 格式 | 文件组织 | 标注内容 | 主要消费方 |
|---|---|---|---|
| VOC | 每图一个 XML | 绝对像素坐标、类别名 | PASCAL VOC 工具链、自研脚本 |
| COCO | 单 JSON 内含全部图 | 绝对像素坐标、类别 ID、area | MMDetection、Detectron2 |
| YOLO | 每图一个 txt | 归一化中心点+宽高 | Ultralytics YOLO 全系列 |
转换的时候最核心的数学关系只有一组:VOC 的 (xmin, ymin, xmax, ymax) 是绝对像素值,YOLO 需要的是中心点坐标和宽高除以图片宽高后的比例,COCO 则是 xmin, ymin, w, h 的绝对像素值。也就是说 COCO 和 VOC 之间只差一个“宽高换右下角坐标”,YOLO 则是多一步归一化。理解了这层关系,任何格式互转都不会出错。
2.3 拿到数据集先做“数据体检”再开训
我拿到任何标注好的数据集,第一步不是训练,而是跑一个数据体检脚本——统计各类别框数量、框的尺寸分布、图片分辨率分布。这一步能提前暴露绝多大数训练翻车的原因。
import os import glob from collections import Counter # 以 YOLO 格式为例做数据体检 label_dir = "datasets/fight/labels/train" img_dir = "datasets/fight/images/train" size_buckets = {"small": 0, "medium": 0, "large": 0} class_count = Counter() total_boxes = 0 for label_file in glob.glob(os.path.join(label_dir, "*.txt")): img_file = os.path.join(img_dir, os.path.basename(label_file).replace(".txt", ".jpg")) if not os.path.exists(img_file): print(f"[缺失] 标签存在但图片缺失: {img_file}") continue W, H = 1920, 1080 # 建议读取图片头信息获得真实宽高 with open(label_file, "r") as f: for line in f: parts = line.strip().split() if len(parts) != 5: print(f"[异常] 格式错误: {label_file} -> {line}") continue cls, xc, yc, w, h = parts class_count[cls] += 1 total_boxes += 1 # 按归一化宽高和图片尺寸换算绝对像素面积,判断大小框 box_pixels = float(w) * W * float(h) * H if box_pixels < 32 * 32: size_buckets["small"] += 1 elif box_pixels < 96 * 96: size_buckets["medium"] += 1 else: size_buckets["large"] += 1 print("类别统计:", dict(class_count)) print("框尺寸分布:", size_buckets) print("总框数:", total_boxes)这段逻辑不复杂,但很多人会跳过。它的价值在于:如果打架类只有 800 个框、而负样本类有 4000 个框,你训练时就要准备应对类别不平衡;如果 small 占比超过一半,说明你的很多目标是远处的小人影打架,训练时得把 imgsz 调到 960 或者加 P6 检测层,否则小目标根本检测不到。体检输出的“标签存在但图片缺失”这类问题,会直接影响训练时 mAP 异常低——这个坑我在第 5 章会展开讲。
3. 把 VOC/COCO/YOLO 串起来:格式互转脚本与边界校验
3.1 转换逻辑的三个关键映射
做三种格式互转,核心要维护好三组映射关系:图片文件名是唯一主键;类别名与类别 ID 的对应关系必须固定,不能 VOC 里叫 fight、COCO 里叫 1、YOLO 里又变成 0;坐标系上要严格区分绝对像素和归一化坐标。
常见的误用是把 YOLO 的 txt 当成通用检测框格式,直接喂给读 VOC 的脚本,导致 x_center 被当成 xmin,w 被当成 xmax。这类问题在日志里表现为 loss 下降但 mAP=0,因为检测框的坐标语义完全是错的。所以下面的转换脚本里,我会在函数名和注释里把坐标系写清楚,并加了边界校验,不合法就直接打印警告。
3.2 一份可复用的 VOC/YOLO/COCO 互转脚本
import os import json import glob import xml.etree.ElementTree as ET from collections import defaultdict class FormatConverter: def __init__(self, class_names): # 类别顺序表:索引就是 YOLO/COCO 的 cls_id,name 就是 VOC 里的标签名 self.class_names = class_names self.cls_name_to_id = {name: idx for idx, name in enumerate(class_names)} def _parse_voc_xml(self, xml_path): """解析 VOC XML,返回图片尺寸和绝对坐标框列表""" root = ET.parse(xml_path).getroot() size_el = root.find("size") W = int(size_el.find("width").text) H = int(size_el.find("height").text) boxes = [] for obj in root.iter("object"): name = obj.find("name").text if name not in self.cls_name_to_id: print(f"[跳过] XML 里出现未定义类别 {name} @ {xml_path}") continue bnd = obj.find("bndbox") xmin = float(bnd.find("xmin").text) ymin = float(bnd.find("ymin").text) xmax = float(bnd.find("xmax").text) ymax = float(bnd.find("ymax").text) # 边界收缩:防止 xmin/xmax 或 ymin/ymax 写反 xmin, xmax = min(xmin, xmax), max(xmin, xmax) ymin, ymax = min(ymin, ymax), max(ymax, ymin) boxes.append({"name": name, "bbox": [xmin, ymin, xmax, ymax]}) return W, H, boxes def voc2yolo(self, voc_dir, yolo_dir): """VOC XML 转 YOLO txt 的核心函数""" os.makedirs(yolo_dir, exist_ok=True) for xml_path in glob.glob(os.path.join(voc_dir, "*.xml")): W, H, boxes = self._parse_voc_xml(xml_path) base = os.path.basename(xml_path).replace(".xml", "") lines = [] for box in boxes: xmin, ymin, xmax, ymax = box["bbox"] cls_id = self.cls_name_to_id[box["name"]] # YOLO 用中心点 + 宽高,全部归一化到 0~1 xc = (xmin + xmax) / 2.0 / W yc = (ymin + ymax) / 2.0 / H w = (xmax - xmin) / W h = (ymax - ymin) / H # 越界保护:归一化后坐标不在 0~1 的处理掉或裁剪 if xc < 0 or yc < 0 or xc > 1 or yc > 1 or w <= 0 or h <= 0: print(f"[异常] 非法框 {box['bbox']} @ {base}") continue lines.append(f"{cls_id} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}") with open(os.path.join(yolo_dir, base + ".txt"), "w") as f: f.write("\n".join(lines)) if not lines: print(f"[警告] {base} 转换后无有效框,原 XML 可能为空标注") def yolo2coco(self, yolo_dir, img_dir, out_json): """YOLO txt 转 COCO json,COCO 用绝对像素 x,y,w,h""" images = [] annotations = [] img_id = 1 ann_id = 1 for txt_path in sorted(glob.glob(os.path.join(yolo_dir, "*.txt"))): base = os.path.basename(txt_path).replace(".txt", "") img_file = None for ext in [".jpg", ".jpeg", ".png"]: cand = os.path.join(img_dir, base + ext) if os.path.exists(cand): img_file = cand break if img_file is None: print(f"[缺失] {base} 找不到对应图片,跳过") continue W, H = 1920, 1080 images.append({"id": img_id, "file_name": os.path.basename(img_file), "width": W, "height": H}) with open(txt_path, "r") as f: for line in f: parts = line.strip().split() cls_id, xc, yc, w, h = map(float, parts) xmin = (xc - w / 2) * W ymin = (yc - h / 2) * H bw = w * W bh = h * H annotations.append({ "id": ann_id, "image_id": img_id, "category_id": int(cls_id), "bbox": [xmin, ymin, bw, bh], "area": bw * bh, "iscrowd": 0 }) ann_id += 1 img_id += 1 categories = [{"id": i, "name": n} for i, n in enumerate(self.class_names)] with open(out_json, "w") as f: json.dump({"images": images, "annotations": annotations, "categories": categories}, f) print(f"[完成] COCO 标注已写入 {out_json},共 {len(images)} 图 / {len(annotations)} 框") if __name__ == "__main__": # 类别顺序要与 YOLO11 训练时的 data.yaml 保持一致 converter = FormatConverter(class_names=["fight"]) converter.voc2yolo("datasets/Annotations", "datasets/labels") converter.yolo2coco("datasets/labels", "datasets/images", "datasets/annotations.json")转换脚本里有两个细节值得说。第一个是“类别顺序”:YOLO 和 COCO 都是拿数字 ID 做类别索引的,一旦顺序写错,所有框的类别都会错位,而且极难发现。我的习惯是把类别表写成一个独立列表,然后让格式转换脚本和后续训练脚本共用同一份配置,从源头避免两边不一致。第二个是“越界保护”:手工标注时偶尔会出现 xmin 大于 xmax 的情况,或者标注框超出了图片边界几条像素,直接转换后训练会报错或产生 NaN loss,转换时顺手修剪或跳过,能省下后面大半排错时间。
3.3 转换后的三分钟自检
转换完成不代表没问题。我每次跑完转换都会做一个交叉验证:随机抽 20 张图,把转换后的框可视化画在原图上,肉眼确认类别和位置对不对。这种做法比任何自动化校验都先一步发现问题。自动化校验则可以做三件事:检查 YOLO txt 的每行是否五个数字且归一化坐标在 0-1 之间;检查 COCO JSON 的 image_id 是否都对应真实图片文件;检查每个标注框的面积是否大于 0。
可视化抽检时最常看到的问题是“框错位了半个人身”——大概率是宽高比换算时把中心点坐标和宽高搞混了。这属于典型的手滑,肉眼一看就能定位到具体是哪个函数哪个公式的问题,比对着日志猜高效得多。如果你用的是 Jupyter,直接 matplotlib 画框即可;如果是在服务器上,把图存成文件再看也行。
4. YOLO11 一键训练脚本:三平台环境配置与参数收敛
4.1 GPU / CPU / Mac 三平台的环境差异
YOLO11 由 ultralytics 这个库承载,pip 安装一行能搞定,但真正把训练跑起来,三台机器的坑完全不一样。GPU 机器最顺,前提是 CUDA、cuDNN 和 PyTorch 的版本要匹配,常见翻车是装好 torch 后torch.cuda.is_available()返回 False,基本是 CUDA 装得不对或者 torch 是 CPU 版。纯 CPU 服务器跑训练没问题,只是慢,但很多人会忽略torch.get_num_threads()默认值偏低,不设置的话 CPU 利用率上不去。Mac 上则是 mps 后端,Apple Silicon 的 GPU 能参与训练,不过 ultralytics 对 mps 的算子支持是逐步完善的,版本太老或太新都可能报算子 not implemented。
我的建议是用一段环境自检代码放在训练脚本的前面,先自动探测设备再启动训练,而不是让用户手动选平台。这也是标题里“一键”的底气所在。
import torch def auto_select_device(): if torch.cuda.is_available(): device = "0" print(f"[设备] GPU: {torch.cuda.get_device_name(0)}") elif torch.backends.mps.is_available(): device = "mps" print("[设备] Apple Silicon MPS") else: device = "cpu" print("[设备] CPU") return device这段代码按优先级选设备。需要注意一个关键点:mps 和 cpu 在 ultralytics 的训练逻辑里都不能沿用 GPU 的 batch size 习惯。MPS 显存占用和 CUDA 计算方式不同,batch size 设太大容易在某个 epoch 中途抛出一个晦涩的算子错误;CPU 则完全受线程数影响,需要在训练脚本里显式设置torch.set_num_threads。我一般建议 GPU 上 batch 设在 16-32,CPU 和 MPS 上 4-8,并且同时把workers参数调小,否则数据加载器会成为真正的瓶颈。
4.2 数据集配置与一键训练脚本拆解
ultralytics 训练需要先写一个数据集 YAML 文件,告诉框架图片和标签在哪、类别有哪些。下面是我基于这套打架检测数据集整理的配置文件:
# datasets/fight.yaml path: ../datasets/fight # 数据集根目录 train: images/train # 相对 path 的训练图片目录 val: images/val # 相对 path 的验证图片目录 names: 0: fight # 类别索引 0 对应打架YAML 里的 path、train、val 三者拼接出的路径会直接传给 ultralytics 的数据加载器。这里最常见的误用是相对路径没写对,尤其当训练脚本和数据集目录不在同一层时,path: ../datasets/fight会解析出一个不存在的目录,导致启动训练直接报 dataset not found。我的习惯是直接用绝对路径,或者用 os.path.abspath 动态拼,避免路径依赖。
训练脚本本身不长,核心调用只有一行:
from ultralytics import YOLO # 固定类别顺序与刚才的转换脚本保持一致 model = YOLO("yolo11n.pt") # 或 yolo11s.pt / yolo11m.pt model.train( data="datasets/fight.yaml", epochs=120, imgsz=640, batch=16, device=auto_select_device(), patience=20, lr0=0.001, seed=42, save_period=10, project="runs/fight_det", name="yolo11n_baseline", pretrained=True, )这里有几个参数直接影响打架检测的效果,得展开说。pretrained=True意味着从 COCO 预训练权重启程,对打架这种数据量不大的任务基本是必须的,随机初始化会让收敛极慢且不稳定。imgsz=640是默认分辨率,如果你的监控画面里人是小目标(比如校园操场全景),要提高到 960,代价是显存占用大幅上升。patience=20是早停耐心值,连续 20 个 epoch 验证集 mAP 不涨就提前停,省时间但不建议设太小,打架检测的指标本身就容易波动。lr0=0.001是我在这个方向比较稳的初始学习率,数据规模小,学习率大了很容易直接发散,训练日志里 loss 猛涨时先检查这里。
4.3 训练时盯着哪几个关键日志
训练一轮后,日志里最该看的是Box(P)、Box(R)和mAP50这三列。mAP50 是指 IoU 阈值 0.5 下的平均精度均值,打架检测这种交互行为场景,mAP50 做到 0.75 以上就已经具备实用价值;mAP50-95 是更严格的指标,会明显低一些,不用被它吓到。如果看到 mAP 徘徊在 0.3 以下,先不要调模型结构,回头检查数据:类别数量对不对、标签有没有错位、验证集是不是和训练集分布差异太大。
训练结束后会在runs/fight_det/yolo11n_baseline/里产出weights/best.pt和last.pt。best.pt 是验证集上表现最好的权重,部署和测试一律用 best.pt,不要为了省事用 last.pt——训练后期模型可能过拟合,last.pt 的表现往往比 best.pt 差不少。我习惯每次都把 best.pt 单独拷出来重命名加上数据集描述和时间戳,毕竟读代码的人不一定记得住这次训练到底用的哪套数据。
4.4 用训练好的模型跑一段 RTSP 监控流验证
数据集训练出来的模型最终要在视频流上验证,不能只看验证集的静态指标。常见做法是用 cv2 拉取监控摄像头的 RTSP 流再丢给模型推理,下面是这个方向的常用骨架:
import cv2 from ultralytics import YOLO model = YOLO("runs/fight_det/yolo11n_baseline/weights/best.pt") cap = cv2.VideoCapture("rtsp://your_monitor_stream") # 换成实际可用的视频流地址 while cap.isOpened(): ret, frame = cap.read() if not ret: break results = model.predict(frame, imgsz=640, conf=0.25, device="0") for r in results: boxes = r.boxes.xyxy.cpu().numpy() confs = r.boxes.conf.cpu().numpy() for box, conf in zip(boxes, confs): x1, y1, x2, y2 = map(int, box) label = f"fight {conf:.2f}" cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imshow("fight detection", frame) if cv2.waitKey(1) == ord("q"): break cap.release() cv2.destroyAllWindows()推理时要格外注意conf这个阈值。打架检测场景宁可误报不可漏报,conf 设在 0.15-0.25 之间比较合理;如果误报太多,再往 0.3 以上调。但单帧模型的召回率再高也有限,真实场景还要配合时序平滑——这是最后一章要展开讲的进阶内容。
5. 打架检测训练避坑:5 条血泪经验汇总
5.1 标签错位:mAP 为 0 但 loss 正常下降
现象:训练了 50 个 epoch,mAP 始终是 0,loss 却一路下降,玄学得像黑匣子。
原因:标签文件与图片文件没有严格一一对应。数据集里的图片可能经过裁剪或重命名,但 XML/txt 里的文件名没同步更新,导致模型读到的图片和标签不是同一张图。常见于手工整理数据集时只拷贝了图片忘记拷贝标签,或者转换脚本以标签文件名反查图片时找到了另外一张同名图。
解决:在上面的数据体检脚本基础上加一道强校验——遍历所有图片文件,逐个确认存在同名标签文件;反过来遍历所有标签文件,确认存在同名图片。两边都缺一不可。一旦发现缺失,不要手动补,直接删除这对孤儿文件换新数据源,因为手工补标签容易补错坐标语义。
5.2 框坐标越界导致训练直接中断或产生 NaN loss
现象:训练到某个 epoch 突然报错,提示边界框数值非法,或者 loss 值变成 nan。
原因:标注工具生成的框超出了图片宽高边界,比如 xmax=1921 而图片宽只有 1920。YOLO 格式归一化后百分比可能超过 1.0,ultralytics 在训练数据增强阶段对这种框极其敏感。另一个常见来源是格式转换脚本里坐标公式写错,导致宽高算成负数,越界框得以存活进训练管线。
解决:在转换脚本里增加归一化坐标的越界检查,把越界框修剪到图片边界内,而不是直接丢弃——有些框只是超出几个像素,丢掉会损失有效标注。修剪逻辑很简单,xmin 和 xmax 分别 clamp 到 [0, W],ymin 和 ymax 分别 clamp 到 [0, H],然后重新计算中心点和宽高。这条规则要写进格式转换的公共函数里,不能只修一次,因为数据会不断更新迭代。
5.3 Mac MPS 训练中途报粗粒度的算子错误
现象:在 MacBook 上启动训练顺利,跑了两千多个 iteration 后,突然抛出一个 PyTorch 算子 not implemented 的错误,训练直接中断。
原因:MPS 后端对部分算子的支持不完整,数据增强里的某些操作(尤其和旋转、透视变换相关的)在 MPS 上没有实现或实现有 bug。ultralytics 版本更新很快,某次升级后原本能跑的脚本也开始报错,属于常见情况。
解决:核心思路是让训练环境的脆弱面最小化。第一,固定 ultralytics 版本,不要随意升级;第二,在训练参数里关闭重型数据增强,比如把degrees调成 0、perspective调成 0;第三,如果还是报错,退回 CPU 训练,Mac 的 CPU 训练速度其实还能接受,毕竟打架检测数据集只有 3000 张图,多等几个小时换来稳定性值得。我在 Mac 上跑这个数据集,通常直接选择 CPU 训练,省下的排错时间远大于 GPU 加速带来的收益。
5.4 类别不平衡:正样本太少导致模型漏检严重
现象:训练完的模型在测试视频里一个打架行为都检不出来,但正常行人倒是框得很准。
原因:数据集中 fight 类的框数远少于背景或者其他负样本类,模型天然偏向多数类,把赌注全压在“没有打架”上能获得很高的表面指标。尤其当验证集里打架框占比很低时,mAP 会虚高,掩盖漏检问题。
解决:先用 2.3 的体检脚本算出分类别框数。如果 fight 框不足整体 20%,做两类操作:一是复制正样本图片并在复制时做 mosiac/random_perspective 增强,二是调小 loss 里类别惩罚权重对多数类的倾斜。ultralytics 里可以调整cls参数来提高分类损失权重,把这个值从默认 0.5 调到 1.0-1.5 之间,模型会更卖力去拟合少数类。调参之后一定要单独观察 fight 类的召回率,而不仅是整体 mAP。
5.5 单帧误检:拥抱、握手、搀扶被识别成打架
现象:模型在测试视频里看到两个人拥抱就报警,误报率高到没法上线。
原因:单张静态图里,“打架”和“拥抱/搀扶”在姿态上是高度混淆的。打架有大幅度肢体摆动、头部后仰、身体倾斜,但这在某一帧截图上和拥抱区别不大,模型根本没有足够信息做判断。这是行为检测的固有难题,不是数据量能直接解决的。
解决:训练集里加入大量高相似度的负样本,比如握手、拥抱、拍肩、搀扶,把它们单独作为 one more 类标出来,让模型学会区分。部署侧则用多帧投票——连续 N 帧里至少 M 帧检出 fight 才触发报警。我一般用 N=10、M=6(即 1 秒内至少 0.6 秒被判定为打架),误报能压掉一半以上。这个方案不增加训练复杂度,只在推理环节加一个计数逻辑,是性价比最高的后处理手段。
6. 把模型从“能跑”变成“能上线”:时序投票与导出部署
训练得到一个 best.pt 只是开始。打架检测这种场景要实际部署,至少得做两件进阶事:一是给模型加上时序平滑逻辑,二是把它导出成部署格式。
时序投票的代码不长,但逻辑必须想清楚——它维护一个滑动窗口计数器,按帧推进,只有当窗口内的打架检出帧占比超过阈值时,才真正输出报警信号:
from collections import deque class FightVoteFilter: def __init__(self, window_size=10, vote_threshold=6): self.window = deque(maxlen=window_size) self.threshold = vote_threshold def update(self, is_fight_frame): self.window.append(1 if is_fight_frame else 0) return sum(self.window) >= self.threshold这个 10 帧滑动窗口对应 1 秒左右的视频长度。window_size 和 vote_threshold 的取值要按实际场景微调,如果监控场景是密集人群,窗口可以拉长到 15 帧;如果要求秒级报警,win_size=5、threshold=3 也够用。我接项目时一般先按上面这组默认值跑,再根据客户对误报的容忍度调——安防客户能接受漏报比例低但误报频繁吗?通常不能,所以宁可窗口大一点、反应慢一秒。
导出部署也有讲究。如果你要部署到 GPU 服务器上跑实时推理,把 best.pt 导出成 TensorRT 格式会快非常多;如果要扔到边缘盒子或没有 PyTorch 的环境,ONNX 是通用选择。ultralytics 里一行命令搞定:
yolo export model=runs/fight_det/yolo11n_baseline/weights/best.pt format=onnx dynamic=False imgsz=640导出 ONNX 时最容易踩的坑是 dynamic 参数——默认固定分辨率会让模型在其他分辨率下推理出错,但开 dynamic 又要检查部署框架支不支持动态输入。我的建议是先固定分辨率导出,部署时把输入帧 resize 到 640 再送进模型,这套流程最稳。
我个人的习惯是:跑任何行为检测类项目,都会留出 20% 的时间单独做数据清洗而非调参——格式转换、标签校验、负样本补充,这些环节的边际收益永远比换模型结构大得多。打架检测数据集做到现在这个配合度(3000 图 + 三格式 + 三平台脚本),真正价值在于把重复劳动压缩到了最低,省出来的时间值得全砸到场景适配上去。希望组里正在做类似方向的你,能靠着这套组合拳少踩几个坑。
本文还有配套的精品资源,点击获取