# Arrow Out 自动求解研发文档(Java,输入为图片) ## 1. 目标与范围 - 输入:一张关卡截图(包含 UI、背景、彩色箭头线条与箭头头部)。 - 输出: - `List`:每个箭头的 `id`、离散坐标 `(gx, gy)`、方向 `dir(8方向)`。 - `List`:一条可消除序列(按 `id` 返回)。 - 不在范围: - 自动点击/自动操作游戏。 - 复杂的通用 OCR/UI 识别;本方案仅提取箭头(位置+方向)。 ## 2. 规则抽象(用于建模与求解) ### 2.1 箭头属性 每个箭头只保留两类信息: - 位置:`(x, y)`(工程上最终用离散坐标 `(gx, gy)`)。 - 朝向:8 方向之一(`N, NE, E, SE, S, SW, W, NW`)。 > 颜色、线条粗细、形状风格不影响解题,忽略。 ### 2.2 “可消除”判定 对任意箭头 `A`: - 从 `A` 的箭头尖端(tip)沿其朝向作射线。 - 若该射线上存在任意其他箭头(不论颜色与距离),则 `A` **不可消除**。 - 若该射线直到边界都没有遇到任何箭头,则 `A` **可消除**。 消除后会改变其他箭头的判定,因为阻挡关系会变化。 ## 3. 总体架构 分两层: - `Vision`(识别层):图片 → 箭头列表(像素坐标 tip + 方向)→ 离散化网格坐标 `(gx, gy)`。 - `Solver`(求解层):箭头列表 `(gx,gy,dir)` → 消除序列。 接口建议: - `VisionExtractor.extract(Mat screenshot) -> List` - `ArrowOutSolver.solve(List arrows) -> List` - `solveFromImage(String imagePath) -> List`(端到端封装) ## 4. Vision:从图片提取 `(x,y,dir)`(OpenCV Java) ### 4.1 坐标系约定 - OpenCV 图像坐标:`x` 向右增大,`y` 向下增大。 - 求解模型可以沿用该坐标系,也可以转换为数学坐标系(`y` 向上增大)。 - **要求:方向枚举与坐标系必须一致**。 建议:内部统一用 OpenCV 坐标系,方向定义也按“y 向下”为正。 ### 4.2 预处理:裁剪棋盘 ROI(去 UI) 目的:减少误检(顶部文字/按钮/底部工具栏等)。 推荐方案(鲁棒):利用“彩色线条像素”的饱和度/亮度特征定位主区域。 1) 转 HSV:`cvtColor(bgr, hsv, COLOR_BGR2HSV)` 2) 前景掩码:`mask = (S > s0) && (V > v0)`(只抓彩色高饱和区域) 3) 形态学:`close` 填洞、`open` 去噪 4) 找最大连通区域或最大外接矩形 contour,取其外接矩形为 `boardRoi` 输出:`Mat board`(棋盘区域图像)。 ### 4.3 二值化:生成箭头像素掩码 目标:得到只包含箭头线条/箭头头部的 `arrowMask`。 - 在 `board` 上按 HSV 阈值生成二值图(仍然只依赖 S/V): - `arrowMask = (S > s0) && (V > v0)` - 对 `arrowMask` 做 `morphology close/open`,让箭头头部轮廓更连续。 输出:`Mat arrowMask`(0/255)。 ### 4.4 检测箭头头部:定位 tip 与方向 核心思想:箭头头部通常呈“尖角三角形/箭头尖”,几何特征稳定;方向可由“尖点指向”得到。 步骤: 1) `findContours(arrowMask, ...)` 获取轮廓集合 2) 对每个 contour 过滤: - 面积在 `[amin, amax]`(避免把长线条当头部,或把噪声当头部) - `approxPolyDP` 多边形近似 3) 取 tip(尖点) - 对多边形各顶点计算内角,选择 **内角最小** 的顶点作为 `tip` 4) 取中心 `center` - 用 `moments` 计算轮廓质心 5) 方向向量 - `vec = tip - center` - `angle = atan2(vec.y, vec.x)`(按 OpenCV 坐标系) 6) 角度量化到 8 方向 - 每 45° 一档;误差超过 22.5° 的标记为“不确定”进入兜底 7) 去重/聚类 - 按 `tip` 的欧式距离把相近候选聚成一簇(阈值 5~15 像素) - 每簇保留面积最大的候选 输出(像素级):`List{ tipX, tipY, dir }`。 ### 4.5 离散化:从像素坐标到网格坐标 `(gx, gy)` 求解层需要稳定的“同一行/列/对角线”关系,建议把像素 tip 坐标离散化到网格。 推荐做法:对 `x` 与 `y` 分别做 1D 容差聚类。 1) 收集全部 `tipX`,排序 2) 从左到右扫描:相邻差值 < `tx` 的归为同一簇 3) 每簇中心取均值 `cx`,簇序号即 `gx` 4) 对 `tipY` 同理得到 `gy` 参数建议: - `tx/ty` 可取“箭头头部平均宽度”的 0.5~1 倍。 - 或先验经验值(需按分辨率调参):10~25 像素。 输出:`List{ id, gx, gy, dir }`。 ### 4.6 识别质量校验(必须做) - 方向置信度:量化前角度与目标方向夹角 > 22.5° 视为不可信。 - 唯一性:同一 `(gx,gy)` 不应出现多个箭头(出现则说明去重/聚类失败)。 - Debug 可视化:在 `board` 上画 tip 点与方向短线,输出调试图,便于快速调参。 ### 4.7 兜底方案(可选增强) 若几何检测不稳: - 模板匹配:8 个方向头部模板,匹配得到方向与 tip。 - 轻量检测模型:训练在 Python,推理可用 ONNXRuntime Java 或 OpenCV DNN。 ## 5. Solver:由 `(gx,gy,dir)` 求消除序列 ### 5.1 blocker/blockedBy 核心结构 对每个箭头 `i` 维护: - `blocker[i]`:沿自身方向最近的箭头 id;若不存在为 `-1`。 - `blockedBy[i]`:所有当前把 `i` 当作 blocker 的箭头集合(反向索引,用于增量更新)。 - `ready`:所有 `blocker == -1` 且未删除的箭头队列。 流程:不断从 `ready` 取出可消除箭头删除,并只更新受影响的箭头(即 `blockedBy[removed]`)。 ### 5.2 最近阻挡者查询(4 类直线索引) 8 方向只会落在 4 类直线: - 行:`y = const`,按 `x` 排序 - 列:`x = const`,按 `y` 排序 - 主对角线:`x - y = const`,按 `x` 排序 - 副对角线:`x + y = const`,按 `x` 排序 建议索引结构: - `Map> rows; // y -> (x -> id)` - `Map> cols; // x -> (y -> id)` - `Map> diags; // (x-y) -> (x -> id)` - `Map> antiDiags; // (x+y) -> (x -> id)` `computeBlocker(i)`: 1) 根据 `dir` 选择索引与外层 key 2) 使用 `TreeMap.higherEntry(index)` 或 `lowerEntry(index)` 找最近邻 3) 找到则返回其 id,否则返回 `-1` 删除箭头时同步从 4 套索引中移除,确保后续查询不会返回已删除对象。 ### 5.3 多候选时的选择策略 - 优先消除 `blockedBy[u].size()` 最大的箭头(更可能解锁更多箭头)。 - 或用简单队列先做一版;若出现卡死再引入回溯(见 6)。 ## 6. 无解与回溯(增强项) 纯“可消除就删”的贪心可能在某些关卡卡死(`ready` 为空但仍有剩余)。 可选策略: - `A(先落地)`:贪心卡死即返回“无解/需回溯”。 - `B(增强)`:在 `ready` 多候选时做 DFS 回溯;需实现状态回滚(操作日志优于全量拷贝)。 - `C(启发式)`:结合入度优先等启发式减少回溯概率。 ## 7. Java 类型与接口建议 ### 7.1 数据结构 - `enum Direction { N, NE, E, SE, S, SW, W, NW }` - `class Arrow { int id; int gx, gy; Direction dir; boolean removed; }` ### 7.2 核心类 - `class VisionExtractor { List extract(Mat screenshot); }` - `class ArrowOutSolver { List solve(List arrows); }` - `class Pipeline { List solveFromImage(String path); }` ## 8. 测试与验收标准 ### 8.1 Vision 层 - 能从给定截图中输出合理数量的箭头(不漏检、不明显误检)。 - 输出的 `(gx,gy)` 唯一且稳定(同一箭头在同一位置)。 - 输出方向 `dir` 与肉眼一致(允许少量不确定,但需可追踪)。 ### 8.2 Solver 层 - 对构造的小规模数据集:`solve` 输出序列长度等于箭头数量。 - 删除序列合法:每一步删除的箭头在当步满足 `blocker == -1`。 - 若卡死:能明确返回失败(或触发回溯策略)。 ## 9. 调参建议(研发流程) 1) 先固定 ROI(或实现自动 ROI) 2) 调 `s0/v0` 使 `arrowMask` 只保留彩色线条 3) 调形态学核大小使箭头头部轮廓连续 4) 调 `amin/amax` 让头部候选稳定 5) 调 `tx/ty` 让网格离散化不抖动 6) 用 debug 图逐轮验证 tip 与方向