Tonight Badminton · 真实运转拆解

一场球的九步拆解

素材:B 站业余单打「8.29 晚单打」· 正后方固定机位 · 1920×1080 · 30 帧 · 4 分 57 秒 · 任务号 4bfb3ce76054 · 所有截图来自本机真实运行,没有示意图

为什么选这场:它是六场跑通全管线的素材里最接近 App 拍摄引导教的机位(场地正后方、固定、单打、离人近),也是产品真正要面对的画质,不是转播级。它的中间产物缓存都还在,每一步都能拿到真实输出。

怎么读:每一步先说"做什么",再给"进什么、出什么",然后是这一步的真实画面和数字,最后是它会在哪里出错。页尾有一张"每个指标怎么算出来的"总表和分步耗时。

297秒
输入视频
9
切出回合
95
听到的拍数
54
击球点测点
1.96m
最高击球点
253秒
出战报的耗时(集锦另需 1065 秒)
0

上传与预检:先问这段视频能不能分析

视频进来后先做体检:时长在一小时以内、能解码、画面里有人。任何一项不过就当场退回,不进管线。

进一个 mp4 文件,46 MB
出视频档案:30 帧 · 1920×1080 · 8897 帧 · 296.6 秒 · 有人画面占比 100%
输入视频第 60 秒的原始画面,正后方机位,近端球员背对镜头
输入视频的原始画面(第 60 秒)。镜头在场地正后方,近端球员背对镜头,这就是"正后方机位"的含义。
1

检查画面稳不稳

在全片抽九个时刻,比较相邻画面的整体位移。手机被碰过、有人挪过三脚架,这里会看出来。晃动大的视频后面的位置类数字都不可信,所以先判。

进整段视频
出中位漂移 0.11 像素 · 最大漂移 2.5 像素 · 判定:稳 通过

已知边界:这一步按固定时距抽样,视频越长越粗;剪辑过的集锦会在切换点上被判"晃",这是正确行为,用户传原片没有这个问题。

2

找人、算走位:每秒抽一帧,把画面里的人都找出来

这是最花时间的一步。每秒取一帧,跑一遍人体姿态模型,得到每个人 17 个关节点。然后按"脚在画面下半部、身高像素够大"把近端球员挑出来,其余算远端或路人。

进每秒一帧,共 296 帧
出每一帧里每个人的关节点与脚点 → 近端球员逐秒位置、远端球员逐秒位置
第 103.3 秒的画面,检出四个人的骨架,橙线是近端判定线,近端球员脚点 y=861 身高像素 255
真实一帧的检测结果:检出 4 个人,橙色横线是"近端判定线"(脚点要在画面高度的 60% 以下)。左侧球员脚点 861、身高 255 像素,判为近端;右侧三人身高像素只有几十,判为远端或路人。模型是 rtmlib 的均衡档(YOLOX-m 找人 + RTMPose-m 定关节)。
  • 会错在哪:邻场的人和坐着的人也会被检出,靠"脚点位置 + 身高像素"过滤;双打或有人走进近端半场时,"近端球员"可能张冠李戴。
  • 为什么慢:找人的检测器每帧 0.4 秒;而且每秒只用一帧,却把 30 帧全解了码。这是 4.4 提速清单里的第一刀。
3

画走位热力图

把第 2 步得到的逐秒脚点直接叠在画面上:近端球员一种颜色、远端一种,待得久的地方更亮。不做球场标定,所以它是"画面上的热点",不是俯视图。

进近端与远端球员的逐秒脚点
出一张热力图,写入 assets/heatmap.png
真实输出的走位热力图,近端球员的热点集中在近场中路,远端球员在远场
这一场的真实热力图。近端球员(你)集中在近场中路偏左,远端球员在远场右侧。

"跑动距离"本该在这一步顺手算出来,但需要场地标定把像素变成米,目前引擎输出为空,所以战报里跑动那一格是横杠。

4

按声音切回合:一响是一拍,一串响是一回合

把音轨降到 8 千赫,取能量包络,找出"突然响一下"的瞬态,每个瞬态记为一次击球声。瞬态密集的一段是一个回合,长时间没响声就是捡球和休息。回合边界附近的孤立响声会把边界向外延伸。

