python案例如何评估门将扑救反应速度?

wen python案例 1

门将扑救反应速度评估案例

下面是一个完整的 Python 案例,展示如何通过视频分析评估门将扑救反应速度,核心思路是:追踪球的位置和门将手/身体位置,计算从球出现威胁到门将启动扑救的时间差。

python案例如何评估门将扑救反应速度?


评估指标设计

指标 说明
反应时间 (Reaction Time) 球离开射门脚 → 门将重心开始移动的时间差
出手时间 (Initiation Time) 球离开脚 → 门将手开始朝向球方向移动
位移启动延迟 球轨迹确定 → 门将身体位移 > 阈值
扑救成功率关联 反应时间与是否扑出的相关性

完整代码示例

环境依赖

pip install opencv-python numpy scipy pandas matplotlib ultralytics

球与门将追踪

import cv2
import numpy as np
from ultralytics import YOLO
from collections import deque
# 加载 YOLOv8 模型(球和 person 均可检测,也可以训练专用模型)
model = YOLO("yolov8n.pt")
def track_ball_and_keeper(video_path, fps=30):
    """
    返回: 
      ball_positions: list[(frame_idx, x, y)]
      keeper_positions: list[(frame_idx, x, y, w, h)]  门将整体框
    """
    cap = cv2.VideoCapture(video_path)
    ball_positions = []
    keeper_positions = []
    # 用颜色阈值辅助找球(足球多为白/黑,训练比赛可用颜色)
    # 这里用 YOLO 的 sports ball 类 + 简单过滤
    prev_ball = None
    while cap.isOpened():
        ret, frame = cap.read()
        if not ret:
            break
        idx = int(cap.get(cv2.CAP_PROP_POS_FRAMES))
        results = model(frame, verbose=False)[0]
        ball_candidates = []
        keeper_boxes = []
        for box in results.boxes:
            cls = int(box.cls[0])
            conf = float(box.conf[0])
            x1, y1, x2, y2 = map(int, box.xyxy[0])
            cx, cy = (x1 + x2) // 2, (y1 + y2) // 2
            if cls == 32 and conf > 0.3:      # sports ball
                ball_candidates.append((cx, cy, conf))
            elif cls == 0:                     # person
                # 只保留右侧半场(门将区域),可按实际场景过滤
                keeper_boxes.append((x1, y1, x2, y2, conf))
        # 选择最接近上一帧的球(防误检)
        if ball_candidates:
            if prev_ball is None:
                ball = max(ball_candidates, key=lambda c: c[2])
            else:
                ball = min(ball_candidates,
                           key=lambda c: (c[0]-prev_ball[0])**2 + (c[1]-prev_ball[1])**2)
            prev_ball = ball[:2]
            ball_positions.append((idx, ball[0], ball[1]))
        # 选择最大的 person 作为门将(通常最靠近球门)
        if keeper_boxes:
            biggest = max(keeper_boxes, key=lambda b: (b[2]-b[0])*(b[3]-b[1]))
            keeper_positions.append((idx, *biggest[:4]))
    cap.release()
    return ball_positions, keeper_positions

计算反应时间

from scipy.signal import savgol_filter
def compute_reaction_time(ball_positions, keeper_positions, fps=30,
                          speed_thresh_kmh=80, keeper_move_thresh=6):
    """
    1) 检测射门帧:球速突增超过阈值
    2) 检测门将启动帧:门将质心横/纵位移速度超过阈值
    3) 反应时间 = (启动帧 - 射门帧) / fps
    """
    # --- 球的速度 ---
    frames = np.array([p[0] for p in ball_positions])
    xs = np.array([p[1] for p in ball_positions], dtype=float)
    ys = np.array([p[2] for p in ball_positions], dtype=float)
    # 平滑
    if len(xs) > 7:
        xs_s = savgol_filter(xs, 7, 2)
        ys_s = savgol_filter(ys, 7, 2)
    else:
        xs_s, ys_s = xs, ys
    # 像素速度 → 实际速度需标定(此处假设 1m ≈ 50px)
    PX_PER_M = 50
    vx = np.diff(xs_s) * fps / PX_PER_M   # m/s
    vy = np.diff(ys_s) * fps / PX_PER_M
    speed_ms = np.hypot(vx, vy)
    speed_kmh = speed_ms * 3.6
    # 射门帧:球速首次>阈值
    shot_idx = np.argmax(speed_kmh > speed_thresh_kmh)
    if speed_kmh[shot_idx] < speed_thresh_kmh:
        return None  # 未检测到射门
    # --- 门将质心位移速度 ---
    kframes = np.array([k[0] for k in keeper_positions])
    kcx = np.array([(k[1]+k[1]+k[3])/2 for k in keeper_positions])  # 质心x
    kcy = np.array([(k[2]+k[2]+k[4])/2 for k in keeper_positions])  # 质心y
    # 对齐帧
    common = np.intersect1d(kframes, frames)
    if len(common) < 5:
        return None
    # 门将质心速度(像素/帧)
    d_move = np.hypot(np.diff(kcx), np.diff(kcy))
    # 找到射门帧之后,门将启动帧
    shot_frame = frames[shot_idx]
    mask = kframes[:-1] >= shot_frame
    if not mask.any():
        return None
    d_after = d_move[mask]
    idx_after = np.where(mask)[0]
    # 首次超过阈值
    start_candidates = np.where(d_after > keeper_move_thresh)[0]
    if len(start_candidates) == 0:
        return None
    start_idx = idx_after[start_candidates[0]]
    start_frame = kframes[start_idx]
    reaction_time_s = (start_frame - shot_frame) / fps
    return {
        "shot_frame": int(shot_frame),
        "keeper_start_frame": int(start_frame),
        "reaction_time_ms": round(reaction_time_s * 1000, 1),
        "ball_speed_kmh": round(float(speed_kmh[shot_idx]), 1)
    }

