模型加载中...
1 2 3
上传视频 AI 分析 查看结果

上传比赛视频

系统自动逐帧检测球员姿态,通过卡尔曼滤波 + De Leva 运动学模型 + 圆柱体原则,判断带球撞人 / 阻挡犯规

拖拽视频文件到此处,或点击选择文件

支持 MP4、AVI、MOV、MKV、WebM 格式,最大 200MB

YOLO11-Pose Kalman Filter
运动学分析 圆柱体原则

技术原理与算法流程

分析流程图

开始
用户上传比赛视频
YOLO11-Pose 逐帧姿态估计(17个关键点)
卡尔曼滤波平滑关键点轨迹
攻击方 / 防守方球员分配
De Leva 人体节段模型 → 重心 (CoM) 计算
逐帧速度 / 加速度向量计算
加速度尖峰 → 检测到碰撞?
回到"YOLO11-Pose 逐帧姿态估计"继续处理下一帧
进入判罚逻辑,提取碰撞帧上下文
防守方速度投影 + 圆柱体原则 → 判罚?
v_parallel ≥ 阈值 防守方横向移动 → 阻挡犯规 (Block)
v_parallel < 阈值 防守方正对站定 → 带球撞人 (Charge)
输出标注视频 + 判罚结论 JSON

流程开始时,用户上传篮球比赛视频。系统使用 YOLO11-Pose 模型逐帧提取 17 个 COCO 关键点,经卡尔曼滤波消除逐帧抖动后,通过球员空间位置分配攻击方与防守方。基于 De Leva 1996 人体节段参数模型分别计算两位球员的重心坐标和帧间速度/加速度向量。当检测到加速度尖峰(即碰撞事件)时,提取防守方速度矢量在进攻方向上的投影分量 v_parallel,结合 FIBA 圆柱体原则判断防守方是否处于合法防守位置,最终输出判罚结论并生成可视化标注视频。

技术参数与模型信息

姿态估计YOLO11-Pose (yolo11n-pose.pt),17 个 COCO 关键点
滤波算法卡尔曼滤波 (Kalman Filter),逐帧平滑关键点坐标
人体模型De Leva 1996 节段参数模型,重心 (CoM) 计算
碰撞检测加速度尖峰检测 + 滑动窗口 (默认 15 帧回看)
判罚依据防守方速度矢量投影 v_parallel + FIBA 圆柱体原则
后端框架Python FastAPI + uvicorn (ASGI)
可视化OpenCV 骨架绘制 + 重心轨迹 + 判罚结论叠加
前端原生 HTML5 + CSS3 + JavaScript (Fetch API)

软件系统设计

  1. 路径自动管理:系统自动创建 uploads(视频存储)、outputs(标注视频)目录,确保文件结构规范,每次分析以唯一 video_id 隔离,处理完成后自动清理过期文件(超过 2 小时)。
  2. 姿态估计引擎:基于 ultralytics YOLO11-Pose 实现,支持 GPU 加速推理。模型输出 17 个 COCO 关键点 (x, y, confidence),覆盖头、肩、肘、腕、髋、膝、踝等关键关节位置。
  3. 卡尔曼滤波平滑:KeypointSmoother 类实现逐帧关键点卡尔曼滤波,消除 YOLO 单帧检测抖动,确保速度/加速度计算稳定。FPS 自适应的过程噪声协方差矩阵。
  4. 球员分配策略:基于上一帧位置进行匈牙利匹配,自动区分攻击方(持球者)与防守方,支持球员短暂遮挡后的重识别(通过运动预测继续跟踪)。
  5. 运动学计算:center_of_mass() 基于 De Leva 1996 模型将 17 个关键点加权合并为单一重心坐标。velocity() 计算相邻帧重心位移除以时间间隔。CollisionDetector 通过加速度幅值尖峰判定碰撞帧。
  6. Charge/Block 判罚:ChargeBlockJudge 在碰撞帧提取防守方速度矢量在进攻方向上的投影 v_parallel。结合圆柱体原则(防守方躯干圆柱体是否被入侵),综合判定带球撞人 (Charge) 或阻挡犯规 (Block)。
  7. 可视化输出:逐帧叠加攻击方/防守方骨架(不同颜色)、重心运动轨迹线、速度矢量箭头、判罚结论文字标注,输出 MP4 格式标注视频。

参数配置

参数说明
姿态模型yolo11n-pose.pt,17 个 COCO 关键点,置信度阈值 0.5
碰撞检测阈值ACCELERATION_THRESHOLD,默认加速度幅值超过阈值触发碰撞
判罚速度阈值VELOCITY_PARALLEL_THRESHOLD,v_parallel 低于阈值判 Charge
回看窗口COLLISION_LOOKBACK_FRAMES=15 帧,碰撞前防守方状态参考窗
轨迹保留MAX_TRAIL=60 帧,重心运动轨迹线的最大保留长度
上传限制MAX_CONTENT_LENGTH=200MB,支持 MP4/AVI/MOV/MKV/WebM 格式
任务管理异步逐帧处理,前端 Fetch API POST 上传,等待完整 JSON 响应
文件清理自动清理超过 2 小时的 uploads/outputs 临时文件

碰撞检测与 Charge/Block 判罚模块

class CollisionDetector:
    """Detect collision frame by monitoring acceleration spikes."""
    def __init__(self, threshold=cfg.ACCELERATION_THRESHOLD, lookback=15):
        self.threshold = threshold
        self.lookback = lookback
        self._history = []

    def push(self, com, vel):
        acc_mag = 0.0
        if len(self._history) > 0:
            acc = vel - self._history[-1][1]
            acc_mag = np.linalg.norm(acc)
        self._history.append((com.copy(), vel.copy(), acc_mag))
        return acc_mag > self.threshold


class ChargeBlockJudge:
    """Judge charge vs block based on defender velocity vector."""
    def judge(self, attacker_com, defender_com, defender_vel,
              attacker_vel, attacker_chest, defender_shoulder_l,
              defender_shoulder_r):
        # v_parallel: defender's velocity projected onto attack direction
        attack_dir = attacker_com - defender_com
        attack_dir_norm = attack_dir / np.linalg.norm(attack_dir)
        v_parallel = np.dot(defender_vel, attack_dir_norm)

        # Cylinder principle: is attacker's chest inside defender's torso?
        cyl_intrusion = self._check_cylinder(
            attacker_chest, defender_shoulder_l, defender_shoulder_r)

        if v_parallel < cfg.VELOCITY_PARALLEL_THRESHOLD or cyl_intrusion:
            return {"verdict": "charge", ...}
        else:
            return {"verdict": "block", ...}

运动学分析模块

# De Leva 1996 人体节段参数 — 各节段占体重百分比
SEGMENT_WEIGHTS = {
    "head_neck": 0.0694, "trunk": 0.4346,
    "upper_arm": 0.0271, "forearm": 0.0162, "hand": 0.0061,
    "thigh": 0.1416, "shank": 0.0433, "foot": 0.0137,
}

def center_of_mass(kps):
    """Calculate center of mass from 17 COCO keypoints."""
    com = np.zeros(2)
    total_weight = 0.0
    for seg_name, (i, j, w) in SEGMENT_MAPPING.items():
        mid = (kps[i, :2] + kps[j, :2]) / 2
        weight = SEGMENT_WEIGHTS[seg_name]
        com += mid * weight
        total_weight += weight
    return com / total_weight


def velocity(com_current, com_prev, fps):
    """Calculate velocity vector between two consecutive frames."""
    return (com_current - com_prev) * fps  # pixels/second