进音轨
出101 个击打声瞬态 → 9 个回合,每回合的起止时间和拍数(回合内的瞬态数)
音频包络曲线,橙色竖线是 101 个击打声瞬态,九个绿色色块是切出的回合,各标注拍数
真实的音频包络。橙色短线是检出的 101 个击打声,绿色色块是切出的 9 个回合。第 2 回合 41 到 90 秒最长,24 拍;第 4 回合只有 3 拍。
回合起止拍数边界击球点测点数
10.7 – 15.7 秒24ok8
241.1 – 90.3 秒24ok8
396.9 – 105.9 秒7ok7
4114.2 – 120.2 秒3ok3
5127.0 – 143.5 秒4extended4
6188.3 – 200.1 秒4ok4
7209.9 – 226.3 秒5ok5
8231.6 – 250.8 秒7extended7
9250.8 – 292.4 秒17extended8
  • 这一步决定了"回合数"和"拍数"两个门面数字,而它完全靠声音。邻场的击球声、球拍碰地、有人拍手都会被记成一拍;安静馆里准,嘈杂馆里会虚高。这就是拍摄引导里"场馆越安静越好"的来由。
  • 拍数从没和人工数拍对过账。101 个瞬态里有 95 个落在回合内,其余 6 个被当成回合外杂音。
  • 球轨迹切回合的代码存在,但真实球局从未启用,因为整场跑球检测太贵(见第 7 步)。
5

测量击球点:在每次响声的那一帧量手腕有多高

每个回合最多取 8 个击球声时刻,每个时刻看前后共 3 帧,找出近端球员,量"持拍手腕离地多高"。尺子不是球场线,是人自己:假设身高 1.75 米,鼻子到脚的像素距离就对应 1.645 米(0.94 是鼻到脚占身高的比例)。然后给每个测点打质量标签,手臂关节点不清楚的标"受限"并降权,不删除。

进第 4 步的击球声时刻 + 视频帧
出54 个测点,每个带高度、质量标签、脚点和身高像素 → 本场最高 1.96 米(可信)、可信测点 33 个、受限 21 个
第 103.3 秒的画面,标出鼻子到脚 256 像素、脚到手腕 299 像素,换算 1.92 米
本场最高一拍的真实换算。鼻子到脚 256 像素对应 1.645 米,所以 1 米等于 156 像素;脚到手腕 299 像素,除以 156 得 1.92 米。战报里显示 1.96 米,因为系统取的是前后 3 帧里的最高值,这张图是其中一帧。
击球点高度 = (脚点 y − 手腕 y) ÷ [ (脚点 y − 鼻子 y) ÷ (1.75 × 0.94) ]  本帧:299 ÷ (256 ÷ 1.645) = 1.92 米
54 个测点按时间排布的散点图,绿色为可信 33 个,灰色为受限 21 个,橙圈标出最高 1.96 米,虚线是可信测点平均 0.91 米
全场 54 个测点。绿点可信、灰点受限。橙圈是本场最高 1.96 米(第 103.3 秒,第 3 回合)。虚线是可信测点的平均 0.91 米,也就是战报里的"场均击球点"。淡绿横带是 1.5 到 3.4 米的显示带,带外的数字只留标记不出数。
  • 为什么场均只有 0.91 米、高位占比只有 3%:测点取自"击球声时刻",而声音只说"响了",不说"是谁打的、是不是高手位"。近端球员接网前球、对手击球时近端正在准备,这些时刻量出来的手腕都是腰部高度。所以"场均"混进了大量非高手位时刻,对懂球的人会显得偏低。这是口径问题,不是测量错误,需要改成"只统计过肩挥拍的时刻"。
  • 身高假设是系统项:真实身高 1.65 的人所有数字偏高 6%,1.85 的人偏低 5%。自己和自己比时抵消,跨人比时不抵消。App 填真实身高可消掉。
  • 30 帧的时间误差:击球瞬间前后一帧手腕高度差约 5 到 8 厘米,取 3 帧最高值是为了压这个误差。
6

剪回合回放

按第 4 步的起止时间,把每个回合从原片里剪成独立短片,缩到 960 宽以便手机流式播放。这是"每个数字都能点开看画面"的底层素材。

