用 OpenStreetMap 造一座冰岛小城:2 万多栋楼里,只有 36 栋的高度被测量过
把冰岛哈布纳菲厄泽 168 平方公里的 OSM 建筑数据 + SRTM 真实地形做成 3D 微缩城市,并借此揭示开放地理数据的真相:屏幕上绝大多数楼的高度其实都是推算出来的。
最近看到有人用 AI 在 43 分钟里把整座首尔做成 3D 微缩模型,26.7 万栋建筑可以旋转、缩放、昼夜切换。视觉效果确实漂亮。
但让我更在意的是原文里那句不起眼的声明:
Where height data is unavailable, estimated heights derived from the source data are used. (在缺少高度数据的地方,使用基于源数据推算的估算高度。)
这句话到底占多大比重?我决定自己量一次。
一、先说结论:92.5%
我选了冰岛的港口小城哈布纳菲厄泽(Hafnarfjörður),取它整片建成区与周边熔岩地貌,共 168 平方公里、20,632 栋建筑,连同真实地形一起做成 3D 模型。然后统计每一栋的高度是从哪来的:
| 高度来源 | 建筑数 | 占比 |
|---|---|---|
OpenStreetMap 实测 height 标签 | 36 | 0.2% |
由 building:levels 楼层数折算 | 1503 | 7.3% |
| 按建筑类型与占地面积估算 | 19,093 | 92.5% |
整座城 2 万多栋楼里,只有 36 栋的高度是被真正测量过的——不到千分之二。
把范围缩小到市中心最密的那 2.12 平方公里(1839 栋)时,实测 height 是 0 栋;放大到全市尺度,才有了这 36 栋零星记录。作为对照,我实测首尔市中心是 10.5% 带 height、35.1% 带 building:levels。
在交互页面里把配色切到「数据来源」,整片城区会瞬间变成灰蓝色——那就是被推算出来的部分。绿色(实测)只有零星几点,黄色(楼层折算)稀疏分布,其余全是蓝。
二、高度是怎么算出来的
开放数据里,一栋楼的高度有三个来源,可靠性依次递减:
- 实测(
height):有人真的量过或查过图纸。全市 36 栋。 - 楼层折算(
building:levels):知道有几层,按每层约 3.2 米乘上去。全市 1503 栋。 - 类型与面积估算:既无高度也无层数,只能猜。全市 19,093 栋。
第三档的规则是我按建筑用途和 footprint 面积定的,大致是:
- 车库、棚屋:3 米
- 独栋住宅:按占地分档,6 / 7 / 9 米
- 公寓楼:按占地开方推层数(2–6 层),再乘 3.2 米
- 商业:8 米;厂房与学校:9 米;教堂:16 米
这套规则谈不上精确,但它是显式、可核查、可改进的——这比隐藏在数据黑箱里的”AI 生成”要诚实。
三、为什么这里几乎没数据
众包地理数据的完整度,与”当地有多少人愿意录入”强相关。
首尔是全球关注度极高的都市,OSM 贡献者众多;而哈布纳菲厄泽是三万多人的冰岛港口城市,几乎无人去标注建筑高度。这不是数据错误,而是众包数据固有的地理不均衡。
这件事值得记住:任何基于 OSM 的”数字孪生”“城市大模型”,其精度在全球范围内是高度不均的。在西欧和日韩的城市里,它可能相当接近现实;换到南美、非洲或小城镇,可能就只剩下轮廓。
四、但”低平”本身是真实的
一个重要的反驳是:会不会因为 92.5% 都是估算,所以这座城看起来才这么平?
不会。看已知数据:
- 有楼层记录的建筑集中在 1–2 层和 4 层
- footprint 中位数仅 142 平方米(小住宅)
- 全市高度中位数 6.4 米,P90 9.0 米,最高约 77.6 米
这是一座真实的低层小城,最高那几栋也只是公寓楼级别。把大量建筑估成 6–7 米,误差远小于在摩天楼城市里做同样的估算。平坦的天际线不是数据缺陷造成的假象,它就是这座城市本来的样子。
反过来说,如果同样的估算规则用在首尔或香港,误差会被放大到不可接受——因为那里的高度方差极大。
五、数据链路
从原始数据到屏幕,一共六步:
- 抓取:Overpass API 查询
building与natural/landuse/waterway/geological/highway/man_made等要素,返回带几何的 JSON;地形高程另取 AWS Terrain Tiles(SRTM) 的 terrarium PNG 解码。公共 Overpass 实例对大范围查询会返回 504,需要分片。 - 量化:坐标相对原点转为整数分米,体积从每栋 820 字节压到 62 字节。
- 赋高:按上面三档规则给高度,并同时标记来源(0/1/2),这个标记一路带到渲染阶段用于配色。
- 地形:SRTM 高程双线性采样到 50 米网格,叠加 OSM 海岸线、河流、熔岩地、植被等面状要素,按高程 / 坡度 / 地表类型上色。
- 打包:20,632 栋 + 169 平方公里地形共 3.0 MB(gzip 1.1 MB),以
window.CITY挂载。 - 渲染:浏览器端还原轮廓 → 挤出体块 → 合并成单个几何体 → 一次 draw call;地形约 80 万顶点。
这里有个技术细节值得记一笔:我最初用扇形三角化 roofs,结果发现 超过一半的建筑 footprint 是凹多边形,扇形会在凹陷处产生翻转或零面积的三角形。改成逐三角形判断叉积符号强制朝向、并跳过退化面后,退化面归零,所有屋顶法线朝上、侧面法线 100% 朝外。
六、这一版做了什么扩展
相比最初 2.12 平方公里的核心区块,这一版把范围扩到了 168 平方公里,并补上了真实地貌:
- 真实地形:SRTM 高程,最低 −41 米(海底),最高 658 米的山丘,海域占 15.3%;地形默认夸张 1.8 倍以看清起伏。
- 海岸与河流:104 段海岸线勾勒出峡湾与半岛,268 条河流 / 溪流以蓝色廊道穿过城区。
- 熔岩地貌:144 片火山熔岩原、175 片裸岩,按真实冰岛配色(深玄武岩灰 + 苔原荒野的橄榄褐)呈现,未标注的裸地也用伪噪声打散成自然的熔岩—荒野过渡,而不是单调色块。
- 港口设施:11 座码头伸出海面。
未标注地表之所以能”像真的”,是因为我按冰岛实际景观做了分层着色:低处熔岩原与苔原,随高程过渡到裸岩、峰顶残雪,陡峭处显岩、高处积雪。
七、两处必须声明的”不真实”
对照开头那篇首尔项目,我也做了同样的处理,但要在文章里说清楚:
- 高度被夸张了 4 倍(默认)。因为 6 米的房子放在 1.5 公里的场景里几乎是平的。滑块可以拉回 1× 看真实比例——那时候你会看到一片几乎贴地的色块,那才是真实尺度。
- 地形被夸张了 1.8 倍(默认),否则 658 米的山在 13 公里宽的场景里也近乎平坦。
- 轮廓是简化过的。只保留 footprint 外轮廓,屋顶结构、女儿墙、烟囱、附属建筑一概没有。这不是照片级重建。
另外所有建筑都是纯色块,没有贴图,也没有加载任何底图瓦片——所以它不是一个地图服务,只是一个几何 + 高程数据的可视化。
八、你也可以做一个
最小复现只需要一条 Overpass 查询:
1
2
3
[out:json][timeout:120];
way["building"](64.0000,-22.0800,64.1200,-21.8200);
out geom;
把 bbox 换成你感兴趣的地方即可。拿到 JSON 后,关键是不要假设 height 字段存在——先统计它缺失的比例,再决定这个可视化值不值得做。
完整实现是一个单文件 HTML + Three.js(ES Module + importmap 从 jsDelivr 加载),没有构建步骤,直接挂在 /assets/hafnarfjordur/ 下。
九、许可与边界
建筑与水系几何数据 © OpenStreetMap 贡献者,遵循 ODbL 1.0 协议;地形高程 © AWS Terrain Tiles(Mapzen / SRTM)。
需要明确的是:本页面是数据可视化演示,不是测绘成果,不可用于工程、导航或任何需要精度的用途。文中所有高度除标注外均为推算值,地形起伏为可视化夸张后的结果。
最后回到开头那个问题。「43 分钟造一座城」很酷,但真正值得传播的科普点不是速度,而是这座城有多少是真的。
当你知道眼前 20,632 栋楼里只有 36 栋的高度来自实测,你再去看它,看到的就不是一座城市,而是开放地理数据当今的模样——轮廓丰富,细节稀疏,且在地理上极不均衡。
这比一座漂亮的假城市有意思得多。