打架斗殴检测算法详解
目录
概述
这是一套基于深度学习的实时打架斗殴检测算法 ,应用于地铁、学校、商场等公共场所的安防监控场景。系统采用多模型级联 + 时序分析的方案,能够有效识别挥拳、踢腿、推搡等暴力行为,同时过滤摔倒、拥抱、搀扶等误报场景。
核心特性
- 多模型协同: YOLOv8 检测 + YOLOv8 Pose 姿态估计 + TSM 时序动作分类
- 时序分析: 基于 16 帧连续视频片段进行行为判断,避免单帧误判
- 目标跟踪: 使用 BYTETracker 实现跨帧目标追踪,保证行为分析的连续性
- 姿态校验: 基于人体关键点检测挥拳、踢腿等攻击性动作
- 智能过滤: 自动过滤搭肩、摸头、摔倒等非打架场景
- 滑动窗口: 8 帧滑动重叠,保证时序分析连续性
- 命中帧机制: YOLO 命中率校验,过滤误检测
系统架构
┌─────────────────────────────────────────────────────────────────┐
│ 打架斗殴检测流水线 │
└─────────────────────────────────────────────────────────────────┘
输入视频流
│
▼
┌──────────────────┐
│ 1. YOLOv8 检测 │ ← 检测画面中所有人员 (打架检测模型)
│ 2. YOLOv8-Pose │ ← 提取每个人体关键点 (17 个点)
│ (并行推理) │
└──────────────────┘
│
▼
┌──────────────────┐
│ 3. 姿态规则过滤 │ ← 基于角度/距离校验攻击姿态 (挥拳/踢腿)
│ │ 过滤搭肩/摸头/摔倒等误报
└──────────────────┘
│
▼
┌──────────────────┐
│ 4. BYTETracker │ ← 跨帧跟踪目标,维持 ID 一致性
│ │ 卡尔曼滤波预测丢失目标
└──────────────────┘
│
▼
┌──────────────────┐
│ 5. 16 帧采集缓冲 │ ← 滑动窗口收集连续帧
│ + 命中帧记录 │ 记录检测每帧命中情况
└──────────────────┘
│
▼
┌──────────────────┐
│ 6. 命中率校验 │ ← 命中率≥45% 才触发 TSM
│ │ 命中率<25% 停止采帧
└──────────────────┘
│
▼
┌──────────────────┐
│ 7. TSM 分类器 │ ← 时序动作分类 (正常/打架)
└──────────────────┘
│
▼
┌──────────────────┐
│ 8. 时序阈值判断 │ ← 持续时间超过配置阈值则报警
└──────────────────┘
│
▼
报警输出
模型介绍
1. YOLOv8 检测模型
用途: 检测画面中可能存在打架行为的人员区域,为后续模型排除噪声,锁定感兴趣区域。
模型配置
| 参数 | 值 |
|---|---|
| 模型版本 | YOLOv8s (Small) |
| 输入尺寸 | 640×384 |
| 输出 | (4, 5, 5040) - 4 坐标 + 1 置信度 |
| 检测类别 | 单分类 (fight) |
| 置信度阈值 | 0.7 |
| IoU 阈值 | 0.5 |
训练数据
- 数据集规模: 8000 张标注图片
- 数据场景: 地铁、车站等公共场所的监控视频截图
- 标注内容: 打架斗殴人员检测框
- 数据增强: Mosaic、Mixup、随机翻转、色彩抖动等
版本选择说明
选择 YOLOv8s 而非更大版本的原因:
- 精度平衡: s 版本在精度和速度之间取得良好平衡
- 实时性要求: 监控场景需要高帧率处理,s 版本推理速度更快
- 场景特点: 打架检测对极致精度要求不高,后续有 TSM 二次校验
2. YOLOv8-Pose 姿态估计模型
用途: 提取每个人体的 17 个关键点,用于后续姿态分析
模型配置
| 参数 | 值 |
|---|---|
| 模型版本 | YOLOv8m-pose (Medium) |
| 权重来源 | YOLOv8 官方预训练权重 |
| 输入尺寸 | 640×384 |
| 输出 | (4, 56, 5040) - 4 坐标 + 52 关键点 (17 点×3 坐标) |
| 置信度阈值 | 0.25 |
| IoU 阈值 | 0.45 |
关键点定义
0:鼻子,1:左眼,2:右眼,3:左耳,4:右耳
5:左肩,6:右肩,7:左肘,8:右肘,9:左手腕
10:右手腕,11:左髋,12:右髋,13:左膝,14:右膝
15:左脚踝,16:右脚踝
版本选择说明
选择 YOLOv8m-pose 并使用官方预训练权重的原因:
- 关键点精度: m 版本在关键点检测精度上明显优于 s 版本
- 迁移学习: 使用 COCO 数据集预训练权重,无需重新训练即可获得良好效果
- 通用性强: 人体关键点在不同场景下具有较好的泛化能力
- 算力可接受: pose 模型与检测模型并行推理,m 版本延迟可接受
3. TSM 时序分类模型
用途: 对 16 帧连续视频片段进行时序分析,判断是否为打架行为
模型配置
| 参数 | 值 |
|---|---|
| 模型架构 | TSM (Temporal Shift Module) |
| 输入格式 | N×3×224×224 (N 为帧数) |
| 采样帧数 | 16 帧 |
| 采样方式 | 均匀分段采样 (16 段每段取中心帧) |
| 输出 | (2,) - [正常,打架] 的概率分布 |
| 类别 | 0:正常行走,1:打架斗殴 |
训练数据
- 数据集规模: 2100 个精选打架视频片段,包括双人对打和多人混打
- 数据来源: 真实监控录像剪辑
- 数据清洗: 人工筛选高质量、清晰度的打架视频
- 视频裁剪: 对原始视频进行裁剪,聚焦于打架区域

