状态:草案(2026-10-26),已反映实测结果。 规则读取自 2026-10-25 的仓库代码(
ttscore/,backend/static/tt/play/),数字是我们通过公开比赛视频亲自测算的值(日期和条件见各个表格)。使用手机拍摄的实际乒乓球馆视频的准确度尚未实测,因为目前还没有带标签的视频。 编写规则:以实测数据为基础,未能测算的部分标记为“未实测”。
目录
- 1. 自动判定是“建议”,而非记分牌
- 2. 从球坐标到事件
- 3. 裁判:不看“谁击球”
- 4. 对打成组:死球、胜者、重新同步
- 5. “需要确认”:四个方向
- 6. 如何区分热身、练习与比赛
- 7. 芯片:人和机器的分工
- 8. 实测:目前准确度到哪一步了
- 9. 总结
- 参考文献
- 延伸阅读
1. 自动判定是“建议”,而非记分牌
首先明确立场。摄像头记分牌真正的得分是第 2 篇中看到的 人输入并修正的历史记录。自动判定只是向该历史记录逐个提出诸如“本回合 A 得分,原因:出界,置信度 0.83”之类的 建议,只有被确认的建议才会成为历史记录中的得分栏(backend/static/tt/play/lib/camauto.mjs 前言“记分牌只有一个”)。因此,即使自动判定出错,也无需与人修正的历史记录单独匹配。
本文将跟随球坐标传入的那一刻起到芯片变成得分的全过程。
- 球坐标 → 事件(弹地·网球·出界·结束)
- 事件 → 裁判状态机 → 对打胜者
- 对打成组、死球、重新同步
- “需要确认” 判断
- 芯片与自动反映、人与机器的分工
- 在这中间穿插着 将热身·练习与比赛区分开的装置
。第 4 篇将介绍球坐标是如何生成的,前一系列的 第 5 篇则探讨了该引擎从 66.9% 提升到 93.1% 的历史。本文是 当前运行中规则的全貌。
2. 从球坐标到事件
工作线程仅保留最近 2.5 秒的球坐标(EventFeed, KEEP_S=2.5),每当有新样本输入时,便填补短暂的空白(3 个样本或以下)并重新提取事件(ttscore/events.py 的 detect_events)。由于是实时运行,无法预知未来,因此采用的是 不断重新读取最近区间的方式。
弹地:同时满足五个条件
只有在 同时 满足以下条件时才会判定为弹地。
| 条件 | 内容 | 原因 |
|---|---|---|
| V 字形 | 画面 y 先增大(下降)后减小(上升) | 弹跳球的基本形态 |
| 乒乓球台带内 | 乒乓球台顶面矩形内。每条边有 8px 的余量,末端边在平面上延伸到 0.15m 以外 | 弹地瞬间球中心比接触点高一个半径(20mm),映射到平面上会有偏移 |
| 间隔 | 与上一次弹地间隔 0.12 秒以上 | 避免重复计算同一次弹地 |
| 保持横向 | 前后横向移动都达到 2px 以上且方向相反则丢弃 | 在球台弹地的球会继续前进,击中球拍的球会返回。击球也会画出 V 字 |
| 丢弃网前 V 字 | 网 x ±2% 以内且击中网 ±2 帧以内的 V 字会被丢弃 | 挂网球的晃动 |
如果是从末端拍摄的画面(摄像头在端线后),则会多看一项。沿长度方向的速度必须达到 0.3m/s 以上。因为在末端画面中,画面 y 混合了高度和距离,容易产生假 V 字。
弹地的一侧(L·R)基于平面
弹地落在哪位选手的一侧,不是由画面的左侧或右侧决定的,而是通过 球台平面坐标来决定的。利用球台的四个顶点构建画面到平面(2.74m × 1.525m)的单应性矩阵(第 4 篇),将球接触点的坐标转换到平面上,并通过长度方向坐标 u 的正负来确定一侧(ttscore/table.py 的 ball_side)。这样一来,无论是从侧面拍还是从末端拍,公式都是一样的。如果矩形扭曲得无法被识别为矩形(可能是顶点弄错了),则会退回到旧规则,即基于网 x 基准线的左侧或右侧。因为扭曲矩形的平面变换会倾斜网线并颠倒两侧。
平面投影 仅在球接触球台的瞬间 准确。如果将空中的球投影到平面上,穿过摄像机中球的直线会与平面相交,球越高,偏移越远。因此,平面坐标仅用于判断弹地的一侧,空中区间的判断则单独进行。
横向速度:判定的关键
每次弹地都伴随着长度方向的横向速度。弹地后 vx,紧邻前 vx_in,单位是 台长/秒。由于它是除以球台长度而不是像素得到的值,因此与摄像机距离和分辨率无关。这一个数字在下一节的裁判中用于区分发球、回球和假弹地。
网球、出界、结束
- 网球:当球穿过网 x ±4%,且前后 6 帧的横向速度比降至 0.7 以下(仅在前方速度达到 0.3 以上时)时,视为触网。仅在侧面画面中判定。
- 出界:仅在弹地后开启。如果球飞出球台边缘并落到顶面带下方,则为出界,一侧为飞出的那一头。
- 结束(end):最后一次弹地后 1.5 秒 内没有新的弹地,或者球在 1 秒 内不可见,则视为对打结束。1.5 秒这一数值来自实测,即实际对打中 98% 的弹地间隔都在 1.5 秒以内(公开比赛 12 场,2026-10-11)。
置信度
每个事件都附带置信度。 conf = min(검출 신뢰도, 공백 신뢰도) ,空白置信度为 1 − 놓친 프레임 / (fps × 0.25초) 。如果在前方丢失球超过 0.25 秒,则为 0。这意味着期间可能漏掉了一次弹地,因此不可信。检测置信度也单独(det_conf)记载,用于发球的第一下弹地。
3. 裁判:不看“谁击球”
裁判(ttscore/referee.py 的 RallyReferee)的核心思想在前言的第一行:st. 谁击球不是从视频中直接看到的,而是从弹地顺序推断出来的。 如果球在 S 侧弹地,接下来 S 侧的选手必须将其击打并落入对方一侧一次。如果未能做到(在同一侧再次弹地、未弹地直接飞出、无事发生直接结束),则 S 侧选手失利。唯独发球是个例外,球必须在发球方一侧弹地一次,然后在接发球方一侧弹地一次。
接触球拍是视频中最难找的事件。球被球拍遮挡、模糊、并融入选手身体。此设计在没有该事件的情况下也能计分。代价也很明显。如果漏掉一次弹地,或者夹杂了一次假弹地,“轮到谁击球”就会彻底错位。因此,以下大部分规则都是 过滤假弹地和对打外之球的装置。
状态有四个。 serve0(等待发球)→ serve1(发球进行)或 direct(直达)→ rally(对打)→ done(结束)。
等待发球:只接收发球的开始
在对打与对打之间,球也有很多运动。去捡球、传给对手、发球前在自己这侧弹几下、手里的球晃动。如果这些被捕获为事件,就会凭空产生得分。实际上,在将整场比赛输入引擎的第一盘中,49 个判定中有 20 个不是对打(传球 13 个,发球前在自己这侧弹 5 个等,2026-10-11, docs/tt/PERFORMANCE.md 2.6).
因此,在等待发球状态下, 只接收看起来像发球开始的球 。
# ttscore/referee.py _start() 에서 줄임
f = self._forward(ev, self.server, serve=True) # 서버 쪽에서 네트 쪽으로 가는 가로 속도(바운드 앞뒤 중 큰 쪽)
if ev.side == self.server:
if f is not None and f < SERVE_MIN_VX: # 0.7 테이블 길이/초
return False # 서브가 아니다 — 버리고 계속 기다린다
self.phase = "serve1"
return True
if f is not None and f >= TOWARD_NET_VX: # 리시버 쪽에, 서버 쪽에서 날아온 방향으로 떨어진 공
self.phase, self.expect = "direct", self.server
self.conf = min(self.conf, AMBIGUOUS_CONF) # 0.5 — 서버 쪽 바운드를 못 봤으니 확인 필요
return True
return False
条件有两个。 落在记分牌已知的发球方一侧,且, 朝网的方向移动达到 0.7 台长/秒以上 。其余的球会被丢弃,也不会扣减置信度。数字 0.7 的依据是实测(公开比赛 12 场,2026-10-11)。
- 实际 271 个发球的第一下弹地,其网向速度均在 1.07 以上(弹地前后较大者)。
- 在从 0:0 开始延续的 5 场比赛中,对打之外的发球方一侧的 55 个球中,除 4 个看起来像发球的球(赛前练习发球、无标签发球)外,其余均在 0.61 以下。接回的球为负数,在自己一侧弹跳接近 0。
不接收记分牌发球方对侧“看起来像发球”开始的球。 如果接发球方向发球方抛球,球会从接发球方一侧向网的方向弹起并落在发球方一侧。形状与发球相同,但方向相反。将两侧都作为发球接收的变体,使 5 场比赛的假判定从 3 个增加到了 16 个。
有一个例外。在接发球方一侧, 沿从发球方飞来的方向(远离网且达到 0.7 以上)落下的球。这可能是漏掉了发球方一侧弹地的发球,也可能是未在发球方一侧弹地的发球失误。这作为 direct(直达)状态接收,并将置信度降低至 0.5 发送至需要确认。发球的第一下弹地由于被抛球和球拍遮挡而经常漏掉。因此,在直达之后如果接发球方再次弹地或直接结束,根据开关(TTSCORE_DIRECT_MISS,开启)视为 接发球方未接住(发球方得分) 。因为在实际视频实验(院长 E27)中,“再次弹地”判定 60 个中有 48 个,“结束”判定 49 个中有 36 个是发球方得分。
发球进行:1 秒内到达接发球方
serve1 在此状态下等待接发球方一侧的弹地。
- 接发球方一侧的弹地必须在发球方弹地后 1.0 秒内 到达,方为正常发球(实际最大 0.55 秒,30fps·慢发球余量)。超出则为发球失误(接发球方得分)。
- 如果在发球方一侧再次弹地,但球仍然朝着网的方向(0.7 以上)前进,则视为跨越网面时捕获的假弹地,并继续观察发球(实际 5 起)。如果停止或返回,则是挂网并落在自己一侧的发球失误(实际 2 起,横向速度接近 0)。
- 如果未弹地直接出界或结束,则为发球失误(
serve_out)。
擦网球:推迟到下一个事件
发球期间触网且(net) 如果球在接球方一侧弹地,则记为候选擦网(let)。不会立即宣告擦网,而是 推迟到下一个事件。
- 如果下一个弹地在发球方一侧,则说明接球方回击的球已经落地。由于网球检测有误或者运动员未喊擦网而继续击球,因此取消擦网并继续对局,但标记为需要确认(0.5)。
- ) 否则即为擦网。不计分(
winner_side=None).
最初会立即判定擦网,但在实时处理路径中(2026-10-13)两次重现了擦网判定后的“死球”规则(第 4 节)导致后续真实对局的所有弹地被全部抛弃、该对局得分消失的问题。推迟判定的方案修复了该问题,离线对局级别的结果并未改变。
对局:在交替的方向间
在对局中,只保留「expect下一个弹地应当落下的方向」。
# referee.py feed() 의 랠리 부분에서 줄임
f = self._forward(ev, ev.side) # 바운드 뒤 가로 속도
if f is not None and f >= TOWARD_NET_VX: # 네트 쪽으로 가는 「바운드」 는 가짜 — 버린다
return None
if ev.side == self.expect: # 기대한 쪽: 정상. 다음은 반대쪽
self.expect = flip(self.expect)
return None
if ev.t - prev_bounce_t < DOUBLE_BOUNCE_SURE_S: # 0.25초 안의 같은 쪽 두 바운드 — 미룬다
self.pending = ev
return None
return self._call(flip(ev.side), "double_bounce", ev) # 같은 쪽에 또: 그 쪽 선수가 진다
- 朝向网的弹地是假的。 实时对局中,S 方的真实弹地由于是对手击回的球,会朝 S 方底线方向、即远离球网的方向移动。向网的方向移动 0.7 以上的“弹地”是对越过网顶的球或击球的误读(实际对局中 1,094 个里有 56 个)。仅凭这一条规则,实际对局的胜者就从 235 增加到了 241。
- 二次弹地。 如果同一个半场再次弹地,则说明该侧运动员未能接住。
- 快速二次弹地予以推迟。 如果同侧的两次弹地间隔小于 0.25 秒,则无法区分究竟是真正的短球,还是将弹地瞬间的击球误读为了弹地。在实测中,此类 39 个判定中有 16 个(41%)是错误的(其余判定的错误率为 8%)。因此将置信度降低至 0.5 并推迟到下一个事件。如果下一个弹地在反侧(回球落下),则说明第二次弹地是假的,将其丢弃并继续对局。通过这一规则,被改变判定的 9 个实际对局全部由错改对。
出界与结束:最后弹地一侧的运动员未能接住
- 出界:忽略越过刚刚弹地一侧底线的出界。因为该侧运动员正在球台高度以下准备接球(削球、中远台弧圈球)。如果没接住,则由结束(end)来决定。
- 其他出界或结束意味着“最后弹地一侧的运动员未能回击”。该运动员输掉,原因为
out或no_return。
判定的置信度
单个判定的置信度为该对局内事件置信度的 最小值。但发球第一跳按扣除空白期后的检测置信度(det_conf)计算。因为发球第一跳之前的空白期是由抛球和球拍遮挡造成的,期间不可能漏掉弹地。在扣除这一点之前,发球第一跳的 70% 置信度为 0,这也是“需要确认达 86%”的主要原因(PERFORMANCE 2.6)。
4. 对局打包:死球、胜者、重新同步
一个裁判负责一个对局。将对局首尾相连制作成记分牌的工作由 ttscore/analyze.py 的 LiveScorer 完成。
- 丢弃比赛开始前的部分。 丢弃“比赛开始”时间(
start_t)之前的所有事件。这是第 6 节的主题。 - 死球 2 秒。 做出判定后,在之后的 2 秒内(若事件持续则不断延长)或直到结束(end)到来之前,丢弃所有事件。当判定因弹地或出界产生时,球其实仍在运动中。比如连续弹跳的发球、二次弹地后滚动的球、擦网后被回击的球。如果将这些误读为下一局的发球,就会凭空产生得分。
- 从方位到运动员。 裁判按 L/R 方判定胜者。记分牌(
rules.Scoreboard)反映换边情况,根据当前站在该侧的运动员给出得分。下一局裁判的发球方由记分牌的发球轮次重新确定。 - 重新同步信号。 如果记分牌错了一分,发球轮次就会错位,真正的发球就会被当作“从发球方反侧开始的球”而丢弃。因此引擎会针对每场对局单独测量 实际见到的发球方。它是上一局结束到本次对局开始 + 0.5 秒内最后的“发球形态对”(S 方弹地朝向网侧 0.7 以上 → 1.0 秒内在反侧弹地)的 S。只要它与记分牌的发球方 哪怕只有一次 不同,就会发出信号。不改变比分,只是向人工询问“发球对吗?”。在实测中,标签发球 ±0.8 秒内的配对与标签发球方在 267 个中有 266 个相同。不采用“连续两次错位”的原因是:如果记分牌错了一分,发球每两分轮换一次,因此发球方会每隔一场对局错位一次,连续两次的情况几乎不会发生(在 64 个错位对局中,连续两次规则只抓到了 10 个,而单次规则抓到了 60 个,误报率为 2,PERFORMANCE 2.7)。
5. “需要确认”:四个分支
每个判定都可以附加“需要确认”标记。即保持判定不变,让人工看一眼。附加该标记的途径有四条。
| 分支 | 条件 | 原因(why) |
|---|---|---|
| ① 校准回归 | 学习得到的逻辑回归给出的“判定错误的概率” > 0.0442(校准 v2)。关闭校准则使用旧规则“置信度 < 0.6” | cal + 两个对结果推动最大的特征组合(球弱、漏球、长对局、结束不明、未看到发球、发球方不同、判定类型) |
| ② 底线带 | 落点在底线边界 ±0.08m 以内 | edge |
| ③ 信号冲突与球弱 | 若球模型之间互相冲突,或者对局区间内检测到球的帧比例低于 0.5,则将置信度扣减至 0.59 | conflict 等 |
| ④ 球台 | 对局正在重新寻找球台或跨越了丢失区间 | table |
此外,带有重新同步信号(resync)的判定也会在芯片中被单独询问。
为什么单一置信度不够
最初的规则是“若对局置信度 < 0.6 则需要确认”。在 2026-10-14 评估该置信度是否真的能区分错误时发现 它无法区分(院长 E20,保持实时路径不变,公开测试 7 场比赛 × 11 种退化条件,共 597 个判定)。置信度 0.0–0.1 区间的正确率为 0.86,0.9–1.0 区间也是 0.86,用于区分错误的 AUC 为 0.51(抛硬币)。因为在 30fps 的实时路径中,只要漏掉一次球,最小值就会跌到 0.6 以下。
因此,引擎在判定瞬间利用已知的 19 个值训练了一个小型逻辑回归(ttscore/reviewcal.py)。包括置信度、检测置信度最小值、弹地平均置信度、弹地/事件数量、弱事件/空白事件数量、对局长度、最后是否为“结束”、是否观察到发球形态对、该侧是否与记分牌不同、判定原因等。计算时每个判定只需一次内积,因此没有实时开销。原理与训练将在第 4 篇中介绍。
| 判定 | 条件 | 未经确认即错误 (†) | 需要确认 | 错误区分 AUC |
|---|---|---|---|---|
| 置信度 < 0.6 | 测试 7 场比赛 × 11 种条件(597 个判定,E20) | 26 | 395 | 0.51 |
| 校准 v1 | 相同 | 12 | 430 | — |
| 校准 v2(生产) | 测试 7 场比赛 × 11 种条件(661 个判定,反映标签修正,E25) | 20 → 8 | 450 → 497 | 0.71 → 0.73 |
v2 行中“→”前方是相同条件下的 v1。†v2 行包含了未经确认而放过的幽灵判定,因此不能与上方两行直接对比。校准选择阈值的方向是: 稍微增加需要确认的次数,但大幅减少人工盲目放过的错误回答 。阈值是在训练比赛单位的 5 折交叉验证中,以“需要确认次数不高于当前的前提下漏掉的错误最少”来确定的。
附加原因的理由也在实测中得到了验证。校准前原因处于空白状态,芯片无法说明需要确认什么(院长 E19-2)。现在,回归判定为需要确认的每一个判定,都会附带最多两个“将结果推向错误方向影响最大的特征”的人类语言词组发送出去。
底线带
球是击中底线(擦边)还是落在线外,是用单台相机最难区分的判定。如果落地点在底线边界 ±0.08m 以内,胜者保持不变,并发送至需要确认(TTSCORE_EDGE_REVIEW,院长 E34)。
6. 热身、练习与比赛的区分方法
引擎无法区分练习发球
乒乓球比赛以热身对局开始。双方互相对打几分钟,还会尝试练习发球。问题在于 练习发球与真实发球具有相同的形态。在发球方弹地一次,在接球方弹地一次,网前速度也与真实情况一致。
有实测案例。公开测试赛之一(test_4)在比赛开始 2.05 秒时拍到了赛前练习发球。从发球方到网侧为 4.2 台长/秒,球越过了球网。按照第 3 节的发球确认规则,无法将其与真实发球区分开。引擎将其当作第一分记下,而这一分导致整场比赛的发球轮次错位,该比赛的“对局后比分正确”在 21 个对局中为 0(PERFORMANCE 2.6)。
我们还评估了仅通过视频区分练习的方法。“丢弃记分牌第一发球方第一个完整发球之前的部分”(N1)以及“丢弃任意一侧第一个发球形态对之前的部分”(N2)。两者都没有改善(相比无信号引擎,5 场比赛的比分正确数 24 → 24,胜者 41 → 40,第 12 篇全篇无判定 18 → 21)。test_4 的真正第一球是一个挂网的发球失误,因此它不是“完整的发球”,并且两条规则都丢弃了真正的第一分。由于练习发球案例仅有 1 个,因此无法通过数据确定规则。 所以没有采用。
结论很简单: 比赛何时开始由人工告知。 这对运动员来说很简单,对机器来说几乎是不可能的任务。
“比赛开始”信号的传输路径
相机记分牌的热身状态(第 2 篇)正是接收该信号的门户。
- 画面。 开启相机即为热身状态(
CamMatch.startedAt === null)。即使点击计分格也不会增加比分,而是会晃动“比赛开始”按钮。遥控器的开始键仅在热身期间有效。 - 开始。 点击“比赛开始”后,
startMatch()将比赛开始时间记入历史记录,并以tt:match-start事件发出{type:'score', on:true, start, server_side}。 - 对齐时钟。 自动计分画面(
cam-auto.js)接收该信号并调用scoreOn(),将设备时钟(Date.now)记录的开始时间转换为 帧时钟(与操作人员连接的瞬间为 0)(startOffset)。因为操作人员不知道设备时钟,只知道附在帧上的时间。 - 操作人员。 经由网页直播,操作人员的会话调用
scorer.start(server_side, t)并新建LiveScorer。遗忘过去的判定和区间,丢弃该时间之前的所有事件。 - 双重锁定。 若比赛尚未开始,浏览器不会发送此信号,且操作人员在收到信号前不会做出判定(仅发送球的位置和弹地显示)。浏览器端也不会将热身期间或比赛结束后到达的判定生成建议(
autoProposal为 null,代码注释“热身对局不是得分”)。
发球方也会随之调整。记分牌每次改变时(得分、撤销、人工修正发球权),都会将当前的发球方发送给操作人员,引擎借此校准下一局的发球方。 人工修正的发球方优先。
实测:单个信号的效果
在公开比赛期间,针对 0:0 开始、标签与规则相符的 5 场比赛(49 个回合),这是模拟由人工在第一记发球前瞬间按下「比赛开始」信号所测得的数值(2026-10-11,PERFORMANCE 2.7)。
| 指标(5场比赛 49个回合) | 无信号 | 有信号 |
|---|---|---|
| 回合后得分(比分·局分)正确 | 27 / 49 | 36 / 49 |
| 胜者正确 | 44 / 49 | 45 / 49 |
| 非标签回合的判定(假得分) | 3 | 2 |
test_4 从 0/21 上升到了 9/21。即使在第一记发球前 0.3、1、2、3 秒按下信号,结果也是一样的, 5秒前(在练习发球之前)按下则没有效果。因此,界面会引导用户在「热身结束、第一记发球之前」按下。
比赛中的「练习球」
即使比赛开始后,回合之间的球仍在继续运动。这由第 3、4 节的装置来过滤。
- 过滤发球等待:丢弃回传球、自己这边的弹跳球、手持球、地上的球。在同一场 5 局比赛中,假判定从 30 减少到 5,回合后得分正确从 8/49 变为 24/49(PERFORMANCE 2.6,应用发球确认规则前前后后)。
- 死球 2 秒:丢弃判定后立即弹跳的球。
- 球台区间:错过球台或重新寻找球期间的球不计入判定,跨越该区间的回合将被发送至“需要确认”。
剩余局限
- 形似真实发球的球仍然会计分。 如果选手在比赛中重新发擦网球(Let),或者在回合之间练习发球,引擎无法将其与真实发球区分开来。仅应用发球确认规则时,5 场比赛中剩余的 5 个假判定里,有 3 个是此类球(练习发球、疑似擦网且无标签的发球)。
- 如果不按「比赛开始」 热身回合就会被计分。在手动记分牌上,热身期间的得分栏会通过摇晃开始按钮来提示,但如果人工按得太早(在练习发球之前),则毫无用处。
- 局间休息时间没有单独的「暂停」信号。此时段的球仅依赖于发球等待过滤。
与记分牌无关的「练习」功能
这两个名字相似且容易混淆的功能与记分牌及自动判定 无关。
- 反应速度回合(
/tt/play/reaction-rally:这是一个单人反应游戏,屏幕上的球会落入正手/反手 × 短球/长球四个区域之一。不使用摄像机判定。 - 练习日常(
/tt/practice:选择水平和时间(15–180分钟)后,会按热身、技术、比赛、整理的顺序生成练习计划。记录作为练习记录保存,与评级(Rating)分开(第 6 篇)。
7. 芯片:人机职责分工
判定成为芯片的过程
当操作员发送判定时,浏览器会将其 以当时的左侧选手为准 进行转换并以芯片形式浮现。因为操作员是以原始视频的 L/R 为准叙述的,屏幕可能是镜像的,而且每局比赛座位都会互换。不提议擦网(Let)和无判定。
芯片上有四个按钮:「正确」、「(左侧选手)得分」、「(右侧选手)得分」、「无效」。
如果没有答复则直接录入
没有人会在每个回合都去按立在远处的手机。因此,如果没有答复,则直接录入(backend/static/tt/play/lib/scorestatus.mjs).
| 判定 | 等待答复的时间 | 依据 |
|---|---|---|
| 确信 | 4秒 | — |
| 需要确认 | 9秒 | 比人工查看芯片并答复的时间(约 2.5 秒 + 「请稍等」3 秒)更充裕,且在实际视频判定间隔(3.8–17.4 秒)之内 |
这条规则背后有段故事。2026-10-13 将公开比赛视频交由运营操作员测试时,摄像机记分牌在 50 秒内收到四个判定,比分却依然是 0:0。大多数判定都是“需要确认”,而屏幕在人工答复“需要确认”之前不会将其录入,直到下一个判定到来时便将其丢弃。并且它没有说明为什么没有录入。现在,“需要确认”也会在 9 秒后录入,如果在满 9 秒前有下一个判定到来,则会在那时录入之前的判定。
芯片队列最多为两个。当第三个判定到来时,将最旧的直接录入,且一旦浮现的芯片至少显示 2.5 秒。
答复的处理
| 答复 | 进入历史记录的栏目 | 修改记录 |
|---|---|---|
| 「正确」 | 得分(来源 auto) | confirm |
| 超时直接录入 | 得分(来源 auto) | 不留记录(并非人工确认) |
| 与判定相同的选手 | 得分(来源 auto) | confirm |
| 与判定不同的选手 | 得分(来源 tap) | flip |
| 「无效」 | 不录入 | void |
手势即为芯片的答复
最重要的一条规则是:首要规则是, 当芯片浮现时如果按下了得分栏,该手势即成为芯片的答复(handOverride)。如果是同一位选手则为正确,如果是不同选手则为修改。如果没有这条规则,人工会习惯性地按下手动记分,4 秒后自动判定也会录入,从而导致 一个回合得 2 分。
综上所述,职责划分如下。
| 人工 | 机器 | |
|---|---|---|
| 确定比赛何时开始 | 确定(「比赛开始」) | 丢弃该时间点之前的内容 |
| 谁在发球 | 可以修改,修改即获胜 | 从记分牌接收并使用,如果不一致则询问「发球正确吗?」 |
| 回合胜者 | 在芯片上答复或手动按下(手势即答复) | 提出建议。如果没有答复,则在 4/9 秒后录入 |
| 模糊判定 | 查看 | 附加“需要确认”及原因 |
| 错误录入的分数 | 从历史记录中删除(第 2 篇) | — |
人工在芯片上的答复记录(修改记录)可能会保存在服务器中。视频不会被保存,仅导出同意用于训练的内容。该记录将去往何处以及至今如何使用将在第 6 篇中探讨。
8. 实测:目前准确度如何
所有行均是在公开比赛视频(OpenTTGames 测试 7 场比赛)中测得的数值。训练和阈值选择使用了其他比赛,测试比赛留到最后查看。不过,由于规则引擎本身是在 2026-10-11–12 观察测试比赛的过程中不断完善的,因此基于规则基准的测试分数是乐观的(负责人关于数据泄露的警告)。
| 测量 | 条件 | 结果 |
|---|---|---|
| 运营流程固定基准表 | 测试 7 场比赛 × 降级 11 种条件,标签 638,与运营相同的路径 + 弹跳过滤、校准 v2、直通规则(2026-10-16,负责人 E25·E27) | 正确 508, 未经确认即错误 8,需要确认 497,幽灵球(无球时给出的得分)51,判定延迟 p90 1.32秒 |
| 摄像机记分牌从头到尾 | 将测试 7 场比赛的 851 秒以虚拟摄像机 30fps 流式传输至真实浏览器/操作员代码中,标签 58 个回合(2026-10-17,负责人 E35) | 正确 47,错误 7,无判定 4,幽灵球 9, 未经确认即错误 0,需要确认 53/65 |
| 分条件公开视频 | 八种拍摄条件 27 个剪辑(2026-10-14–16,PERFORMANCE 2.16–2.17) | 垂直竖立的手机视频中,能看到球的帧占 5%,判定为 0 |
| 手机拍摄的真实乒乓球馆 | — | 未实测(带标签的视频 0 部) |
这两行说明了当前的特性: 未经确认即出错的情况很少见,但需要确认的情况很多。 在摄像机记分牌从头到尾的测试中,65 个判定中有 53 个需要确认。9 秒自动应用机制代替人工付出了代价,“减少需要确认的次数,同时不增加未经确认即出错的次数”便是下阶段的改进目标。
9. 总结
- 自动判定不是记分牌,而只是一项建议。真正的得分是由人工录入并修改过的一次历史记录。
- 裁判不看球拍,仅凭弹跳顺序和 横向速度 0.7 这一单一数值来区分发球、回传球和假弹跳。
- 练习发球的形状与真实发球相同,因此引擎无法将其区分。因此,“比赛开始”需要由人工按下,该信号会传递至屏幕 → 帧时钟 → 操作员 → 引擎,从而丢弃该时间点之前的所有内容。仅凭这一个信号,5 场比赛中得分正确的比例就从 27/49 提高到了 36/49。
- 由于无法仅凭置信度这一单一指标来区分错误判定(AUC 0.51),“需要确认”已被替换为通过学习得出的校准,如果在没有答复的情况下,芯片会在 4/9 秒后直接录入,并且人工的手势即为芯片的答复。
TT 的一句话。 热身发球和真实发球在我看来一模一样。所以请务必在第一记发球前瞬间按下「比赛开始」。那一次按键比十分还要重要。
参考文献
- International Table Tennis Federation. ITTF Statutes 2026, Chapter 2: The Laws of Table Tennis, §§2.10–2.15. https://db.ittf.com/document
- R. Voeikov, N. Falaleev, R. Baikulov. "TTNet: Real-time Temporal and Spatial Video Analysis of Table Tennis." CVPR Workshops, 2020. arXiv:2004.09927. 这是通过神经网络直接捕捉弹跳和网球的代表性先前研究,而我们的不同之处在于通过球轨迹规则提取事件。
- Extended OpenTTGames(回合结束原因·胜者标签)。arXiv:2512.19327。本文中的回合胜者标准答案。
- 我们的测量:仓库
docs/tt/PERFORMANCE.md2.6·2.7·2.16·2.17,研究日志 E20·E25·E27·E34·E35。代码ttscore/events.py·referee.py·analyze.py·reviewcal.py,backend/static/tt/play/lib/camauto.mjs·scorestatus.mjs.
延伸阅读
- 本系列 第 2 篇:手动记分牌与比赛规则引擎:「不是状态而是历史」,热身状态。
- 本系列 第 4 篇:模型的原理与论文:如何学习弹跳过滤 1D CNN 与需要确认的校准回归。
- 前系列 TT Score 模型剖析第 5 篇:事件与裁判:从 66.9% 到 93.1% 每场比赛所做出的改变与经历的历程。
- 视觉模型阅读 2,第 10 篇:精准寻找事件:通过学习寻找事件的方法。