--- name: map-road-extractor description: 从地图截图(高德/百度/Google/OSM 截图、校园或园区平面图)自动提取可寻路的道路网络,输出路网叠加图、栅格化寻路数据与最短路径计算结果。当用户需要「让地图能算路线」「从截图里提取路网」「做校园或园区导航」「规划多点跑腿路线」「计算两点骑行时间」而手头没有路网数据时使用。适用于任何支持 SKILL.md 的 Agent(Claude Skills、DeepSeek Harness 等),也可作为独立命令行工具运行。 license: MIT --- # 地图截图 → 可寻路路网 把一个**只能看的截图**变成**能算最短路的图结构**。 ## 何时使用 触发场景: - 用户给了地图截图,问"从 A 到 B 骑车多久、怎么走" - 需要多目标路径规划(取快递 + 取外卖 + 上课顺路串起来) - 有地图但没有路网数据,也不想依赖 OSM(校园内部道路在 OSM 上常年缺失或过时) - 要做一个园区/校园导航,但拿不到楼栋级精度的导航 API **不适用**:卫星影像(没有矢量道路线条)、道路与背景对比度极低的手绘图。 ## 核心原理(四步) ``` 地图截图 │ ├─ ① 阈值掩膜 道路通常被画成高亮白色线条 │ 条件:min(R,G,B) ≥ 243 且 max−min ≤ 9 │ ├─ ② 形态学净化 开运算削掉文字/图标的细笔画 │ 闭运算把被削断的路面接回 │ ├─ ③ 裁切 去掉地图自带搜索框/工具栏 + 城外孤立道路 │ └─ ④ 栅格化 膨胀弥合断口 → 按格内密度判定道路格 → 按行 RLE 压缩 ↓ 可供 Dijkstra 直接寻路 ``` ## 用法 ```bash pip install numpy pillow scipy python road_from_map.py --image 地图.png --out ./out ``` 产物: | 文件 | 用途 | | --- | --- | | `road_overlay.png` / `road_overlay_b64.txt` | 半透明青色路网层,直接 `` 盖在底图上 | | `roadgrid.json` | 栅格路网(RLE 压缩),浏览器端可直接跑 Dijkstra | | `base_map_cropped.png` | 裁切后的底图(与 roadgrid 坐标对齐) | | `roads_only.png` | 纯路网二值图,肉眼验收提取质量 | | `debug_overlay_on_map.png` | 红线路网压在底图上,核对贴合度 | ## 参数与调参 ### 掩膜阈值 | 现象 | 调整 | | --- | --- | | 浅色街区被误判成路 | `--min-channel 246 --max-sat 6` | | 较浅的路面被漏掉 | `--min-channel 240 --max-sat 10` | ### 形态学净化(关键) - **`--open-r 2`(默认)**:地名文字笔画约 3~5px,半径 2 的开运算能整段削掉。 **半径不要超过 3** —— 实测 r=3 会连细路面一起削掉 52% 的像素。 - **`--close-r 3`(默认)**:把开运算削断的路面接回。 - **顺序不能反**:先闭后开会让文字与路面粘连,反而更难清理。 ### 裁切 - 地图截图的搜索框、工具栏**本身就是白色块**,会被误判成路,**必须裁掉**。 用 `--crop x0,y0,x1,y1` 显式指定。 - 不指定时按最大连通域外接矩形自动裁切,可去掉城外的孤立道路。 ### 寻路网格 | 参数 | 说明 | | --- | --- | | `--cell 14` | 网格边长。越小路线越贴合,但 JSON 越大 | | `--dilate 5` | **最重要的参数**。见下方"坑 1" | | `--density 0.18` | 格内路面占比达到该值即视为道路格 | ## 三个必须知道的坑 ### 坑 1:视觉层和寻路层必须用两份掩膜 形态学去文字的同时会把细路面削成虚线。**直接拿净化后的掩膜建寻路网格会断链**, Dijkstra 被迫绕远最高可达 8 倍(实测:某路段 804px 变成 3391px)。 正确做法(工具已内置): | 层 | 掩膜 | 为什么 | | --- | --- | --- | | 视觉层 | 净化后 | 干净、无文字 | | 寻路层 | 原始未净化 | 连通完整、不绕远 | 配合 `--dilate 5`,把最大绕远倍率从 8.8 压到 2.08。 ### 坑 2:不要用 CSS mask 给路网着色 试过 1-bit PNG + `-webkit-mask-image`(体积只要 20KB),结果**整张地图被铺满颜色**—— mask 静默失效且不报错。改用 Python 直接生成 RGBA PNG 当普通 `` 叠加, 体积多几十 KB,但 100% 可靠。 ### 坑 3:文字与道路宽度有重叠,单靠宽度切不干净 实测路面宽度 4~28px,而文字笔画也能到 4px。所以: - 连通域级别的"笔画宽度判别"对**连在道路上的文字**无效(会被整体放行) - **必须用形态学开运算做像素级剥离**,不能只靠连通域统计 ## 速度标定 `--px-per-min` 把像素长度换算成时间。标定方法: 找一个已知实际距离或耗时的路段,量出像素长度反推。 参考量级:一张 2600px 宽、覆盖约 1.5km 的校园图,骑行 15km/h → **约 400 像素/分钟**(步行约 160)。 ## 在浏览器端寻路 `roadgrid.json` 的 RLE 结构在前端重建道路格后可直接跑 Dijkstra: ```js const road = new Uint8Array(GW * GH); for (let r = 0; r < GH; r++) for (const [a, b] of DATA.rows[r]) for (let c = a; c <= b; c++) road[r * GW + c] = 1; ``` 要点: - **8 邻接**,斜向代价 √2,且**斜向移动必须至少有一个正交邻居也是道路**,防止斜穿拐角 - 地标先**吸附到最近道路格**(逐环向外搜索),再算路 —— 这就是"骑车到楼前,再步行进楼" - 路径用 **Douglas-Peucker** 简化后再绘制,避免几千个点 - 用 `--dilate` 保证连通后,正常情况下不应出现"不通" ## 验证方法(必做) **不要靠读代码判断前端渲染是否正确。** 用无头浏览器截图实际看: ```bash msedge.exe --headless=new --disable-gpu --hide-scrollbars \ --window-size=1900,1150 --virtual-time-budget=15000 \ --screenshot=out.png "file:///绝对路径/page.html" ``` 截图判断标准: | 现象 | 含义 | | --- | --- | | 颜色铺满整图 | mask 失效 | | 线条虚断 | 连通性问题,加大 `--dilate` | | 路线绕大圈 | 网格断点 | ## 回归自检清单 每次提取完至少核对: 1. `roads_only.png` 里**还有没有文字残留**(有则加大 `--open-r`,上限 3) 2. `debug_overlay_on_map.png` 里**红线是否贴着底图白路**(不贴则调阈值) 3. 跑 10 条以上不同方向的路线,**绕远倍率(路径长 ÷ 直线距离)是否都 < 2.6** 4. 有没有"不通"的地点组合(有则加大 `--dilate` 或调低 `--density`)