进原片 + 9 个回合的起止
出9 个 mp4,写入 assets/rally_00.mp4 到 rally_08.mp4
九个回合短片各取一帧拼成的九宫格,标注时长、拍数和边界质量
九个回合短片各取第 1 秒的一帧。标注是各自的时长、拍数和边界质量。
7

合成集锦:挑瞬间、验球、跟人裁剪、慢放、标球

先按规则挑出 4 个候选瞬间:全场最高的可信击球点做"签名瞬间"(慢放)、拍数多的回合收尾做"多拍激战"(最多 2 个)、不够就按回合时长补位,同一回合最多一个。每个候选瞬间前后半秒跑一次球检测,看球是不是真的飞进了挥拍者附近并且在动,不是的话淘汰换下一个。通过的瞬间再按人和球的位置裁成竖版、触球前后放 0.5 倍慢、把球的轨迹画上去,最后把人声压掉保留击打声,拼成一条。

进第 5 步的测点、第 4 步的回合、原片
出19.3 秒竖版成片 1080×1920,4 个瞬间全部通过验球,写入 highlight.mp4 与清单 highlight_manifest.json
第 103.3 秒画面上叠加了球检测得到的黄色轨迹,从远场高处飞向近端球员头顶,验球结论为通过
签名瞬间的真实验球结果。黄色是 TrackNetV3 在前后半秒里检出的球位置,从远场高处飞到近端球员头顶。系统的原话结论:"球入邻域 8 帧,位移 686 像素",判定通过。这一步是"瞬间真精彩"的闸门,也是唯一用到球检测的地方。
成片四个瞬间各取一帧:多拍激战第 2 回合、击球点 1.96 米慢放、精彩回合第 8 回合、多拍激战第 9 回合
成片里 4 个瞬间各取一帧,按播放顺序。第 2 个是签名瞬间(击球点 1.96 米,慢放),标注写的是它的选材规则和验球状态。
顺序标签来源选材规则慢放验球时长
1多拍激战 · 第 2 回合约 24 拍88.8 秒拍数多的回合收尾,锚在最后一次实测击球否通过4.5 秒
2击球点 1.96m · 估算值103.3 秒全场最高可信击球点 = 签名瞬间0.5×通过4.3 秒
3精彩回合 · 第 8 回合242.0 秒按回合时长补位否通过4.5 秒
4多拍激战 · 第 9 回合约 17 拍292.4 秒拍数多的回合收尾否通过4.5 秒
  • 这是 60 帧视频耗时的大头。球检测在纯 CPU 上约 100 倍时长,所以只在候选瞬间跑;整段球标注还要再跑一遍,而且每次重新加载模型。
  • "精彩"的定义是规则写死的:最高击球点、拍数多、时长长。它不懂"这一分打得漂亮",只懂数字。
8

导出定格帧

取全场最高的可信击球点那一帧,存成图片,同时算出手腕在画面里的相对位置,让 App 能把"1.96 米"的高度线压在手腕上。

进第 5 步的最高可信测点(103.3 秒,第 3 回合)
出hero.jpg + 手腕纵向位置 0.51、脚点横向位置 0.29
真实导出的定格帧,近端球员举拍击球瞬间
真实导出的定格帧,就是 App 首页和战报顶部那张图的来源。
9

出数与页面:一个 JSON 变成你看到的战报

前面八步的结果写成一个数据文件 match.json,页面直接读它。战报上一些数字是页面当场算的,不是管线算的:高位击球占比、场均击球点、活跃度都在这一层。

处理中页面:四步进度,合成集锦 96%,已用 21 分 52 秒
处理中页(真实截图,本次重跑)。四步进度是给人看的粗分,后台真实阶段有九个。"已超过初始估算"是因为这次重跑和截图脚本在抢 CPU。
战报页真实截图:9 个回合共 95 拍,最高击球点 1.96 米,跑动无数据,战力分暂不可用,站位分布热力图,本场回合列表
战报页(真实截图)。9 个回合、95 拍、最高 1.96 米,跑动"暂无数据",战力分"暂不可用"。这两个横杠不是坏了,是闸门在起作用,原因见下表。
回合 3 单球慢放页真实截图,本回合最高击球点 1.96 米,3 拍数据已降权
回合 3 的单球慢放页(真实截图)。播放的是第 6 步剪出的回合原片,"3 拍数据已降权"来自第 5 步的质量门。
高光页真实截图:19 秒 4 个精彩瞬间,可下载
高光页(真实截图)。播放第 7 步的成片,下面列出 4 个瞬间及其来源秒数。

