---
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`)