diff --git a/.gitignore b/.gitignore index d6bf07e..3e9d7c6 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,9 @@ venv/ .DS_Store Thumbs.db *.log + +# Runtime / debug artifacts (regenerable) +preview/ +results/ +failures/ +samples/raw/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..422b873 --- /dev/null +++ b/AGENTS.md @@ -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) 的合规边界。 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8bf6e28 --- /dev/null +++ b/CHANGELOG.md @@ -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 遮罩与会话层皮肤策略。 diff --git a/DESIGN.md b/DESIGN.md index 8032602..48b6bab 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -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 约 8–21 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// -├── 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` | 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 | +| `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/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` +- [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` diff --git a/README.md b/README.md index 7bac36c..72b55af 100644 --- a/README.md +++ b/README.md @@ -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 分数 + Top1−Top2 分差双阈值 → 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//`(`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//<帧>.png`。 -只看某张截图的禁用识别:`python grid.py samples/raw//<帧>.png`。 - -> **主菜单里收不到数据是正常的。** Dota 的 GSI 与 CS 不同,客户端**第一次载入对局后** -> 才开始发 HTTP 请求,挂在主菜单时不会有任何推送。看到 `[gsi] connected` 才算链路通。 -> 一直没有的话,先确认启动项里有 `-gamestateintegration`。 - -人机对战(Play with bots)同样会推送,HUD 顶栏与天梯一致,适合反复测试。 - -**尚未标定时**会自动进入「只截图」模式——按 `matchid` 存到 `samples/raw//`, -拿其中一张跑 `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) 或独立发布。合规边界:只使用屏幕可见信息,不读内存、不注入。 diff --git a/build_library.py b/build_library.py deleted file mode 100644 index 6292600..0000000 --- a/build_library.py +++ /dev/null @@ -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() diff --git a/common.py b/common.py index 7fc2c67..cf7fcda 100644 --- a/common.py +++ b/common.py @@ -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 diff --git a/config.json b/config.json index 39b0a04..254137a 100644 --- a/config.json +++ b/config.json @@ -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, diff --git a/evaluate.py b/evaluate.py index 21f63de..6c29968 100644 --- a/evaluate.py +++ b/evaluate.py @@ -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] = [] diff --git a/preview/ask_slot1.png b/preview/ask_slot1.png deleted file mode 100644 index 10c0995..0000000 Binary files a/preview/ask_slot1.png and /dev/null differ diff --git a/preview/ask_slot6.png b/preview/ask_slot6.png deleted file mode 100644 index 627eb71..0000000 Binary files a/preview/ask_slot6.png and /dev/null differ diff --git a/preview/ask_slot7.png b/preview/ask_slot7.png deleted file mode 100644 index 27ef785..0000000 Binary files a/preview/ask_slot7.png and /dev/null differ diff --git a/preview/autocalibrate_check.png b/preview/autocalibrate_check.png deleted file mode 100644 index 132d47c..0000000 Binary files a/preview/autocalibrate_check.png and /dev/null differ diff --git a/preview/autocalibrate_slots.png b/preview/autocalibrate_slots.png deleted file mode 100644 index 00da160..0000000 Binary files a/preview/autocalibrate_slots.png and /dev/null differ diff --git a/preview/recognize_sheet.png b/preview/recognize_sheet.png deleted file mode 100644 index d5927e0..0000000 Binary files a/preview/recognize_sheet.png and /dev/null differ diff --git a/preview/slot1_vs_templates.png b/preview/slot1_vs_templates.png deleted file mode 100644 index 64ebacd..0000000 Binary files a/preview/slot1_vs_templates.png and /dev/null differ diff --git a/recognize.py b/recognize.py index 50ec674..8f01166 100644 --- a/recognize.py +++ b/recognize.py @@ -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") diff --git a/requirements.txt b/requirements.txt index d12425e..ff35fc3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ opencv-python>=4.10 numpy>=2.0 requests>=2.32 +mss>=9.0 diff --git a/results/draft_20260725_143742.json b/results/draft_20260725_143742.json deleted file mode 100644 index 8b55c46..0000000 --- a/results/draft_20260725_143742.json +++ /dev/null @@ -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 - ] - ] - } - ] -} \ No newline at end of file diff --git a/results/draft_20260725_144131.json b/results/draft_20260725_144131.json deleted file mode 100644 index ad046be..0000000 --- a/results/draft_20260725_144131.json +++ /dev/null @@ -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 - ] - ] - } - ] -} \ No newline at end of file diff --git a/results/draft_20260725_145136.json b/results/draft_20260725_145136.json deleted file mode 100644 index 48886a0..0000000 --- a/results/draft_20260725_145136.json +++ /dev/null @@ -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 - ] - ] - } - ] -} \ No newline at end of file diff --git a/results/draft_20260725_151021.json b/results/draft_20260725_151021.json deleted file mode 100644 index 5bf326f..0000000 --- a/results/draft_20260725_151021.json +++ /dev/null @@ -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 - ] - ] - } - ] -} \ No newline at end of file diff --git a/results/draft_20260725_151828.json b/results/draft_20260725_151828.json deleted file mode 100644 index 9508f5f..0000000 --- a/results/draft_20260725_151828.json +++ /dev/null @@ -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 - ] - ] - } - ] -} \ No newline at end of file diff --git a/results/draft_20260725_162404.json b/results/draft_20260725_162404.json deleted file mode 100644 index ed1d508..0000000 --- a/results/draft_20260725_162404.json +++ /dev/null @@ -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 - ] - ] - } - ] -} \ No newline at end of file diff --git a/results/draft_20260725_172530.json b/results/draft_20260725_172530.json deleted file mode 100644 index fa09bcd..0000000 --- a/results/draft_20260725_172530.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/results/draft_20260725_183826.json b/results/draft_20260725_183826.json deleted file mode 100644 index 9ea263a..0000000 --- a/results/draft_20260725_183826.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/results/draft_20260725_192111.json b/results/draft_20260725_192111.json deleted file mode 100644 index 766cd49..0000000 --- a/results/draft_20260725_192111.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/results/draft_20260725_200324.json b/results/draft_20260725_200324.json deleted file mode 100644 index ee20436..0000000 --- a/results/draft_20260725_200324.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/results/draft_20260725_210727.json b/results/draft_20260725_210727.json deleted file mode 100644 index c507bb2..0000000 --- a/results/draft_20260725_210727.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/results/draft_20260725_220249.json b/results/draft_20260725_220249.json deleted file mode 100644 index 7682c3a..0000000 --- a/results/draft_20260725_220249.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/results/draft_20260725_225409.json b/results/draft_20260725_225409.json deleted file mode 100644 index 1b03d53..0000000 --- a/results/draft_20260725_225409.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/results/draft_20260725_233032.json b/results/draft_20260725_233032.json deleted file mode 100644 index 67a49b3..0000000 --- a/results/draft_20260725_233032.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/samples/labels.json b/samples/labels.json index 58dad2c..ac32abe 100644 --- a/samples/labels.json +++ b/samples/labels.json @@ -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// 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// when using per-match folders).", "frames": {} } diff --git a/templates/real/axe/20260725_200018_s8.png b/templates/real/axe/20260725_200018_s8.png deleted file mode 100644 index 4157f59..0000000 Binary files a/templates/real/axe/20260725_200018_s8.png and /dev/null differ diff --git a/templates/real/bloodseeker/20260725_210215_s5.png b/templates/real/bloodseeker/20260725_210215_s5.png deleted file mode 100644 index 15c8ea7..0000000 Binary files a/templates/real/bloodseeker/20260725_210215_s5.png and /dev/null differ diff --git a/templates/real/centaur/20260725_160212_s9.png b/templates/real/centaur/20260725_160212_s9.png deleted file mode 100644 index eec7279..0000000 Binary files a/templates/real/centaur/20260725_160212_s9.png and /dev/null differ diff --git a/templates/real/crystal_maiden/20260725_150302_s1.png b/templates/real/crystal_maiden/20260725_150302_s1.png deleted file mode 100644 index 0a3f88c..0000000 Binary files a/templates/real/crystal_maiden/20260725_150302_s1.png and /dev/null differ diff --git a/templates/real/death_prophet/20260725_150246_s8.png b/templates/real/death_prophet/20260725_150246_s8.png deleted file mode 100644 index acbd476..0000000 Binary files a/templates/real/death_prophet/20260725_150246_s8.png and /dev/null differ diff --git a/templates/real/dragon_knight/20260725_151116_s3.png b/templates/real/dragon_knight/20260725_151116_s3.png deleted file mode 100644 index 81bd1ac..0000000 Binary files a/templates/real/dragon_knight/20260725_151116_s3.png and /dev/null differ diff --git a/templates/real/drow_ranger/20260725_150246_s7.png b/templates/real/drow_ranger/20260725_150246_s7.png deleted file mode 100644 index 0975141..0000000 Binary files a/templates/real/drow_ranger/20260725_150246_s7.png and /dev/null differ diff --git a/templates/real/earthshaker/20260725_150246_s2.png b/templates/real/earthshaker/20260725_150246_s2.png deleted file mode 100644 index 5a430dd..0000000 Binary files a/templates/real/earthshaker/20260725_150246_s2.png and /dev/null differ diff --git a/templates/real/elder_titan/20260725_150711_s1.png b/templates/real/elder_titan/20260725_150711_s1.png deleted file mode 100644 index b3d80e3..0000000 Binary files a/templates/real/elder_titan/20260725_150711_s1.png and /dev/null differ diff --git a/templates/real/hoodwink/20260725_200018_s7.png b/templates/real/hoodwink/20260725_200018_s7.png deleted file mode 100644 index 2fd2bb1..0000000 Binary files a/templates/real/hoodwink/20260725_200018_s7.png and /dev/null differ diff --git a/templates/real/invoker/20260725_160212_s4.png b/templates/real/invoker/20260725_160212_s4.png deleted file mode 100644 index 8ac7257..0000000 Binary files a/templates/real/invoker/20260725_160212_s4.png and /dev/null differ diff --git a/templates/real/juggernaut/20260725_150302_s9.png b/templates/real/juggernaut/20260725_150302_s9.png deleted file mode 100644 index d428e16..0000000 Binary files a/templates/real/juggernaut/20260725_150302_s9.png and /dev/null differ diff --git a/templates/real/kunkka/20260725_210215_s4.png b/templates/real/kunkka/20260725_210215_s4.png deleted file mode 100644 index 75ae874..0000000 Binary files a/templates/real/kunkka/20260725_210215_s4.png and /dev/null differ diff --git a/templates/real/legion_commander/20260725_160212_s1.png b/templates/real/legion_commander/20260725_160212_s1.png deleted file mode 100644 index da97f6c..0000000 Binary files a/templates/real/legion_commander/20260725_160212_s1.png and /dev/null differ diff --git a/templates/real/lich/20260725_150246_s9.png b/templates/real/lich/20260725_150246_s9.png deleted file mode 100644 index c7dfc6b..0000000 Binary files a/templates/real/lich/20260725_150246_s9.png and /dev/null differ diff --git a/templates/real/lion/20260725_150246_s5.png b/templates/real/lion/20260725_150246_s5.png deleted file mode 100644 index 4bd78cb..0000000 Binary files a/templates/real/lion/20260725_150246_s5.png and /dev/null differ diff --git a/templates/real/luna/20260725_210215_s8.png b/templates/real/luna/20260725_210215_s8.png deleted file mode 100644 index 482c708..0000000 Binary files a/templates/real/luna/20260725_210215_s8.png and /dev/null differ diff --git a/templates/real/necrolyte/20260725_150302_s7.png b/templates/real/necrolyte/20260725_150302_s7.png deleted file mode 100644 index 0a5fe46..0000000 Binary files a/templates/real/necrolyte/20260725_150302_s7.png and /dev/null differ diff --git a/templates/real/phantom_assassin/20260725_160212_s7.png b/templates/real/phantom_assassin/20260725_160212_s7.png deleted file mode 100644 index 9e26bb9..0000000 Binary files a/templates/real/phantom_assassin/20260725_160212_s7.png and /dev/null differ diff --git a/templates/real/queenofpain/20260725_210215_s10.png b/templates/real/queenofpain/20260725_210215_s10.png deleted file mode 100644 index e400259..0000000 Binary files a/templates/real/queenofpain/20260725_210215_s10.png and /dev/null differ diff --git a/templates/real/rubick/20260725_160212_s10.png b/templates/real/rubick/20260725_160212_s10.png deleted file mode 100644 index a05f812..0000000 Binary files a/templates/real/rubick/20260725_160212_s10.png and /dev/null differ diff --git a/templates/real/shadow_shaman/20260725_200018_s3.png b/templates/real/shadow_shaman/20260725_200018_s3.png deleted file mode 100644 index c270cb6..0000000 Binary files a/templates/real/shadow_shaman/20260725_200018_s3.png and /dev/null differ diff --git a/templates/real/silencer/20260725_160212_s5.png b/templates/real/silencer/20260725_160212_s5.png deleted file mode 100644 index 6af32c3..0000000 Binary files a/templates/real/silencer/20260725_160212_s5.png and /dev/null differ diff --git a/templates/real/skeleton_king/20260725_150302_s3.png b/templates/real/skeleton_king/20260725_150302_s3.png deleted file mode 100644 index 8775afd..0000000 Binary files a/templates/real/skeleton_king/20260725_150302_s3.png and /dev/null differ diff --git a/templates/real/skywrath_mage/20260725_160212_s6.png b/templates/real/skywrath_mage/20260725_160212_s6.png deleted file mode 100644 index f7683cd..0000000 Binary files a/templates/real/skywrath_mage/20260725_160212_s6.png and /dev/null differ diff --git a/templates/real/sniper/20260725_150302_s4.png b/templates/real/sniper/20260725_150302_s4.png deleted file mode 100644 index aba43fb..0000000 Binary files a/templates/real/sniper/20260725_150302_s4.png and /dev/null differ diff --git a/templates/real/sven/20260725_150302_s5.png b/templates/real/sven/20260725_150302_s5.png deleted file mode 100644 index 5f3101c..0000000 Binary files a/templates/real/sven/20260725_150302_s5.png and /dev/null differ diff --git a/templates/real/tidehunter/20260725_150711_s7.png b/templates/real/tidehunter/20260725_150711_s7.png deleted file mode 100644 index fcb0eb0..0000000 Binary files a/templates/real/tidehunter/20260725_150711_s7.png and /dev/null differ diff --git a/templates/real/undying/20260725_151116_s1.png b/templates/real/undying/20260725_151116_s1.png deleted file mode 100644 index bab95ec..0000000 Binary files a/templates/real/undying/20260725_151116_s1.png and /dev/null differ diff --git a/templates/real/vengefulspirit/20260725_150302_s5.png b/templates/real/vengefulspirit/20260725_150302_s5.png deleted file mode 100644 index 6be133d..0000000 Binary files a/templates/real/vengefulspirit/20260725_150302_s5.png and /dev/null differ diff --git a/templates/real/venomancer/20260725_210215_s2.png b/templates/real/venomancer/20260725_210215_s2.png deleted file mode 100644 index 5a91175..0000000 Binary files a/templates/real/venomancer/20260725_210215_s2.png and /dev/null differ diff --git a/templates/real/viper/20260725_150302_s3.png b/templates/real/viper/20260725_150302_s3.png deleted file mode 100644 index 73198cc..0000000 Binary files a/templates/real/viper/20260725_150302_s3.png and /dev/null differ diff --git a/templates/real/warlock/20260725_150302_s2.png b/templates/real/warlock/20260725_150302_s2.png deleted file mode 100644 index ed37d95..0000000 Binary files a/templates/real/warlock/20260725_150302_s2.png and /dev/null differ diff --git a/templates/real/weaver/20260725_210215_s3.png b/templates/real/weaver/20260725_210215_s3.png deleted file mode 100644 index 911549c..0000000 Binary files a/templates/real/weaver/20260725_210215_s3.png and /dev/null differ diff --git a/templates/real/witch_doctor/20260725_150246_s3.png b/templates/real/witch_doctor/20260725_150246_s3.png deleted file mode 100644 index 6b25fe8..0000000 Binary files a/templates/real/witch_doctor/20260725_150246_s3.png and /dev/null differ diff --git a/templates/real/zuus/20260725_150302_s7.png b/templates/real/zuus/20260725_150302_s7.png deleted file mode 100644 index fbca4f2..0000000 Binary files a/templates/real/zuus/20260725_150302_s7.png and /dev/null differ