批量评估 + 可视化

import pandas as pd
import matplotlib.pyplot as plt
def evaluate_goalkeeper(videos, labels):
    """
    videos: list of 视频路径
    labels: list of True(扑出)/False(未扑出)
    """
    rows = []
    for vid, saved in zip(videos, labels):
        ball_p, keeper_p = track_ball_and_keeper(vid)
        res = compute_reaction_time(ball_p, keeper_p)
        if res:
            res["video"] = vid
            res["saved"] = saved
            rows.append(res)
    df = pd.DataFrame(rows)
    print(df.describe())
    print("\n平均反应时间:", df["reaction_time_ms"].mean(), "ms")
    # 反应时间 vs 是否扑出
    saved_rt = df[df.saved]["reaction_time_ms"]
    missed_rt = df[~df.saved]["reaction_time_ms"]
    print(f"扑出时平均反应: {saved_rt.mean():.0f}ms")
    print(f"未扑出平均反应: {missed_rt.mean():.0f}ms")
    # 画图
    fig, ax = plt.subplots(1, 2, figsize=(12, 4))
    df.boxplot(column="reaction_time_ms", by="saved", ax=ax[0])
    ax[0].set_title("反应时间 vs 扑救结果")
    ax[1].scatter(df["ball_speed_kmh"], df["reaction_time_ms"],
                  c=df["saved"].map({True:"green",False:"red"}))
    ax[1].set_xlabel("球速 (km/h)"); ax[1].set_ylabel("反应时间 (ms)")
    plt.tight_layout(); plt.show()
    return df
# 使用示例
if __name__ == "__main__":
    videos = ["shot1.mp4", "shot2.mp4", "shot3.mp4"]
    labels = [True, False, True]
    df = evaluate_goalkeeper(videos, labels)
    df.to_csv("goalkeeper_reaction.csv", index=False)

关键要点与注意事项

阈值选择

  • 球速阈值:职业射门 80–120 km/h,青少年可降到 40–60 km/h
  • 门将位移阈值:需按视频分辨率调整,通常质心位移 > 5–10 px/帧
  • 需像素→米标定:用球门宽度(7.32m)标定 PX_PER_M

更精确的门将动作检测

# 用 MediaPipe / YOLO-pose 检测手腕关键点,比整体框更准
import mediapipe as mp
# 追踪 wrist 关键点 → 计算手腕速度 → 更贴近"真实出手时机"

数据采集规范

  • 固定机位(正对球门 / 侧面)
  • 高帧率(>60fps 更精确,30fps 误差约 ±33ms)
  • 排除遮挡帧

常见误差来源

来源 影响 改进
帧率低 ±33ms 误差 用 120fps 高速摄像
球误检 速度突变 卡尔曼滤波平滑轨迹
标定不准 速度计算偏差 用球门+禁区线标定
门将判定帧主观 启动时刻 定义"位移>3%身宽"等客观规则

进阶方向

  1. 卡尔曼滤波跟踪球,减少误检导致的假射门帧
  2. 姿态估计(YOLO-pose)直接追踪门将手部启动
  3. 深度学习(LSTM / TCN)建模反应时间与扑救成功率关系
  4. 对比分析:不同站位、不同球速下的反应时间分布

抱歉,评论功能暂时关闭!