如何用Python判断假摔和夸张表演行为
这是一个计算机视觉+行为分析的复合问题,下面我从技术路线、关键特征、代码实现三个层面给出可落地的方案。

整体技术路线
视频输入
↓
人体检测/姿态估计 (YOLO + MediaPipe/HRNet)
↓
轨迹跟踪 (ByteTrack/DeepSORT)
↓
关键特征提取 (运动学 + 姿态 + 交互)
↓
规则引擎 + 机器学习分类器
↓
假摔/夸张表演判定
判别"假摔"vs"真摔"的关键特征
| 维度 | 真摔特征 | 假摔特征 |
|---|---|---|
| 重心轨迹 | 突然下坠,无预兆 | 有减速、屈膝等预备动作 |
| 躯干姿态 | 身体失控、翻滚 | 手部主动撑地保护自己 |
| 加速度 | 触地瞬间冲击大 | 触地前有明显减速 |
| 接触点 | 头/背/髋直接着地 | 手脚先着地缓冲 |
| 反应时间 | 摔倒后短暂"懵" | 立刻看向裁判/捂脸喊叫 |
| 周围接触 | 有明显身体对抗力 | 对方接触轻微或无接触 |
夸张表演特征:
- 头部/手部动作幅度异常大
- 倒地后频繁滚动、抱腿、捂脸
- 面部表情变化剧烈(可用 FER 检测)
- 疼痛动作与受击部位不符
核心代码实现
环境依赖
pip install ultralytics mediapipe opencv-python numpy scipy scikit-learn
姿态与关键点提取
import cv2
import mediapipe as mp
import numpy as np
from collections import deque
mp_pose = mp.solutions.pose
class FallAnalyzer:
def __init__(self, window=30):
self.pose = mp_pose.Pose(
static_image_mode=False,
model_complexity=1,
min_detection_confidence=0.6
)
self.history = deque(maxlen=window) # 存储历史关键点
def extract(self, frame):
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
res = self.pose.process(rgb)
if not res.pose_landmarks:
return None
lm = res.pose_landmarks.landmark
return {
'nose': (lm[0].x, lm[0].y),
'l_shoulder':(lm[11].x, lm[11].y),
'r_shoulder':(lm[12].x, lm[12].y),
'l_hip': (lm[23].x, lm[23].y),
'r_hip': (lm[24].x, lm[24].y),
'l_knee': (lm[25].x, lm[25].y),
'r_knee': (lm[26].x, lm[26].y),
'l_wrist': (lm[15].x, lm[15].y),
'r_wrist': (lm[16].x, lm[16].y),
'l_ankle': (lm[27].x, lm[27].y),
'r_ankle': (lm[28].x, lm[28].y),
}
特征工程(最核心部分)
def compute_features(history):
"""history: 最近N帧关键点序列"""
if len(history) < 10:
return None
arr = {k: np.array([h[k] for h in history]) for k in history[0]}
# 1. 重心 y 坐标下降速度 & 加速度
centroid_y = (arr['l_hip'][:,1] + arr['r_hip'][:,1]) / 2
v_y = np.diff(centroid_y)
a_y = np.diff(v_y)
# 2. 躯干角度(肩-髋连线与竖直方向夹角)
mid_s = (arr['l_shoulder'] + arr['r_shoulder']) / 2
mid_h = (arr['l_hip'] + arr['r_hip']) / 2
torso_vec = mid_s - mid_h
torso_angle = np.degrees(np.arctan2(torso_vec[:,0], -torso_vec[:,1]))
torso_angle_abs = np.abs(torso_angle) # 0=直立, 90=横躺
# 3. 手部保护特征:摔倒前手腕是否低于胸口
wrist_y = (arr['l_wrist'][:,1] + arr['r_wrist'][:,1]) / 2
shoulder_y = (arr['l_shoulder'][:,1] + arr['r_shoulder'][:,1]) / 2
# 手在摔倒过程中下探(保护动作)
hand_reach_down = np.max(wrist_y - shoulder_y)
# 4. 触地前减速(真摔触地前速度最大,假摔会刹车)
if len(v_y) > 5:
pre_impact_v = np.min(v_y[-8:-3]) if len(v_y)>=8 else np.min(v_y)
impact_v = np.min(v_y[-3:])
braking = impact_v - pre_impact_v # >0 表示减速
else:
braking = 0
# 5. 手部动作幅度(夸张表演时挥手频繁)
lw = arr['l_wrist']; rw = arr['r_wrist']
wrist_motion = np.mean(np.linalg.norm(np.diff(lw, axis=0), axis=1)) \
+ np.mean(np.linalg.norm(np.diff(rw, axis=0), axis=1))
# 6. 头/膝踢动幅度(夸张翻滚)
knee_motion = np.mean(np.linalg.norm(np.diff(arr['l_knee'], axis=0), axis=1))
return {
'max_velocity': float(np.max(v_y)), # 下坠最大速度
'max_accel': float(np.max(np.abs(a_y))), # 冲击加速度
'torso_angle': float(np.max(torso_angle_abs)),# 最大倾倒角度
'hand_protect': float(hand_reach_down),
'braking': float(braking),
'wrist_motion': float(wrist_motion),
'knee_motion': float(knee_motion),
'impact_velocity':float(np.min(v_y[-3:])),
}
规则+打分模型判别
def judge_dive(feat):
"""返回 (is_dive, score, reasons)"""
score = 0
reasons = []
# 真摔的打分(负分表示倾向真摔)
if abs(feat['max_velocity']) > 0.02: # 下坠速度快
score -= 2; reasons.append("高速下坠")
if feat['max_accel'] > 0.01: # 触地冲击大
score -= 2; reasons.append("强冲击")
if feat['torso_angle'] > 60: # 躯干大幅倾倒
score -= 1
if feat['braking'] > 0.005: # 触地前减速
score += 2; reasons.append("触地前刹车")
if feat['hand_protect'] > 0.15: # 手主动撑地
score += 2; reasons.append("主动护体")
if feat['wrist_motion'] > 0.03: # 手腕挥动大
score += 2; reasons.append("手部夸张")
if feat['knee_motion'] > 0.02: # 膝关节翻动大
score += 1; reasons.append("夸张翻滚")
is_dive = score >= 3
return is_dive, score, reasons
训练机器学习分类器(推荐做法)
from sklearn.ensemble import RandomForestClassifier
import joblib
# 采集标注数据:0=真摔, 1=假摔
# X = [feat 向量...], y = [标签...]
X = np.array([...]) # 每行一个特征向量
y = np.array([...])
clf = RandomForestClassifier(n_estimators=200, max_depth=8, random_state=42)
clf.fit(X, y)
joblib.dump(clf, 'dive_detector.pkl')
# 预测
proba = clf.predict_proba(feat_vec[None])[0][1] # 假摔概率
print(f"假摔概率: {proba:.2f}")
视频主流程
def run(video_path, clf):
cap = cv2.VideoCapture(video_path)
analyzer = FallAnalyzer(window=30)
dive_frame_count = 0
while cap.isOpened():
ret, frame = cap.read()
if not ret: break
kp = analyzer.extract(frame)
if kp is None: continue
analyzer.history.append(kp)
if len(analyzer.history) == analyzer.history.maxlen:
feat = compute_features(analyzer.history)
if feat:
vec = np.array(list(feat.values()))
prob = clf.predict_proba(vec[None])[0][1]
is_dive, score, rs = judge_dive(feat)
# 融合判断
label = "假摔嫌疑" if (prob > 0.6 or is_dive) else "正常"
if label == "假摔嫌疑":
dive_frame_count += 1
cv2.putText(frame, f"{label} p={prob:.2f}",
(30,50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255), 2)
for i, r in enumerate(rs):
cv2.putText(frame, r, (30, 90+i*30),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0,255,255), 2)
cv2.imshow("Fall Analysis", frame)
if cv2.waitKey(1) & 0xFF == 27: break
cap.release()
cv2.destroyAllWindows()
return dive_frame_count > 15 # 连续多帧判为假摔才输出结论
进阶增强方案
| 需求 | 技术方案 |
|---|---|
| 更强判别力 | 加入骨架时序模型:ST-GCN / PoseC3D |
| 交互判断 | 加入对方球员检测,判断接触力度 |
| 面部夸张 | 用 FER / DeepFace 检测痛苦表情持续时长 |
| 真实数据训练 | 用 SoccerNet / 足球假摔数据集微调 |
| 多模态融合 | 视频 + 音频(惨叫)+ 裁判视角 |
ST-GCN 简要结构(骨架节点分类)
# 使用开源库 pyskl 或 mmaction2
# 输入:T帧 × V关节 × C坐标
# 输出:真摔 / 假摔 概率
from mmaction.apis import init_recognizer, inference_recognizer
model = init_recognizer('stgcn_dive.py',
'stgcn_dive.pth', device='cuda:0')
result = inference_recognizer(model, 'skeleton.pkl')
# result: [[真摔概率, 假摔概率]]
注意事项
- 不能100%准确:假摔判定本身就是足球判罚的难点,AI 只能做辅助决策。
- 数据偏差:公开假摔数据集少,建议自建数据集(可从比赛录像标注)。
- 视角影响:单目视角判断接触容易出错,多机位融合更可靠。
- 合规性:在正式比赛中应用需符合体育仲裁与隐私要求。
推荐起步路径:
- 用 MediaPipe + 上述规则特征做一个 demo,验证关键特征是否有效。
- 收集 200~500 个标注片段(真摔/假摔),训随机森林或 XGBoost。
- 若效果不够,升级为 ST-GCN 时序骨架模型。
- 最后接入多机位+音频融合。
需要我给出具体某个环节的完整代码(如 ST-GCN 训练、接触力判断、面部表情融合),可以继续告诉我。