面向Google编程CHARLES ZHANG

AI DAILY / 2026-09-20

geo-sleuth:能展示推理过程的图片地理定位Agent技能

Oldcircle/geo-sleuth

AI 编程实践GitHub · 2026-09-18

全文中文翻译 · AI 生成,仅供学习交流

Oldcircle/geo-sleuth

,运行脚本,回传相机位置、相机朝向,外加一张卫星证据图。想自己复制文件夹?看 Installation。

为什么是 geo-sleuth

一张照片,一句话。

把一张照片丢给智能体(agent),告诉它「找到这张照片是在哪儿拍的」。它会回你相机位置、相机朝向,再加一张卫星证据图。

画面里什么字都没有的时候,它也能干活。

没有路牌、没有车牌、没有地标,靠 OpenStreetMap 的几何数据、海拔数据、卫星瓦片和街景,搜索照样能跑下去。

几何替掉猜测。

桥墩间距当距离尺,阴影当方位角,山脊线变成可与海拔数据匹配上的指纹。

每个结论都要落到一个文件上。

结论必须说清楚这次会话里实际跑过哪条命令,生成了哪个文件。人口和名气不算证据。

脚本排,模型挑。

二十个单一用途的脚本负责搜索、打分、排序,模型只在排在前面的那几个里挑。

一项技能,所有智能体通用。

标准的 Agent Skill,SKILL.md 加纯 Python 脚本,同一份文件夹在 Claude Code、Codex、Cursor、Gemini CLI、OpenCode、GitHub Copilot 里都能跑。

答案带误差半径。

坐标 ± 半径、相机朝向、一张证据图,外加一个分过级的置信度。

案例:一张照片,画面里什么字都没有

一张手机拍的、EXIF 已被抹掉的照片,收割过的稻田边摆着一台白色烤箱,远处是一座长长的高架桥,右侧是一座陡山。画面里没有一个字符。只要把这张照片发给一个装了这个技能的智能体,它就回传了相机位置和镜头朝向。

photo → 27,335 → 171 → 14,372 → 22 → 3 → 1 → ±2 m| Step(步骤) | What it did(做了什么) | Candidates left(剩余候选) |
|---|---|---|
| Read the photo(读图) | 高架桥上的杆子是接触网支柱,所以这是电气化铁路。把桥墩间距当尺子(按 32 m 跨度估算),左侧那段约 0.5 km 外,右侧那段超过 1 km。陡山大约在 3 km 外。稻子已收、草还青,说明还没下过霜。 | |
| South China, as a bet, not a proof(中国南方,赌一把,不是证明) | | |
| Region scan(区域扫描) | 从 OpenStreetMap 拉出区域内每一条铁路桥梁,共 27,335 段。每 400 m 取一个点,根据海拔数据算出每个点的 360° 地平线。只保留「附近地势平坦、几公里内有座清晰的山、山旁边的地平线也是平的」的点。 | 171 个点 |
| Skyline fit(天际线拟合) | 在每个候选点周围放一圈候选相机位置,渲染从每个位置看到的山脊线,共 14,372 个位置。排名前 20 的结果彼此相差不到 0.1°,于是再加一条约束,桥梁必须在左侧近、右侧远。 | 22 个候选 |
| Overlay check(叠加检查) | 把前三名的山脊线画回照片上。#1(福州)那条线在烤箱后面藏了个凸起,这就是它分数高的原因。#3(惠州)的坡度走向和照片里那段平缓对不上。#2(清远)从山脚一直合到画面边缘都对得上。 | 3 个候选 |
| Pier count(桥墩计数) | 照片里 17 个桥墩相当于从相机出发的 17 条方位线。它们打到铁路线上的交点,间距必须均匀。再叠上天际线约束,先是一条约 300 m 长的带状区域,再收束到一个点。 | 1 个候选 |
| ±2 m | 桥墩当尺子用,左侧间距宽说明近,右侧间距紧说明远。 | |

左:排名前三的山脊线被叠加到照片上。右:该技能生成的证据图。

这次跑出来的更多图

区域扫描,区域内每一条铁路桥梁(灰色),以及通过地平线测试的候选点(橙色)。

桥墩计数,指向 17 个桥墩的方位线与铁路相交,只有一个相机位置能让间距保持均匀。

