从 dota2-draft-vision 迁出并定名,作为天梯选将识别项目起点。 Co-authored-by: Cursor <cursoragent@cursor.com>
22 KiB
上分帝(Climperor)—— 方案与实施细节
本文档记录 上分帝(Climperor) 的背景、技术选型、实现细节与推进计划。
README.md 是操作手册,本文是设计依据与决策记录。
1. 背景与目标
要解决的问题
在 Dota 2 选将阶段(以及进入游戏后),几秒内自动获取双方 10 个英雄,用于选将辅助或赛后分析。
为什么 GSI 做不到
官方 Game State Integration 在普通玩家视角下不提供双方 pick:
| 场景 | GSI 可获得的阵容数据 |
|---|---|
| 排位 / 普通 All Pick | 仅自己的 hero.id;draft 通常为空 |
| Captains Mode | 历史上有部分 pick/ban,不稳定 |
| 观战 / 裁判视角 | 阵容字段较全 |
| 赛后 | 需依赖 OpenDota 等外部 API |
Valve 官方 issue 中已明确:All Pick 的实时 draft 数据因隐私考量被关闭 (#9562、 #7193), 且被标记为 not planned。
dota2-hex 中的 lineup_probe 埋点(src/gsi/telemetry.rs)正是为验证此事而写,
其单元测试即假定 AP 模式下 draft:{} 不含阵容键。
候选方案对比
| 方案 | 准确率 | 延迟 | 合规性 | 门槛 | 结论 |
|---|---|---|---|---|---|
| 官方 GSI | — | — | 好 | 低 | 拿不到双方 pick |
| Overwolf GEP | 很高 | 实时 | 好(与 Valve 有协议) | 玩家须装 Overwolf | 备选,偏重 |
| 读游戏内存 | 高 | 实时 | 风险高 | 低 | 排除,违反项目合规边界 |
| 截屏 + 模板匹配 | 中高(可迭代) | < 1s | 好 | 低 | 选定 |
| 截屏 + 云端大模型 | 低(实测不可靠) | 数秒 | 好 | 需联网/付费 | 排除为主路径 |
实测记录:为什么不用「整图问大模型」
用一张 1024×576 的对局截图直接让多模态模型识别顶栏阵容, 10 个英雄几乎全部识别错误;换用裁剪后的顶栏特写(1024×71), 准确率提升到 8/10。结论:
- 整图 → 单个英雄头像只有几十像素,信息量不足
- 通用视觉模型不按英雄库分类,会「脑补」出看似合理实则错误的阵容
- 必须先裁格子再识别,且输出需约束在英雄白名单内
社区独立工具(dota-hero-picker、 dota2-picker、 ability-draft-plus) 的共同做法也是:裁固定 ROI + OpenCV 模板匹配 / 小型 CNN。
2. 技术方案
处理流程
决策时间截图(PNG,原生分辨率)
↓ ① 按相对坐标裁出 10 个头像格
↓ ② 去除 UI 边饰(顶部玩家颜色条、底部 ID 名牌),缩放到统一尺寸
↓ ③ 与模板库逐一做归一化相关匹配 TM_CCOEFF_NORMED
↓ ④ 双阈值门控:Top-1 分数 + (Top1 - Top2) 分差
↓
{"radiant": [...], "dire": [...]} 每格给出 hero_key 或 null
关键设计决策
① 相对坐标而非像素坐标
Dota 2 的顶栏 UI 以屏幕顶部中央为锚点、随分辨率等比缩放。因此坐标存储为:
- 横向:
(格子中心 x - 屏幕宽/2) / 屏幕高 - 纵向、宽高:
值 / 屏幕高
在 1440p 标定一次,1080p / 4K / 大部分 16:10 可直接套用。 这是通用性的第一层保障——目标是所有 Dota 玩家可用,不能写死单一分辨率。
② 两层模板库
| 层 | 路径 | 来源 | 特点 |
|---|---|---|---|
| real | templates/real/{hero}/*.png |
选人阶段默认脸实拍 | 与目标同源;不收皮肤/至宝;逐步积累 |
| cdn | templates/cdn/{hero}.png |
Steam 官方 CDN 头像 | 全 127 英雄覆盖;按顶栏实际窗口裁切后实测已可 100% |
CDN 层保证「库里绝不会缺英雄」——缺模板时匹配器只能在已有英雄里硬选, 必然乱配(前述实测错误正是此类)。real 层随使用逐步替换 CDN 层。
③ 宁可不认,不可乱认
同时满足两个条件才输出结果,否则返回 null:
score >= min_score(默认 0.45)margin = Top1 - Top2 >= min_margin(默认 0.04)
分差门控用于排除「两个英雄都像」的情况,比单一分数阈值更可靠。
④ 失败即样本
recognize.py --truth 会把认错的格子连同正确答案存入 failures/
(文件名含正确 hero_key)。确认是默认脸后再移入 templates/real/{key}/。
皮肤顶栏靠会话策略(决策阶段只补空槽、best 优先选人帧),不靠穷举皮肤模板。
3. 代码结构
Climperor/
├── config.json # 相对坐标、裁切参数、匹配阈值
├── heroes.json # 127 英雄 id / key / 英文名对照(自动生成)
├── common.py # 配置读写、坐标换算、裁切预处理、模板库加载
├── capture.py # 屏幕捕获(单张 / 定时连拍 / 供程序调用的 grab_frame)
├── calibrate.py # ROI 手动标定 + 可视化校验(自动标定失败时的退路)
├── autocalibrate.py # 靠玩家颜色条自动标定 10 格,免手工框选
├── fetch_cdn_templates.py # 拉取全英雄 CDN 头像 + 生成 heroes.json
├── build_library.py # 裁格子 → 人工标注 → 入 real 模板库
├── recognize.py # 识别 + 准确率评估 + 失败样本归档
├── roles.py # 定位匹配的位置文字识别 + 判断哪一格是「我」
├── draft_session.py # 跟踪整局选将过程,逐轮记录 pick 时间线
├── evaluate.py # 按 samples/labels.json 批量评测所有已标注截图
├── samples/labels.json # 已标注截图的真值(10 个 hero_key,未知用 ?)
├── gsi_setup.py # 定位 Dota 目录,安装 / 卸载 GSI 配置
├── gsi_watch.py # 监听 GSI 状态 → 跟踪选将 → 识别 → 输出 JSON
├── templates/roles/ # 5 个位置文字的二值模板
├── samples/raw/ # 手动截图;GSI 会话写入 raw/<matchid>/
├── results/ # gsi_watch.py 每局的识别结果 JSON
├── templates/cdn/ # 127 张兜底模板(已下载)
├── templates/real/ # 默认脸实拍模板(不含皮肤)
├── preview/ # build_library.py 的裁切预览
└── failures/ # 识别失败的格子,待标注入库
config.json 参数说明
| 字段 | 含义 | 默认 |
|---|---|---|
slots[] |
10 个格子的相对中心坐标 cx_rel / cy_rel |
待标定 |
slot_w_rel / slot_h_rel |
格子宽高 / 屏幕高 | 待标定 |
crop_trim |
裁掉的边饰比例(上 10% 颜色条、下 22% 名牌、左右各 8%) | 见文件 |
canonical_size |
统一缩放后的边长(px) | 96 |
match.min_score |
Top-1 最低分 | 0.45 |
match.min_margin |
Top1−Top2 最小分差 | 0.04 |
match.cdn_penalty |
CDN 模板得分惩罚,优先采信 real 模板 | 0.05 |
gsi.port |
GSI 监听端口 | 3223 |
gsi.trigger_states |
启动跟踪会话的游戏状态 | [HERO_SELECTION, STRATEGY_TIME] |
gsi.poll_interval |
选将期间的轮询间隔(秒) | 1.0 |
gsi.confirm_polls |
同一格连续几帧认出同一英雄才算确认 | 2 |
gsi.session_timeout |
单局跟踪的最长时间(秒) | 300 |
text_rows.name / text_rows.role |
头像下方姓名行 / 位置行的相对纵坐标 | 见文件 |
roles.min_iou |
位置文字模板匹配的最低 IoU | 0.55 |
roles.self_min_gap |
「我」那格姓名亮度需高出次亮格多少 | 25.0 |
阈值需在积累一定样本后按实测重新调优,当前为经验初值。
自动标定(autocalibrate.py)
2560×1440 实测:颜色条位于 y=17,头像区 y=896(高 88),格宽 111,
槽位中心间距 165px,10 格全部命中。
三个关键判据:
- 颜色条定位——10 个玩家颜色是游戏固定值,逐列取最近颜色(容差 60) 找连续段,既给出横向位置又天然给出槽位编号
- 中心线性拟合——同队 5 格等距,对 index→center 做最小二乘, 可修复被身后头像污染的个别颜色条(实测槽位 9/10 偏差 3~6px 被纠正)
- 头像下沿——用「格内列 vs 格间空隙」的亮度差:有头像时差值 20~67, 头像结束瞬间塌到 0。不能用逐行差分,因为下方玩家名字的跳变更大, 会误判到名字行(初版就踩了这个坑,把底边定到 129 而非 96)
实测结论(2026-07-25,四局人机,2560×1440 无边框)
| 阶段 | CDN-only | 完整库 |
|---|---|---|
| 初版(正方形裁切 + 线性拟合) | 32/40 | — |
| 修正裁切 + 鲁棒拟合后 | 40/40(最低分 0.784) | 40/40(最低分 1.000) |
其中 draft_145136.png 是修复完成前就抓好的帧,未参与任何调参,
旧配置下 8/10、新配置下 10/10,属于干净的盲测样本。
单次识别 340~460ms,远低于 1 秒目标。用 python evaluate.py 复现。
发现一:顶栏头像是静态图标,可像素级匹配
real 模板命中时分数恒为 1.000。顶栏图标每局渲染逐像素相同, 所以一个英雄只要入库一次,之后永远满分命中——不需要"积累多个变体求鲁棒", 一张就够(同皮肤前提下)。
发现二:CDN 模板的裁切方式原本就是错的
初版按 1:1 正方形取中心裁切,但顶栏格子是 111×88(约 1.26:1),比例根本对不上, 平均匹配分只有 0.649。以 15 张实拍模板为标准答案做网格搜索, 反推出正确窗口是 x0=38, w=182, 全高(源图 256×144),平均分升到 0.936。
改用相对比例 CROP_X0/CROP_X1 写死在 fetch_cdn_templates.py 里,重新生成 127 张后,
仅靠 CDN 模板即可 30/30 全对。这意味着不必靠打几十局去攒模板库——
全英雄覆盖开箱即用。
教训:当兜底层表现明显低于预期时,先怀疑素材处理方式, 而不是急着靠堆数据去补。这里省下了约 70 局的采集成本 (127 英雄按优惠券收集问题估算)。
发现三:坏点会拖垮最小二乘拟合
夜魇方槽位 9/10 的颜色条被身后头像污染,检测宽度 141/133(正常 105~119), 中心偏了几像素。最小二乘对离群点没有抵抗力,算出的槽距是 166.2 而非真实的 165, 导致槽位 10 裁切偏 4px、同一英雄的匹配分从 1.000 掉到 0.567。
改为先按条宽剔除不可靠的条,再取中位数槽距、中位数截距, 槽位中心与实测真值完全吻合。
阈值的已知不足:瘟疫法师曾出现分差 0.294(很笃定)但分数 0.412 未过 0.45 的情况。 现行门控要求分数与分差同时达标,对"分差极高但分数中等"偏严。 修正裁切后此问题不再出现(最低分 0.784),暂不调整。
发现四:天梯段位徽章是稳定遮挡,可用遮罩匹配规避
天梯全英雄选择时,每个顶栏头像底部有「传奇 III」半透明条、右侧有金色段位勋章。
GSI 的 map 不含 game_mode / lobby_type,但画面上金色勋章可稳定检测
(has_ranked_overlay:≥60% 的格子右下角有金色像素 → 判定为天梯 UI)。
处理方式:检测到天梯 UI 后,匹配时屏蔽底部 32% + 右侧 22%(ranked_match_mask),
只对剩余面部区域做归一化相关。人机 / 普通匹配无勋章,走原全图匹配,互不干扰。
实测:
| 集合 | 修复前 | 遮罩 + real 入库后 |
|---|---|---|
| 5 局人机 | 50/50 | 50/50(未误触发遮罩) |
| 1 局天梯 | 9/10(LC 被段位条打崩) | 10/10(分数 1.000) |
注意:遮罩解决的是遮挡。皮肤/至宝改头像时不要往 real 里堆变体——选人阶段 用默认脸多帧确认,决策阶段禁止改判即可;CDN 对默认脸仍偏弱的英雄才补一张 real。
发现五:位置文字与「我」那一格都能纯视觉读出
定位匹配(Ranked Roles)里,只有我方 5 格的头像下方会画出位置文字:
优势路 / 中路 / 劣势路 / 辅助 / 纯辅助。2560×1440 下位于 y=143~159。
这行文字是纯灰(饱和度≈0),用 value>110 且 saturation<0.08 就能干净抠出来。
不做 OCR,改成二值掩膜 IoU 匹配:候选只有 5 个,且宽度各不相同 (图标+2字 ~ 图标+3字),同帧自匹配 IoU=1.0,跨类差距极大。掩膜先紧裁再 归一化到固定高度 24px,因此换分辨率不用重建模板。
哪一格是「我」,最终由 GSI 直接回答:player 块里带 team_slot(队内 0~4),
顶栏就是按队内序号排的,所以 slot = team_slot + 1(天辉)或 + 6(夜魇)。
实测 team_slot=4 / radiant / drow_ranger 对应顶栏第 5 格,与画面一致。
视觉判据仍保留为兜底(GSI 在载入对局前不发 player 块)——
自己的名字是亮白色,其余九人是偏蓝的灰:
| 局 | 我那格亮度 | 其余最高 | 差值 |
|---|---|---|---|
| 天梯定位局 | 229.9 | 170.2 | 59.7 |
| 人机局 ×3 | 229.8~230.2 | 175.1 | ~55 |
用绝对阈值会在「整屏都亮」的界面上误判(实测游戏内 HUD 帧十格都是 248), 所以改成相对判据:最亮格需比次亮格高出 25 以上。8 张选将截图全部命中, 9 张非选将截图全部正确返回 None。
官方选将规则(决定采集节奏)
天梯全英雄选择的规则来自 Dota 2 Wiki / Liquipedia:
- 禁用:全员 15 秒投票,每人 1 个不可重复;每票各有 50% 概率生效; 系统再按该 MMR 段位的 ban 率补随机禁用,最终固定 16 个
- 选人分 3 轮:25 秒 / 每队 2 人 → 25 秒 / 每队 2 人 → 20 秒 / 每队 1 人
- 本轮结束前双方互相不可见;同轮撞英雄则该英雄被禁、本轮重来(最多 2 次)
关键推论:顶栏英雄是按 2/2/1 成批揭晓的。原来只在决策时间截一次, 拿到的是最终阵容,选人顺序信息全丢——而顺序恰好是后续给建议最需要的。
GSI 自动化链路
GSI 拿不到双方 pick,但能可靠告诉我们现在处于哪个阶段,正好用作触发器:
Dota 2(-gamestateintegration)
↓ POST JSON,每 0.5s
gsi_watch.py 本地 HTTP 服务(127.0.0.1:3223)
↓ map.game_state 进入 HERO_SELECTION(错过则 STRATEGY_TIME 兜底)
DraftSession:每秒轮询 → recognize_image() + detect_roles()
↓ 某格连续 2 帧认出同一英雄才算确认,避免头像淡入时的抖动
确认集合变化 → 追加一条时间线事件(轮次由每队已选人数推出)
↓ 状态离开选将 / 10 格认满
终端摘要 + results/draft_<时间戳>.json
设计要点:
- 按
matchid去重,一局一个跟踪会话,不论从哪个阶段接入 - 轮询与识别在工作线程,HTTP 回调立即返回,不阻塞游戏侧推送
- 确认需连续 2 帧,单帧可能拍到半透明的入场动画
- 位置与「我」只解析一次,这两项全局不变,认出后不再重复计算
- 「我」优先用 GSI 的
team_slot,视觉判据仅在 GSI 尚未提供时兜底 - 未标定时降级为「只截图」,仍在正确时机存帧,供
calibrate.py使用
4. 环境与依赖
- Python 3.14(本机已装)
opencv-python >= 4.10(实装 5.0.0)、numpy >= 2.0、requests >= 2.32、mss(屏幕捕获)
已完成的初始化:
pip install -r requirements.txt
pip install mss
python fetch_cdn_templates.py # 127/127 模板下载成功
已验证 capture.py 可正常截取主显示器,输出为原生 2560×1440 PNG。
5. 推进计划
| 阶段 | 内容 | 状态 |
|---|---|---|
| 0 | 项目骨架、依赖、CDN 模板库 | 已完成 |
| 1 | 采集真实决策时间截图 | 已完成 |
| 2 | ROI 标定,生成 config.json 坐标 |
已完成(自动标定) |
| 3 | 首轮识别(纯 CDN 兜底),摸底准确率 | 已完成(8/10) |
| 4 | 积累 real 模板,迭代阈值,统计准确率 | 已完成(30/30,CDN-only 亦 30/30) |
| 5 | 锚点自动定位,去除标定依赖 | 已完成(autocalibrate.py) |
| 6 | 接入 GSI 自动触发,全流程免操作 | 已完成(真机人机局已验证) |
验收标准(判定方案是否可行)
| 指标 | 目标 |
|---|---|
| real 模板命中格子的准确率 | ≥ 95% |
| 仅 CDN 兜底格子的准确率 | ≥ 70% |
| 单张图识别耗时 | < 1 秒 |
| 错误类型 | 以「输出 null」为主,而非「输出错误英雄」 |
6. 通用性设计(面向所有玩家)
核心原则:成品阶段玩家零操作。 当前 demo 的手动步骤均为验证期临时措施。
| Demo 期手动操作 | 成品自动方案 |
|---|---|
| 手动截图 | GSI 检测 DOTA_GAMERULES_STATE_STRATEGY_TIME 自动触发(gsi_watch.py,已实现) |
| 手动框选 10 个格子标定 | 锚点自动定位(见下) |
| 人工标注建模板库 | 模板库随程序内置,开箱即用 |
--truth 对答案 |
仅开发期使用 |
| 手动设置无边框窗口 | 首次运行自动检测,或改用 Windows Graphics Capture |
分辨率与宽高比适配(三层递进)
- 相对坐标(已实现)——覆盖同宽高比下的任意分辨率
- 锚点自动定位(阶段 5)——运行时在截图中自动找顶栏:
- 中央倒计时区域作水平锚点
- 10 条固定玩家颜色条(蓝、青、紫、黄、橙 / 粉、灰绿、浅蓝、墨绿、棕) 既是定位标记,也天然给出槽位编号
- 由锚点反推格子位置,任何分辨率、宽高比免配置
- 多尺度匹配——0.9~1.1 倍尺度搜索,吸收残余缩放误差
已知风险
| 风险 | 应对 |
|---|---|
| 21:9 等特殊宽高比布局差异 | 阶段 5 锚点定位;短期可分档标定 |
| 英雄皮肤(至宝 / 身心)改变头像 | 不入库皮肤模板;决策阶段只补空槽、禁止改判;best 帧优先选人阶段默认脸 |
| 头像为平行四边形,矩形 ROI 会带入邻格边缘 | 当前靠内缩裁切规避;必要时加仿射纠正 |
| 独占全屏截图可能为黑帧 | 建议无边框窗口;或改用 Windows Graphics Capture |
| Dota 更新改动 HUD 布局 | 锚点方案对布局微调更鲁棒;必要时重标定 |
禁用英雄识别(grid.py)
天梯 AP 固定禁用 16 个,这个信息不在顶栏,只在英雄选择网格里。最后的做法 完全不用模板匹配,因为网格的排布是可推算的。
一开始确实试了匹配。网格用的是竖版英雄卡,Steam CDN 上唯一对得上的素材是
遗留路径 images/heroes/{key}_vert.jpg(235×272),拟合出的裁切窗口
x[0.15,0.80] y[0.00,0.85] 宽高比 0.66 与实拍卡片的 52/79 完全吻合。但均分
只有 0.50,且下半段的 top1/top2 几乎没有间距——这批图是 2015 年前后的旧原画,
大量英雄重做过,根本对不上。
真正的突破口是排布本身。127 个英雄按主属性分成四块从左到右排列,块内按
客户端本地化名称排序、行优先填充,多出来的空格永远在块尾。用实战帧
交叉验证了三点:四块各 36 / 35 / 34 / 22 格,与英雄表的属性数量分毫不差;
5 个空格全在最后一行的块尾;聊天栏里点名的 9 个禁用英雄,按此推算出的格子
全部画着禁用斜杠。所以英雄表(heroes.json 现在带 attr 和 name_loc,
都取自 Valve 自己的 datafeed/herolist)就足以定位每一格。
禁用态的判定绕了点弯路。斜杠本身不好测——试过方向梯度直方图和错切后找亮脊, 两者都失败,后者甚至完全反向(禁用格反而排在最后)。原因是禁用卡整张被压暗 去饱和,低对比度才是主特征。实测那一局:17 张不可选卡片的灰度 std 落在 8–21,其余 110 张全部 ≥32.6,中间空出 11.6 的间隔,直接卡阈值即可。 17 = 16 个禁用 + 1 个已被选走的莉娜,与官方规则严丝合缝。
read_grid() 在格数对不上英雄表时返回 ok=False 而不是给一份残缺名单——
鼠标悬停会弹出大号英雄卡遮住网格,这类帧就是这样被挡掉的。禁用名单全程不变,
DraftSession 只取第一帧读成功的结果,之后不再重复读。
7. 与 dota2-hex 的关系
本项目为独立验证 demo,不修改 dota2-hex。
- 选用 Python 是因为验证期迭代快,非最终技术栈
- 准确率验证通过后,可选:用 Rust 重写并入
dota2-hex,或保留为本地旁路服务通过 HTTP 回传 - 无论哪种,都需遵守
dota2-hex的合规边界:仅使用玩家屏幕上可见的信息, 不读进程内存、不注入(见AGENTS.md「技术约束」)
dota2-hex 现有的阶段判定(src/phase.rs)可直接复用为截图触发信号。
8. 参考资料
- Valve #9562 GSI get draft data during draft — 官方关闭 AP 实时 draft
- Valve #14915 How can I get live pick data — 开发者自述 OCR 方案约 85% 准确率
- Overwolf Dota 2 GEP —
roster.draft/bans字段定义 - OpenCV Template Matching
- Steam CDN 头像:
https://cdn.cloudflare.steamstatic.com/apps/dota2/images/dota_react/heroes/{key}.png