TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

整体结构:从手机到判定服务器,以及流出设备的数据

状态:草稿(2026-10-26),已反映实测数据。 结构基于 2026-10-25 的仓库代码读取,数据为我们直接测量所得,每个表格均附有日期和条件。经由互联网的实际用户路径延迟与手机现场准确率暂未实测。 编写规则:以实测数据为基础,未测出的内容标记为“未实测”。

目录

1. 一个记分牌上挂载了多少台机器

在乒乓球馆使用 TT Lab 的过程非常简单。将手机架设在球台旁边并打开 /tt/play/cam 点击“开启摄像头并开始”。屏幕上的摄像头画面上方悬浮着一个大记分牌,点击得分方一侧的屏幕半区即可增加分数。发球轮次、默频、局末换边等规则均由记分牌按规则提示。

到这里为止, 只有一台手机。记分规则、历史记录、读分播报以及录像全部在手机浏览器内部运行。服务器开始介入的情况仅限用户选择以下三项之一时:

  1. 开启自动计分。将摄像头帧发送至服务器的 GPU 工作进程以获取回合胜者建议。
  2. 进行实时共享。仅将得分事件发送至服务器供他人实时观看。
  3. 点击记录本场比赛。将已结束比赛的局分留在服务器上,经对手确认后计入积分。

虽然还有通过单独上传录像文件进行分析的途径(/tt/videos),但这属于用户直接上传视频文件的独立功能。

本文将这一结构绘制成一张导图,记录各部分之间传输的内容,尤其是 哪些数据流出了设备,哪些没有流出。判定规则的内部原理将在第 2·3 篇中展开,模型的原理在第 4 篇,训练在第 5 篇,记录在第 6 篇。

2. 四个部分的导图

① 휴대폰 브라우저 손 점수판 · 이력 · 점수 읽기 · 녹화와 편집 이력은 휴대폰 localStorage 에 — 기본으로 서버에 안 감 라이브 공유 득점 이벤트 기록하기 게임 점수만 자동 점수 512×288 JPEG ↑ 판정 제안 ② 웹 서버 (같은 코드 두 대) 로그인 · 시간 몫 · 자동 점수 중계(WebSocket) 경기 기록 · 레이팅 · 라이브 · 고친 기록 → DB 프레임 → (저장 안 함) ← 판정 올린 영상 내부 API 만 ③ 실시간 판정 작업자 공 → 탁구대 → 이벤트 → 심판 서버 GPU 시간 조각 하나 세션 3개까지 프레임은 창 3장만 메모리에 ④ 녹화본 분석 작업자 같은 엔진으로 올린 영상 분석 DB·저장소 열쇠 없음 동시 작업 1 원본은 30일 뒤 지움 모델 저장소(공유 볼륨) — 엔진 코드 · 모델 코드 · 가중치 sha256 을 대조해 심고, 작업자를 다시 시작하면 그것이 배포 읽기 읽기
图:手机浏览器、Web 服务器、实时判定工作进程、录像分析工作进程四个部分及其之间传输的内容
部分 职责 运行环境 代码
① 手机浏览器 记分规则·历史·撤销,读分,录像与休息时间剔除,自动计分芯片,发送视频帧 用户手机 backend/static/tt/play/ (无构建的 ES 模块)
② Web 服务器 登录,自动计分中继(WebSocket),实时观赛,比赛记录与积分,保存修正记录 两台 Web 服务器(相同代码) backend/app/tt_*.py (FastAPI)
③ 实时判定工作进程 寻球,寻找并追踪球台,提取事件,裁判,“需要确认”判断 服务器 GPU(24GB 级)的一个时间切片 live/(GPU 部分), ttscore/(纯 Python 引擎)
④ 录像分析工作进程 用相同引擎分析上传的视频以生成比赛记录和集锦 同一服务器 GPU 的一个切片(并发任务 1) 引擎文件夹的 batch 部分

为什么把引擎分成两块