整个流程端到端跑了约 72 分钟,差不多一半时间在等计算。

安装

geo-sleuth 是个标准的 Agent Skill,一个文件夹,里面有 SKILL.md、scripts/、references/ 和 data/。用 skills CLI 装,或者自己复制。

六个智能体、用户级、一条命令搞定:

npx skills add Oldcircle/geo-sleuth -g -a claude-code -a codex -a cursor -a gemini-cli -a opencode -a github-copilot -y

手动来:

git clone https://github.com/Oldcircle/geo-sleuth
mkdir -p ~/.agents/skills ~/.claude/skills
cp -r geo-sleuth/skills/geo-sleuth ~/.agents/skills/
# Codex、Cursor、Gemini CLI、OpenCode、GitHub Copilot
ln -s ~/.agents/skills/geo-sleuth ~/.claude/skills/geo-sleuth
# Claude Code

~/.agents/skills/ 会被 Codex、Cursor、Gemini CLI、OpenCode 和 GitHub Copilot 读到,所以一份拷贝就覆盖五个。每个智能体自己的文件夹,按各自文档来:

| 智能体 | 用户级 | 单项目 |
|---|---|---|
| Claude Code | ~/.claude/skills/ | .claude/skills/ |
| Codex | ~/.agents/skills/ | .agents/skills/ |
| Cursor | ~/.cursor/skills/~/.agents/skills/ | .cursor/skills/.agents/skills/ |
| Gemini CLI | ~/.gemini/skills/~/.agents/skills/ | .gemini/skills/.agents/skills/ |
| OpenCode | ~/.config/opencode/skills/~/.agents/skills/ | .opencode/skills/.agents/skills/ |
| GitHub Copilot | ~/.copilot/skills/~/.agents/skills/ | .github/skills/.agents/skills/ |任何其他能读 SKILL.md 并跑 shell 命令的智能体都一样,把文件夹放到它找技能的地方就行。

工作原理

活儿分三层。脚本拍板,脚本看图排序,模型只在排在前面的几个里挑。

flowchart LR
    A["photo"] --> B["intake.py<br/>EXIF · OCR · reverse image search"]
    B --> C["board.py<br/>候选板:线索、似然比、排序、下一步"]
    C --> D{"哪条分支?"}
    D --> E["sun.py · terrain.py · osm.py · pose.py<br/>阴影、天际线、OSM 走廊、相机姿态"]
    D --> F["sat_scan.py · match.py · gsv.py · baidu_pano.py<br/>CLIP 打分的卫星瓦片、DINOv2+SIFT 街景"]
    E --> G["board.py 校验 · 报告"]
    F --> G
    G --> H["evidence.py<br/>坐标 ± 半径 · 证据图 · 分级置信度"]

Loading| Layer(层) | Who(谁) | Tools(工具) |
|---|---|---|
| Decide(决策):选哪些候选、证据怎么打分、什么能排除、下一步扫哪儿 | 脚本(候选板) | board.py |
| Perceive(感知):读文字、查表、在卫星瓦片里找目标、对比街景 | 脚本先排序,再由人看前几个 | intake.py ocr.py clues.py sat_scan.py match.py geo.py |
| Judge(判断):从画面里抽线索、抛假说、在排好序的几个里挑 | 模型 | SKILL.md references/ |每个结论都得指向这次会话里实际跑过的命令和它生成的文件。要排除必须有读过的或算出来的证据,光看和猜只能压低候选的权重。

工具箱

二十个脚本,一人干一件事。完整表格和数据源在 skills/geo-sleuth/references/data-sources.md