实验对比
在训练过程中,测试了两种输入配置:
| 配置 | 输入形状 | 效果 |
|---|---|---|
| 方案 A | 8×3×224×224 | 8 帧输入,时序信息不足 |
| 方案 B | 16×3×224×224 | 16 帧输入,效果明显提升 |
结论 : 16 帧输入相比 8 帧在打架检测任务上具有明显提升,原因:
- 时序信息更丰富: 能够捕捉更完整的动作过程
- 动作趋势更清晰: 对抗性行为的连续性更容易识别
- 误报率更低: 瞬时动作不易被误判为打架
TSM 原理简述
TSM (Temporal Shift Module) 是一种高效的时间序列建模方法:
┌─────────────────────────────────────────────────────┐
│ TSM 核心思想 │
└─────────────────────────────────────────────────────┘
输入:[16 帧 × 3 通道] → 合并为 [48 通道]
│
▼
普通 CNN 卷积 (2D)
│
▼
在通道维度上进行时间移位操作
(让相邻帧的信息能够交互)
│
▼
输出:分类结果 [正常,打架]
优势:
- 无需额外的时序建模模块 (如 LSTM、Transformer)
- 可以直接使用 2D CNN 骨干网络
- 推理效率高,适合实时场景
核心算法流程
第一阶段:检测与姿态估计 (并行推理)
两模型并行推理,使用线程池加速:
python
def __call__(self, image: np.ndarray):
"""
并行执行检测和姿态估计,提高推理效率
关键优化:
- 检测和 Pose 模型独立推理,互不依赖
- 使用线程池并行提交任务
- 等待两个结果都返回后再继续
"""
detection_future = executor.submit(detect_model, image)
pose_future = executor.submit(pose_model, image)
detection_result = detection_future.result() # 检测结果
pose_result = pose_future.result() # 姿态结果
return list(zip(detection_result, pose_result))
并行推理效果:
- 串行执行:检测耗时 + Pose 耗时 ≈ 60ms + 100ms = 160ms
- 并行执行:max(检测耗时,Pose 耗时) ≈ 100ms
- 速度提升约 37%
第二阶段:姿态规则过滤
基于人体关键点检测攻击性动作,核心逻辑如下:
2.1 区域过滤
- 每个检测框内必须包含 ≥2 人 (通过 Pose 框中心点判断)
- 单框人数不足直接跳过
2.2 攻击姿态识别
挥拳检测:
python
# 计算肘关节角度 (肩 - 肘-腕)
angle = calculate_angle(shoulder, elbow, wrist)
# 判定条件:
# 1. 肘部角度 < 150° (手臂弯曲成攻击姿态)
# 2. 手腕高度 < 肩膀高度 × 0.85 (手臂向前伸出)
# 3. 手腕未搭在肩膀上 (排除搭肩)
# 4. 手腕未接触自己头部 (排除摸头/打电话)
if angle < PUNCH_ANGLE_THRESH and height_diff < limit_h:
if not is_rest and not is_self_touch:
has_attack_action = True
踢腿检测:
python
# 计算膝关节角度 (髋 - 膝-踝)
angle = calculate_angle(hip, knee, ankle)
# 判定条件:
# 1. 膝关节角度 < 135° (腿部弯曲)
# 2. 踝部高于膝部 (抬腿动作)
# 3. 踝部高度差 < 身体比例 × 0.45 (踢击高度合理)
# 4. 检测到踢向他人 (踝部靠近他人髋部)
if angle < KICK_ANGLE_THRESH and is_high and is_hit:
has_attack_action = True
2.3 过滤规则
| 场景 | 过滤逻辑 |
|---|---|
| 搭肩 | 检测手腕是否 resting 在他人肩膀上 |
| 摸头/打电话 | 检测手腕是否接触自己头部关键点 |
| 摔倒 | 单人剧烈动作,无对抗目标 |
| 搀扶/拥抱 | 肢体接触但无攻击角度 |
第三阶段:目标跟踪与"捞回"机制
使用 BYTETracker 进行目标跟踪,核心功能:
python
class ActionTracker:
def __init__(self, tlwh, score, class_id):
super().__init__(tlwh, score, class_id)
self.capture_mode = False # 采帧模式开关
self.frame_buffer = deque(16) # 16 帧裁剪缓冲
self.hit_history = deque(16) # 检测命中历史
self.is_fighting = False # 是否正在打架
self.alarm_flag = False # 报警标志
"捞回"机制:
python
# 1. 存入当前帧成功匹配的活跃目标
for tracker in tracked_results:
valid_tracks[tracker.id] = tracker
# 2. 捞回那些正在采帧,但当前帧被检测漏检的目标
for tid, tracker in active_captures.items():
if tid not in valid_tracks:
# 只要还没被彻底移除,卡尔曼滤波就在盲推坐标,我们必须继续采帧!
if tracker.state != REMOVED:
valid_tracks[tid] = tracker
# 更新字典
active_captures = valid_tracks
为什么需要"捞回":
- 检测存在漏检可能 (遮挡、快速运动等)
- Tracker 的卡尔曼滤波可以预测丢失目标的位置
- 保持采帧连续性,避免因单帧漏检中断分析
第四阶段:16 帧采集与滑动窗口
4.1 动态裁剪框选择
python
# 检查检测当前帧是否在该目标区域有检测结果 (IoU > 0.35)
is_detected = False
best_box = None
best_iou = 0
for det_box in detection_results:
iou = compute_iou(tracker_box, det_box)
if iou > 0.35 and iou > best_iou:
is_detected = True
best_iou = iou
best_box = det_box
# --- 核心优化:动态决定用于 TSM 裁剪的基准框 ---
# 如果检测命中了,说明检测框更贴合真实人体边缘,优先使用
# 如果检测漏检了,使用 Tracker 的预测框维持画面连续性
crop_base_box = best_box if is_detected else tracker_box
4.2 滑动窗口机制
python
# 满 16 帧进行分析
if len(tracker.frame_buffer) == 16:
# 计算命中比例
hit_ratio = sum(tracker.hit_history) / 16.0
# 判断比例是否达标 (按规定设置 45%,即 0.45)
if hit_ratio >= 0.45:
# 触发 TSM 分析
tsm_result = model_tsm.predict(list(tracker.frame_buffer))
# ... 处理结果
else:
# 命中率太低,跳过 TSM 节省算力
tracker.is_fighting = False
# 控制后续行为
# 如果目标已经完全不再被检出 (比例很低),彻底结束采集模式节约算力
if hit_ratio < 0.25:
tracker.capture_mode = False
tracker.frame_buffer.clear()
tracker.hit_history.clear()
else:
# --- 滑动窗口:弹出最旧 8 帧,保持采帧模式进行下一轮推理 ---
for _ in range(8):
tracker.frame_buffer.popleft()
tracker.hit_history.popleft()
滑动窗口详解:
┌─────────────────────────────────────────────────────────────────┐
│ 滑动窗口工作机制 │
└─────────────────────────────────────────────────────────────────┘
第 1 轮分析 (帧 1-16):
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │10 │11 │12 │13 │14 │15 │16 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
↓ TSM 分析
↓ 弹出 8 帧 (1-8)
第 2 轮分析 (帧 9-24):
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 9 │10 │11 │12 │13 │14 │15 │16 │17 │18 │19 │20 │21 │22 │23 │24 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
↓ TSM 分析
↓ 弹出 8 帧 (9-16)
第 3 轮分析 (帧 17-32):
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│17 │18 │19 │20 │21 │22 │23 │24 │25 │26 │27 │28 │29 │30 │31 │32 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
窗口大小:16 帧
滑动步长:8 帧
重叠比例:50%
滑动窗口优势:
- 时序连续性: 50% 重叠保证动作不会在窗口边界被截断
- 实时响应: 每 8 帧进行一次分析,响应延迟更低
- 计算效率: 复用上一轮的部分帧,减少重复计算
第五阶段:命中帧机制与校验
5.1 检测命中率计算
python
# 记录每一帧检测是否命中该目标
tracker.hit_history.append(is_detected)
# 满 16 帧后计算命中率
hit_ratio = sum(tracker.hit_history) / 16.0
5.2 命中率阈值策略
| 命中率范围 | 处理方式 | 说明 |
|---|---|---|
| ≥ 45% | 触发 TSM 分析 | 检测稳定,可信度高 |
| 25% ~ 45% | 不分析,继续采帧 | 检测不稳定,等待更多数据 |
| < 25% | 停止采帧,重置状态 | 目标已消失或严重遮挡 |
为什么需要命中率校验:
场景 1: 检测误检
- 单帧检测到"打架",但后续帧无对应目标
- 命中率会很低 (<25%)
- 直接跳过 TSM,节省算力
场景 2: 目标部分遮挡
- 目标在画面中时断时续
- 命中率中等 (25%~45%)
- 继续采帧,等待更完整数据
场景 3: 真实打架持续
- 目标持续在画面中
- 命中率高 (≥45%)
- 触发 TSM 分析
第六阶段:时序分类与报警
python
# TSM 预测为打架且置信度达标
if tsm_result['class_id'] == 1 and tsm_result['confidence'] >= action_confidence:
if not tracker.is_fighting:
tracker.is_fighting = True
tracker.fight_start_time = current_time
# 持续时间超过阈值才报警
duration = current_time - tracker.fight_start_time
if duration >= fight_duration_threshold:
tracker.alarm_flag = True # 触发报警
else:
# 不是打架或置信度低,重置状态
tracker.is_fighting = False
tracker.fight_start_time = 0.0
tracker.alarm_flag = False
关键代码解析
核心函数:detect_attack_actions 姿态过滤
位置:核心姿态校验逻辑
python
def detect_attack_actions(detection_boxes, pose_boxes, pose_keypoints):
"""
逐框校验检测结果:筛选出"框内≥2 人 + 至少 1 人有打架动作"的检测框
参数:
detection_boxes: 人员检测框 [x1, y1, x2, y2, conf, cls]
pose_boxes: 关键点框 [x1, y1, x2, y2, conf, cls]
pose_keypoints: 每个人体的 17 个关键点坐标 [17, 3] (x, y, confidence)
返回:
valid_boxes: 通过校验的检测框
优化技巧:
1. 预计算所有 Pose 框中心点,避免重复计算
2. 提前返回:Pose 框少于 2 直接返回空
3. 提前 break: 发现攻击动作立即跳出循环
"""
# ========== 基础校验:Pose 框少于 2 人直接返回 ==========
if len(pose_boxes) < 2:
return [] # 不足 2 人不可能打架
# ========== 预计算优化:所有 Pose 框中心点 ==========
pose_centers = []
for pose_box in pose_boxes:
cx = (pose_box[0] + pose_box[2]) / 2
cy = (pose_box[1] + pose_box[3]) / 2
pose_centers.append((cx, cy))
# 配置阈值
KEYPOINT_CONF_THRESH = 0.30
PUNCH_ANGLE_THRESH = 150
KICK_ANGLE_THRESH = 135
# ========== 逐个分析每个检测框 ==========
valid_boxes = []
for det_idx, det_box in enumerate(detection_boxes):
# 步骤 1: 找出当前检测框内包含的所有 Pose 关键点框
pose_indices_in_box = []
for pose_idx, (cx, cy) in enumerate(pose_centers):
if point_in_box(cx, cy, det_box):
pose_indices_in_box.append(pose_idx)
# 步骤 2: 当前检测框内 Pose 人数不足 2,跳过
if len(pose_indices_in_box) < 2:
continue
# 收集区域内所有人的关键部位 (肩膀、髋部)
all_shoulders = []
all_hips = []
for idx in pose_indices_in_box:
kp = pose_keypoints[idx]
if kp[5][2] > KEYPOINT_CONF_THRESH: all_shoulders.append(kp[5][:2])
if kp[6][2] > KEYPOINT_CONF_THRESH: all_shoulders.append(kp[6][:2])
if kp[11][2] > KEYPOINT_CONF_THRESH: all_hips.append(kp[11][:2])
if kp[12][2] > KEYPOINT_CONF_THRESH: all_hips.append(kp[12][:2])
# 步骤 3: 检查该检测框内是否有至少 1 人具备打架动作
has_attack_action = False
for pose_idx in pose_indices_in_box:
kps = pose_keypoints[pose_idx]
# 关键点有效性检查
def is_valid(kp_idx):
return kps[kp_idx][2] > KEYPOINT_CONF_THRESH
# 动态比例尺:根据肩到髋的距离自适应
body_scale = 50.0
if is_valid(5) and is_valid(11):
body_scale = max(30.0, calculate_distance(kps[5][:2], kps[11][:2]))
# 辅助函数:判断手腕是否搭在他人肩膀上
def is_resting_on_shoulder(wrist_pt):
for sh_pt in all_shoulders:
if calculate_distance(wrist_pt, sh_pt) < body_scale * 0.5:
return True
return False
# 辅助函数:判断是否踢向他人目标
def is_kick_hitting_target(ankle_pt, own_hip_pt):
for hip_pt in all_hips:
if calculate_distance(hip_pt, own_hip_pt) < body_scale * 0.3:
continue # 跳过自己的髋部
if calculate_distance(ankle_pt, hip_pt) < body_scale * 1.2:
return True
return False
# 辅助函数:判断是否触摸自己头部
def is_touching_own_head(wrist_pt):
HEAD_KPS = [0, 1, 2, 3, 4]
head_touch_thresh = body_scale * 0.6
for head_idx in HEAD_KPS:
if is_valid(head_idx):
head_pt = kps[head_idx][:2]
if calculate_distance(wrist_pt, head_pt) < head_touch_thresh:
return True
return False
# ---------- 右手挥拳检测 ----------
if is_valid(6) and is_valid(8) and is_valid(10): # 右肩 - 右肘 - 右手腕
angle = calculate_angle(kps[6][:2], kps[8][:2], kps[10][:2])
height_diff = kps[10][1] - kps[6][1]
is_rest = is_resting_on_shoulder(kps[10][:2])
is_self_touch = is_touching_own_head(kps[10][:2])
limit_h = body_scale * 0.85
if angle < PUNCH_ANGLE_THRESH: # 肘部角度小于阈值
if height_diff < limit_h: # 手腕低于肩膀
if not is_rest: # 不是搭肩
has_attack_action = True
break # 提前退出,无需继续检查
elif is_self_touch:
pass # 摸头,过滤
else:
pass # 搭肩,过滤
# ---------- 左手挥拳检测 (逻辑同右手) ----------
# ---------- 右腿踢击检测 ----------
# ---------- 左腿踢击检测 ----------
# 步骤 4: 判断结果
if has_attack_action:
valid_boxes.append(det_box)
return valid_boxes
关键点提取说明:
python
# COCO 17 点关键点索引
# 0:鼻子,1:左眼,2:右眼,3:左耳,4:右耳
# 5:左肩,6:右肩,7:左肘,8:右肘
# 9:左手腕,10:右手腕,11:左髋,12:右髋
# 13:左膝,14:右膝,15:左脚踝,16:右脚踝
# 示例:获取右手腕坐标 (置信度>0.30 才有效)
if kps[10][2] > KEYPOINT_CONF_THRESH:
right_wrist = kps[10][:2] # (x, y)
角度计算函数:
python
def calculate_angle(a, b, c):
"""
计算角 abc 的角度 (三点坐标)
示例:calculate_angle(肩,肘,腕) -> 肘部角度
数值稳定性处理:
- 使用 np.clip 防止 cos 值越界
- 检查模长避免除零错误
"""
a = np.array(a)
b = np.array(b)
c = np.array(c)
ba = a - b
bc = c - b
# 余弦定理
norm_ba = np.linalg.norm(ba)
norm_bc = np.linalg.norm(bc)
if norm_ba == 0 or norm_bc == 0:
return 180.0 # 返回平角,表示无效
cosine_angle = np.dot(ba, bc) / (norm_ba * norm_bc)
cosine_angle = np.clip(cosine_angle, -1.0, 1.0) # 防止数值溢出
angle = np.degrees(np.arccos(cosine_angle))
return angle
核心函数:main_processing_loop 主处理循环
python
def main_processing_loop():
"""
主处理循环,串联整个检测流水线
流程概览:
1. 检测结果坐标还原
2. 多边形区域过滤
3. 姿态规则初步过滤
4. BYTETracker 跟踪更新
5. "捞回"丢失目标
6. 16 帧采集 + 滑动窗口
7. 命中率校验
8. TSM 时序分类
9. 报警触发
"""
# 初始化跟踪器
byte_tracker = ByteTracker(
frame_rate=fps,
image_shape=(width, height),
match_thresh=0.7
)
active_captures = {} # 活跃目标字典
while not stop_event.is_set():
# 1. 获取检测结果
results = get_detection_results()
results_pose = get_pose_results()
# 2. 坐标还原到原图尺寸
results = map_to_original(results)
results_pose = map_to_original(results_pose)
# 3. 多边形区域过滤 (只分析关注区域)
results = filter_by_area(results)
# 4. 姿态简单过滤
if len(results) > 0:
results = detect_attack_actions(results, results_pose, pose_keypoints)
# 5. 更新跟踪器
track_results = byte_tracker.update(results)
# 6. "捞回"机制:维持丢失但正在采帧的目标
current_valid_tracks = {}
for trk in track_results:
current_valid_tracks[trk.id] = trk
for tid, trk in active_captures.items():
if tid not in current_valid_tracks:
if trk.state != REMOVED:
current_valid_tracks[tid] = trk
active_captures = current_valid_tracks
# 7. 遍历所有有效追踪区域
for trk in active_captures.values():
# 7.1 检查检测是否命中该目标区域
is_detected = compute_iou(trk.box, detection_boxes) > 0.35
# 7.2 动态裁剪框选择
crop_base_box = best_det_box if is_detected else trk.box
# 7.3 状态机:开始采帧
if not trk.capture_mode and is_detected:
trk.capture_mode = True
trk.frame_buffer.clear()
# 7.4 正在采帧,收集画面
if trk.capture_mode:
crop_img = crop_region(frame, crop_base_box, expand_ratio=1.3)
trk.frame_buffer.append(crop_img)
trk.hit_history.append(is_detected)
# 7.5 满 16 帧进行分析
if len(trk.frame_buffer) == 16:
hit_ratio = sum(trk.hit_history) / 16.0
if hit_ratio >= 0.45: # 命中率达标
# TSM 时序分类
tsm_result = model_tsm.predict(list(trk.frame_buffer))
# 判断是否为打架
if tsm_result['class_id'] == 1 and \
tsm_result['confidence'] >= action_confidence:
trk.is_fighting = True
# 检查持续时间
duration = current_time - trk.fight_start_time
if duration >= fight_duration_threshold:
trk.alarm_flag = True # 触发报警
else:
# 重置状态
trk.is_fighting = False
trk.fight_start_time = 0
# 7.6 滑动窗口
if hit_ratio < 0.25:
trk.capture_mode = False
trk.frame_buffer.clear()
else:
for _ in range(8):
trk.frame_buffer.popleft()
# 8. 触发报警
if alarm_flag:
generate_alarm_output()
算法技巧与优化
1. 并行推理优化
python
# 检测和 Pose 模型并行执行
detection_future = executor.submit(detect_model, image)
pose_future = executor.submit(pose_model, image)
detection_result = detection_future.result()
pose_result = pose_future.result()
效果: 速度提升约 37%
2. "捞回"机制
python
# 捞回检测漏检但正在采帧的目标
for tid, tracker in active_captures.items():
if tid not in valid_tracks:
if tracker.state != REMOVED: # 未被彻底移除
valid_tracks[tid] = tracker
效果: 避免因单帧漏检中断分析
3. 滑动窗口 (8 帧重叠)
python
# 弹出最旧 8 帧,保留最新 8 帧
for _ in range(8):
tracker.frame_buffer.popleft()
# 继续采 8 帧,下一轮分析时仍有 16 帧
效果:
- 50% 重叠保证时序连续性
- 每 8 帧分析一次,响应更快
4. 命中帧机制
python
# 记录每一帧检测是否命中
tracker.hit_history.append(is_detected)
# 满 16 帧后计算命中率
hit_ratio = sum(tracker.hit_history) / 16.0
# 命中率阈值策略
if hit_ratio >= 0.45:
# 触发 TSM 分析
elif hit_ratio < 0.25:
# 停止采帧,重置状态
效果:
- 过滤检测误检
- 节省无效 TSM 计算
5. 动态裁剪框选择
python
# 检测命中时用检测框 (更精准)
# 检测漏检时用 Tracker 预测框 (维持连续性)
crop_base_box = best_det_box if is_detected else tracker_box
效果: 裁剪区域更精准,同时保证连续性
6. 预计算优化
python
# 预计算所有 Pose 框中心点,避免重复计算
pose_centers = []
for pose_box in pose_boxes:
cx = (pose_box[0] + pose_box[2]) / 2
cy = (pose_box[1] + pose_box[3]) / 2
pose_centers.append((cx, cy))
效果: 减少重复计算,提升效率
7. 提前退出优化
python
# 发现攻击动作立即跳出循环
if has_attack_action:
break
效果: 避免不必要的后续检查
8. 动态比例尺
python
# 根据肩到髋的距离自适应阈值
body_scale = max(30.0, calculate_distance(kps[5][:2], kps[11][:2]))
rest_thresh = body_scale * 0.5
kick_target_thresh = body_scale * 1.2
效果: 适应不同距离、不同身高的人物
配置说明
主要配置参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
fight_duration_threshold |
0 | 打架持续时间阈值 (秒),超过此时长才报警 |
action_confidence |
0.85 | 动作置信度阈值,TSM 预测置信度需超过此值 |
resample |
1 | 采样频率,1 表示每帧处理 |
stop_alarm |
True | 是否夜间停止报警 |
alarm_interval_time |
30 | 报警间隔 (秒),防止重复报警 |
iou_threshold |
0.5 | 检测框 IoU 阈值 |
姿态校验阈值 (代码内固定):
| 参数 | 默认值 | 说明 |
|---|---|---|
KEYPOINT_CONF_THRESH |
0.30 | 关键点置信度阈值 |
PUNCH_ANGLE_THRESH |
150° | 挥拳角度阈值 |
KICK_ANGLE_THRESH |
135° | 踢腿角度阈值 |
命中率阈值 (代码内固定):
| 参数 | 默认值 | 说明 |
|---|---|---|
| 触发 TSM | ≥ 45% | 检测命中率达标才分析 |
| 停止采帧 | < 25% | 命中率太低,目标可能消失 |
| 滑动窗口 | 8 帧 | 每轮弹出 8 帧,保留 8 帧 |
常见问题
Q1: 为什么需要 16 帧时序分析?
A: 单帧姿态检测容易产生误报。例如:
- 一个人挥手打招呼可能被误判为挥拳
- 摔倒时的肢体伸展可能被误判为踢腿
通过 16 帧时序分析,TSM 模型可以:
- 观察动作的连续性和趋势
- 区分瞬时动作和持续对抗
- 结合多人互动关系判断
Q2: 滑动窗口为什么选择 8 帧重叠?
A: 这是一个平衡选择:
- 重叠太多 (如 12 帧): 计算冗余大,响应慢
- 重叠太少 (如 4 帧): 可能截断连续动作
- 8 帧重叠: 50% 重叠,既保证连续性又有较好响应
Q3: 命中率阈值为什么是 45% 和 25%?
A: 基于实验经验:
- ≥45%: 表示目标稳定存在,可信度高
- 25%~45%: 目标时断时续,继续观察
- <25%: 目标基本消失,停止分析
由于打架行为复杂多变,比如挥拳动作就包含:抬手 - 蓄力 - 出拳 - 收拳,一套动作中检测出的有效帧实际可能只有出拳的那几帧,再加上抽帧推理等设计,45% 的命中阈值较为合理。
Q4: 如何过滤摔倒场景?
A: 摔倒过滤通过多层机制实现:
- 姿态规则层: 摔倒通常是单人动作,不满足"框内≥2 人"条件
- 对抗检测层 : 踢腿检测需要
is_hit- 检测到踢向他人目标 - 时序分析层: TSM 模型训练时包含摔倒样本,能够识别并分类为"正常"
Q5: 检测漏检怎么办?
A: 系统使用多重机制应对:
- "捞回"机制: 维持 Lost 状态目标的跟踪
- 动态裁剪框: 使用 Tracker 预测框代替检测框
- 命中帧记录: 不依赖单帧检测结果
Q6: 如何调整灵敏度?
A: 可通过以下配置调整:
| 参数 | 调高 | 调低 |
|---|---|---|
action_confidence |
减少误报 | 提高召回 |
fight_duration_threshold |
过滤短暂冲突 | 快速报警 |
| Pose 角度阈值 | 更严格 | 更宽松 |
| 命中率阈值 | 更严格 | 更宽松 |
总结
本算法采用检测→姿态→跟踪→时序分类的级联方案,通过多层校验有效平衡准确率与误报率。核心设计思想:
- 规则 + 学习: 姿态规则快速过滤明显非打架场景,TSM 模型处理复杂时序判断
- 时序连贯: 16 帧缓冲 + 8 帧滑动窗口保证行为分析的连续性
- 鲁棒性设计: 支持检测漏检,使用 Tracker 预测维持分析
- 命中校验: 检测命中率机制过滤误检,节省算力
- 可配置: 通过配置文件灵活调整报警阈值和策略
核心算法技巧:
- 并行推理 (检测+Pose) - 速度提升 37%
- "捞回"机制 - 应对检测漏检
- 滑动窗口 (8 帧重叠) - 保证时序连续性
- 命中帧机制 - 过滤误检,节省算力
- 动态裁剪框 - 精准 + 连续
- 预计算优化 - 减少重复计算
- 提前退出 - 发现即停止
本算法适合地铁、学校、商场、办公室等公共场所的打架斗殴行为实时监测场景。