一段 6 秒视频里,拍摄者连续做了两次举臂跳投。如果程序把全片关键点汇总成一个 78 分,使用者无法知道哪一次动作需要复看,甚至无法确定系统有没有把两次都识别出来。这里的第一道工程题不是"怎样给动作打分",而是先找到每次动作的时间边界,并把观察证据归还给对应事件。
本文用一组明确标为合成数据的时间序列演示:第一次举臂约在 1.0~1.6 秒,第二次约在 3.4~4.1 秒。实例来自一套单机位二维姿态分析原型的分段结构与测试思路,但不公开真人画面、内部模型配置和生产参数。本文讨论的是可复核的动作事件,不是投篮命中判定,更不是专业生物力学诊断。
目录
一、问题不在平均分,而在分析单位
以整段视频为分析单位,系统往往只返回一个总分和一串建议。假设第一次举臂完整、第二次身体被遮挡:总分可能看起来正常,却掩盖了第二次根本不具备评分条件。如果再把第一段球的轨迹误关联到第二段,就会产生有凭据外观、无实际对应关系的结论。
合理的对象关系是:一个视频任务包含零到多条动作事件;每条事件有起止时间、峰值时间、关键点覆盖情况、可选的球轨迹证据和独立建议。总览可以显示"识别到 2 次、可评估 1 次、待复核 1 次",但不能只留下两次分数的平均值。
| 层级 | 最低限度要回答的问题 | 示例 |
|---|---|---|
| 视频 | 文件能否解码?时间轴与帧率是否可信? | 6 秒、逐帧时间戳有效 |
| 事件 | 有几次候选动作?各自在哪段? | 1.0~1.6 秒;3.4~4.1 秒 |
| 证据 | 该段人体关键点、球轨迹是否可用? | 第二次球未可靠检出 |
| 解释 | 哪些指标可计算?哪些只能提示复核? | 缺点位不输出确定分数 |

图1:合成视频含两段动作;"两次动作"和"两个分数"不是同一件事,必须先确认事件与证据。
二、输入信号怎样变成候选事件
姿态模型输出的是逐帧关键点,不直接输出"这是第几次跳投"。对于单机位侧拍,可以把手腕相对肩的位置变化作为候选举臂信号 :例如取肩到髋的长度作为尺度,计算 (肩部 y - 手腕 y) / 躯干尺度。这样比固定像素高度更能适应人在画面中的大小变化,但相机仰拍、遮挡、人物旋转仍会改变该信号。它只负责提名事件,不能单独证明离手。
从采样到事件至少经过四道门槛:时间戳必须单调;关键点缺失要显式标记;高值应连续出现而非仅一帧尖峰;同一次动作中的短暂漏检应允许合并,但两次独立动作不能因为间隔参数过大而混在一起。真实系统可以再结合肘角、膝角、人物跟踪 ID 和球轨迹;这些是交叉证据,不是用来倒填已经缺失的关键点。
例如每 0.1 秒采一次,1.0~1.6 和 3.4~4.1 是两段持续高值;2.7 秒的一帧孤立高值只应视为噪声。若 1.2 秒手腕点丢失一帧,不能立即把第一次动作拆成两次。下图用中文标出这三种情况。

图2:连续时间、持续高值、短间断容忍与邻段合并共同决定候选事件;图中的阈值不是通用经验值。
Google 的 MediaPipe 视频姿态接口明确要求按时间戳提交视频帧,并让输入时间戳单调递增。本文的教学代码只接收已提取好的标量采样,因此不绑定某个姿态模型;实际接入时,还应记录模型版本、可见性阈值、采样策略和跟踪主体。多人同时入镜时,不先稳定关联同一人,单凭"画面里最高的手腕"甚至会把不同人的动作拼成一个事件。
三、事件边界如何避免互相串扰
候选高值的开始和结束,不一定就是动作分析窗口的完整边界。屈膝准备可能早于举臂,落地可能晚于峰值,因此可以在候选段前后加缓冲。但如果两次动作靠得近,第一次的后缓冲与第二次的前缓冲可能重叠,导致同一帧被两次评分或球轨迹跨次关联。
处理方式是先保存原始候选段,再根据相邻事件的中点裁剪分析窗口:前界不早于视频起点,后界不晚于视频时长,两个事件的窗口不能交叉。回放入口可指向加缓冲的窗口,结果页同时显示候选段和回放段,避免用户误解"系统说开始于 0.3 秒,而我 1.0 秒才举手"。
峰值时间也要讲清含义。用最高的手腕位置定位的是举臂代理峰值,不是精确离手时刻;球在候选段附近被检测到,只表示有球形轨迹证据,不能推断一定命中。现有原型在球未可靠检出时仍可给出姿态观察,但会明确说明不判断离手与命中,这个边界比强行输出结论重要。

