从 dota2-draft-vision 迁出并定名,作为天梯选将识别项目起点。 Co-authored-by: Cursor <cursoragent@cursor.com>
429 lines
22 KiB
Markdown
429 lines
22 KiB
Markdown
# 上分帝(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](https://github.com/ValveSoftware/Dota2-Gameplay/issues/9562)、
|
||
[#7193](https://github.com/ValveSoftware/Dota2-Gameplay/issues/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](https://github.com/YaShock/dota-hero-picker)、
|
||
[dota2-picker](https://github.com/mohsenheydari/dota2-picker)、
|
||
[ability-draft-plus](https://github.com/Tiarin-Hino/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=1~7,头像区 y=8~96(高 88),格宽 111,
|
||
槽位中心间距 165px,10 格全部命中。
|
||
|
||
三个关键判据:
|
||
|
||
1. **颜色条定位**——10 个玩家颜色是游戏固定值,逐列取最近颜色(容差 60)
|
||
找连续段,既给出横向位置又天然给出槽位编号
|
||
2. **中心线性拟合**——同队 5 格等距,对 index→center 做最小二乘,
|
||
可修复被身后头像污染的个别颜色条(实测槽位 9/10 偏差 3~6px 被纠正)
|
||
3. **头像下沿**——用「格内列 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`(屏幕捕获)
|
||
|
||
已完成的初始化:
|
||
|
||
```powershell
|
||
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 |
|
||
|
||
### 分辨率与宽高比适配(三层递进)
|
||
|
||
1. **相对坐标**(已实现)——覆盖同宽高比下的任意分辨率
|
||
2. **锚点自动定位**(阶段 5)——运行时在截图中自动找顶栏:
|
||
- 中央倒计时区域作水平锚点
|
||
- 10 条固定玩家颜色条(蓝、青、紫、黄、橙 / 粉、灰绿、浅蓝、墨绿、棕)
|
||
既是定位标记,也天然给出槽位编号
|
||
- 由锚点反推格子位置,任何分辨率、宽高比免配置
|
||
3. **多尺度匹配**——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](https://github.com/ValveSoftware/Dota2-Gameplay/issues/9562) — 官方关闭 AP 实时 draft
|
||
- [Valve #14915 How can I get live pick data](https://github.com/ValveSoftware/Dota2-Gameplay/issues/14915) — 开发者自述 OCR 方案约 85% 准确率
|
||
- [Overwolf Dota 2 GEP](https://dev.overwolf.com/ow-native/live-game-data-gep/supported-games/dota-2/) — `roster.draft` / `bans` 字段定义
|
||
- [OpenCV Template Matching](https://pyimagesearch.com/2021/03/22/opencv-template-matching-cv2-matchtemplate/)
|
||
- Steam CDN 头像:`https://cdn.cloudflare.steamstatic.com/apps/dota2/images/dota_react/heroes/{key}.png`
|