统计克鲁伊夫转身次数 - Python案例
这是一个足球数据分析的典型案例,通过骨骼关键点或球员姿态识别来统计"克鲁伊夫转身"(Cruyff Turn)动作的次数。

克鲁伊夫转身动作特征
克鲁伊夫转身是足球中的经典假动作,具有以下特征:
- 假射/假传:一只脚做出射门或传球的姿势
- 扣球变向:用同一只脚的脚内侧将球从身后扣到另一侧
- 身体转向:支撑脚为轴,身体180°转向
- 方向突变:球员运动方向与球的方向相反
基于骨骼关键点的识别思路
关键特征提取
import numpy as np
import pandas as pd
from scipy.signal import find_peaks
# 假设已通过 MediaPipe / OpenPose 提取了骨骼关键点
# 每帧包含: 左/右髋、膝、踝、脚、肩等坐标
def extract_features(keypoints_df):
"""从关键点数据中提取克鲁伊夫转身相关特征"""
features = pd.DataFrame()
# 1. 脚踝相对髋部的位置(判断支撑脚)
features['left_ankle_rel'] = keypoints_df['left_ankle_x'] - keypoints_df['left_hip_x']
features['right_ankle_rel'] = keypoints_df['right_ankle_x'] - keypoints_df['right_hip_x']
# 2. 膝盖弯曲角度(假射腿会明显弯曲)
features['left_knee_angle'] = compute_angle(
keypoints_df['left_hip'], keypoints_df['left_knee'], keypoints_df['left_ankle']
)
features['right_knee_angle'] = compute_angle(
keypoints_df['right_hip'], keypoints_df['right_knee'], keypoints_df['right_ankle']
)
# 3. 躯干朝向(用于检测身体旋转)
features['torso_orientation'] = np.arctan2(
keypoints_df['left_shoulder_y'] - keypoints_df['right_shoulder_y'],
keypoints_df['left_shoulder_x'] - keypoints_df['right_shoulder_x']
)
# 4. 脚部速度(扣球时脚速突然变化)
features['left_ankle_speed'] = keypoints_df['left_ankle_x'].diff().abs()
features['right_ankle_speed'] = keypoints_df['right_ankle_x'].diff().abs()
return features
def compute_angle(p1, p2, p3):
"""计算三点构成的角度"""
v1 = p1 - p2
v2 = p3 - p2
cos_angle = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) + 1e-6)
return np.degrees(np.arccos(np.clip(cos_angle, -1, 1)))
动作识别状态机
class CruyffTurnDetector:
"""克鲁伊夫转身动作检测器(状态机模式)"""
def __init__(self, fps=30):
self.fps = fps
self.state = 'IDLE'
self.count = 0
self.events = [] # 记录每次转身的时间戳
self.state_start_frame = 0
self.phase_frames = [] # 记录各阶段持续帧数
def detect(self, features_df):
"""逐帧检测状态转换"""
for i, row in features_df.iterrows():
self._update_state(i, row)
return self.count
def _update_state(self, frame_idx, row):
state_duration = (frame_idx - self.state_start_frame) / self.fps
# 状态1: 空闲 → 假动作准备(脚抬高、膝盖弯曲)
if self.state == 'IDLE':
if (row['right_knee_angle'] < 100 and
row['right_ankle_rel'] > 0.15):
self.state = 'FAKE_SHOT'
self.state_start_frame = frame_idx
# 状态2: 假动作 → 扣球(脚速突增 + 脚踝轨迹方向反转)
elif self.state == 'FAKE_SHOT':
if (row['right_ankle_speed'] > 0.05 and
state_duration > 0.2 and state_duration < 1.5):
self.state = 'PULL_BACK'
self.state_start_frame = frame_idx
elif state_duration > 2.0: # 超时重置
self._reset()
# 状态3: 扣球 → 身体转向(躯干朝向变化 > 90°)
elif self.state == 'PULL_BACK':
if state_duration > 3.0:
self._reset()
# 状态4: 转向 → 完成(方向突变 + 速度恢复)
# ... 后续状态判断
def _reset(self):
self.state = 'IDLE'
self.state_start_frame = 0
简化版:基于方向突变检测
如果只有球员位置跟踪数据(更常见的场景):
def count_cruyff_turns(player_positions, fps=30,
min_angle_change=120,
speed_drop_ratio=0.4,
window_sec=1.0):
"""
通过轨迹方向突变检测克鲁伊夫转身
- 位置突变: 方向改变 > 120度
- 速度下降: 转身瞬间速度下降
- 转身前后轨迹呈"回头"形态
"""
window = int(window_sec * fps)
velocities = np.diff(player_positions, axis=0)
speeds = np.linalg.norm(velocities, axis=1)
headings = np.arctan2(velocities[:, 1], velocities[:, 0])
turn_events = []
for i in range(window, len(headings) - window):
# 转弯角度 = 前后两段速度方向夹角
v_before = velocities[i-window:i].mean(axis=0)
v_after = velocities[i:i+window].mean(axis=0)
cos_theta = np.dot(v_before, v_after) / (
np.linalg.norm(v_before) * np.linalg.norm(v_after) + 1e-6)
angle_change = np.degrees(np.arccos(np.clip(cos_theta, -1, 1)))
# 判断是否为克鲁伊夫式转身
v_avg = speeds[max(0,i-5):i+5].mean()
if (angle_change > min_angle_change
and speeds[i] < v_avg * speed_drop_ratio
and np.linalg.norm(v_before + v_after) < np.linalg.norm(v_before) * 0.5):
turn_events.append(i)
# 去重(同一转身只计一次)
turn_events = deduplicate_events(turn_events, min_gap=int(1.5*fps))
return len(turn_events), turn_events
def deduplicate_events(events, min_gap):
if not events:
return []
result = [events[0]]
for e in events[1:]:
if e - result[-1] > min_gap:
result.append(e)
return result
完整使用示例
import cv2
import mediapipe as mp
# ============ 处理视频 ============
def analyze_video(video_path):
cap = cv2.VideoCapture(video_path)
mp_pose = mp.solutions.pose
pose = mp_pose.Pose()
all_kp = []
fps = cap.get(cv2.CAP_PROP_FPS)
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
result = pose.process(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
if result.pose_landmarks:
kp = {lm.name: [lm.x, lm.y]
for lm in result.pose_landmarks.landmark}
all_kp.append(kp)
else:
all_kp.append(None)
cap.release()
# 转成 DataFrame 并提取特征
kp_df = pd.DataFrame([k for k in all_kp if k is not None])
features = extract_features(kp_df)
# 检测
detector = CruyffTurnDetector(fps=fps)
n_turns = detector.detect(features)
print(f"克鲁伊夫转身次数: {n_turns}")
analyze_video('player_highlight.mp4')
# 输出示例: 克鲁伊夫转身次数: 3
统计结果可视化
import matplotlib.pyplot as plt
def plot_turn_analysis(player_positions, turn_events, fps=30):
"""可视化球员轨迹与转身点"""
plt.figure(figsize=(12, 8))
plt.plot(player_positions[:, 0], player_positions[:, 1], 'b-', alpha=0.5)
for i, evt in enumerate(turn_events):
plt.scatter(player_positions[evt, 0], player_positions[evt, 1],
c='red', s=200, marker='*',
label='克鲁伊夫转身' if i == 0 else '')
plt.annotate(f'#{i+1} ({evt/fps:.1f}s)',
player_positions[evt], fontsize=10)
plt.title(f'球员轨迹与克鲁伊夫转身检测(共 {len(turn_events)} 次)')
plt.xlabel('X 位置')
plt.ylabel('Y 位置')
plt.legend()
plt.gca().invert_yaxis()
plt.show()
关键难点与优化建议
| 难点 | 解决方案 |
|---|---|
| 与普通变向区分 | 阈值要求转向角 ≥120° + 速度剧降 |
| 与马赛回旋混淆 | 检测是否双脚交替踩球(马赛特征) |
| 与假射区分 | 需检测"扣球"而非"直接射门"(球轨迹) |
| 左右脚识别 | 判断哪只脚在做动作 |
| 上身遮挡 | 结合多角度摄像头/IMU传感器 |
提升精度的方法:
- 加入球轨迹(球位置检测 YOLO)——球的突然反向是克鲁伊夫转身的最强信号
- 时序模型:用 LSTM / ST-GCN / Transformer 直接做动作分类,而非规则
- 数据标注:收集 100+ 个克鲁伊夫转身片段训练分类器
预期输出
=== 视频分析报告 ===
文件: cruyff_highlights.mp4
时长: 45.2s (1356 帧)
检测动作:
- 克鲁伊夫转身: 4 次
- 时间戳: [3.2s, 12.5s, 28.1s, 41.7s]
每个动作平均耗时: 0.85s
左/右脚分布: 右 3 / 左 1
如果需要针对性实现某一部分(如 MediaPipe 关键点提取、LSTM 模型、球检测),可以告诉我具体场景,我可以给出更完整的代码。