Drop real template library; keep CDN-only matching.

Remove build_library and runtime artifacts, ignore regenerable outputs, and add README/AGENTS/DESIGN/CHANGELOG for the simplified project.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
voson
2026-07-26 11:59:48 +08:00
co-authored by Cursor
parent f32d24b8f8
commit a089660ca9
69 changed files with 299 additions and 5050 deletions
+44 -188
View File
@@ -1,228 +1,84 @@
# 上分帝(Climperor
Dota 2 天梯选将识别:从「决策时间截图中识别双方 10 个英雄(模板匹配,本地、离线、秒级)
Dota 2 **天梯选将识别**:从选人 / 决策时间截图中识别双方 10 个英雄。
用于验证可行性,成熟后可移植到 [dota2-hex](../dota2-hex) 或独立发布
本地、离线、秒级;模板来自 Steam CDN 官方头像。GSI 只负责触发时机,阵容靠视觉读顶栏
方案背景、技术选型与推进计划见 [DESIGN.md](DESIGN.md);本文是操作手册
设计背景见 [DESIGN.md](DESIGN.md);协作约定见 [AGENTS.md](AGENTS.md);变更记录见 [CHANGELOG.md](CHANGELOG.md)
## 原理
```
决策时间截图(PNG
→ 按相对坐标裁出 10 个头像格(分辨率无关)
每格纠裁、去 UI 边饰、缩放到统一尺寸
→ 与模板做归一化相关匹配(TM_CCOEFF_NORMED
→ Top-1 分数 + Top1-Top2 分差 双阈值门控 → hero_key 或 null
→ 缩放到统一尺寸;天梯局自动屏蔽段位条/勋章遮挡区
→ 与 CDN 模板做归一化相关匹配(TM_CCOEFF_NORMED
→ Top-1 分数 + Top1Top2 分差双阈值 → hero_key 或 null
```
模板库分两层:
- `templates/real/{hero}/*.png` —— 真实截图裁出的**默认脸**格子(高精度;不收皮肤变体)
- `templates/cdn/{hero}.png` —— Steam CDN 官方头像(全覆盖兜底,匹配时降权)
皮肤差异靠会话策略处理(选人阶段多帧确认,决策阶段禁止改判),不堆皮肤模板。
## 安装
```powershell
pip install -r requirements.txt
python fetch_cdn_templates.py # 一次性:heroes.json + templates/cdn/
```
## 使用流程
### 1. 下载 CDN 兜底模板(一次性)
```powershell
python fetch_cdn_templates.py
```
生成 `heroes.json`(英雄 id/key 对照)和 `templates/cdn/`(全英雄头像)。
### 2. 采集截图
程序可自行截屏,无需手动按 PrintScreen
```powershell
python capture.py # 单张,存入 samples/raw/
python capture.py --loop 300 3 # 每 3 秒一张,持续 300 秒
```
GSI 自动跟踪时,截图按对局写入 `samples/raw/<matchid>/``draft_*.png``draft_best_*.png` 等)。
手动 `capture.py` 仍落在 `samples/raw/` 根目录。
Dota 2 需运行在**无边框窗口**或窗口模式(独占全屏可能截出黑帧)。
也可以交给 GSI 自动触发,见下方「自动运行」。
### 3. 标定 ROI(一次性)
拿一张**原始分辨率、未裁剪**的决策时间全屏截图,自动标定:
```powershell
python autocalibrate.py samples/raw/draft_141704.png
```
靠顶栏那 10 条固定玩家颜色条定位,无需手动框选,任何分辨率都适用。
输出 `preview/autocalibrate_check.png`(整条顶栏带框)和
`preview/autocalibrate_slots.png`(10 格裁切拼图)供核对,坐标以相对值存入 `config.json`
只核对不写配置:加 `--check`
手动标定仍可用(自动失败时的退路):
```powershell
python calibrate.py samples/full.png
```
在弹出窗口中依次框选 10 个英雄头像格(每框完一个按空格,全部完成按 ESC)。
### 4. 建真实模板库(可多次,逐步积累)
先预览裁切是否正确:
```powershell
python build_library.py samples/shot1.png
# 查看 preview/slot_1.png ... slot_10.png
```
确认无误后带标注入库(10 个 hero_key 从左到右,跳过用 `?`):
```powershell
python build_library.py samples/shot1.png tinker,earthshaker,juggernaut,dazzle,vengefulspirit,axe,sniper,slark,lion,drow_ranger
```
hero_key 见 `heroes.json`(即 Steam 内部名去掉 `npc_dota_hero_` 前缀)。
### 5. 识别 + 验证准确率
```powershell
python recognize.py samples/shot2.png
python recognize.py samples/shot2.png --sheet # 输出带预测标签的对照图
python recognize.py samples/shot2.png --truth tinker,earthshaker,... # 对答案
```
`--sheet` 生成 `preview/recognize_sheet.png`:10 格裁切并排,每格标注预测英雄与
分数/分差,绿色表示过阈值、橙色表示存疑(前缀 `?`)。核对时比读 JSON 快得多。
批量评测所有已标注截图(真值写在 `samples/labels.json`):
```powershell
python evaluate.py # 完整模板库
python evaluate.py --cdn-only # 只用 CDN 层,衡量开箱即用的表现
```
`--truth` 时输出每格对错与总准确率;认错的格子自动存入 `failures/`
(文件名含正确 key)。仅当裁切是**选人阶段默认脸**时再挪进 `templates/real/{key}/`
皮肤/至宝头像不要入库(靠会话多帧 + 决策阶段禁改判处理)。
## 自动运行(GSI 触发)
装好后全程零操作:进入决策时间自动截图、识别、输出 JSON。
### 一次性配置
```powershell
python gsi_setup.py # 自动找到 Dota 2 并写入 GSI 配置
```
然后在 **Steam 库 → Dota 2 → 属性 → 启动项**中加上 `-gamestateintegration`,重启游戏。
其他用法:`--check` 只查看状态,`--remove` 卸载配置,
`--path "D:\Steam\steamapps\common\dota 2 beta"` 手动指定目录。
### 开着它打游戏
## 快速开始(GSI 自动跟踪)
```powershell
python gsi_setup.py # 写入 Dota GSI 配置
# Steam → Dota 2 → 属性 → 启动项加上 -gamestateintegration,重启游戏
python gsi_watch.py
```
监听 `127.0.0.1:3223`,游戏一进入英雄选择就开始跟踪整局选将,每秒轮询一次,
每有新英雄揭晓就打印一行,结束后把完整时间线存入 `results/draft_<时间戳>.json`
每局只跟踪一次;中途启动程序会从决策时间兜底接入。
进入英雄选择后自动跟踪;凑齐 10 人或超时后写出 `results/draft_<时间戳>.json`(该目录已 gitignore)。
天梯全英雄选择是**分 3 轮成批揭晓**的(每队 2 / 2 / 1,本轮结束前互相不可见),
所以输出长这样:
> 主菜单收不到 GSI 是正常的:客户端**第一次载入对局后**才开始推送。看到 `[gsi] connected` 才算链路通。
```
[draft] grid: 16 heroes unavailable (contrast margin 11.0) - 术士, 殁境神蚀者, ...
[draft] + 4.2s round1 radiant slot2 冥魂大帝 [优势路]
[draft] + 4.2s round1 dire slot7 幻影刺客
[draft] + 31.5s round2 radiant slot3 狙击手 [中路]
[draft] ~ 33.0s slot2 幽鬼 -> 主宰 (score 0.52 -> 0.86)
...
you : slot 5 radiant 沉默术士 - position 5 (纯辅助)
lanes : 1:冥魂大帝, 2:狙击手, 3:军团指挥官, 4:祈求者, 5:沉默术士
bans : 16 - 术士, 殁境神蚀者, 拉比克, 冥界亚龙, 沉默术士, ...
## 手动流程
### 截图
```powershell
python capture.py # 单张 → samples/raw/
python capture.py --loop 300 3 # 每 3 秒一张,持续 300 秒
```
`~` 开头的是**改判**。刚揭晓的那一帧最不适合下判断——立绘还在淡入、天梯段位条又盖住下半张脸
已确认的槽位仍可被更高分的稳定读数覆盖(`revise_gain`,默认 0.15)。
需无边框窗口或窗口模式(独占全屏可能黑帧)
顶栏在**所有人选完之前**用默认头像,皮肤要等全员锁定后才上。因此视觉会读完选人阶段,
并在决策阶段继续读一段时间(`strategy_tail_polls`),直到凑齐 10 人或超时——避免你已进
决策界面、别人还没选完时漏掉最后一人。凑齐后才停视觉,再等 GSI 公布你自己的英雄。
### 标定 ROI(一次性)
选人界面顶部计时器下方的模式字(如「全英雄选择」)也会识别,写入结果的 `mode` 字段。
```powershell
python autocalibrate.py samples/raw/<某帧>.png
# 失败时退路:python calibrate.py samples/raw/<某帧>.png
```
定位匹配局里,我方 5 格头像下方的位置文字会被一并识别。「我」是哪一格直接取自
GSI 的 `player.team_slot`;GSI 还没开始推送时,靠姓名颜色兜底(自己的名字是亮白,
其余九人偏蓝)。非定位局没有位置文字,就只报槽位不报位置。
### 识别 / 评测
天梯 AP 固定禁用的 16 个英雄从英雄选择网格里读出,不依赖任何图像匹配:网格按
主属性分四块、块内按客户端英雄名排序行优先填充,位置可直接推算;被禁或已被选走的
卡片整张压暗,灰度对比度会塌到 21 以下(正常卡 33 以上),据此判定。
```powershell
python recognize.py samples/raw/<>.png
python recognize.py samples/raw/<>.png --sheet
python recognize.py samples/raw/<>.png --truth hero1,...,hero10
python evaluate.py # 按 samples/labels.json 批量评测
```
不想开游戏调试时:`python gsi_watch.py --once`(立即截一次并识别)。
想改触发时机:`python gsi_watch.py --states HERO_SELECTION,STRATEGY_TIME`
只看某张截图的位置识别:`python roles.py samples/raw/<matchid>/<帧>.png`
只看某张截图的禁用识别:`python grid.py samples/raw/<matchid>/<帧>.png`
> **主菜单里收不到数据是正常的。** Dota 的 GSI 与 CS 不同,客户端**第一次载入对局后**
> 才开始发 HTTP 请求,挂在主菜单时不会有任何推送。看到 `[gsi] connected` 才算链路通。
> 一直没有的话,先确认启动项里有 `-gamestateintegration`。
人机对战(Play with bots)同样会推送,HUD 顶栏与天梯一致,适合反复测试。
**尚未标定时**会自动进入「只截图」模式——按 `matchid` 存到 `samples/raw/<matchid>/`
拿其中一张跑 `calibrate.py` 即可完成标定。这是当前推荐的第一步。
相关参数在 `config.json``gsi` 段:
| 字段 | 含义 | 默认 |
|------|------|------|
| `port` | 监听端口 | 3223 |
| `trigger_states` | 启动跟踪的游戏状态 | `HERO_SELECTION` + `STRATEGY_TIME` |
| `poll_interval` | 选将期间的轮询间隔(秒) | 1.0 |
| `confirm_polls` | 连续几帧认出同一英雄才算数 | 2 |
| `session_timeout` | 单局跟踪上限(秒) | 300 |
| `keep_event_frames` | 是否为每次揭晓存一张原图 | true |
| `dump_selection_every` | 选将网格开着时每几秒存一帧(0 关闭) | 4 |
| `target_slots` | 认满几格就提前停 | 10 |
辅助脚本:`roles.py`(位置字)、`grid.py`(禁用名单)、`modes.py`(模式字)。
## 截图要求
- **PNG 格式、原始分辨率**(不要经聊天工具/微信转发,会被压缩)
- 画面为对局内「决策时间」阶段,顶栏 10 个英雄完整可见
- 无边框窗口或窗口模式截图均可;直接把文件放入 `samples/`
- PNG、原始分辨率(勿经聊天工具压缩)
- 顶栏 10 个英雄完整可见
- 优先选人阶段默认脸帧(皮肤尚未上顶栏)
## 判定标准(demo 验收)
## 已知限制
| 指标 | 目标 | 实测(6 局 / 60 格,含 1 局天梯) |
|------|------|-----------------------------------|
| real 模板命中的格子准确率 | ≥ 95% | 100%(分数恒为 1.000 |
| 仅 CDN 兜底的格子准确率 | ≥ 70% | 人机 100%;天梯 9/10(皮肤差异) |
| 单张图识别耗时 | < 1 秒 | 270~520ms |
- 宽高比差异大(如 21:9)时可能需重标定
- 客户端改网格排序后 `grid.py``ok=False`,不会瞎报禁用名单
- 新英雄上线后重跑 `fetch_cdn_templates.py`
- 位置文字模板取自简体中文客户端
天梯顶栏会叠段位条和勋章:程序检测到后自动屏蔽底部/右侧遮挡区再匹配,
人机局不受影响。带皮肤的英雄仍需一张 real 模板。
## 相关项目
## 已知限制 / 后续方向
- 斜切头像目前按矩形内缩裁切,未做仿射纠正(够用则不加)
- 分辨率靠相对坐标适配;宽高比差异大(21:9)时需重标定或做锚点自动定位(玩家颜色条 + 中央倒计时)
- 英雄皮肤(至宝/身心)可能改变头像,需为常见皮肤补充 real 模板;
计划加入「识别有误时手动校正」的入口,校正结果直接沉淀为 real 模板
- 禁用识别依赖网格默认排序(子类=属性)。若在客户端里改了排序或筛选方式,
格数会对不上英雄表,此时 `grid.py` 会直接报 `ok=False` 而不是给错名单
- 新英雄上线后需重跑 `python fetch_cdn_templates.py` 刷新 `heroes.json`
- 位置文字模板取自简体中文客户端,换语言需重新执行
`python roles.py <帧>.png --build off,safe,mid,soft_support,hard_support`
成熟后可移植到 [dota2-hex](../dota2-hex) 或独立发布。合规边界:只使用屏幕可见信息,不读内存、不注入。