| 功能 | 脚本 |
|---|---|
| EXIF:GPS、拍摄时间、等效焦距、朝向 | exif.py |
| OCR:对整图、缩放裁剪和分块瓦片做文字识别(macOS 用 Apple Vision,其他平台用 RapidOCR) | ocr.py |
| 百度和 Yandex 反向图像搜索,把相似图拼成带编号的拼图,按关键词搜图 | revimg.py |
| 步骤 0–3 一条命令搞定,元数据、边缘裁剪、变体、OCR、反向搜索 → intake.md | intake.py |
| 缩放裁剪、边缘和角落裁剪、平铺分块、等距结构(比如桥墩)的像素列 | imgprep.py |
| 速查表,车牌前缀、固定电话区号、电话区号、行驶方向、属地、行政区划 | clues.py data/ |
| 候选板,候选、线索、似然比、排除、排序、扫描顺序、出报告前的校验 | board.py |
| 地名词典,列出带边界框的子行政区、城镇建成区范围 | gazetteer.py |
| 地名/复合名/店名 → 坐标候选,列出每一个同名地 | poi.py |
| 太阳和阴影,纬度带、时段、街道走向、从受光面得朝向、真方位 | sun.py |
| OSM Overpass,要素共现、线到点、路径走廊、线交叉、街道网格模板 | osm.py |
| 卫星瓦片拼接、标记、带编号的缩略拼图 | tiles.py |
| 对卫星网格单元或候选点做 CLIP 零样本打分(跑道、工厂、筒仓、水坝……) | sat_scan.py |
| 百度全景 / Google 街景,找点位、渲染朝向、缩略拼图、历史批次 | baidu_pano.py gsv.py |
| 把候选地面图像和实拍图排序,DINOv2 全局相似度 + SIFT 内点 | match.py |
| 海拔,合成山形视图、天际线叠加、剖面;线性要素 × 地形扫描、山脊提取、批量天际线打分 | terrain.py |
| 多点相机姿态,经纬度、高度、朝向、俯仰、横滚,附带误差半径 | pose.py |
| 方位、距离、通视相交、对齐线、画面/遮挡检查、由等距结构求相机位置 | geo.py |
| 证据图,卫星瓦片 + 相机视锥 + 对比网格 | evidence.py |上面案例里的三个步骤(区域扫描、批量天际线打分、由桥墩间距求相机位置)被做成子命令直接内置在技能里,terrain.py scan / ridge / fitimgprep.py piersgeo.py spacing。专门为那张照片调过的案例脚本放在 examples/rail-skyline-session/ 里作参考。

基准测试

按算子测的指标:

| 脚本 | 测试 | 结果 |
|---|---|---|
| match.py | 8 个案例,把一批历史百度全景渲染成照片形式,候选取 150 m 范围内的全景(深圳) | 真值排名 1/2/4/1/1 和 5/1/6,全部进前 6,一半是 #1 |
| sat_scan.py | 4×8 km,364 个 z17 网格单元,40 个带 OSM 标签的跑道做真值,多尺度(深圳) | recall@20 17/40,@30 22/40,@100 32/40,中位排名 23 |
| terrain.py scan / fit geo.py spacing | 在上面那张案例照片上做有界复跑 | 真值聚类排第 1,最终位置离真值约 2 m |
| clues.py | 抽查 6 张表里的 9 个值 | 9/9 正确 |这套方法是从拆解 14 段在线地理定位创作者的视频、22 道谜题和若干真实运行案例来的,把里头管用的部分提炼成规则和脚本。v2 把凡是能写成代码的规则都搬进了 board.py,让规则真正被执行,而不只是被人读一读。

环境要求

Python 3.10+、uv,再一个能跑 shell 命令的智能体。每个脚本自己声明依赖,uv run 在第一次用时自动装。

可选:Google Chrome 用来跑反向图像搜索(uvx playwright install chromium 也行);用 export GEO_PROXY=socks5h://127.0.0.1:<port> 把所有联网脚本走代理。

路线图

- 在合成地形案例上对 terrain.py scan / fit 做算子级测试
- Google Lens 作为第三个反向图像搜索引擎
- Linux 和 Windows 上的 CI
- 一套公开的盲测照片集,给出端到端准确率

贡献

欢迎提 Issue 和 Pull Request,详见 CONTRIBUTING.md。最有用的贡献是,往 references/clues/ 加一条能迁移的线索(附出处),加一份带许可证的新数据源,或者拿你自己的照片跑一次、说清楚技能哪儿出了岔子、为什么。

致谢

OpenStreetMap 贡献者(ODbL)。本仓库没有打包任何 OSM 数据;脚本都是实时去查的。发布查询结果时请注明 © OpenStreetMap contributors(译注:ODbL 即 Open Database License,要求衍生作品以相同方式共享并署名)。