观察 ③ 的内部可以发现它分为两层。需要 GPU 和 OpenCV 的部分(live/)以及仅接收球坐标与事件的 纯 Python 引擎(ttscore/。由于引擎是不需要 GPU 也不需要 OpenCV 的函数集合,因此在 CI 中仅通过合成轨迹来测试视频。即使更换球模型,裁判规则也必须照常通过测试。这种分离在前序系列 第 1 篇中作为“识别·事件·裁判·延迟确认”的四层结构进行了详细说明。

比赛规则被额外复制了一份。由于手机屏幕必须在不呼叫服务器的情况下计分,因此使用了将 Python 规则引擎(ttscore/rules.py)移植为 JavaScript 的 backend/static/tt/play/lib/scorekeeper.mjs 。实时观赛服务器(backend/app/tt_watch.py)上也存在相同规则的第三份副本。如果存在三份副本,它们迟早会产生偏差,因此我们让 Python 测试和 Node 测试读取 相同的案例文件(backend/static/tt/play/data/rules-cases.json。修改其中一方就会导致另一方的测试失败。

手机端:无构建的模块与 Service Worker

屏幕代码是不经打包器处理、由浏览器直接读取的 ES 模块。模块地址使用了带有内容哈希(?v=)的 import map,确保代码更改时地址随之改变,不会与旧缓存混淆。乒乓球区域设有 Service Worker(/tt/sw.js,范围 /tt/),它会缓存屏幕代码,但不会缓存 API 和记录界面。这是因为即使乒乓球馆的 Wi-Fi 断开,手工记分牌也必须持续运行,且分数记录必须始终查看服务器上的最新值。

/tt/play/cam 的首屏不会在一开始就加载沉重的区块(自动计分、录像、观赛),而是在使用时才加载。因为绝大多数人只使用手工记分牌。

3. 开启自动计分时发生的事情

自动计分是该结构中最长的链路。下面按顺序追踪单个视频帧转化为记分建议的全过程。

  1. 浏览器压缩并发送视频帧。 将摄像头画面缩小至 512×288 并以 0.7 的 JPEG 质量进行压缩,最高以每秒 30 张的速度发送(backend/static/tt/play/cam-auto.js, lib/live.mjs)。虽然屏幕上以镜像显示,但发送的是未翻转的原图。帧的前面附带有 8 字节的头部(序号,拍摄时间戳 ms)。如果累积 3 天未收到答复的帧,则不再发送新帧。这是一种防止在低速网络中队列无限变长的机制。
  2. Web 服务器把守大门。 WebSocket 中继(backend/app/tt_live.py)仅接收 Google 登录会员,并计算席位与时间配额。视频帧仅负责透传。
  3. 工作进程将视频帧转换为球坐标。 顺序为:增加亮度 → 球模型(BlurBall) → 选择上一球附近的候选 → 维持最近 2.5 秒的球轨迹 → 填补短时空白 → 提取事件 → 过滤学习到的反弹(veto)(live/core.py).
  4. 引擎进行判定。 只有在收到“比赛开始”信号后裁判才会启动,回合结束后会输出胜者、原因、置信度以及是否“需要确认”(第 3 篇)。
  5. 浏览器以芯片形式询问。 判定结果不会直接写入记分牌,而是以芯片形式弹出。如果无人响应,确定的判定结果会在 4 秒后、需要确认的判定结果会在 9 秒后直接写入(第 3 篇)。

球台在此流程之外被单独搜寻。在比赛开始前进行一次“寻找球台”(使用比球模型重得多的分割模型),此后每帧通过光流法进行追踪(第 4 篇)。

时间配额与限制

GPU 价格昂贵且属于共享资源。因此存在多重限制。以下是代码的默认值(运营配置文件中是否有单独的值尚未确认)。

限制 数值 席位
单次开启可使用时长 20分钟 Web 中继
单人每日配额 40分钟 Web 中继
站点整体每日配额 360分钟 Web 中继
并发使用席位 3 Web 中继与工作进程皆有
单帧大小 120,000 字节 Web 中继
帧率 36张/秒(瞬间可多10张) Web 中继
无数据传输时的断开时间 15秒(工作进程为20秒) Web 中继与工作进程
预先寻找球台 每人每小时 30 次 Web 中继

当连接断开时,浏览器会以 1、2、4、8、15 秒的间隔最多重新连接五次。对于重新连接也无济于事的关闭情况(如需要登录或配额耗尽等关闭代码 4401、4429、4430、4431 等),则不会重新连接。

实测:当并发会话增加时

单个工作进程最多接收三个会话。当三个会话同时接入时,三者平分一块 GPU 的时间切片。以下是在 2026-10-11 与语音合成服务共享同一 GPU 状态下的实测值(docs/tt/PERFORMANCE.md 4.2).

并发会话数 单会话处理 fps 帧往返时间
1 29.4 p99 约 32ms
3 16–20 p50 70–95ms,p99 约 127ms

当处于三个会话时,21%–30% 发送的帧在处理前就被新帧覆盖而丢弃。这是因为工作进程的 GPU 循环只有一个,每次循环时仅将各会话的 最新窗口打包成一个批次进行处理,并使积压的窗口被新窗口覆盖(live/server.py 抬头,“延迟优先”)。对于记分判定而言, 不迟到比不遗漏任何一帧更重要。代价是当球速较快时,30fps 实际上会降至 16–20fps,正如前序系列 第 5 篇中所展示的那样,该引擎的帧率越低,“无需确认即出错”的情况就会增加。

实测:从回合结束到分数显示

目标是“回合结束 → 判定在 1.5 秒内”(研究负责人的实时预算)。引擎在最后一次反弹后 1.5 秒内如果没有新反弹或球持续 1 秒不可见,则判定回合结束。也就是说,预算的相当一部分消耗在 “是否真的结束了”的等待时间上。

测量项目 条件 数值
判定延迟(回合结束 → 判定) 公开视频测试 7 场比赛,清晰条件,模拟实时路径(2026-10-13·14,负责人 E00·E07) p90 1.31秒
判定延迟 八种拍摄条件 E2E(2026-10-14–15,PERFORMANCE 2.16) 中位数 0.67–0.90秒
直至球台建议弹出 摄像头记分牌 E2E(2026-10-14,PERFORMANCE 2.13) 2.8–3.4秒
单次寻找球台 运营实时工作进程测试图 211 次,服务器 GPU(24GB 级),预热后(2026-10-25–26,负责人 E44) 中位数 1.73秒,p95 2.14秒
寻找球台首次调用 相同条件,将模型加载至内存的第一次 12.9秒
经由互联网的实际用户路径 — 未实测

尽管曾多次尝试通过缩短结束阈值来降低延迟(例如球消失时更快结束等),但均被驳回(负责人 E03·E10·E24·E28)。如果过早结束,会在长回合的防守中过早判定得分。

4. 部署:从模型仓库读取代码与权重

Web 端和工作进程的部署方式有所不同。由于这种差异曾导致过一次实际事故,在此详细记录。

Web 端采用镜像方式

Web 服务器采用常规方式。提交至仓库后,CI 运行测试并构建容器镜像,修改 GitOps 仓库中的镜像标签后,ArgoCD 会自动将其同步至开发和运营环境。屏幕代码(backend/static/tt/)也包含在该镜像内部。

工作进程使用共享卷而非镜像

GPU 工作进程 不会重新构建新镜像。 它们直接使用同一台 GPU 服务器上已经在运行的 AI 服务镜像(PyTorch 2.9、Python 3.11),并从 模型仓库(共享卷) 中读取并加载引擎代码、BlurBall 模型代码、权重以及 OpenCV wheel。引擎代码经常变动,但像 PyTorch 这样庞大的依赖项几乎不变,因此每次修改代码时,都不需要重新构建达数 GB 的 CUDA 镜像。不过代价是镜像的 Python 版本会被固定,这个代价将在第 4 篇 SAM 3 的故事中再次提及。

注入代码和权重的脚本(live/seed-pvc.sh)步骤如下:

  1. 核对权重的 sha256 是否与代码中指定的值一致。球模型基础权重 3545206c…、乒乓球桌模型(SAM 2.1·EfficientTAM)则接收官方分发包,校验 sha256 后解压。
  2. 先将新文件下载到 .new 文件夹中,下载完成后一次性替换。这可以防止工作进程读取到下载了一半的代码。
  3. 重启工作进程。这就是所谓的部署。

工作进程在读取权重时会再次校验 sha256,并且仅使用 PyTorch 的 weights_only=True 读取(live/blurball.py)。由于权重文件是 pickle 格式,可能会执行任意代码,该选项旨在防止创建除张量之外的其他对象。

通过环境变量开关启用功能

几乎所有的新功能都采用 代码默认关闭、通过环境变量开启 的方式。截至 2026-10-25,实时工作进程配置中启用的开关如下:

开关 修改内容 篇章
TTSCORE_BOUNCE_VETO 丢弃由规则生成的、经训练的 1D CNN 假边界 第 3·4 篇
TTSCORE_BRIGHTEN 在寻球前增加帧亮度 第 4 篇
TTSCORE_REVIEW_CAL 将“需要确认”改由训练好的回归模型替代 0.6 的置信度来决定 第 3·4 篇
TTSCORE_DIRECT_MISS 服务器端未看到第一条边界时发球方的判定方向 第 3 篇
TTSCORE_EDGE_REVIEW 将接近底线的着落点设为需要确认 第 3 篇
TT_TABLE_FINDERS 乒乓球桌查找模型的顺序 第 4 篇
TT_TABLE_KP 将训练好的角点模型加入乒乓球桌查找中(2026-10-25 开启) 第 4 篇

相反,微调过的球权重(TTSCORE_BALL_WEIGHTS) 和弹道填补 (TTSCORE_BALLISTIC_FILL) 虽然完成了实验,但由于未达到开启条件而不在配置中(处于关闭状态)。开关方式的好处在于 关闭起来非常容易。只需保持代码和权重不变,删掉一行配置即可恢复到以往的行为。

这种方式引发的事故

我们也确实经历过它的缺点。与网页端不同,工作进程的配置在自动部署之外,需要由人工应用,而代码则是另外注入的。因此, 配置和代码可能会出现脱节。 在 2026-10-18 时,确实曾出现过配置中开启了 TTSCORE_EDGE_REVIEW=1 ,但模型仓库的引擎代码中尚无该功能的实际状态(研发日志 E41)。开关虽然开着,但没有执行任何操作,也没有报错。通过重新注入代码解决了该问题,此后在开启功能时,会严格遵循「应用配置 → 注入代码 → 重启 → 确认工作进程内的代码包含该功能」的顺序。

出于同样的原因,在 2026-10-25 开启角点模型(TT_TABLE_KP) 时,我们不仅查看了配置文件,还将测试图像实际发送给运行中的工作进程,以检查响应中是否存在角点模型的痕迹。全部 212 个响应中都有角点模型运行的记录(研发日志 E44,第 4 篇)。

格子之间的藩篱

由于工作进程负责接收用户的视频,因此我们为其设置了多层防护网。

5. 哪些数据会离开设备,哪些不会

휴대폰 안에 머무는 것 서버로 가는 것 카메라 원본 영상 원본 해상도 자동 점수를 켤 때만 512×288 JPEG · 저장 안 함 경기 이력 전체 localStorage 「이 경기 기록하기」 게임 점수만 녹화 파일 · 편집본 폰에 저장 「라이브로 공유」 득점 이벤트만 점수 읽어 주기 브라우저 음성 합성 칩에 답한 기록 영상 없음 · 동의 표시 설정 · 리모컨 키 localStorage 올린 영상(별도 기능) 줄여 올림 · 30일 보관 기기 경계 오른쪽은 모두 사용자가 켜거나 누를 때만 일어난다. 켜지 않으면 서버로 아무것도 가지 않는다.
图:按功能划分保留在手机内与传输至服务器的数据

TT Lab 乒乓球画面的原则在 backend/static/tt/play/cam.js 前言中有一行简述: 相机视频绝不离开设备。 按功能整理如下:

功能 保留在手机内的数据 发送至服务器的数据 留在服务器上的数据
手势记分牌 完整比赛历史(localStorage)、设置 无 无
语音播报比分 浏览器语音合成(speechSynthesis) 无 无
录制与剔除休息时间 录制文件(保存于手机)、动作指示 无 无
实时共享 视频 得分·撤销·结束事件(附带序号) 直播比赛与事件队列
记录本场比赛 各回合历史记录 局分、比赛时长、对手 比赛记录、积分
自动判分 原始分辨率视频 512×288 JPEG 帧(最高每秒 30 张) 帧数据 不予保存。修正后的判罚记录(包含同意标识,无视频)
上传录像分析 — 用户选择的视频文件(由浏览器压缩至 360p60 500kbps 后上传) 原始文件保留 30 天,分析结果

自动判分是该原则的 例外。因为寻球需要 GPU 观察视频,而目前该 GPU 位于服务器上。因此,我们将例外范围缩小了。

录像分析是一项由用户直接选择并上传文件的功能,其性质有所不同。原始文件会在 30 天后删除,并有每日凌晨运行的清理任务。

长期计划:将 GPU 搬到手机上

将自动判分回归到原则之内的途径,是将寻球功能移至手机端运行。尽管球模型很小,只有 149 万个参数,但它能否在手机浏览器中跑满 60fps 尚未经过实测。目前「发送至服务器但不保存」是一个现实的妥协。

6. 总结

TT 的肺腑之言。 记分牌本来有一台手机就足够了。服务器只是在你想要「帮我一起看下球」时才被召唤的朋友。而我们或许诺过那位朋友,绝对不在日记本里记下它看到的画面。

参考文献

  1. T. Gossard, F. Radovic, A. Ziegler, A. Zell. "BlurBall: Joint Ball and Motion Blur Estimation for Table Tennis Ball Tracking." CVPR Workshops (CVsports), 2026. arXiv:2509.18387.
  2. N. Ravi 等. "SAM 2: Segment Anything in Images and Videos." ICLR, 2025. arXiv:2408.00714.
  3. Y. Xiong 等. "Efficient Track Anything." ICCV, 2025. arXiv:2411.18933.
  4. 我们的测量:仓库 docs/tt/PERFORMANCE.md 2.13·2.16·4.2,研究日志 E00·E07·E41·E44。

延伸阅读

登录后即可点赞

评论

还没有评论。

登录后即可发表评论