图3:每次事件分别保存时间、峰值、可用采样与证据缺口;总览不能覆盖逐次记录。
四、用一段可运行代码复现两次分段
下面是公开教学版实现,输入为已经归一化的手腕高度,不包含姿态模型推理、球跟踪或产品内部阈值。代码完整版本见文末示例文件;示例环境为 Python 3.11+,只用标准库。
python
from dataclasses import dataclass
@dataclass(frozen=True)
class Sample:
time: float
wrist_height: float | None
def segment_events(samples, threshold=0.4, min_samples=2, max_gap=0.25):
if any(b.time <= a.time for a, b in zip(samples, samples[1:])):
raise ValueError("timestamps must increase")
groups = []
for row in samples:
if row.wrist_height is None or row.wrist_height < threshold:
continue
if not groups or row.time - groups[-1][-1].time > max_gap:
groups.append([])
groups[-1].append(row)
return [(round(g[0].time, 2), round(g[-1].time, 2))
for g in groups if len(g) >= min_samples]
运行时构造 0.0~5.9 秒、每 0.1 秒一条采样;让 1.0~1.6 和 3.4~4.1 秒的高度为 0.7,其余为 0.0,再把 1.2 秒设为缺点、2.7 秒设为孤立尖峰。输出应是 [(1.0, 1.6), (3.4, 4.1)]。仓库里的完整示例还记录峰值与可用点数,并对近邻候选段做冷却合并。这里的 0.4、0.25 秒只是让教学序列可运行的示例参数;真实机位应通过人工标注视频调参、报告误合并与误拆分,不能抄数值上线。
为什么先写这个小函数?它把模型波动和业务结果之间的决策显式化:当结果是零次、一次或两次时,可以追溯到底是缺点、持续时间不足、间隔合并还是输入时间轴错误,而不必把所有失败笼统归因于"模型不准"。
五、逐次结果该保存什么
建议每条事件至少保存 number、候选起止、分析窗口起止、峰值时间、可用采样数、关键点质量、评估状态、球证据状态及提示语。评估状态不应只有"成功/失败":可评估、证据不足、待人工复核能帮助页面正确呈现。分数是可选字段,不能评分时用空值,不用 0 分冒充动作差。
| 检查对象 | 合成案例预期 | 发现异常时如何处理 |
|---|---|---|
| 候选数量 | 两条,按时间排序 | 人工看是否漏检或误拆 |
| 时间关系 | 第一次结束早于第二次开始 | 重算窗口,拒绝重叠回放 |
| 峰值 | 每次落在本次候选段内 | 标为数据错误,不直接展示 |
| 姿态证据 | 分别记录有效点位与覆盖 | 不足时不评分,只提示补拍 |
| 球证据 | 可有、可无、可不可靠 | 不以"球出现"推断命中 |
原型里会对每次候选段独立计算屈膝准备、腿部伸展、手臂伸展和平衡的二维观察值,再对可评分事件求一个总览均值。这个均值只是方便浏览,不代表两次动作都可靠,更不能解释为运动员能力分。发布页面宜先展示两次卡片和回放,再显示总览。
六、测试、误判与上线验收
只测"正常两次"不够,至少还要测孤立尖峰、缺点、无动作、非法时间序列、两次间隔很近和动作部分出画。示例测试中,单帧 0.8 的尖峰返回零条;时间戳倒序抛错;1.2 秒缺点后仍保留第一次事件。这些都是分段规则的单元验证,不是系统在真实篮球视频上的准确率。
python
def test_two_events():
rows = [Sample(i / 10, 0.7 if 10 <= i <= 16 or 34 <= i <= 41 else 0.0)
for i in range(60)]
rows[12] = Sample(1.2, None)
rows[27] = Sample(2.7, 0.8)
assert segment_events(rows) == [(1.0, 1.6), (3.4, 4.1)]
真实验收应另外准备不同拍摄距离、左右手、遮挡、多人入镜和不完整动作的获授权样本,逐条人工标注动作次数与起止时间。分别记录漏检、误拆、误合并,而不是只报一个平均分。若要展示示意截图,应先完成真人素材授权和脱敏;本文五张图完全是合成技术图,不含原片人物。

图4:单帧尖峰、短遮挡、近邻误合并和回放越界对应不同修复手段,不宜统称为"模型问题"。
实际产品还需要核查两个时间口径:解码帧序号除以名义帧率,只在固定帧率且帧时间基稳定时适合作近似;可变帧率视频应优先使用帧时间戳。播放器 seek 后也要校验实际解码位置,不能假设请求 1.00 秒一定精确从该帧开始。发布前抽看每条事件首帧、峰值帧和结束帧,再核查结果 JSON 与播放器显示是否对应。

图5:合成数据验证分段逻辑;只有标注过的真实样本才能支持检测效果的结论。
七、适用范围与参考资料
"先分事件、后逐次解释"不只适用于篮球。跳跃、挥拍、搬运动作、跌倒候选和工位操作复盘都可能遇到同一问题:连续视频里有多次相似动作,整段摘要无法告诉用户到底哪一次出了问题。迁移时要重新定义动作信号、事件边界和人工验收标准,不能把本例的手腕高度阈值原封不动套过去。
本文能证明的是:示例代码对给定合成序列返回两条互不重叠的候选事件,并在异常输入上有明确行为。它不能证明真实视频中的命中率、评分有效性或跨机位稳定性。要往下一层走,就要做获授权的人工标注集,把姿态质量、球证据与事件时间轴分别验收。
参考资料:MediaPipe Pose Landmarker 视频接口、OpenCV VideoCapture 时间位置属性。可运行的分段示例与单元测试用于复现文中的合成案例,不包含真人素材或产品内部模型参数。