这个函数之前说到了非常重要,我们会分两篇来讲完,这里先讲一下工作流程:
完整函数如下:
python
def foul_fn(left, right, fps, frame):
left_sh, left_el, left_wr = left
right_sh, right_el, right_wr = right
frames_needed = int(0.5 * fps)
# 判断左右手是否举起
left_up = left_wr[1] < left_sh[1]
right_up = right_wr[1] < right_sh[1]
single_up = (left_up and not right_up) or (right_up and not left_up)
# ==============================
# Phase 0:检测单手举起
# ==============================
if state.get("phase", 0) == 0:
if single_up:
hand = "left" if left_up else "right"
if state.get("single_hand") == hand:
state["single_up_count"] += 1
else:
state["single_hand"] = hand
state["single_up_count"] = 1
else:
state["single_up_count"] = 0
state["single_hand"] = None
if state["single_up_count"] >= frames_needed:
state["phase"] = 1
state["history"] = {"left": [], "right": []}
return None
# ==============================
# Phase 1:记录历史并判定犯规
# ==============================
max_history = 12
swing_amp = 10
lag_tol = 3
# 如果左右手高度差太大,清空历史
if abs(left_wr[1] - right_wr[1]) > 50:
state["history"]["left"] = []
state["history"]["right"] = []
return None
state["history"]["left"].append(left_wr[1])
state["history"]["right"].append(right_wr[1])
if len(state["history"]["left"]) > max_history:
state["history"]["left"].pop(0)
state["history"]["right"].pop(0)
hL = state["history"]["left"]
hR = state["history"]["right"]
required_count = int(fps * 0.5)
if is_block(
left_wr, left_el, left_sh,
right_wr, right_el, right_sh,
state, frames_needed
):
return "阻挡"
bump_result = is_bump(
left_sh, left_el, left_wr,
right_sh, right_el, right_wr,
frame,
state,
frames_needed=3,
dist_thresh=20,
elbow_angle_thresh=140,
hand_close_thresh=60,
finger_var_thresh=50
)
if bump_result is not None:
return bump_result
pull_result = is_pull(right_wr, right_sh, left_wr, frame,state, frames_needed)
if pull_result is not None:
return pull_result
if is_push(
left_sh, left_el, left_wr,
right_sh, right_el, right_wr,
frame, state
):
return "推人"
if is_infringe_cylinder(hL, hR):
state["consec_infringe"] += 1
else:
state["consec_infringe"] = max(state["consec_infringe"] - 1, 0)
counters = {
"侵犯圆柱体": state["consec_infringe"]
}
# 找分数最高的
max_type = max(counters, key=counters.get)
if counters[max_type] > 0: # 必须至少有连续帧才算
return max_type
return None
foul_fn 是项目中 动作识别阶段的"心脏" ,输入左右手关键点和帧率,输出犯规类型(推人、拉人、阻挡、侵犯圆柱体等)。它用 分阶段状态机 + 历史记录 + 阈值判断 实现稳定判定。
一、输入与初始化
python
left_sh, left_el, left_wr = left
right_sh, right_el, right_wr = right
frames_needed = int(0.5 * fps)
-
left和right分别是左右肩、肘、腕的像素坐标。 -
frames_needed表示连续帧数阈值(0.5秒),用于防抖动。
二、单手举起检测(Phase 0)
python
left_up = left_wr[1] < left_sh[1]
right_up = right_wr[1] < right_sh[1]
single_up = (left_up and not right_up) or (right_up and not left_up)
-
判断手是否抬起(y 越小越高)。
-
single_up表示单手抬起,这是判定犯规动作的触发条件。
状态机逻辑:
python
if state.get("phase", 0) == 0:
if single_up:
hand = "left" if left_up else "right"
if state.get("single_hand") == hand:
state["single_up_count"] += 1
else:
state["single_hand"] = hand
state["single_up_count"] = 1
else:
state["single_up_count"] = 0
state["single_hand"] = None
if state["single_up_count"] >= frames_needed:
state["phase"] = 1
state["history"] = {"left": [], "right": []}
return None
-
连续帧检测避免误判。
-
当单手举起持续超过阈值 → 进入 Phase 1(开始正式犯规判断)。
三、历史记录与噪声过滤(Phase 1)
python
max_history = 12
swing_amp = 10
lag_tol = 3
-
维护左右手腕高度的历史
state["history"],限制最大长度。 -
如果左右手高度差过大(异常动作) → 清空历史,防止误判。
python
state["history"]["left"].append(left_wr[1])
state["history"]["right"].append(right_wr[1])
if len(state["history"]["left"]) > max_history:
state["history"]["left"].pop(0)
state["history"]["right"].pop(0)
这种历史窗口策略是典型 时间滤波 + 防抖动 的工程手段。
四、具体犯规类型判定
调用不同的规则函数,按优先级顺序判断:
python
if is_block(...):
return "阻挡"
bump_result = is_bump(...)
if bump_result is not None:
return bump_result
pull_result = is_pull(...)
if pull_result is not None:
return pull_result
if is_push(...):
return "推人"
-
阻挡 、撞人/碰撞 、拉人 、推人,按顺序判断。
-
每个规则函数内部都是几何 + 阈值判断,结合手肘角度、手腕位置、距离、手指展开状态等。
五、连续判定与"侵犯圆柱体"
python
if is_infringe_cylinder(hL, hR):
state["consec_infringe"] += 1
else:
state["consec_infringe"] = max(state["consec_infringe"] - 1, 0)
-
连续帧累积,保证短暂误动作不会触发犯规。
-
consec_infringe数值越大 → 犯规可信度越高。 -
最后根据计数选分数最高的犯规类型返回。
六、工程亮点
-
状态机分阶段:Phase 0 防抖动、Phase 1 历史判定 → 避免短时动作误报。
-
历史窗口:保持手腕高度历史,减少抖动干扰。
-
优先级判断:推人 / 拉人 / 阻挡有明确顺序。
-
连续帧计数:只有连续动作才判定为犯规,增强稳定性。
-
规则可扩展 :只需修改
is_push/is_pull等函数,不影响主流程。
下一章我们讲一下
python
if is_block(...):
return "阻挡"
bump_result = is_bump(...)
if bump_result is not None:
return bump_result
pull_result = is_pull(...)
if pull_result is not None:
return pull_result
if is_push(...):
return "推人"
这些函数,看看具体的判断是怎么实现的