摘要

用户的目标是:玩的时候几乎实时地拿到所有能分析的信息。飘出来的伤害数字很难读,用户提出另一条路:双方每次行动之间至少有约 1 秒的思考时间,在两次行动之间读每支部队(兵堆)的数量,就能知道每次行动造成的减员。

但当时 App 的实时选帧在战斗中 40 秒才取一张战场图,远不够按行动读数。本实验的关键发现是:战场上方的行动条(一排卡片,按顺序显示接下来轮到哪些部队)每张卡下面就写着该部队的数量,而且每次行动结束整条会左移一格。所以只要检测到“左移并静止”,读一次行动条就能拿到最多 13 支部队的数量,不必去战场上逐个找数量牌。

在一段训练录像(10 月 10 日 16:57,23 分钟)的 App 离线回放中,经过几轮修复,66 次行动结束全部读到;在“空闲时先读行动条再读关键帧”之后,“行动结束→读完”的 p95 从 9.25 秒降到 5.53 秒。之后又接入了实时攻击预览读取和“英雄施法后补读行动条”。这些都在离线回放里测得,实时模式还没验证。

要回答的问题

  1. 能否可靠地检测“一次行动结束”?
  2. 每次行动结束后读一次行动条,能否在 App 里及时完成并交给状态估计(SE,把观察累积成对局状态推断的模块)?
  3. 行动条读不到的变化(英雄施法、攻击预览)怎么补?

数据与来源

只用用户选定的那段训练录像。逐行动对比用的参考是模型目视核对(MODEL_VISUAL_CHECKED),不是人工审核。

方法

检测行动结束

每秒取 10 帧行动条区域(原生坐标 x 590–2020、y 10–125),缩成 64×8 灰度“签名”;签名明显变化(平均变化超过阈值 6)后又静止 0.5 秒,记为一次行动结束。

读取

每个事件帧交给路由(router,把截图分给各读取器的程序)只做战场任务。App 每会话最多 64 次读取,这个上限写在存储格式里,所以行动条读数走单独的旁路:App 的观察器在回放/实时帧上检测行动结束,空闲时送读,结果存在会话的单独目录里,作为额外帧交给 SE。

英雄施法补读

英雄施法会改变部队数量,但行动条不左移。比较双方 HUD 上魔法数字框(施法要付魔法,鼠标碰不到 HUD 角落):发现变化并静止后,2 秒后(等法术效果落地)补读一次行动条;如果期间正常行动已经读过就不补。

实时攻击预览

鼠标放在攻击目标上会弹出预览(击杀数和伤害区间)。App 战斗中每 0.5 秒把一个约 8 KiB 的“白色像素掩码”随帧发给关键帧服务,服务用已有的骷髅图标模板线索判断有没有预览;连续两次超过阈值算一次出现,同一次只读一次,走单独的攻击预览旁路,结果同样作为额外帧交给 SE。

设置

所有端到端轮次都是用 App 的离线视频工具回放同一段录像。回放比实时快,部分轮次 CPU 上同时有其他验证任务。

结果

检测与读数(390–600 秒,3.5 分钟战斗)

  • 检测到 15 次行动结束,逐张目视每次都是左移一格,卡片数量变化清楚(例如 55→8、16→13、7→3)。
  • 与逐行动参考对比:实际 16 次行动,检出 15 次,无重复;漏的 1 次是 583.1 秒的一次“等待”,只交换前两张卡,签名平均变化最大 4.7,低于阈值 6。等待不改变数量,漏掉不影响减员。
  • 15 帧中 12 帧读出 9–13 张卡的兵种、阵营色和数量,与目视一致;3 帧没读出卡片。

速度

第一轮每帧 10–14 秒(当时后台有其他 CPU 任务)。在常驻进程里打开两个已有的加速开关后,热状态每帧 0.9–3.0 秒(首帧冷启动 17.4 秒)。瓶颈是 CPU 模板匹配;试过 GPU 版匹配器,但与 OCR 库在同一进程里有 cuDNN 动态库冲突,没有继续。

端到端回放的几轮修复

轮次 结果 发现与修复
e2e-01 检出 8 次,读了 0 次 回放解码比实时快,路由总被关键帧占着。修复:有待读行动条时暂停解码(最多 30 秒)
e2e-02 ~ 05 卡在第一个行动条 图片写入函数在含中文的会话目录下静默失败。修复:先编码再写字节,拒绝原因写进日志
e2e-06 80 次全读、0 次被替代;但 79 次读数中 24 次 0 张卡,1 次被判为结果不符 ① 画面上有日志窗口、生物面板、攻击预览时,战场任务走了“只读 HUD”分支;② 当前回合只剩 1 张卡时读取器要求至少 2 张;③ 战斗结束的菜单帧返回“拒绝”,App 当成错误。修复:行动条请求忽略遮挡分支,连同下一回合的卡一起读;App 接受原生的全部状态

用 10 帧(含 8 个原来 0 张卡的帧)复查:修复前 8 帧中 7 帧 0 张,修复后全部 12–13 张卡。

延迟(离线回放实测)

