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
+6
View File
@@ -7,3 +7,9 @@ venv/
.DS_Store
Thumbs.db
*.log
# Runtime / debug artifacts (regenerable)
preview/
results/
failures/
samples/raw/
+88
View File
@@ -0,0 +1,88 @@
# 协作说明(面向 AI 与贡献者)
本文件描述 **上分帝(Climperor** 的职责边界、代码布局与修改注意点。
## 项目是什么
Windows 上的 Dota 2 **天梯选将识别**工具:用 GSI 感知选将阶段,截屏后对顶栏 10 格做 OpenCV 模板匹配,输出双方阵容与选将时间线。
- 中文名:上分帝
- 英文名 / 仓库目录:Climperor / `climperor`
- 模板来源:**仅** Steam CDN 官方头像(`templates/cdn/`
- **已放弃** real 实拍模板库补充;不要再引入 `templates/real/` 或入库流水线
## 目录与模块
| 路径 | 职责 |
|------|------|
| `common.py` | 配置 IO、槽位几何、裁切、NCC 匹配、天梯遮罩、CDN 模板加载 |
| `recognize.py` | 单帧识别;`recognize_image()` 供会话复用 |
| `draft_session.py` | 整局选将跟踪、改判、皮肤规避策略 |
| `gsi_watch.py` / `gsi_setup.py` | GSI 监听与 cfg 安装 |
| `fetch_cdn_templates.py` | 拉取 CDN 头像 + 生成 `heroes.json` |
| `autocalibrate.py` / `calibrate.py` | ROI 自动 / 手动标定 |
| `capture.py` | 屏幕捕获 |
| `evaluate.py` | 按 `samples/labels.json` 批量评测 |
| `roles.py` / `grid.py` / `modes.py` | 位置字、禁用网格、模式字 |
| `config.json` | 相对坐标、阈值、GSI 参数 |
| `templates/cdn/` | 全英雄 CDN 模板(需提交或由脚本生成) |
| `templates/roles/` / `templates/modes/` | 位置 / 模式辅助模板 |
运行时产物(**勿提交**,见 `.gitignore`):
- `samples/raw/` — 截图
- `preview/` — 标定 / sheet 预览
- `results/` — 每局 JSON
- `failures/``--truth` 调试用错识裁切
## 数据流
```
Dota 2 GSI → gsi_watch.py (:3223)
→ DraftSession 轮询截屏
→ recognize_image (CDN 模板 + 可选天梯遮罩)
→ roles / grid / modes(辅助)
→ results/draft_*.json + 终端时间线
```
## 技术约束(修改前必读)
- **合规**:仅 GSI + 屏幕截图;**禁止**读进程内存、注入、绕过 VAC。
- **GSI 范围**:普通玩家视角拿不到双方 pick;GSI 只作阶段触发与 `team_slot` 等自身信息。
- **模板策略**:只维护 CDN 层。皮肤问题用会话策略(选人可改判、决策 `allow_revise=False`、best 帧偏 HERO_SELECTION),不要为皮肤加模板库。
- **坐标**:一律相对坐标(相对屏幕高 / 相对中心),勿写死像素分辨率。
- **宁可不认,不可乱认**`min_score` + `min_margin` 双门控;不确定就 `null`
- **平台**:面向 Windows;截屏依赖无边框/窗口模式。
## 开发命令
```powershell
pip install -r requirements.txt
python fetch_cdn_templates.py
python gsi_setup.py --check
python gsi_watch.py --once
python recognize.py samples/raw/<>.png --sheet
python evaluate.py
```
## 修改原则
- 只改任务所需逻辑,避免无关重构与大范围格式化。
- 代码标识符、日志、错误信息、注释用**英文**;用户可见摘要可用中文。
- 改匹配阈值或裁切时,用 `evaluate.py` / 标注帧验证,并更新 `CHANGELOG.md` 与必要时的 `DESIGN.md`
- 改 GSI cfg 时同步核对 `gsi_setup.py` 与 Dota `gamestate_integration` 目录。
-`heroes.json` 结构时同步 `grid.py`(依赖 `attr` / `name_loc`)。
- 不要重新引入 real 模板双层库、`cdn_penalty``build_library.py`
## 文档分工
| 文件 | 内容 |
|------|------|
| `README.md` | 安装与操作手册 |
| `DESIGN.md` | 背景、选型、关键实测与决策记录 |
| `AGENTS.md` | 本文件:给协作者 / AI 的约束 |
| `CHANGELOG.md` | 面向用户的版本变更 |
## 与 dota2-hex
本仓库独立演进。并入或旁路对接时,仍须遵守 [dota2-hex/AGENTS.md](../dota2-hex/AGENTS.md) 的合规边界。
+35
View File
@@ -0,0 +1,35 @@
# Changelog
本文件记录 **上分帝(Climperor** 的用户可见变更。格式参考 [Keep a Changelog](https://keepachangelog.com/)。
## [Unreleased]
### Changed
- 项目定名:**上分帝** / **Climperor**(目录 `climperor`),由 `dota2-draft-vision` 迁出独立仓库。
- **放弃 real 实拍模板库**:删除 `build_library.py``templates/real/`;匹配仅使用 CDN 层。
- 移除 `match.cdn_penalty``evaluate.py --cdn-only`(双层库对比已无意义)。
- 模板加载 API 简化为 `(hero_key, image)`
- 运行产物纳入 `.gitignore``preview/``results/``failures/``samples/raw/`
- 文档拆分为 `README`(操作)、`DESIGN`(决策)、`AGENTS`(协作)、`CHANGELOG`(变更)。
### Removed
- real 库补充流水线与相关验收指标。
- 仓库内历史 `preview/``results/` 调试产物。
### Fixed
- (继承自原型)CDN 头像按顶栏窗口裁切,而非中心正方形;CDN-only 即可高准确率。
- (继承自原型)天梯段位条 / 勋章遮罩匹配;选人多帧 + 决策禁改判规避皮肤。
## [0.1.0] - 2026-07-25
### Added
- 决策时间顶栏 10 格模板匹配识别。
- 相对坐标 ROI、自动标定(玩家颜色条)。
- Steam CDN 全英雄模板拉取(`fetch_cdn_templates.py`)。
- GSI 触发的选将会话跟踪(`gsi_watch.py` / `draft_session.py`)。
- 位置文字、禁用网格、模式字辅助识别。
- 天梯 UI 遮罩与会话层皮肤策略。
+102 -359
View File
@@ -1,7 +1,6 @@
# 上分帝(Climperor)—— 方案与实施细节
本文记录 **上分帝(Climperor** 的背景、技术选型、实现细节与推进计划
`README.md` 是操作手册,本文是设计依据与决策记录。
`README.md` 是操作手册;本文记录背景、选型与关键决策。协作约束见 `AGENTS.md`
---
@@ -9,12 +8,10 @@
### 要解决的问题
在 Dota 2 选将阶段(以及进入游戏后)**几秒内自动获取双方 10 个英雄**,用于选将辅助或赛后分析。
在 Dota 2 选将阶段(以及进入游戏后)**几秒内自动获取双方 10 个英雄**,服务天梯选将辅助或赛后分析。
### 为什么 GSI 做不到
官方 Game State Integration 在**普通玩家视角**下不提供双方 pick:
| 场景 | GSI 可获得的阵容数据 |
|------|---------------------|
| 排位 / 普通 All Pick | 仅自己的 `hero.id``draft` 通常为空 |
@@ -22,38 +19,21 @@
| 观战 / 裁判视角 | 阵容字段较全 |
| 赛后 | 需依赖 OpenDota 等外部 API |
Valve 官方 issue 中已明确:All Pick 的实时 draft 数据因隐私考量被关闭
Valve 已明确关闭普通玩家视角的实时 draft
[#9562](https://github.com/ValveSoftware/Dota2-Gameplay/issues/9562)、
[#7193](https://github.com/ValveSoftware/Dota2-Gameplay/issues/7193)
且被标记为 not planned。
[#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 | 备选,偏重 |
| 读游戏内存 | **排除**(合规) |
| 截屏 + 模板匹配 | **选定** |
| 整图问多模态大模型 | 排除为主路径(实测整图几乎全错;裁顶栏后仍不如白名单匹配) |
| 方案 | 准确率 | 延迟 | 合规性 | 门槛 | 结论 |
|------|--------|------|--------|------|------|
| 官方 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**。
社区同类工具共同点:**裁固定 ROI + 模板匹配 / 小模型**,输出约束在英雄白名单内。
---
@@ -62,367 +42,130 @@ Valve 官方 issue 中已明确:All Pick 的实时 draft 数据因隐私考量
### 处理流程
```
决策时间截图(PNG,原生分辨率)
↓ ① 按相对坐标裁出 10 个头像格
↓ ② 去除 UI 边饰(顶部玩家颜色条、底部 ID 名牌),缩放到统一尺寸
↓ ③ 与模板库逐一做归一化相关匹配 TM_CCOEFF_NORMED
↓ ④ 双阈值门控:Top-1 分数 + (Top1 - Top2) 分差
{"radiant": [...], "dire": [...]} 每格给出 hero_key 或 null
截图 → 相对坐标裁 10 格 →(天梯则遮罩段位条)→ CDN NCC 匹配
→ score + margin 双门控 → hero_key | null
```
### 关键设计决策
### 关键决策
**① 相对坐标而非像素坐标**
**① 相对坐标**
Dota 2 的顶栏 UI 以屏幕顶部中央为锚点、随分辨率等比缩放。因此坐标存储为:
横向 `(cx - W/2) / H`,纵向与宽高 `/ H`。同宽高比下分辨率无关;21:9 等需锚点或重标定。
- 横向:`(格子中心 x - 屏幕宽/2) / 屏幕高`
- 纵向、宽高:`值 / 屏幕高`
**② 单一 CDN 模板库**
在 1440p 标定一次,1080p / 4K / 大部分 16:10 可直接套用
这是**通用性的第一层保障**——目标是所有 Dota 玩家可用,不能写死单一分辨率。
`templates/cdn/{hero}.png` 来自 Steam 官方头像,按顶栏实际窗口裁切后写入
**② 两层模板库**
| 层 | 路径 | 来源 | 特点 |
|----|------|------|------|
| real | `templates/real/{hero}/*.png` | 选人阶段默认脸实拍 | 与目标同源;**不收皮肤/至宝**;逐步积累 |
| cdn | `templates/cdn/{hero}.png` | Steam 官方 CDN 头像 | 全 127 英雄覆盖;按顶栏实际窗口裁切后实测已可 100% |
CDN 层保证「库里绝不会缺英雄」——缺模板时匹配器只能在已有英雄里硬选,
必然乱配(前述实测错误正是此类)。real 层随使用逐步替换 CDN 层。
曾尝试「CDN 兜底 + real 实拍」双层库。实测修正 CDN 裁切后 **CDN-only 即可全对**
继续攒 real 库性价比低,已放弃(见 §3 发现二)。皮肤靠会话策略,不堆变体模板。
**③ 宁可不认,不可乱认**
同时满足两个条件才输出结果,否则返回 `null`
- `score >= min_score`(默认 0.45
- `margin = Top1 - Top2 >= min_margin`(默认 0.04
- `margin = Top1 Top2 >= min_margin`(默认 0.04
分差门控用于排除「两个英雄都像」的情况,比单一分数阈值更可靠。
**④ 会话层处理皮肤与淡入**
**④ 失败即样本**
`recognize.py --truth` 会把认错的格子连同正确答案存入 `failures/`
(文件名含正确 hero_key)。确认是默认脸后再移入 `templates/real/{key}/`
皮肤顶栏靠会话策略(决策阶段只补空槽、best 优先选人帧),不靠穷举皮肤模板。
- 选人阶段:多帧确认、允许更高分改判(`revise_gain`
- 决策阶段:只补空槽、禁止改判(避免皮肤顶栏覆盖默认脸结论)
- best 帧优先选人阶段默认脸
---
## 3. 代码结构
## 3. 关键实测(2026-07-25
环境:2560×1440 无边框;人机 + 天梯。
### 发现一:CDN 裁切窗口决定上限
初版按正方形取中心,比例与顶栏 111×88 不符,均分约 0.65。
网格搜索得到正确窗口 **x0=38, w=182, 全高**(源 256×144),均分约 0.94。
写入 `fetch_cdn_templates.py``CROP_X0/CROP_X1` 后,**CDN-only 30/30 / 40/40**。
> 兜底层差时先查素材处理,再考虑堆数据。
### 发现二:不必维护 real 库
CDN-only 开箱即用后,「打几十局攒默认脸」不再是主路径。项目改为只维护 CDN。
### 发现三:自动标定靠玩家颜色条
10 条固定玩家色定位槽位;中心用中位数槽距拟合(剔除被身后头像污染的宽条);
头像下沿用「格内列 vs 格间空隙」亮度差,勿用逐行差分(易误判到名字行)。
### 发现四:天梯段位条用遮罩
GSI 不含可靠 lobby 类型时,用右下金色勋章检测(`has_ranked_overlay`),
匹配时屏蔽底部 32% + 右侧 22%。人机无勋章走全图匹配。
### 发现五:禁用名单不靠模板
网格按主属性分块 + 客户端本地化名行优先排布;禁用/已选卡片对比度塌陷
std 约 821 vs 正常 ≥33)。`heroes.json` 需含 `attr` / `name_loc`
格数对不上时 `ok=False`,不返回残缺名单。
### 发现六:位置与「我」
定位局位置字用二值 IoU(非 OCR)。「我」优先 GSI `team_slot`
否则姓名亮度相对差(最亮比次亮 ≥ 25)兜底。
---
## 4. 代码结构
```
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/ # 识别失败的格子,待标注入库
climperor/
├── config.json / heroes.json
├── common.py / recognize.py / draft_session.py
├── gsi_watch.py / gsi_setup.py
├── fetch_cdn_templates.py
├── autocalibrate.py / calibrate.py / capture.py
├── evaluate.py / roles.py / grid.py / modes.py
├── templates/cdn|roles|modes/
── samples/labels.json # 评测真值;raw/ 见 .gitignore
```
### config.json 参数说明
运行时目录 `preview/``results/``failures/``samples/raw/` 不入库。
### 主要配置
| 字段 | 含义 | 默认 |
|------|------|------|
| `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` | Top1Top2 最小分差 | 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 |
| `match.min_score` / `min_margin` | 识别门控 | 0.45 / 0.04 |
| `match.ranked_mask` | 天梯遮罩比例 | bottom 0.32, right 0.22 |
| `gsi.port` | 监听端口 | 3223 |
| `gsi.confirm_polls` | 连续同结果帧数 | 2 |
| `gsi.revise_gain` | 改判所需分数增益 | 0.15 |
| `gsi.strategy_tail_polls` | 决策阶段继续视觉轮询 | 8 |
阈值需在积累一定样本后按实测重新调优,当前为经验初值。
---
### 自动标定(`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,但能可靠告诉我们**现在处于哪个阶段**,正好用作触发器:
## 5. GSI 自动化
```
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
Dota 2 (-gamestateintegration)
POST → gsi_watch.py :3223
→ DraftSession 轮询 recognize + roles/grid/modes
→ results/draft_*.json
```
设计要点:
要点:`matchid` 去重;识别在工作线程;未标定时降级为只截图。
- **按 `matchid` 去重**,一局一个跟踪会话,不论从哪个阶段接入
- **轮询与识别在工作线程**,HTTP 回调立即返回,不阻塞游戏侧推送
- **确认需连续 2 帧**,单帧可能拍到半透明的入场动画
- **位置与「我」只解析一次**,这两项全局不变,认出后不再重复计算
- **「我」优先用 GSI 的 `team_slot`**,视觉判据仅在 GSI 尚未提供时兜底
- **未标定时降级为「只截图」**,仍在正确时机存帧,供 `calibrate.py` 使用
天梯 AP 选人按 2/2/1 成批揭晓,本轮结束前互不可见——因此必须在
**HERO_SELECTION** 跟踪,不能只在决策时间截一张终局图。
---
## 4. 环境与依赖
## 6. 与 dota2-hex
- 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。
独立验证项目。可选:Rust 重写并入,或本地旁路 HTTP 回传。
合规同 `dota2-hex`**屏幕可见信息 only**。
---
## 5. 推进计划
## 7. 参考
| 阶段 | 内容 | 状态 |
|------|------|------|
| 0 | 项目骨架、依赖、CDN 模板库 | **已完成** |
| 1 | 采集真实决策时间截图 | **已完成** |
| 2 | ROI 标定,生成 `config.json` 坐标 | **已完成**(自动标定) |
| 3 | 首轮识别(纯 CDN 兜底),摸底准确率 | **已完成**8/10 |
| 4 | 积累 real 模板,迭代阈值,统计准确率 | **已完成**30/30CDN-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 落在
821,其余 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`
- [Valve #9562](https://github.com/ValveSoftware/Dota2-Gameplay/issues/9562)
- [Valve #14915](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/)
- Steam CDN`https://cdn.cloudflare.steamstatic.com/apps/dota2/images/dota_react/heroes/{key}.png`
+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) 或独立发布。合规边界:只使用屏幕可见信息,不读内存、不注入。
-78
View File
@@ -1,78 +0,0 @@
"""Crop slots from a labeled screenshot and add them to the real template library.
Usage:
# 1) preview: crop 10 slots to preview/ so you can see what each slot contains
python build_library.py samples/shot1.png
# 2) import: provide 10 comma-separated hero keys (left to right), '?' to skip a slot
python build_library.py samples/shot1.png tinker,earthshaker,juggernaut,dazzle,vengefulspirit,axe,sniper,slark,lion,drow_ranger
Hero keys must match Steam internal names without the npc_dota_hero_ prefix
(see heroes.json after running fetch_cdn_templates.py).
"""
import json
import sys
import time
from pathlib import Path
import cv2
from common import ROOT, TEMPLATES_REAL, crop_slot, load_config
PREVIEW_DIR = ROOT / "preview"
def known_hero_keys() -> set[str]:
path = ROOT / "heroes.json"
if not path.exists():
return set()
with open(path, encoding="utf-8") as f:
return {h["key"] for h in json.load(f)}
def main() -> None:
if len(sys.argv) < 2:
sys.exit(__doc__)
image_path = sys.argv[1]
labels = sys.argv[2].split(",") if len(sys.argv) > 2 else None
img = cv2.imread(image_path)
if img is None:
sys.exit(f"cannot read image: {image_path}")
cfg = load_config()
if not cfg["slots"]:
sys.exit("config.json has no slots - run calibrate.py first")
crops = [(slot["index"], crop_slot(img, slot, cfg)) for slot in cfg["slots"]]
if labels is None:
PREVIEW_DIR.mkdir(exist_ok=True)
for idx, crop in crops:
if crop is not None:
cv2.imwrite(str(PREVIEW_DIR / f"slot_{idx}.png"), crop)
print(f"wrote {len(crops)} crops to {PREVIEW_DIR}/ - inspect them, then rerun with labels")
return
if len(labels) != 10:
sys.exit(f"expected 10 labels, got {len(labels)}")
known = known_hero_keys()
stamp = time.strftime("%Y%m%d_%H%M%S")
added = 0
for (idx, crop), label in zip(crops, labels):
label = label.strip()
if label == "?" or crop is None:
continue
if known and label not in known:
print(f" WARNING slot {idx}: '{label}' not in heroes.json - saved anyway, double-check spelling")
hero_dir = TEMPLATES_REAL / label
hero_dir.mkdir(parents=True, exist_ok=True)
cv2.imwrite(str(hero_dir / f"{stamp}_s{idx}.png"), crop)
added += 1
total = sum(1 for _ in TEMPLATES_REAL.rglob("*.png"))
heroes = sum(1 for d in TEMPLATES_REAL.iterdir() if d.is_dir())
print(f"added {added} templates; library now {total} images / {heroes} heroes")
if __name__ == "__main__":
main()
+9 -17
View File
@@ -8,7 +8,6 @@ import numpy as np
ROOT = Path(__file__).parent
CONFIG_PATH = ROOT / "config.json"
TEMPLATES_REAL = ROOT / "templates" / "real"
TEMPLATES_CDN = ROOT / "templates" / "cdn"
@@ -123,20 +122,13 @@ def has_ranked_overlay(img: np.ndarray, cfg: dict) -> bool:
return checked > 0 and hits >= max(6, checked * 0.6)
def load_template_library() -> list[tuple[str, str, np.ndarray]]:
"""Return list of (hero_key, source, image). source is 'real' or 'cdn'."""
lib: list[tuple[str, str, np.ndarray]] = []
if TEMPLATES_REAL.is_dir():
for hero_dir in sorted(TEMPLATES_REAL.iterdir()):
if not hero_dir.is_dir():
continue
for png in hero_dir.glob("*.png"):
img = cv2.imread(str(png))
if img is not None:
lib.append((hero_dir.name, "real", img))
if TEMPLATES_CDN.is_dir():
for png in TEMPLATES_CDN.glob("*.png"):
img = cv2.imread(str(png))
if img is not None:
lib.append((png.stem, "cdn", img))
def load_template_library() -> list[tuple[str, np.ndarray]]:
"""Return list of (hero_key, image) from Steam CDN portraits."""
lib: list[tuple[str, np.ndarray]] = []
if not TEMPLATES_CDN.is_dir():
return lib
for png in sorted(TEMPLATES_CDN.glob("*.png")):
img = cv2.imread(str(png))
if img is not None:
lib.append((png.stem, img))
return lib
-1
View File
@@ -65,7 +65,6 @@
"match": {
"min_score": 0.45,
"min_margin": 0.04,
"cdn_penalty": 0.05,
"ranked_mask": {
"comment": "Ignore the bottom rank-title bar and right-side medal when ranked overlays are detected.",
"bottom": 0.32,
+2 -7
View File
@@ -4,13 +4,11 @@ Labels live in samples/labels.json as {frame filename: 10 hero keys}, '?' for
slots nobody has identified yet. Those slots are skipped, not counted wrong.
Usage:
python evaluate.py # use the full template library
python evaluate.py --cdn-only # ignore templates/real, measure the fallback layer
python evaluate.py
"""
import json
import sys
from pathlib import Path
import cv2
@@ -22,7 +20,6 @@ RAW_DIR = ROOT / "samples" / "raw"
def main() -> None:
cdn_only = "--cdn-only" in sys.argv
if not LABELS_PATH.is_file():
sys.exit(f"missing {LABELS_PATH}")
frames = json.loads(LABELS_PATH.read_text(encoding="utf-8"))["frames"]
@@ -31,9 +28,7 @@ def main() -> None:
if not cfg["slots"]:
sys.exit("config.json has no slots - run autocalibrate.py first")
library = load_template_library()
if cdn_only:
library = [t for t in library if t[1] == "cdn"]
print(f"library: {len(library)} templates{' (cdn only)' if cdn_only else ''}")
print(f"library: {len(library)} CDN templates")
graded = correct = skipped = 0
misses: list[str] = []
Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 644 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 386 KiB

+11 -9
View File
@@ -1,12 +1,13 @@
"""Recognize the 10 drafted heroes from a strategy-time screenshot.
Usage:
python recognize.py samples/shot2.png
python recognize.py samples/shot2.png --truth tinker,earthshaker,...,drow_ranger
python recognize.py samples/raw/draft.png
python recognize.py samples/raw/draft.png --truth tinker,earthshaker,...,drow_ranger
python recognize.py samples/raw/draft.png --sheet
Outputs per-slot JSON with top-1 hero, score and margin; slots failing the
confidence gate are reported as null. With --truth, prints accuracy and saves
misrecognized crops to failures/ for later labeling.
misrecognized crops to failures/ (debug only, gitignored).
recognize_image() is the reusable entry point used by gsi_watch.py.
"""
@@ -58,13 +59,14 @@ def write_sheet(img: np.ndarray, results: list[dict], cfg: dict) -> str:
def recognize_slot(crop, library, cfg, mask=None):
"""Return (best_hero, best_score, margin, scored list)."""
penalty = cfg["match"]["cdn_penalty"]
"""Return (best_hero, best_score, margin, scored list).
cfg is accepted for call-site compatibility; score gates are applied by the caller.
"""
_ = cfg
best_per_hero: dict[str, float] = {}
for hero, source, tmpl in library:
for hero, tmpl in library:
s = match_score(crop, tmpl, mask)
if source == "cdn":
s -= penalty
if s > best_per_hero.get(hero, -2.0):
best_per_hero[hero] = s
ranked = sorted(best_per_hero.items(), key=lambda kv: kv[1], reverse=True)
@@ -142,7 +144,7 @@ def main() -> None:
sys.exit("config.json has no slots - run calibrate.py first")
library = load_template_library()
if not library:
sys.exit("template library is empty - run fetch_cdn_templates.py and/or build_library.py")
sys.exit("template library is empty - run fetch_cdn_templates.py")
out = recognize_image(img, cfg, library)
results = out.pop("slots")
+1
View File
@@ -1,3 +1,4 @@
opencv-python>=4.10
numpy>=2.0
requests>=2.32
mss>=9.0
-435
View File
@@ -1,435 +0,0 @@
{
"radiant": [
{
"slot": 1,
"hero": "drow_ranger",
"raw_best": "drow_ranger",
"score": 0.598,
"margin": 0.296,
"top3": [
[
"drow_ranger",
0.598
],
[
"treant",
0.302
],
[
"kez",
0.237
]
]
},
{
"slot": 2,
"hero": "warlock",
"raw_best": "warlock",
"score": 0.676,
"margin": 0.367,
"top3": [
[
"warlock",
0.676
],
[
"treant",
0.309
],
[
"monkey_king",
0.221
]
]
},
{
"slot": 3,
"hero": "viper",
"raw_best": "viper",
"score": 0.651,
"margin": 0.256,
"top3": [
[
"viper",
0.651
],
[
"faceless_void",
0.396
],
[
"lycan",
0.317
]
]
},
{
"slot": 4,
"hero": null,
"raw_best": "earthshaker",
"score": 0.434,
"margin": 0.128,
"top3": [
[
"earthshaker",
0.434
],
[
"legion_commander",
0.306
],
[
"bounty_hunter",
0.297
]
]
},
{
"slot": 5,
"hero": null,
"raw_best": "vengefulspirit",
"score": 0.327,
"margin": 0.052,
"top3": [
[
"vengefulspirit",
0.327
],
[
"legion_commander",
0.275
],
[
"bounty_hunter",
0.266
]
]
}
],
"dire": [
{
"slot": 6,
"hero": "lich",
"raw_best": "lich",
"score": 0.603,
"margin": 0.393,
"top3": [
[
"lich",
0.603
],
[
"phantom_lancer",
0.21
],
[
"monkey_king",
0.197
]
]
},
{
"slot": 7,
"hero": "zuus",
"raw_best": "zuus",
"score": 0.461,
"margin": 0.231,
"top3": [
[
"zuus",
0.461
],
[
"abyssal_underlord",
0.23
],
[
"witch_doctor",
0.206
]
]
},
{
"slot": 8,
"hero": "skeleton_king",
"raw_best": "skeleton_king",
"score": 0.691,
"margin": 0.247,
"top3": [
[
"skeleton_king",
0.691
],
[
"wisp",
0.445
],
[
"invoker",
0.396
]
]
},
{
"slot": 9,
"hero": "juggernaut",
"raw_best": "juggernaut",
"score": 0.67,
"margin": 0.201,
"top3": [
[
"juggernaut",
0.67
],
[
"wisp",
0.469
],
[
"ogre_magi",
0.379
]
]
},
{
"slot": 10,
"hero": "lion",
"raw_best": "lion",
"score": 0.509,
"margin": 0.194,
"top3": [
[
"lion",
0.509
],
[
"ogre_magi",
0.315
],
[
"juggernaut",
0.275
]
]
}
],
"recognized": 8,
"library_size": 127,
"elapsed_ms": 358,
"match_id": "0",
"trigger_state": "DOTA_GAMERULES_STATE_STRATEGY_TIME",
"captured_at": "2026-07-25 14:37:42",
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_143742.png",
"slots": [
{
"slot": 1,
"hero": "drow_ranger",
"raw_best": "drow_ranger",
"score": 0.598,
"margin": 0.296,
"top3": [
[
"drow_ranger",
0.598
],
[
"treant",
0.302
],
[
"kez",
0.237
]
]
},
{
"slot": 2,
"hero": "warlock",
"raw_best": "warlock",
"score": 0.676,
"margin": 0.367,
"top3": [
[
"warlock",
0.676
],
[
"treant",
0.309
],
[
"monkey_king",
0.221
]
]
},
{
"slot": 3,
"hero": "viper",
"raw_best": "viper",
"score": 0.651,
"margin": 0.256,
"top3": [
[
"viper",
0.651
],
[
"faceless_void",
0.396
],
[
"lycan",
0.317
]
]
},
{
"slot": 4,
"hero": null,
"raw_best": "earthshaker",
"score": 0.434,
"margin": 0.128,
"top3": [
[
"earthshaker",
0.434
],
[
"legion_commander",
0.306
],
[
"bounty_hunter",
0.297
]
]
},
{
"slot": 5,
"hero": null,
"raw_best": "vengefulspirit",
"score": 0.327,
"margin": 0.052,
"top3": [
[
"vengefulspirit",
0.327
],
[
"legion_commander",
0.275
],
[
"bounty_hunter",
0.266
]
]
},
{
"slot": 6,
"hero": "lich",
"raw_best": "lich",
"score": 0.603,
"margin": 0.393,
"top3": [
[
"lich",
0.603
],
[
"phantom_lancer",
0.21
],
[
"monkey_king",
0.197
]
]
},
{
"slot": 7,
"hero": "zuus",
"raw_best": "zuus",
"score": 0.461,
"margin": 0.231,
"top3": [
[
"zuus",
0.461
],
[
"abyssal_underlord",
0.23
],
[
"witch_doctor",
0.206
]
]
},
{
"slot": 8,
"hero": "skeleton_king",
"raw_best": "skeleton_king",
"score": 0.691,
"margin": 0.247,
"top3": [
[
"skeleton_king",
0.691
],
[
"wisp",
0.445
],
[
"invoker",
0.396
]
]
},
{
"slot": 9,
"hero": "juggernaut",
"raw_best": "juggernaut",
"score": 0.67,
"margin": 0.201,
"top3": [
[
"juggernaut",
0.67
],
[
"wisp",
0.469
],
[
"ogre_magi",
0.379
]
]
},
{
"slot": 10,
"hero": "lion",
"raw_best": "lion",
"score": 0.509,
"margin": 0.194,
"top3": [
[
"lion",
0.509
],
[
"ogre_magi",
0.315
],
[
"juggernaut",
0.275
]
]
}
]
}
-435
View File
@@ -1,435 +0,0 @@
{
"radiant": [
{
"slot": 1,
"hero": "crystal_maiden",
"raw_best": "crystal_maiden",
"score": 0.454,
"margin": 0.222,
"top3": [
[
"crystal_maiden",
0.454
],
[
"naga_siren",
0.232
],
[
"visage",
0.206
]
]
},
{
"slot": 2,
"hero": "witch_doctor",
"raw_best": "witch_doctor",
"score": 0.637,
"margin": 0.277,
"top3": [
[
"witch_doctor",
0.637
],
[
"abyssal_underlord",
0.361
],
[
"invoker",
0.298
]
]
},
{
"slot": 3,
"hero": "skeleton_king",
"raw_best": "skeleton_king",
"score": 0.688,
"margin": 0.255,
"top3": [
[
"skeleton_king",
0.688
],
[
"wisp",
0.433
],
[
"invoker",
0.401
]
]
},
{
"slot": 4,
"hero": "sniper",
"raw_best": "sniper",
"score": 0.652,
"margin": 0.523,
"top3": [
[
"sniper",
0.652
],
[
"primal_beast",
0.129
],
[
"naga_siren",
0.127
]
]
},
{
"slot": 5,
"hero": "sven",
"raw_best": "sven",
"score": 0.693,
"margin": 0.356,
"top3": [
[
"sven",
0.693
],
[
"furion",
0.337
],
[
"ringmaster",
0.324
]
]
}
],
"dire": [
{
"slot": 6,
"hero": null,
"raw_best": "earthshaker",
"score": 0.434,
"margin": 0.128,
"top3": [
[
"earthshaker",
0.434
],
[
"legion_commander",
0.306
],
[
"bounty_hunter",
0.297
]
]
},
{
"slot": 7,
"hero": null,
"raw_best": "necrolyte",
"score": 0.412,
"margin": 0.294,
"top3": [
[
"necrolyte",
0.412
],
[
"omniknight",
0.118
],
[
"alchemist",
0.111
]
]
},
{
"slot": 8,
"hero": "warlock",
"raw_best": "warlock",
"score": 0.711,
"margin": 0.413,
"top3": [
[
"warlock",
0.711
],
[
"treant",
0.297
],
[
"bristleback",
0.241
]
]
},
{
"slot": 9,
"hero": "drow_ranger",
"raw_best": "drow_ranger",
"score": 0.657,
"margin": 0.286,
"top3": [
[
"drow_ranger",
0.657
],
[
"wisp",
0.372
],
[
"treant",
0.273
]
]
},
{
"slot": 10,
"hero": "lich",
"raw_best": "lich",
"score": 0.519,
"margin": 0.336,
"top3": [
[
"lich",
0.519
],
[
"wisp",
0.183
],
[
"nyx_assassin",
0.16
]
]
}
],
"recognized": 8,
"library_size": 127,
"elapsed_ms": 362,
"match_id": "0",
"trigger_state": "DOTA_GAMERULES_STATE_STRATEGY_TIME",
"captured_at": "2026-07-25 14:41:31",
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_144131.png",
"slots": [
{
"slot": 1,
"hero": "crystal_maiden",
"raw_best": "crystal_maiden",
"score": 0.454,
"margin": 0.222,
"top3": [
[
"crystal_maiden",
0.454
],
[
"naga_siren",
0.232
],
[
"visage",
0.206
]
]
},
{
"slot": 2,
"hero": "witch_doctor",
"raw_best": "witch_doctor",
"score": 0.637,
"margin": 0.277,
"top3": [
[
"witch_doctor",
0.637
],
[
"abyssal_underlord",
0.361
],
[
"invoker",
0.298
]
]
},
{
"slot": 3,
"hero": "skeleton_king",
"raw_best": "skeleton_king",
"score": 0.688,
"margin": 0.255,
"top3": [
[
"skeleton_king",
0.688
],
[
"wisp",
0.433
],
[
"invoker",
0.401
]
]
},
{
"slot": 4,
"hero": "sniper",
"raw_best": "sniper",
"score": 0.652,
"margin": 0.523,
"top3": [
[
"sniper",
0.652
],
[
"primal_beast",
0.129
],
[
"naga_siren",
0.127
]
]
},
{
"slot": 5,
"hero": "sven",
"raw_best": "sven",
"score": 0.693,
"margin": 0.356,
"top3": [
[
"sven",
0.693
],
[
"furion",
0.337
],
[
"ringmaster",
0.324
]
]
},
{
"slot": 6,
"hero": null,
"raw_best": "earthshaker",
"score": 0.434,
"margin": 0.128,
"top3": [
[
"earthshaker",
0.434
],
[
"legion_commander",
0.306
],
[
"bounty_hunter",
0.297
]
]
},
{
"slot": 7,
"hero": null,
"raw_best": "necrolyte",
"score": 0.412,
"margin": 0.294,
"top3": [
[
"necrolyte",
0.412
],
[
"omniknight",
0.118
],
[
"alchemist",
0.111
]
]
},
{
"slot": 8,
"hero": "warlock",
"raw_best": "warlock",
"score": 0.711,
"margin": 0.413,
"top3": [
[
"warlock",
0.711
],
[
"treant",
0.297
],
[
"bristleback",
0.241
]
]
},
{
"slot": 9,
"hero": "drow_ranger",
"raw_best": "drow_ranger",
"score": 0.657,
"margin": 0.286,
"top3": [
[
"drow_ranger",
0.657
],
[
"wisp",
0.372
],
[
"treant",
0.273
]
]
},
{
"slot": 10,
"hero": "lich",
"raw_best": "lich",
"score": 0.519,
"margin": 0.336,
"top3": [
[
"lich",
0.519
],
[
"wisp",
0.183
],
[
"nyx_assassin",
0.16
]
]
}
]
}
-435
View File
@@ -1,435 +0,0 @@
{
"radiant": [
{
"slot": 1,
"hero": null,
"raw_best": "elder_titan",
"score": 0.446,
"margin": 0.184,
"top3": [
[
"elder_titan",
0.446
],
[
"warlock",
0.262
],
[
"lion",
0.222
]
]
},
{
"slot": 2,
"hero": "sniper",
"raw_best": "sniper",
"score": 1.0,
"margin": 0.871,
"top3": [
[
"sniper",
1.0
],
[
"primal_beast",
0.129
],
[
"naga_siren",
0.127
]
]
},
{
"slot": 3,
"hero": "lion",
"raw_best": "lion",
"score": 1.0,
"margin": 0.633,
"top3": [
[
"lion",
1.0
],
[
"juggernaut",
0.367
],
[
"skeleton_king",
0.34
]
]
},
{
"slot": 4,
"hero": "juggernaut",
"raw_best": "juggernaut",
"score": 1.0,
"margin": 0.516,
"top3": [
[
"juggernaut",
1.0
],
[
"wisp",
0.484
],
[
"ogre_magi",
0.395
]
]
},
{
"slot": 5,
"hero": null,
"raw_best": "vengefulspirit",
"score": 0.327,
"margin": 0.052,
"top3": [
[
"vengefulspirit",
0.327
],
[
"legion_commander",
0.275
],
[
"death_prophet",
0.266
]
]
}
],
"dire": [
{
"slot": 6,
"hero": "death_prophet",
"raw_best": "death_prophet",
"score": 0.951,
"margin": 0.551,
"top3": [
[
"death_prophet",
0.951
],
[
"ogre_magi",
0.4
],
[
"undying",
0.372
]
]
},
{
"slot": 7,
"hero": "tidehunter",
"raw_best": "tidehunter",
"score": 0.528,
"margin": 0.271,
"top3": [
[
"tidehunter",
0.528
],
[
"juggernaut",
0.257
],
[
"lich",
0.208
]
]
},
{
"slot": 8,
"hero": "lich",
"raw_best": "lich",
"score": 0.96,
"margin": 0.703,
"top3": [
[
"lich",
0.96
],
[
"warlock",
0.257
],
[
"skeleton_king",
0.201
]
]
},
{
"slot": 9,
"hero": "sven",
"raw_best": "sven",
"score": 0.912,
"margin": 0.576,
"top3": [
[
"sven",
0.912
],
[
"lion",
0.336
],
[
"furion",
0.324
]
]
},
{
"slot": 10,
"hero": "witch_doctor",
"raw_best": "witch_doctor",
"score": 0.706,
"margin": 0.338,
"top3": [
[
"witch_doctor",
0.706
],
[
"abyssal_underlord",
0.368
],
[
"zuus",
0.298
]
]
}
],
"recognized": 8,
"library_size": 155,
"elapsed_ms": 447,
"match_id": "0",
"trigger_state": "DOTA_GAMERULES_STATE_STRATEGY_TIME",
"captured_at": "2026-07-25 14:51:36",
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_145136.png",
"slots": [
{
"slot": 1,
"hero": null,
"raw_best": "elder_titan",
"score": 0.446,
"margin": 0.184,
"top3": [
[
"elder_titan",
0.446
],
[
"warlock",
0.262
],
[
"lion",
0.222
]
]
},
{
"slot": 2,
"hero": "sniper",
"raw_best": "sniper",
"score": 1.0,
"margin": 0.871,
"top3": [
[
"sniper",
1.0
],
[
"primal_beast",
0.129
],
[
"naga_siren",
0.127
]
]
},
{
"slot": 3,
"hero": "lion",
"raw_best": "lion",
"score": 1.0,
"margin": 0.633,
"top3": [
[
"lion",
1.0
],
[
"juggernaut",
0.367
],
[
"skeleton_king",
0.34
]
]
},
{
"slot": 4,
"hero": "juggernaut",
"raw_best": "juggernaut",
"score": 1.0,
"margin": 0.516,
"top3": [
[
"juggernaut",
1.0
],
[
"wisp",
0.484
],
[
"ogre_magi",
0.395
]
]
},
{
"slot": 5,
"hero": null,
"raw_best": "vengefulspirit",
"score": 0.327,
"margin": 0.052,
"top3": [
[
"vengefulspirit",
0.327
],
[
"legion_commander",
0.275
],
[
"death_prophet",
0.266
]
]
},
{
"slot": 6,
"hero": "death_prophet",
"raw_best": "death_prophet",
"score": 0.951,
"margin": 0.551,
"top3": [
[
"death_prophet",
0.951
],
[
"ogre_magi",
0.4
],
[
"undying",
0.372
]
]
},
{
"slot": 7,
"hero": "tidehunter",
"raw_best": "tidehunter",
"score": 0.528,
"margin": 0.271,
"top3": [
[
"tidehunter",
0.528
],
[
"juggernaut",
0.257
],
[
"lich",
0.208
]
]
},
{
"slot": 8,
"hero": "lich",
"raw_best": "lich",
"score": 0.96,
"margin": 0.703,
"top3": [
[
"lich",
0.96
],
[
"warlock",
0.257
],
[
"skeleton_king",
0.201
]
]
},
{
"slot": 9,
"hero": "sven",
"raw_best": "sven",
"score": 0.912,
"margin": 0.576,
"top3": [
[
"sven",
0.912
],
[
"lion",
0.336
],
[
"furion",
0.324
]
]
},
{
"slot": 10,
"hero": "witch_doctor",
"raw_best": "witch_doctor",
"score": 0.706,
"margin": 0.338,
"top3": [
[
"witch_doctor",
0.706
],
[
"abyssal_underlord",
0.368
],
[
"zuus",
0.298
]
]
}
]
}
-435
View File
@@ -1,435 +0,0 @@
{
"radiant": [
{
"slot": 1,
"hero": "undying",
"raw_best": "undying",
"score": 0.877,
"margin": 0.441,
"top3": [
[
"undying",
0.877
],
[
"death_prophet",
0.437
],
[
"faceless_void",
0.387
]
]
},
{
"slot": 2,
"hero": "death_prophet",
"raw_best": "death_prophet",
"score": 1.0,
"margin": 0.601,
"top3": [
[
"death_prophet",
1.0
],
[
"ogre_magi",
0.399
],
[
"undying",
0.382
]
]
},
{
"slot": 3,
"hero": "dragon_knight",
"raw_best": "dragon_knight",
"score": 0.917,
"margin": 0.629,
"top3": [
[
"dragon_knight",
0.917
],
[
"juggernaut",
0.289
],
[
"lion",
0.209
]
]
},
{
"slot": 4,
"hero": "drow_ranger",
"raw_best": "drow_ranger",
"score": 0.955,
"margin": 0.583,
"top3": [
[
"drow_ranger",
0.955
],
[
"wisp",
0.372
],
[
"lycan",
0.316
]
]
},
{
"slot": 5,
"hero": "witch_doctor",
"raw_best": "witch_doctor",
"score": 1.0,
"margin": 0.641,
"top3": [
[
"witch_doctor",
1.0
],
[
"abyssal_underlord",
0.359
],
[
"invoker",
0.321
]
]
}
],
"dire": [
{
"slot": 6,
"hero": "vengefulspirit",
"raw_best": "vengefulspirit",
"score": 1.0,
"margin": 0.728,
"top3": [
[
"vengefulspirit",
1.0
],
[
"legion_commander",
0.272
],
[
"death_prophet",
0.269
]
]
},
{
"slot": 7,
"hero": "warlock",
"raw_best": "warlock",
"score": 0.98,
"margin": 0.704,
"top3": [
[
"warlock",
0.98
],
[
"treant",
0.276
],
[
"bristleback",
0.263
]
]
},
{
"slot": 8,
"hero": "tidehunter",
"raw_best": "tidehunter",
"score": 0.968,
"margin": 0.71,
"top3": [
[
"tidehunter",
0.968
],
[
"juggernaut",
0.258
],
[
"lich",
0.199
]
]
},
{
"slot": 9,
"hero": "zuus",
"raw_best": "zuus",
"score": 1.0,
"margin": 0.691,
"top3": [
[
"zuus",
1.0
],
[
"witch_doctor",
0.309
],
[
"dark_willow",
0.243
]
]
},
{
"slot": 10,
"hero": "sven",
"raw_best": "sven",
"score": 0.974,
"margin": 0.631,
"top3": [
[
"sven",
0.974
],
[
"lion",
0.342
],
[
"ringmaster",
0.327
]
]
}
],
"recognized": 10,
"library_size": 167,
"elapsed_ms": 497,
"match_id": "0",
"trigger_state": "DOTA_GAMERULES_STATE_STRATEGY_TIME",
"captured_at": "2026-07-25 15:10:21",
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_151021.png",
"slots": [
{
"slot": 1,
"hero": "undying",
"raw_best": "undying",
"score": 0.877,
"margin": 0.441,
"top3": [
[
"undying",
0.877
],
[
"death_prophet",
0.437
],
[
"faceless_void",
0.387
]
]
},
{
"slot": 2,
"hero": "death_prophet",
"raw_best": "death_prophet",
"score": 1.0,
"margin": 0.601,
"top3": [
[
"death_prophet",
1.0
],
[
"ogre_magi",
0.399
],
[
"undying",
0.382
]
]
},
{
"slot": 3,
"hero": "dragon_knight",
"raw_best": "dragon_knight",
"score": 0.917,
"margin": 0.629,
"top3": [
[
"dragon_knight",
0.917
],
[
"juggernaut",
0.289
],
[
"lion",
0.209
]
]
},
{
"slot": 4,
"hero": "drow_ranger",
"raw_best": "drow_ranger",
"score": 0.955,
"margin": 0.583,
"top3": [
[
"drow_ranger",
0.955
],
[
"wisp",
0.372
],
[
"lycan",
0.316
]
]
},
{
"slot": 5,
"hero": "witch_doctor",
"raw_best": "witch_doctor",
"score": 1.0,
"margin": 0.641,
"top3": [
[
"witch_doctor",
1.0
],
[
"abyssal_underlord",
0.359
],
[
"invoker",
0.321
]
]
},
{
"slot": 6,
"hero": "vengefulspirit",
"raw_best": "vengefulspirit",
"score": 1.0,
"margin": 0.728,
"top3": [
[
"vengefulspirit",
1.0
],
[
"legion_commander",
0.272
],
[
"death_prophet",
0.269
]
]
},
{
"slot": 7,
"hero": "warlock",
"raw_best": "warlock",
"score": 0.98,
"margin": 0.704,
"top3": [
[
"warlock",
0.98
],
[
"treant",
0.276
],
[
"bristleback",
0.263
]
]
},
{
"slot": 8,
"hero": "tidehunter",
"raw_best": "tidehunter",
"score": 0.968,
"margin": 0.71,
"top3": [
[
"tidehunter",
0.968
],
[
"juggernaut",
0.258
],
[
"lich",
0.199
]
]
},
{
"slot": 9,
"hero": "zuus",
"raw_best": "zuus",
"score": 1.0,
"margin": 0.691,
"top3": [
[
"zuus",
1.0
],
[
"witch_doctor",
0.309
],
[
"dark_willow",
0.243
]
]
},
{
"slot": 10,
"hero": "sven",
"raw_best": "sven",
"score": 0.974,
"margin": 0.631,
"top3": [
[
"sven",
0.974
],
[
"lion",
0.342
],
[
"ringmaster",
0.327
]
]
}
]
}
-435
View File
@@ -1,435 +0,0 @@
{
"radiant": [
{
"slot": 1,
"hero": null,
"raw_best": "bounty_hunter",
"score": 0.283,
"margin": 0.01,
"top3": [
[
"bounty_hunter",
0.283
],
[
"grimstroke",
0.273
],
[
"witch_doctor",
0.257
]
]
},
{
"slot": 2,
"hero": "skeleton_king",
"raw_best": "skeleton_king",
"score": 0.793,
"margin": 0.431,
"top3": [
[
"skeleton_king",
0.793
],
[
"invoker",
0.362
],
[
"wisp",
0.328
]
]
},
{
"slot": 3,
"hero": "sniper",
"raw_best": "sniper",
"score": 0.717,
"margin": 0.448,
"top3": [
[
"sniper",
0.717
],
[
"undying",
0.27
],
[
"spirit_breaker",
0.178
]
]
},
{
"slot": 4,
"hero": "invoker",
"raw_best": "invoker",
"score": 0.839,
"margin": 0.385,
"top3": [
[
"invoker",
0.839
],
[
"wisp",
0.454
],
[
"sand_king",
0.445
]
]
},
{
"slot": 5,
"hero": "silencer",
"raw_best": "silencer",
"score": 0.577,
"margin": 0.43,
"top3": [
[
"silencer",
0.577
],
[
"troll_warlord",
0.147
],
[
"necrolyte",
0.137
]
]
}
],
"dire": [
{
"slot": 6,
"hero": "skywrath_mage",
"raw_best": "skywrath_mage",
"score": 0.61,
"margin": 0.282,
"top3": [
[
"skywrath_mage",
0.61
],
[
"undying",
0.328
],
[
"ogre_magi",
0.322
]
]
},
{
"slot": 7,
"hero": "phantom_assassin",
"raw_best": "phantom_assassin",
"score": 0.621,
"margin": 0.382,
"top3": [
[
"phantom_assassin",
0.621
],
[
"crystal_maiden",
0.24
],
[
"drow_ranger",
0.235
]
]
},
{
"slot": 8,
"hero": "queenofpain",
"raw_best": "queenofpain",
"score": 0.686,
"margin": 0.395,
"top3": [
[
"queenofpain",
0.686
],
[
"lycan",
0.291
],
[
"drow_ranger",
0.283
]
]
},
{
"slot": 9,
"hero": "centaur",
"raw_best": "centaur",
"score": 0.71,
"margin": 0.428,
"top3": [
[
"centaur",
0.71
],
[
"sven",
0.282
],
[
"ringmaster",
0.281
]
]
},
{
"slot": 10,
"hero": "rubick",
"raw_best": "rubick",
"score": 0.623,
"margin": 0.415,
"top3": [
[
"rubick",
0.623
],
[
"death_prophet",
0.208
],
[
"elder_titan",
0.153
]
]
}
],
"recognized": 9,
"library_size": 177,
"elapsed_ms": 492,
"match_id": "8912562036",
"trigger_state": "DOTA_GAMERULES_STATE_STRATEGY_TIME",
"captured_at": "2026-07-25 15:18:28",
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_151828.png",
"slots": [
{
"slot": 1,
"hero": null,
"raw_best": "bounty_hunter",
"score": 0.283,
"margin": 0.01,
"top3": [
[
"bounty_hunter",
0.283
],
[
"grimstroke",
0.273
],
[
"witch_doctor",
0.257
]
]
},
{
"slot": 2,
"hero": "skeleton_king",
"raw_best": "skeleton_king",
"score": 0.793,
"margin": 0.431,
"top3": [
[
"skeleton_king",
0.793
],
[
"invoker",
0.362
],
[
"wisp",
0.328
]
]
},
{
"slot": 3,
"hero": "sniper",
"raw_best": "sniper",
"score": 0.717,
"margin": 0.448,
"top3": [
[
"sniper",
0.717
],
[
"undying",
0.27
],
[
"spirit_breaker",
0.178
]
]
},
{
"slot": 4,
"hero": "invoker",
"raw_best": "invoker",
"score": 0.839,
"margin": 0.385,
"top3": [
[
"invoker",
0.839
],
[
"wisp",
0.454
],
[
"sand_king",
0.445
]
]
},
{
"slot": 5,
"hero": "silencer",
"raw_best": "silencer",
"score": 0.577,
"margin": 0.43,
"top3": [
[
"silencer",
0.577
],
[
"troll_warlord",
0.147
],
[
"necrolyte",
0.137
]
]
},
{
"slot": 6,
"hero": "skywrath_mage",
"raw_best": "skywrath_mage",
"score": 0.61,
"margin": 0.282,
"top3": [
[
"skywrath_mage",
0.61
],
[
"undying",
0.328
],
[
"ogre_magi",
0.322
]
]
},
{
"slot": 7,
"hero": "phantom_assassin",
"raw_best": "phantom_assassin",
"score": 0.621,
"margin": 0.382,
"top3": [
[
"phantom_assassin",
0.621
],
[
"crystal_maiden",
0.24
],
[
"drow_ranger",
0.235
]
]
},
{
"slot": 8,
"hero": "queenofpain",
"raw_best": "queenofpain",
"score": 0.686,
"margin": 0.395,
"top3": [
[
"queenofpain",
0.686
],
[
"lycan",
0.291
],
[
"drow_ranger",
0.283
]
]
},
{
"slot": 9,
"hero": "centaur",
"raw_best": "centaur",
"score": 0.71,
"margin": 0.428,
"top3": [
[
"centaur",
0.71
],
[
"sven",
0.282
],
[
"ringmaster",
0.281
]
]
},
{
"slot": 10,
"hero": "rubick",
"raw_best": "rubick",
"score": 0.623,
"margin": 0.415,
"top3": [
[
"rubick",
0.623
],
[
"death_prophet",
0.208
],
[
"elder_titan",
0.153
]
]
}
]
}
-436
View File
@@ -1,436 +0,0 @@
{
"radiant": [
{
"slot": 1,
"hero": "clinkz",
"raw_best": "clinkz",
"score": 0.873,
"margin": 0.478,
"top3": [
[
"clinkz",
0.873
],
[
"legion_commander",
0.395
],
[
"centaur",
0.392
]
]
},
{
"slot": 2,
"hero": "witch_doctor",
"raw_best": "witch_doctor",
"score": 0.962,
"margin": 0.672,
"top3": [
[
"witch_doctor",
0.962
],
[
"invoker",
0.29
],
[
"tiny",
0.26
]
]
},
{
"slot": 3,
"hero": "abyssal_underlord",
"raw_best": "abyssal_underlord",
"score": 0.878,
"margin": 0.418,
"top3": [
[
"abyssal_underlord",
0.878
],
[
"invoker",
0.46
],
[
"legion_commander",
0.432
]
]
},
{
"slot": 4,
"hero": "pudge",
"raw_best": "pudge",
"score": 0.884,
"margin": 0.359,
"top3": [
[
"pudge",
0.884
],
[
"invoker",
0.526
],
[
"centaur",
0.443
]
]
},
{
"slot": 5,
"hero": "drow_ranger",
"raw_best": "drow_ranger",
"score": 0.902,
"margin": 0.491,
"top3": [
[
"drow_ranger",
0.902
],
[
"wisp",
0.411
],
[
"arc_warden",
0.317
]
]
}
],
"dire": [
{
"slot": 6,
"hero": "life_stealer",
"raw_best": "life_stealer",
"score": 0.894,
"margin": 0.525,
"top3": [
[
"life_stealer",
0.894
],
[
"dawnbreaker",
0.369
],
[
"silencer",
0.313
]
]
},
{
"slot": 7,
"hero": "weaver",
"raw_best": "weaver",
"score": 0.877,
"margin": 0.379,
"top3": [
[
"weaver",
0.877
],
[
"sven",
0.498
],
[
"spirit_breaker",
0.497
]
]
},
{
"slot": 8,
"hero": "skywrath_mage",
"raw_best": "skywrath_mage",
"score": 0.998,
"margin": 0.701,
"top3": [
[
"skywrath_mage",
0.998
],
[
"legion_commander",
0.297
],
[
"sand_king",
0.259
]
]
},
{
"slot": 9,
"hero": "skeleton_king",
"raw_best": "skeleton_king",
"score": 0.972,
"margin": 0.621,
"top3": [
[
"skeleton_king",
0.972
],
[
"phantom_assassin",
0.351
],
[
"invoker",
0.305
]
]
},
{
"slot": 10,
"hero": "storm_spirit",
"raw_best": "storm_spirit",
"score": 0.821,
"margin": 0.306,
"top3": [
[
"storm_spirit",
0.821
],
[
"wisp",
0.515
],
[
"skeleton_king",
0.46
]
]
}
],
"recognized": 10,
"ranked_overlay": true,
"library_size": 187,
"elapsed_ms": 507,
"match_id": "8912632259",
"trigger_state": "DOTA_GAMERULES_STATE_STRATEGY_TIME",
"captured_at": "2026-07-25 16:24:04",
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_162404.png",
"slots": [
{
"slot": 1,
"hero": "clinkz",
"raw_best": "clinkz",
"score": 0.873,
"margin": 0.478,
"top3": [
[
"clinkz",
0.873
],
[
"legion_commander",
0.395
],
[
"centaur",
0.392
]
]
},
{
"slot": 2,
"hero": "witch_doctor",
"raw_best": "witch_doctor",
"score": 0.962,
"margin": 0.672,
"top3": [
[
"witch_doctor",
0.962
],
[
"invoker",
0.29
],
[
"tiny",
0.26
]
]
},
{
"slot": 3,
"hero": "abyssal_underlord",
"raw_best": "abyssal_underlord",
"score": 0.878,
"margin": 0.418,
"top3": [
[
"abyssal_underlord",
0.878
],
[
"invoker",
0.46
],
[
"legion_commander",
0.432
]
]
},
{
"slot": 4,
"hero": "pudge",
"raw_best": "pudge",
"score": 0.884,
"margin": 0.359,
"top3": [
[
"pudge",
0.884
],
[
"invoker",
0.526
],
[
"centaur",
0.443
]
]
},
{
"slot": 5,
"hero": "drow_ranger",
"raw_best": "drow_ranger",
"score": 0.902,
"margin": 0.491,
"top3": [
[
"drow_ranger",
0.902
],
[
"wisp",
0.411
],
[
"arc_warden",
0.317
]
]
},
{
"slot": 6,
"hero": "life_stealer",
"raw_best": "life_stealer",
"score": 0.894,
"margin": 0.525,
"top3": [
[
"life_stealer",
0.894
],
[
"dawnbreaker",
0.369
],
[
"silencer",
0.313
]
]
},
{
"slot": 7,
"hero": "weaver",
"raw_best": "weaver",
"score": 0.877,
"margin": 0.379,
"top3": [
[
"weaver",
0.877
],
[
"sven",
0.498
],
[
"spirit_breaker",
0.497
]
]
},
{
"slot": 8,
"hero": "skywrath_mage",
"raw_best": "skywrath_mage",
"score": 0.998,
"margin": 0.701,
"top3": [
[
"skywrath_mage",
0.998
],
[
"legion_commander",
0.297
],
[
"sand_king",
0.259
]
]
},
{
"slot": 9,
"hero": "skeleton_king",
"raw_best": "skeleton_king",
"score": 0.972,
"margin": 0.621,
"top3": [
[
"skeleton_king",
0.972
],
[
"phantom_assassin",
0.351
],
[
"invoker",
0.305
]
]
},
{
"slot": 10,
"hero": "storm_spirit",
"raw_best": "storm_spirit",
"score": 0.821,
"margin": 0.306,
"top3": [
[
"storm_spirit",
0.821
],
[
"wisp",
0.515
],
[
"skeleton_king",
0.46
]
]
}
]
}
-198
View File
@@ -1,198 +0,0 @@
{
"match_id": "8912704440",
"captured_at": "2026-07-25 17:25:30",
"duration_s": 111.0,
"polls": 70,
"self": {
"slot": 3,
"team": "radiant",
"hero": "life_stealer",
"role": "safe",
"role_label": "优势路",
"position": 1,
"gsi_name": "refining"
},
"team_roles": {
"1": {
"position": 2,
"label": "中路",
"hero": "queenofpain"
},
"2": {
"position": 5,
"label": "纯辅助",
"hero": "lina"
},
"3": {
"position": 1,
"label": "优势路",
"hero": "life_stealer"
},
"4": {
"position": 3,
"label": "劣势路",
"hero": "lion"
},
"5": {
"position": 4,
"label": "辅助",
"hero": null
}
},
"final": {
"radiant": [
"queenofpain",
"lina",
"life_stealer",
"lion",
null
],
"dire": [
"spectre",
"lich",
"earth_spirit",
"undying",
"necrolyte"
]
},
"recognized": 9,
"timeline": [
{
"t": 13.8,
"state": "HERO_SELECTION",
"round": 1,
"added": [
{
"slot": 2,
"team": "radiant",
"hero": "lina"
}
],
"radiant": [
"lina"
],
"dire": [],
"count": 1,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_172352.png"
},
{
"t": 30.0,
"state": "HERO_SELECTION",
"round": 1,
"added": [
{
"slot": 4,
"team": "radiant",
"hero": "lion"
},
{
"slot": 7,
"team": "dire",
"hero": "lich"
},
{
"slot": 9,
"team": "dire",
"hero": "undying"
}
],
"radiant": [
"lina",
"lion"
],
"dire": [
"lich",
"undying"
],
"count": 4,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_172409.png"
},
{
"t": 52.9,
"state": "HERO_SELECTION",
"round": 2,
"added": [
{
"slot": 3,
"team": "radiant",
"hero": "life_stealer"
}
],
"radiant": [
"lina",
"life_stealer",
"lion"
],
"dire": [
"lich",
"undying"
],
"count": 5,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_172431.png"
},
{
"t": 54.6,
"state": "HERO_SELECTION",
"round": 2,
"added": [
{
"slot": 1,
"team": "radiant",
"hero": "queenofpain"
},
{
"slot": 6,
"team": "dire",
"hero": "spectre"
},
{
"slot": 8,
"team": "dire",
"hero": "earth_spirit"
}
],
"radiant": [
"queenofpain",
"lina",
"life_stealer",
"lion"
],
"dire": [
"spectre",
"lich",
"earth_spirit",
"undying"
],
"count": 8,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_172433.png"
},
{
"t": 82.2,
"state": "STRATEGY_TIME",
"round": 3,
"added": [
{
"slot": 10,
"team": "dire",
"hero": "necrolyte"
}
],
"radiant": [
"queenofpain",
"lina",
"life_stealer",
"lion"
],
"dire": [
"spectre",
"lich",
"earth_spirit",
"undying",
"necrolyte"
],
"count": 9,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_172501.png"
}
],
"last_frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_172530.png"
}
-250
View File
@@ -1,250 +0,0 @@
{
"match_id": "8912791143",
"captured_at": "2026-07-25 18:38:26",
"duration_s": 124.4,
"polls": 84,
"self": {
"slot": 9,
"team": "dire",
"hero": "drow_ranger",
"role": "safe",
"role_label": "优势路",
"position": 1,
"gsi_name": "refining"
},
"team_roles": {
"6": {
"position": 5,
"label": "纯辅助",
"hero": "witch_doctor"
},
"7": {
"position": 3,
"label": "劣势路",
"hero": null
},
"8": {
"position": 4,
"label": "辅助",
"hero": "faceless_void"
},
"9": {
"position": 1,
"label": "优势路",
"hero": "drow_ranger"
},
"10": {
"position": 2,
"label": "中路",
"hero": "queenofpain"
}
},
"final": {
"radiant": [
"razor",
"nyx_assassin",
"hoodwink",
"zuus",
"antimage"
],
"dire": [
"witch_doctor",
null,
"faceless_void",
"drow_ranger",
"queenofpain"
]
},
"recognized": 9,
"timeline": [
{
"t": 11.0,
"state": "HERO_SELECTION",
"round": 1,
"added": [
{
"slot": 9,
"team": "dire",
"hero": "drow_ranger"
}
],
"radiant": [],
"dire": [
"drow_ranger"
],
"count": 1,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_183633.png"
},
{
"t": 25.7,
"state": "HERO_SELECTION",
"round": 1,
"added": [
{
"slot": 8,
"team": "dire",
"hero": "faceless_void"
}
],
"radiant": [],
"dire": [
"faceless_void",
"drow_ranger"
],
"count": 2,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_183648.png"
},
{
"t": 39.2,
"state": "HERO_SELECTION",
"round": 2,
"added": [
{
"slot": 1,
"team": "radiant",
"hero": "razor"
},
{
"slot": 4,
"team": "radiant",
"hero": "zuus"
},
{
"slot": 6,
"team": "dire",
"hero": "witch_doctor"
}
],
"radiant": [
"razor",
"zuus"
],
"dire": [
"witch_doctor",
"faceless_void",
"drow_ranger"
],
"count": 5,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_183701.png"
},
{
"t": 40.7,
"state": "HERO_SELECTION",
"round": 2,
"added": [
{
"slot": 10,
"team": "dire",
"hero": "queenofpain"
}
],
"radiant": [
"razor",
"zuus"
],
"dire": [
"witch_doctor",
"faceless_void",
"drow_ranger",
"queenofpain"
],
"count": 6,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_183703.png"
},
{
"t": 62.9,
"state": "HERO_SELECTION",
"round": 2,
"added": [
{
"slot": 2,
"team": "radiant",
"hero": "nyx_assassin"
},
{
"slot": 5,
"team": "radiant",
"hero": "antimage"
}
],
"radiant": [
"razor",
"nyx_assassin",
"zuus",
"antimage"
],
"dire": [
"witch_doctor",
"faceless_void",
"drow_ranger",
"queenofpain"
],
"count": 8,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_183725.png"
},
{
"t": 96.8,
"state": "STRATEGY_TIME",
"round": 3,
"added": [
{
"slot": 3,
"team": "radiant",
"hero": "hoodwink"
}
],
"radiant": [
"razor",
"nyx_assassin",
"hoodwink",
"zuus",
"antimage"
],
"dire": [
"witch_doctor",
"faceless_void",
"drow_ranger",
"queenofpain"
],
"count": 9,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_183759.png"
}
],
"bans": [
"venomancer",
"obsidian_destroyer",
"necrolyte",
"silencer",
"ursa",
"nevermore",
"lina",
"lion",
"magnataur",
"ember_spirit",
"snapfire",
"alchemist",
"windrunner",
"sniper",
"pudge",
"abyssal_underlord"
],
"bans_loc": [
"剧毒术士",
"殁境神蚀者",
"瘟疫法师",
"沉默术士",
"熊战士",
"影魔",
"莉娜",
"莱恩",
"马格纳斯",
"灰烬之灵",
"电炎绝手",
"炼金术士",
"风行者",
"狙击手",
"帕吉",
"孽主"
],
"last_frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_183826.png"
}
-256
View File
@@ -1,256 +0,0 @@
{
"match_id": "8912846004",
"captured_at": "2026-07-25 19:21:11",
"duration_s": 65.7,
"polls": 45,
"self": {
"slot": 2,
"team": "radiant",
"hero": "juggernaut",
"role": "safe",
"role_label": "优势路",
"position": 1,
"gsi_name": "refining"
},
"team_roles": {
"2": {
"position": 1,
"label": "优势路",
"hero": "juggernaut"
},
"3": {
"position": 5,
"label": "纯辅助",
"hero": "shadow_shaman"
},
"4": {
"position": 4,
"label": "辅助",
"hero": "death_prophet"
},
"5": {
"position": 3,
"label": "劣势路",
"hero": "tidehunter"
}
},
"final": {
"radiant": [
"necrolyte",
"juggernaut",
"shadow_shaman",
"death_prophet",
"tidehunter"
],
"dire": [
"invoker",
"hoodwink",
"axe",
"earthshaker",
"skeleton_king"
]
},
"recognized": 10,
"timeline": [
{
"t": 7.0,
"state": "HERO_SELECTION",
"round": 1,
"added": [
{
"slot": 5,
"team": "radiant",
"hero": "tidehunter"
}
],
"radiant": [
"tidehunter"
],
"dire": [],
"count": 1,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_192012.png"
},
{
"t": 10.0,
"state": "HERO_SELECTION",
"round": 1,
"added": [
{
"slot": 3,
"team": "radiant",
"hero": "shadow_shaman"
},
{
"slot": 7,
"team": "dire",
"hero": "hoodwink"
},
{
"slot": 9,
"team": "dire",
"hero": "earthshaker"
}
],
"radiant": [
"shadow_shaman",
"tidehunter"
],
"dire": [
"hoodwink",
"earthshaker"
],
"count": 4,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_192015.png"
},
{
"t": 13.0,
"state": "HERO_SELECTION",
"round": 2,
"added": [
{
"slot": 2,
"team": "radiant",
"hero": "juggernaut"
}
],
"radiant": [
"spectre",
"shadow_shaman",
"tidehunter"
],
"dire": [
"hoodwink",
"earthshaker"
],
"count": 5,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_192018.png"
},
{
"t": 32.2,
"state": "HERO_SELECTION",
"round": 2,
"added": [
{
"slot": 4,
"team": "radiant",
"hero": "death_prophet"
}
],
"radiant": [
"spectre",
"shadow_shaman",
"death_prophet",
"tidehunter"
],
"dire": [
"hoodwink",
"earthshaker"
],
"count": 6,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_192038.png"
},
{
"t": 36.6,
"state": "HERO_SELECTION",
"round": 2,
"added": [
{
"slot": 8,
"team": "dire",
"hero": "axe"
},
{
"slot": 10,
"team": "dire",
"hero": "skeleton_king"
}
],
"radiant": [
"spectre",
"shadow_shaman",
"death_prophet",
"tidehunter"
],
"dire": [
"hoodwink",
"axe",
"earthshaker",
"skeleton_king"
],
"count": 8,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_192042.png"
},
{
"t": 65.6,
"state": "STRATEGY_TIME",
"round": 3,
"added": [
{
"slot": 1,
"team": "radiant",
"hero": "necrolyte"
},
{
"slot": 6,
"team": "dire",
"hero": "invoker"
}
],
"radiant": [
"necrolyte",
"spectre",
"shadow_shaman",
"death_prophet",
"tidehunter"
],
"dire": [
"invoker",
"hoodwink",
"axe",
"earthshaker",
"skeleton_king"
],
"count": 10,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_192111.png"
}
],
"bans": [
"disruptor",
"phantom_lancer",
"lion",
"zuus",
"rubick",
"silencer",
"weaver",
"dark_willow",
"abyssal_underlord",
"windrunner",
"medusa",
"razor",
"pudge",
"bristleback",
"drow_ranger",
"sniper",
"huskar"
],
"bans_loc": [
"干扰者",
"幻影长矛手",
"莱恩",
"宙斯",
"拉比克",
"沉默术士",
"编织者",
"邪影芳灵",
"孽主",
"风行者",
"美杜莎",
"雷泽",
"帕吉",
"钢背兽",
"卓尔游侠",
"狙击手",
"哈斯卡"
],
"last_frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_192111_1.png"
}
-240
View File
@@ -1,240 +0,0 @@
{
"match_id": "8912903800",
"captured_at": "2026-07-25 20:03:24",
"duration_s": 80.0,
"polls": 51,
"self": {
"slot": 5,
"team": "radiant",
"hero": "bloodseeker",
"role": "safe",
"role_label": "优势路",
"position": 1,
"gsi_name": "refining"
},
"team_roles": {
"1": {
"position": 3,
"label": "劣势路",
"hero": "pudge"
},
"2": {
"position": 5,
"label": "纯辅助",
"hero": "venomancer"
},
"3": {
"position": 4,
"label": "辅助",
"hero": "weaver"
},
"4": {
"position": 2,
"label": "中路",
"hero": "kunkka"
},
"5": {
"position": 1,
"label": "优势路",
"hero": "bloodseeker"
}
},
"final": {
"radiant": [
"pudge",
"venomancer",
"weaver",
"kunkka",
"bloodseeker"
],
"dire": [
"undying",
"warlock",
"luna",
"hoodwink",
"queenofpain"
]
},
"recognized": 10,
"timeline": [
{
"t": 5.7,
"state": "HERO_SELECTION",
"round": 1,
"added": [
{
"slot": 1,
"team": "radiant",
"hero": "pudge"
}
],
"radiant": [
"pudge"
],
"dire": [],
"count": 1,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_200210.png"
},
{
"t": 28.4,
"state": "HERO_SELECTION",
"round": 1,
"added": [
{
"slot": 2,
"team": "radiant",
"hero": "venomancer"
},
{
"slot": 9,
"team": "dire",
"hero": "hoodwink"
},
{
"slot": 10,
"team": "dire",
"hero": "queenofpain"
}
],
"radiant": [
"pudge",
"venomancer"
],
"dire": [
"hoodwink",
"queenofpain"
],
"count": 4,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_200233.png"
},
{
"t": 51.2,
"state": "HERO_SELECTION",
"round": 2,
"added": [
{
"slot": 3,
"team": "radiant",
"hero": "weaver"
}
],
"radiant": [
"pudge",
"venomancer",
"weaver"
],
"dire": [
"hoodwink",
"queenofpain"
],
"count": 5,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_200256.png"
},
{
"t": 54.3,
"state": "HERO_SELECTION",
"round": 2,
"added": [
{
"slot": 5,
"team": "radiant",
"hero": "bloodseeker"
},
{
"slot": 6,
"team": "dire",
"hero": "undying"
},
{
"slot": 7,
"team": "dire",
"hero": "warlock"
}
],
"radiant": [
"pudge",
"venomancer",
"weaver",
"bloodseeker"
],
"dire": [
"undying",
"warlock",
"hoodwink",
"queenofpain"
],
"count": 8,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_200259.png"
},
{
"t": 79.9,
"state": "STRATEGY_TIME",
"round": 3,
"added": [
{
"slot": 4,
"team": "radiant",
"hero": "kunkka"
},
{
"slot": 8,
"team": "dire",
"hero": "luna"
}
],
"radiant": [
"pudge",
"venomancer",
"weaver",
"kunkka",
"bloodseeker"
],
"dire": [
"undying",
"warlock",
"luna",
"hoodwink",
"queenofpain"
],
"count": 10,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_200324.png"
}
],
"bans": [
"disruptor",
"skywrath_mage",
"phantom_assassin",
"lich",
"lion",
"meepo",
"riki",
"tidehunter",
"axe",
"abyssal_underlord",
"tusk",
"phoenix",
"legion_commander",
"snapfire",
"bounty_hunter",
"sniper"
],
"bans_loc": [
"干扰者",
"天怒法师",
"幻影刺客",
"巫妖",
"莱恩",
"米波",
"力丸",
"潮汐猎人",
"斧王",
"孽主",
"巨牙海民",
"凤凰",
"军团指挥官",
"电炎绝手",
"赏金猎人",
"狙击手"
],
"last_frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_200324_1.png"
}
-141
View File
@@ -1,141 +0,0 @@
{
"match_id": "8913003670",
"captured_at": "2026-07-25 21:07:27",
"duration_s": 116.2,
"polls": 76,
"self": {
"slot": 10,
"team": "dire",
"hero": "juggernaut",
"role": "safe",
"role_label": "优势路",
"position": 1,
"gsi_name": "refining"
},
"team_roles": {
"6": {
"position": 3,
"label": "劣势路",
"hero": null
},
"7": {
"position": 2,
"label": "中路",
"hero": null
},
"8": {
"position": 5,
"label": "纯辅助",
"hero": null
},
"9": {
"position": 4,
"label": "辅助",
"hero": null
},
"10": {
"position": 1,
"label": "优势路",
"hero": "juggernaut"
}
},
"final": {
"radiant": [
"spirit_breaker",
null,
null,
null,
null
],
"dire": [
null,
null,
null,
null,
"juggernaut"
]
},
"recognized": 2,
"revisions": [],
"timeline": [
{
"t": 42.0,
"state": "HERO_SELECTION",
"round": 1,
"added": [
{
"slot": 1,
"team": "radiant",
"hero": "spirit_breaker"
}
],
"radiant": [
"spirit_breaker"
],
"dire": [],
"count": 1,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_210612.png"
},
{
"t": 56.9,
"state": "HERO_SELECTION",
"round": 1,
"added": [
{
"slot": 10,
"team": "dire",
"hero": "invoker"
}
],
"radiant": [
"spirit_breaker"
],
"dire": [
"invoker"
],
"count": 2,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_210627.png"
}
],
"bans": [
"invoker",
"zuus",
"pugna",
"silencer",
"nevermore",
"nyx_assassin",
"magnataur",
"lion",
"pudge",
"gyrocopter",
"storm_spirit",
"mirana",
"axe",
"pangolier",
"abyssal_underlord",
"sven",
"legion_commander",
"dawnbreaker"
],
"bans_loc": [
"祈求者",
"宙斯",
"帕格纳",
"沉默术士",
"影魔",
"司夜刺客",
"马格纳斯",
"莱恩",
"帕吉",
"矮人直升机",
"风暴之灵",
"米拉娜",
"斧王",
"石鳞剑士",
"孽主",
"斯温",
"军团指挥官",
"破晓辰星"
],
"last_frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_210727.png"
}
-148
View File
@@ -1,148 +0,0 @@
{
"match_id": "8913098624",
"captured_at": "2026-07-25 22:02:49",
"duration_s": 12.3,
"polls": 8,
"self": {
"slot": 7,
"team": "dire",
"hero": "phantom_lancer",
"role": "safe",
"role_label": "优势路",
"position": 1,
"gsi_name": "refining"
},
"team_roles": {
"6": {
"position": 5,
"label": "纯辅助",
"hero": "lina"
},
"7": {
"position": 1,
"label": "优势路",
"hero": "phantom_lancer"
},
"8": {
"position": 3,
"label": "劣势路",
"hero": "necrolyte"
},
"9": {
"position": 4,
"label": "辅助",
"hero": "zuus"
},
"10": {
"position": 2,
"label": "中路",
"hero": "lion"
}
},
"final": {
"radiant": [
"weaver",
"huskar",
"venomancer",
"furion",
"juggernaut"
],
"dire": [
"lina",
"phantom_lancer",
"necrolyte",
"zuus",
"lion"
]
},
"recognized": 9,
"revisions": [],
"timeline": [
{
"t": 2.1,
"state": "STRATEGY_TIME",
"round": 3,
"added": [
{
"slot": 1,
"team": "radiant",
"hero": "weaver"
},
{
"slot": 2,
"team": "radiant",
"hero": "huskar"
},
{
"slot": 3,
"team": "radiant",
"hero": "venomancer"
},
{
"slot": 4,
"team": "radiant",
"hero": "furion"
},
{
"slot": 5,
"team": "radiant",
"hero": "juggernaut"
},
{
"slot": 6,
"team": "dire",
"hero": "lina"
},
{
"slot": 7,
"team": "dire",
"hero": "phantom_lancer"
},
{
"slot": 8,
"team": "dire",
"hero": "necrolyte"
},
{
"slot": 10,
"team": "dire",
"hero": "lion"
}
],
"radiant": [
"weaver",
"huskar",
"venomancer",
"furion",
"juggernaut"
],
"dire": [
"lina",
"phantom_lancer",
"necrolyte",
"lion"
],
"count": 9,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_220239.png"
}
],
"best_lineup": {
"recognized": 10,
"t": 0.1,
"state": "STRATEGY_TIME",
"heroes": [
"weaver",
"huskar",
"venomancer",
"furion",
"juggernaut",
"lina",
"phantom_lancer",
"necrolyte",
"zuus",
"lion"
],
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_best_220249.png"
},
"last_frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_best_220249.png"
}
-273
View File
@@ -1,273 +0,0 @@
{
"match_id": "8913190580",
"captured_at": "2026-07-25 22:54:09",
"duration_s": 119.6,
"polls": 81,
"self": {
"slot": 9,
"team": "dire",
"hero": "templar_assassin",
"role": "safe",
"role_label": "优势路",
"position": 1,
"gsi_name": "refining"
},
"team_roles": {
"6": {
"position": 4,
"label": "辅助",
"hero": "windrunner"
},
"7": {
"position": 3,
"label": "劣势路",
"hero": "slardar"
},
"8": {
"position": 5,
"label": "纯辅助",
"hero": "lina"
},
"9": {
"position": 1,
"label": "优势路",
"hero": "templar_assassin"
},
"10": {
"position": 2,
"label": "中路",
"hero": "void_spirit"
}
},
"final": {
"radiant": [
"undying",
"rubick",
"death_prophet",
"legion_commander",
"luna"
],
"dire": [
"windrunner",
"slardar",
"lina",
"templar_assassin",
"void_spirit"
]
},
"recognized": 9,
"revisions": [],
"timeline": [
{
"t": 12.6,
"state": "HERO_SELECTION",
"round": 1,
"added": [
{
"slot": 8,
"team": "dire",
"hero": "lina"
}
],
"radiant": [],
"dire": [
"lina"
],
"count": 1,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_225222.png"
},
{
"t": 29.8,
"state": "HERO_SELECTION",
"round": 1,
"added": [
{
"slot": 2,
"team": "radiant",
"hero": "rubick"
},
{
"slot": 4,
"team": "radiant",
"hero": "legion_commander"
},
{
"slot": 6,
"team": "dire",
"hero": "windrunner"
}
],
"radiant": [
"rubick",
"legion_commander"
],
"dire": [
"windrunner",
"lina"
],
"count": 4,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_225239.png"
},
{
"t": 43.4,
"state": "HERO_SELECTION",
"round": 2,
"added": [
{
"slot": 9,
"team": "dire",
"hero": "templar_assassin"
}
],
"radiant": [
"rubick",
"legion_commander"
],
"dire": [
"windrunner",
"lina",
"templar_assassin"
],
"count": 5,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_225252.png"
},
{
"t": 64.6,
"state": "HERO_SELECTION",
"round": 2,
"added": [
{
"slot": 1,
"team": "radiant",
"hero": "undying"
},
{
"slot": 5,
"team": "radiant",
"hero": "luna"
}
],
"radiant": [
"undying",
"rubick",
"legion_commander",
"luna"
],
"dire": [
"windrunner",
"lina",
"templar_assassin"
],
"count": 7,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_225314.png"
},
{
"t": 84.4,
"state": "HERO_SELECTION",
"round": 2,
"added": [
{
"slot": 10,
"team": "dire",
"hero": "void_spirit"
}
],
"radiant": [
"undying",
"rubick",
"legion_commander",
"luna"
],
"dire": [
"windrunner",
"lina",
"templar_assassin",
"void_spirit"
],
"count": 8,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_225333.png"
},
{
"t": 90.0,
"state": "STRATEGY_TIME",
"round": 3,
"added": [
{
"slot": 3,
"team": "radiant",
"hero": "death_prophet"
}
],
"radiant": [
"undying",
"rubick",
"death_prophet",
"legion_commander",
"luna"
],
"dire": [
"windrunner",
"lina",
"templar_assassin",
"void_spirit"
],
"count": 9,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_225339.png"
}
],
"bans": [
"leshrac",
"techies",
"bane",
"antimage",
"puck",
"queenofpain",
"magnataur",
"ringmaster",
"lion",
"snapfire",
"juggernaut",
"clinkz",
"axe",
"treant",
"pudge",
"rattletrap"
],
"bans_loc": [
"拉席克",
"工程师",
"祸乱之源",
"敌法师",
"帕克",
"痛苦女王",
"马格纳斯",
"百戏大王",
"莱恩",
"电炎绝手",
"主宰",
"克林克兹",
"斧王",
"树精卫士",
"帕吉",
"发条技师"
],
"best_lineup": {
"recognized": 9,
"t": 101.0,
"state": "STRATEGY_TIME",
"heroes": [
"undying",
"rubick",
"death_prophet",
"legion_commander",
"luna",
null,
"slardar",
"lina",
"templar_assassin",
"void_spirit"
],
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_best_225408.png"
},
"last_frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_best_225408.png"
}
-273
View File
@@ -1,273 +0,0 @@
{
"match_id": "8913254704",
"captured_at": "2026-07-25 23:30:32",
"duration_s": 124.6,
"polls": 84,
"self": {
"slot": 10,
"team": "dire",
"hero": "antimage",
"role": "safe",
"role_label": "优势路",
"position": 1,
"gsi_name": "refining"
},
"team_roles": {
"6": {
"position": 4,
"label": "辅助",
"hero": "snapfire"
},
"7": {
"position": 2,
"label": "中路",
"hero": "sniper"
},
"8": {
"position": 5,
"label": "纯辅助",
"hero": "abyssal_underlord"
},
"9": {
"position": 3,
"label": "劣势路",
"hero": "undying"
},
"10": {
"position": 1,
"label": "优势路",
"hero": "antimage"
}
},
"final": {
"radiant": [
"vengefulspirit",
"earthshaker",
"ogre_magi",
"bristleback",
"enchantress"
],
"dire": [
"snapfire",
"sniper",
"abyssal_underlord",
"undying",
"antimage"
]
},
"recognized": 10,
"revisions": [],
"timeline": [
{
"t": 22.9,
"state": "HERO_SELECTION",
"round": 1,
"added": [
{
"slot": 8,
"team": "dire",
"hero": "abyssal_underlord"
}
],
"radiant": [],
"dire": [
"abyssal_underlord"
],
"count": 1,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_232850.png"
},
{
"t": 35.3,
"state": "HERO_SELECTION",
"round": 1,
"added": [
{
"slot": 2,
"team": "radiant",
"hero": "earthshaker"
},
{
"slot": 3,
"team": "radiant",
"hero": "ogre_magi"
},
{
"slot": 6,
"team": "dire",
"hero": "snapfire"
}
],
"radiant": [
"earthshaker",
"ogre_magi"
],
"dire": [
"snapfire",
"abyssal_underlord"
],
"count": 4,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_232902.png"
},
{
"t": 47.5,
"state": "HERO_SELECTION",
"round": 2,
"added": [
{
"slot": 9,
"team": "dire",
"hero": "undying"
}
],
"radiant": [
"earthshaker",
"ogre_magi"
],
"dire": [
"snapfire",
"abyssal_underlord",
"undying"
],
"count": 5,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_232915.png"
},
{
"t": 61.5,
"state": "HERO_SELECTION",
"round": 2,
"added": [
{
"slot": 1,
"team": "radiant",
"hero": "vengefulspirit"
},
{
"slot": 4,
"team": "radiant",
"hero": "bristleback"
}
],
"radiant": [
"vengefulspirit",
"earthshaker",
"ogre_magi",
"bristleback"
],
"dire": [
"snapfire",
"abyssal_underlord",
"undying"
],
"count": 7,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_232928.png"
},
{
"t": 91.5,
"state": "HERO_SELECTION",
"round": 2,
"added": [
{
"slot": 7,
"team": "dire",
"hero": "sniper"
}
],
"radiant": [
"vengefulspirit",
"earthshaker",
"ogre_magi",
"bristleback"
],
"dire": [
"snapfire",
"sniper",
"abyssal_underlord",
"undying"
],
"count": 8,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_232959.png"
},
{
"t": 95.4,
"state": "STRATEGY_TIME",
"round": 3,
"added": [
{
"slot": 5,
"team": "radiant",
"hero": "enchantress"
}
],
"radiant": [
"vengefulspirit",
"earthshaker",
"ogre_magi",
"bristleback",
"enchantress"
],
"dire": [
"snapfire",
"sniper",
"abyssal_underlord",
"undying"
],
"count": 9,
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_233002.png"
}
],
"bans": [
"invoker",
"meepo",
"sven",
"clinkz",
"chaos_knight",
"juggernaut",
"slark",
"elder_titan",
"ember_spirit",
"necrolyte",
"kunkka",
"lion",
"magnataur",
"monkey_king",
"drow_ranger",
"legion_commander"
],
"bans_loc": [
"祈求者",
"米波",
"斯温",
"克林克兹",
"混沌骑士",
"主宰",
"斯拉克",
"上古巨神",
"灰烬之灵",
"瘟疫法师",
"昆卡",
"莱恩",
"马格纳斯",
"齐天大圣",
"卓尔游侠",
"军团指挥官"
],
"best_lineup": {
"recognized": 8,
"t": 89.6,
"state": "HERO_SELECTION",
"heroes": [
"vengefulspirit",
"earthshaker",
"ogre_magi",
"bristleback",
null,
"snapfire",
"sniper",
"abyssal_underlord",
"undying",
null
],
"frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_best_233032.png"
},
"last_frame": "C:\\Users\\Administrator\\Documents\\wrok\\dota2-draft-vision\\samples\\raw\\draft_best_233032.png"
}
+1 -1
View File
@@ -1,4 +1,4 @@
{
"comment": "Ground truth for captured frames: 10 hero keys left to right, '?' for unknown. Used by evaluate.py and build_library.py. Paths are filenames under samples/raw/ (or samples/raw/<matchid>/ when using per-match folders).",
"comment": "Ground truth for captured frames: 10 hero keys left to right, '?' for unknown. Used by evaluate.py. Paths are filenames under samples/raw/ (or samples/raw/<matchid>/ when using per-match folders).",
"frames": {}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB