An agent skill that finds where a photo was taken — OpenStreetMap geometry, elevation skylines, satellite imagery and street view — and shows its work. Works with Claude Code, Codex, Cursor, Gemini CLI, OpenCode and GitHub Copilot.

★ 6,775⑂ 840👁 15⑃ 46● 26▤ 0◈ 0
PythonMITCreated 2026-09-18
6,775
stars
+136
840
forks
15
watchers
46
prs
26
issues
0
releases
0
discussions

README

🧭 geo-sleuth

一个 agent skill,能找到照片的拍摄地点——并展示它的推理过程。


  
  
  



  
  
  
  
  



  适用于
  
  
  
  
  
  
  ……以及其他任何能读取 SKILL.md 并运行 shell 命令的 agent。




没有文字,没有车牌,没有地标。一座桥,一座山。定位误差在 2 米以内。



---

 快速开始

bash
npx skills add Oldcircle/geo-sleuth


按提示勾选你用的 agent。然后把一张照片交给你的 agent,说:

 找出这张照片是在哪拍的

这就是全部的交互方式。你的 agent 会读取 SKILL.md、运行脚本,然后给出机位、镜头朝向和一张卫星证据图。想自己复制文件夹?见安装。

 为什么是 geo-sleuth

- 一张照片,一句话。 把照片交给你的 agent,说一句这张照片是在哪拍的。你会得到机位、镜头朝向和一张卫星证据图。
- 没有文字线索也能查。 没有招牌、没有车牌、没有地标:OpenStreetMap 的几何数据、高程数据、卫星图和街景自己就能把搜索推进下去。
- 靠几何,不靠猜。 桥墩间距变成距离尺,阴影变成方位角,山脊线变成能和高程数据比对的指纹。
- 每个说法都指向一个文件。 结论必须说出本次会话里跑过的命令、以及它产出的文件。人口和名气都不算证据。
- 脚本排序,模型判断。 二十个各司一职的脚本负责搜索、打分和排序;模型只在排名靠前的几个里挑。
- 一个 skill,所有 agent 通用。 这是一个标准的 Agent Skill——SKILL.md 加上普通的 Python 脚本——所以同一个文件夹在 Claude Code、Codex、Cursor、Gemini CLI、OpenCode 和 GitHub Copilot 里都能运行。
- 答案自带误差半径。 坐标 ± 半径、镜头朝向、一张证据图和分档的置信度。

 案例

这几张照片都没有 GPS 信息。每个案例写了 skill 从照片里读到什么、怎么一步步缩小范围、结果离确认的机位有多远。

这些是某个模型某一次运行的记录。skill 告诉 agent 怎么做,每一步查什么、怎么筛、看前几名由模型自己决定,换模型或换决策不保证走到同一个结果。每个案例下面的「复现条件与检查点」列出了输入、提示、关键参数和各阶段应该覆盖到什么,可以用来逐段对照自己的运行在哪一步分开。








1 · 稻田与高架铁路桥 × 天际线 × 数桥墩。
2 · 沙漠天际线高压线 × 天际线 × 手机歪了 1°。
3 · 樱花街行道树开放数据 × 影子 × 街景。


±2 m广东清远
100 m 以内青海大柴旦
3 m加拿大温哥华



 1. 稻田与高架

一张手机照片,EXIF 信息已被抹除:收割完的稻田边一个白色烤炉,远处一条长长的高架,右边一座陡峭的山。画面里没有一个字。给装了这个 skill 的 agent 发一条消息,它就给出了机位和镜头朝向。

照片 → 27,335 → 171 → 14,372 → 22 → 3 → 1 → ±2 m

| 步骤 | 它做了什么 | 剩余候选 |
|---|---|---|
| 读图 | 高架上的杆子是接触网支柱,说明是电气化铁路。把桥墩间距当尺子用(跨距 32 米,假设):左段距拍摄点约 0.5 公里,右段超过 1 公里。陡峭的山约 3 公里外。稻子已经收割,但草还是绿的,说明还没下霜。 | 华南,是判断,不是证明 |
| 区域扫描 | 从 OpenStreetMap 取出该区域内所有铁路桥:27,335 段。每 400 米采样一个点,用高程数据计算每个点 360° 的地平线。保留附近地势平坦、几公里内有明显山峰、且旁边地平线平坦的点。 | 171 处 |
| 天际线拟合 | 在每个候选点周围布置候选机位,渲染每个机位看到的山脊线:14,372 个机位。前 20 名彼此相差不到 0.1°,于是加了一条约束:桥必须左近右远。 | 22 |
| 叠图检查 | 把前三名的山脊线画回照片核对。第一名(福州)有一处凸起正好藏在烤炉后面,所以得分高;第三名(惠州)在照片平坦的地方却有坡度;第二名(清远)从山脚到画面边缘都贴合。 | 1 |
| 数桥墩 | 照片里的 17 根桥墩变成从机位出发的 17 条方位线。它们打在铁路线上的交点必须间距均匀。结合天际线结果:先收窄到约 300 米长的一段,再收窄到一个点。 | ±2 m |



桥墩当尺子:左边间距宽表示近,右边间距密表示远。
 
左:前三名的山脊线画回照片。右:skill 生成的证据图。



本次实战的更多图


区域扫描:区域内所有铁路桥(灰色),通过地平线检验的候选点(橙色)。

数桥墩:17 根桥墩的方位线与铁路线相交;只有一个机位能让间距均匀。
整次运行耗时约 72 分钟,其中大约一半时间在等计算。



复现条件与检查点


- 这是什么记录:上表是这套方法第一次解出这张照片的过程(Claude Opus 5.0),不是盲测复跑;其中的步骤后来写进了 skill。
- 输入:手机照片 1280×1707,EXIF 已抹除。README 里的图是展示用的版本,不适合当输入。
- 候选生成:区域内 OSM 的全部铁路桥 27,335 段,只排除标了 electrified=no 的(当时的 rail_mtn_scan.py 这样做,现在 terrain.py scan 默认也一样),没有按 highspeed 筛。接触网只能说明是电气化铁路;正确的线路是广清城际,它在 OSM 里的 55 段桥都没有 highspeed=yes。
- 检查点:通过地平线检验的 171 处里应包含答案附近的广清城际桥段;天际线拟合 14,372 个机位,前 20 名相差不到 0.1°,靠"桥左近右远"筛到 22;叠图看前三名;最后用 17 根桥墩的方位线定点。
- 案例脚本:examples/rail-skyline-session/ 是当时按这张照片调好参数的脚本。OSM 会把一条线拆成很多段同名的 way,joint.py 会把候选中心附近的同名线路逐条试一遍,结果不受查询返回顺序影响。



 2. 沙漠天际线差了一度

前景是一道沙丘,后面一排深色裸山,山脚有几座电塔。画面里没有字、路和房子,以图搜图只搜到一些泛泛的西北沙漠照片。



照片 → 1,537 处 → 卡在 10 px 上下 → 解出 1° 歪斜 → 1 处 → 一道沙脊

| 步骤 | 它做了什么 | 剩余 |
|---|---|---|
| 读图 | 电塔是画面里唯一的人造物,所以机位离某条高压线不过几公里。沙、戈壁和裸岩指向中国西北。 | 4 省 |
| 高压线 × 地形 | osm.py geom 拉出新疆、甘肃、宁夏、青海的全部高压线,terrain.py scan 沿线用高程数据算,留下山能占满视野的地方。 | 1,537 处 |
| 天际线拟合 | terrain.py ridge 描出照片里的山脊,t

Top issues

Snapshot: 2026-11-10T09:35:25Z · Data license Apache-2.0 · JSON