阶段 e2e-07 p95 e2e-08(行动条优先)中位数 e2e-08 p95 e2e-08 最大
行动结束(静止 0.5 秒后)→ 送读 7.42 秒 0.08 秒 3.52 秒 5.70 秒
送读 → 读完 2.95 秒 2.16 秒 2.78 秒 3.12 秒
行动结束 → 读完 9.25 秒 2.38 秒 5.53 秒 7.80 秒
读完 → SE 结果存盘 1.83 秒 1.05 秒 1.94 秒 2.67 秒

e2e-07(全部修复后):66 次行动结束全部读到,65 次读出 12–13 张卡,唯一 0 张的是战斗结束后的菜单帧。超过 1 秒的送读等待中最长的几次,都是行动条结束后又有 1–2 张关键帧抢先送读;e2e-08 改为空闲时先读待读的行动条,p95 明显下降。行动结束到 SE 可见:中位数约 3.4 秒,p95 约 7.5 秒(两段 p95 直接相加,偏保守)。

SE 不是瓶颈:在常驻并预热的进程里,加一张行动条帧后重算中位数 0.14 秒、p95 0.20 秒。之前一次对外汇报曾误写 SE 是瓶颈,下一次汇报已更正。

e2e-07 的减员与日志关联:32 个减员区间中,12 个能直接对上日志行、2 个对应多次命中、18 个没有日志;21 行日志死亡记录用上 20 行。

低幅度交换的第二阶段确认(影子测试,未接入 App)

为了抓住“等待”这类只交换两张卡的事件,先把阈值放低到 2 作为候选,静止后再把前两张卡位与上一次稳定画面做去亮度相关比较(鼠标高亮只改亮度,相关接近 1;换了兵种相关 0.04–0.71),任一卡位低于 0.9 才确认。390–1300 秒:64 次普通事件,6 次候选被拒(都是高亮),4 次确认中 3 次真实、1 次误报。样本只有这一局,暂不接入。改用“按列/按格取最大变化”的指标虽然也能抓到等待,但会因鼠标高亮、浮动提示多出 5–27 次假事件,所以保留平均指标。

攻击预览

影子测量(390–1300 秒每 0.5 秒):预览在屏幕上的时间占战斗的 17.4%,共 81 次独立出现;线索每帧 13 毫秒;64 次行动结束的静止帧里只有 5 次同时有预览,所以“读行动条时顺便读预览”覆盖不了多少,需要单独触发。

项目 e2e-09 e2e-11(加入目标移动检测)
预览出现 / 读取 46 / 46 54 / 54
成功读出 40 47
每次读取 中位 0.46 秒,最大 0.86 秒 —

曾试过直接发半分辨率 JPEG,会丢掉 20% 的预览(缩小后再取白色会把细笔画冲淡),所以改发掩码。e2e-09 的 6 次拒绝都是路由没确认版面;逐帧追查有三种整帧 OCR 问题:击杀行整行没读出、两个预览并排被合成一行、骷髅图标被读进数字。SE 起初一个预览也没用上,原因是旁路帧没带来源标识,补上后得到 43 行“攻击预览候选”(都标为候选,不是实际结算)。e2e-11 中 SE 对重复的相同预览只列一次:47 个预览帧 → 34 行候选。

英雄施法补读(e2e-10)

影子测试:比较行动条卡片数字区域会被鼠标高亮和光效误触发几百次,不能用;比较 HUD 魔法框,390–1300 秒变化 26 次、合成约 15 组,日志里记录的 7 次施法全部在 1.5 秒内抓到,另有一次是回合开始的魔法恢复,915 秒之后没读到日志无法核对。

e2e-10:66 次行动结束加 13 次施法补读,共 79 次,0 次被替代;预览照常 46/46。减员区间更细:我方巫妖被敌方“审判”打掉的 55→8,区间从 430.7–457.0 秒(混着下一次行动)缩到 430.7–435.0 秒。日志关联:直接对上 13、多次命中 2;21 行日志和 15 个已匹配区间的时间检查全部一致。

失败与负结果

  • 漏检 1 次“等待”;第二阶段确认方案有 1 次误报,样本太少未接入。
  • 巫妖按行动读到 55→8→7→8→3,中间回升一次,可能是复活也可能是读错,没有核实。
  • 卡片数字区域比较、按列取最大变化、GPU 匹配器、半分辨率 JPEG 预览都试过并放弃。
  • 6 次攻击预览读取失败来自整帧 OCR;用图标位置做局部重读的改进还没做。

局限

  • 所有端到端结果都来自同一段录像的离线回放;回放比实时快,关键帧更密,部分轮次有 CPU 竞争。
  • 逐行动参考是模型目视核对,不是人工审核;没有与完整逐行动人工标注对比漏检率之外的项目。
  • 实时模式没有测;项目的“整帧 p95 ≤3 秒”等目标没有达到。

结论与下一步

“检测行动条左移 → 立即读一次行动条”让按行动读数量、算减员成为可能,并在离线回放中做到每次行动都读到。下一步:经用户同意后在实时模式里测一次(不动用户正在运行的 App);实测实时模式下关键帧读取造成的等待;部队被消灭时把“读数消失”当作降到 0,并按时间对齐日志;核对攻击预览失败和预览与实际伤害的关联。