每个指标是怎么算出来的(以这一场为例)

指标算法一句话这一场的值来自第几步会错在哪
回合数击打声瞬态成簇,簇与簇之间的安静段是分界94邻场噪音会多切或少切;从未和人工对账
拍数回合内的瞬态个数954任何"响一下"都算一拍;嘈杂馆虚高
最长回合拍数最多的回合24 拍4同上
最高击球点所有"可信"测点里的最大手腕高度,身高比例尺换算1.96 m5身高假设 ±6%;击球帧差一帧 5 到 8 厘米
场均击球点页面当场算:可信测点的平均值0.91 m5 → 9混进了非高手位时刻,对高手偏低
高位击球占比页面当场算:可信测点里 ≥1.5 米的比例,可信测点少于 5 个不算3%5 → 9同上,口径待改为"只看过肩挥拍"
活跃度回合数 ÷ 视频时长,折成每小时;视频短于 300 秒不算— 296.6 秒,差 3.4 秒9短片外推会离谱,所以设了 300 秒门槛
跑动逐秒脚点换算米数,需要场地标定— 引擎输出为空3目前所有场次都是空
战力分击球点分 + 跑动分 + 活跃度分加权;击球点 1.60 米记 30 分、2.20 米记 60 分、2.90 米记 95 分,线性插值;有效项少于 2 个判"数据不足"— 击球点 47.9 分,但只有这一项5、3、9只有一项时不出分,防止单项虚高
最快杀球没有算法,文案写死"待 60 帧"—无见系统全景 4.2:先做飞行时间分档

这一场把三道闸门全都触发了:活跃度差 3.4 秒没过时长门,跑动引擎为空,战力分因此只剩一项而判"数据不足"。所以战报上有三个横杠。这恰好说明系统宁可不出数也不编数,但对用户来说横杠就是横杠。

每一步花多久

为了拿到分步时间,我把同一段视频用现行代码在空闲的 Mac 上重新跑了一遍,每三秒记录一次后台阶段。结果比 8 月 31 日原始那次的 465 秒长了近两倍:合成集锦一步就占了八成。原因是 8 月 31 日之后集锦线加上了整段球标注、跟踪取景和人声清洁,这三样都要逐帧跑重模型。这印证了系统全景 4.4 的判断:60 帧视频慢,主要慢在集锦,不在战报。

阶段(后台真实名称)开始于第几秒本阶段用时占比
检查画面稳不稳0 秒3 秒0.2%
找人、算走位(每秒一帧,296 帧)3 秒158 秒12%
按球轨迹或声音切回合161 秒3 秒0.2%
测量击球点(54 个测点 × 3 帧)164 秒80 秒6%
剪 9 个回合的回放244 秒9 秒0.7%
合成集锦(验球 + 整段球标注 + 跟踪取景 + 人声清洁 + 渲染)与导出定格帧253 秒1065 秒81%
合计(现行代码,空闲的 M1 Pro,纯 CPU)—1316 秒4.4 倍视频时长
  • 三次运行的总耗时:8 月 31 日原始运行 465 秒(当时的代码没有整段球标注和人声清洁);今天第一次重跑 2412 秒(和本页的截图脚本抢 CPU,不作数);今天第二次重跑 1316 秒(空闲机器,现行代码,上表就是它)。
  • 结果是确定的:今天两次重跑与原始运行的回合数、拍数、最高击球点、可信与受限测点数、四个高光瞬间及其验球结论完全一致。同一段视频不会这次一个数下次一个数。
  • 战报本身只需要前五步,也就是 253 秒,约 0.85 倍视频时长。如果先出战报再后台慢慢合成集锦,用户等待时间可以立刻缩短八成,不需要任何算法改动。