☰
省级PBF建筑白模制作:从原始数据到矢量瓦片的自动化流程
2026/10/16 22:45:25 网站建设 项目流程

做省级尺度的时候,最折磨人的从来不是怎么画楼,而是怎么把几十个GB的原始数据,变成浏览器里那一片干净利落的白色体块。PBF格式的数据包大家都懂,里面塞满了道路、水系、铁路、地块、POI等等内容,三分之一都不一定是建筑。我以前的做法是先下载整包,再用桌面GIS软件手动框选建筑,结果卡死过好几回,而且导出来的GeoJSON还带着一堆用不上的属性。后来我把整条链路改成脚本自动处理,从省级PBF到带高度的GeoJSON,再到矢量瓦片,一条命令跑完。这篇就把这套流程完整拆开讲,包含tag筛选逻辑、高度估算规则、几何清洗方法和切片参数,照着做基本能少走一个月弯路。


1. 为什么入口一定要选PBF:原始数据与GeoJSON之间差着两个量级

1.1 白模是三维场景里最便宜的底料

建筑白模说白了就是给建筑footprint加上高度属性,渲染成交互式三维场景里的体块。这种效果在宏观规划、人口热力叠加、基站选址、地产形态分析这些场景里非常实用,因为你要看的不是某栋楼的窗户和幕墙,而是整个片区内部的体量关系和天际线节奏。

实景三维模型好看,但采集成本高得吓人,处理周期按周算。白模的优势在于数据来源开放、处理链路短、更新速度快,而且在一省甚至一国的宏观尺度下,建筑贴图根本没有意义,轮廓和高度就够了。很多做城市数据可视化的项目,最终底图都是白模,上面再去叠加各类业务图层。

所以想通之后,整个任务被拆成了三个核心环节:从PBF里把建筑要素捞出来、给每栋楼算出合理高度并清洗几何、把结果切片成前端能直接加载的瓦片。

1.2 为什么不是直接下载现成GeoJSON

很多人第一反应是找现成的GeoJSON,但实际试过就知道不靠谱。城市级别的建筑数据偶尔能拿到现成包,省域级别基本没有现成的,即便有,导出时往往也经过了大幅抽稀,细节丢失严重。而且从数据源到切片,中间的属性字段完全是别人定义好的,你拿到的可能是一堆_area、_code之类的字段,高度字段大概率缺失。

PBF数据包是二进制压缩格式,同样面积的数据,体积比GeoJSON小一个数量级以上。一个省份的数据包通常在几百MB到1GB多,如果用GeoJSON形式存,动辄几十GB,下载、传输、解析都极其痛苦。更关键的是,PBF可以被流式读取和过滤,不会出现“把整个文件塞进内存”这种灾难。

PBF还有一个优点:更新频率固定且公开,每次刷新数据只需要重新拉取增量或全量包,跑一遍脚本,白模数据就同步了。这个优势在需要长期维护地图服务的项目里极为重要。

1.3 适用人群和前置基础

如果你是前端工程师,想给项目加一个三维城市底图,这篇文章里从数据准备到渲染的全过程都能直接用。如果你是GIS相关岗位的从业者,想把建筑数据做成白模瓦片,这套流程同样能替代掉大量手工操作。

需要准备的基础就三样:能跑命令行、懂一点Python、知道GeoJSON大概长什么样。命令部分主要用osmium和tippecanoe,Python部分只涉及shapely和json处理,都不复杂。整个流程跑通之后,换一个省份的数据,只需要把输入文件路径改一下,剩下的全自动。


2. 建筑要素筛选:PBF包里的tag和关系才是真正的入口

2.1 省域PBF包里到底有哪些数据类型

PBF格式的地图数据里,核心元素是节点、路径和关系。节点是经纬度点,路径由节点序列组成,关系则是组合规则。建筑在数据里的表现方式比较多样,普通矩形住宅楼通常是一条闭合路径,路径标签里有building字段;而大型商场、机场、小区这类复杂建筑,往往是一个关系。

最容易被忽略的就是关系。一个multipolygon关系会包含外环成员和内环成员,内环代表庭院、天井或者中庭。如果处理关系不当,只是拿到最外圈轮廓直接生成多边形,那这些楼就会变成没有洞的实心水泥块。所以在过滤和导出的每一步,都要确认关系被正确解析成带孔洞的多边形,而不是当成散装路径处理。

另外,PBF数据里建筑的几何精度总体不错,但偶尔会有路径闭合不严、节点重复这类小问题。这些问题在宏观缩放时几乎不可见,一旦进入高倍率的白模渲染,就会出现楼体缺口、自相交导致的面撕裂。

2.2 building、building:part和关系筛选的边界

过滤建筑时,第一个直觉是筛选building=*。这个标签确实覆盖了大量普通建筑,但只筛它会出现明显漏检。现代建筑里,很多高塔、商业综合体、体育场馆的轮廓是由building:part=*表达的,building=yes有可能挂在整栋建筑的关系上,也有可能压根没标。

稳妥的做法是同时匹配两类标签,把完整轮廓和建筑部件都收入同一个图层。在属性里保留source字段,标记这条要素到底来自building还是building:part。后续做精细化渲染时,可以用这个字段对建筑部件做特殊样式,或者根据来源调整高度估算逻辑。如果不区分来源,后期想排查“为什么这栋楼的层高特别离谱”就会非常难。

关系处理方面,保持默认的自动组装逻辑即可。大多数工具在导出时会自动把multipolygon关系解析成带孔洞的多边形,但要注意有些建筑关系缺少外环成员,或者外环被拆成了多条路径,这类问题只能靠几何清洗兜底。

2.3 用osmium一条命令完成过滤和导出

osmium是我用过最顺手的PBF处理工具,流式解析,内存占用低,几个GB的文件也能从容搞定。第一步先做tag过滤,把建筑相关要素抽成子文件;第二步直接把过滤结果导出成GeoJSON。

# 第一步:按tag过滤,保留building和building:part相关要素 osmium tags-filter 某省.pbf building building:part -o /tmp/buildings.pbf --overwrite # 第二步:把过滤结果导出为GeoJSON,强制输出多边形几何 osmium export /tmp/buildings.pbf --geometry-types=polygon -o /tmp/buildings_raw.geojson --overwrite

第一步的作用不只是缩小体积,更重要的是给第二步减负。如果不先过滤,第二步就要对整个省的所有道路水系做几何解析,既慢又浪费磁盘。第二步里的--geometry-types=polygon很关键,它会把闭合路径和multipolygon关系统一输出成Polygon或MultiPolygon,省去自己在后面拼几何的麻烦。

跑完之后打开GeoJSON浏览一下,重点检查有没有缺失的大面积建筑、是否出现大量细碎小面。如果发现某个片区明显是空白,就要回到第一步,检查那个区域是不是用了其他标签表达建筑。


3. 高度估算与几何清洗:白模的“立体感”全靠这一步

3.1 高度数据的三个来源和优先级

建筑高度不会直接从PBF的几何信息里读出来,而是藏在属性标签里。最常见的三个来源按优先级排列:

优先级标签说明
1height直接给出建筑高度,单位通常是米
2building:levels地面以上楼层数,可按默认层高估算
3默认值无任何高度信息时给一个保底高度

对于只有楼层数的建筑,我会用building:levels乘以3米作为默认层高。如果同时存在roof:levels,屋顶层的高度一般按2.5米估算,可以叠加到总高度上。这样处理出来的白模,至少形态上符合一般城市建筑的尺度关系。

还要注意高度单位异常的情况。少数区域的数据里height字段可能用了英尺或者干脆是字符串,直接用会得到几百米的摩天楼,所以脚本里要加单位判断和数值清洗。解析不出合理数值的,一律降级到默认高度。

3.2 用shapely清洗几何轮廓

从osmium导出的GeoJSON虽然已经带有多边形几何,但直接拿去切片会翻车。几何问题包括自相交多边形、重复节点、面积极小碎片、以及多边形内部落入零星小面。这些问题的处理顺序很有讲究。

我通常先统一转换几何对象,然后做三个动作:

from shapely.geometry import shape import shapely import json with open("/tmp/buildings_raw.geojson", "r", encoding="utf-8") as f: data = json.load(f) cleaned_features = [] for feat in data["features"]: geom = shape(feat["geometry"]) # 第一步:buffer(0)修复自相交和退化几何 if not geom.is_valid: geom = geom.buffer(0) # 第二步:过滤明显没有意义的微小面,面积单位是经纬度平方 if geom.area < 0.000001: continue # 第三步:简化几何,去掉不影响宏观展示的锯齿 geom = geom.simplify(0.00001, preserve_topology=True) cleaned_features.append({ "type": "Feature", "properties": feat.get("properties", {}), "geometry": shapely.geometry.mapping(geom) })

buffer(0)是shapely里修复自相交最常用的手段,它能自动消除小环和自交区域,让多边形变成合法的对象。simplify这一步必须放在buffer之后,否则简化过程可能再次触发布尔错误。面积过滤的阈值,我会根据数据密度调整,通常在10到30平方米之间。

3.3 完整的一键处理脚本

实际操作时,我会把几何清洗和高度估算合在同一个Python脚本里执行,输入是上一步导出的原始GeoJSON,输出是带height、min_height等白模属性的GeoJSON。

#!/usr/bin/env bash set -euo pipefail INPUT_PBF="${1:-某省.pbf}" BUILDINGS_PBF=/tmp/buildings.pbf BUILDINGS_RAW=/tmp/buildings_raw.geojson BUILDINGS_ENRICHED=/tmp/buildings_enriched.geojson OUT_MBTILES=./output/buildings.mbtiles mkdir -p output osmium tags-filter "$INPUT_PBF" building building:part -o "$BUILDINGS_PBF" --overwrite osmium export "$BUILDINGS_PBF" --geometry-types=polygon -o "$BUILDINGS_RAW" --overwrite python3 enrich_buildings.py "$BUILDINGS_RAW" "$BUILDINGS_ENRICHED" tippecanoe -f -Z11 -z15 -l building -o "$OUT_MBTILES" "$BUILDINGS_ENRICHED"

bash脚本里的每一步都独立生成中间文件,这样做的好处是方便排查。哪一步出了问题,直接检查对应的中间文件就知道原因,不需要从头跑一遍。第一次调试时尽量不要用管道把命令串起来,等全部稳定了再考虑合并。


4. GeoJSON的组装规范:给切片工具喂对格式比什么都重要

4.1 FeatureCollection组织方式与属性字段设计

切片工具处理GeoJSON时,对格式规范比图形软件更挑剔。GeoJSON必须是标准的FeatureCollection,每个Feature的geometry字段和properties字段缺一不可。properties里需要给切片阶段和渲染阶段准备的信息,我通常保留几个固定字段。

{ "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": { "height": 36.0, "min_height": 0, "source": "building", "is_estimated": false }, "geometry": { "type": "Polygon", "coordinates": [...] } } ] }

每个字段都有明确用途:height是白模挤出的绝对高度,min_height表示底部抬高量,source区分完整轮廓与建筑部件,is_estimated标记高度是否为估算值。如果想做更丰富的白模效果,还可以在properties里加level_category或者building_type这一类字段,前端就可以按类型赋予不同颜色和透明度。

很多人在这里会顺手把原PBF里的所有属性都保留,这其实是给自己埋坑。原数据的属性面板可能有一百多个字段,全保留会让GeoJSON体积膨胀好几倍,切片速度也会被拖累。白模渲染根本用不到那么多原始属性,只保留前端需要的几个字段就够了。

4.2 坐标系、坐标顺序与数字精度

GeoJSON规范要求的坐标系是EPSG:4326,也就是经纬度坐标,且顺序永远是经度在前、纬度在后。初学的人最容易在这里翻车,导出的数据从图形软件里看完全正常,但一切片就飞到了非洲或者海里,十有八九是坐标顺序搞反了。

精度也要注意。经纬度坐标保留6位小数,精度大约是0.1米,对宏观白模来说完全够用。不要为了追求“看起来精确”而保留12位小数,那样只会白白增加文件体积。如果数据原本是Web Mercator投影坐标,一定要在导出GeoJSON之前统一转换成WGS84经纬度,而且转换过程要集中在一个脚本里完成,避免不同工具混用导致精度不一致。

4.3 从G级到M级:简化与抽稀的取舍

省级PBF过滤出来的建筑几何,如果直接导出GeoJSON,体积很容易冲到几个GB。这个体量拿去切片不是不行,但速度会让人崩溃。合理做法是在导出时做第一次简化,把坐标点密度降下来。

简化容差的选择直接影响效果。容差设得太小,文件还是很大;设得太大,城市街区尺度上的建筑锯齿感会特别明显。我常用的经验值是0.00001到0.0001度,相当于1到10米,具体取决于最大渲染层级。如果白模只看宏观体量,容差取0.0001问题不大;如果希望放大到街区尺度仍能看到楼栋轮廓,就要向0.00001靠近。

体积优化不能只靠简化,抽稀也有讲究。面积很小的棚屋、临时建筑、杂物间这类要素,在省级尺度上完全没有展示价值,提前过滤掉能显著降低数据量。判断阈值和zoom层级联动的逻辑,我会在切片阶段用zoom分级来实现,而不是在GeoJSON里一刀切。


5. 切片参数与zoom分级:tippecanoe这样调才不崩

5.1 核心参数组合和一次实战配置

tippecanoe是目前最成熟的GeoJSON切片工具,能把巨大的GeoJSON转换成矢量瓦片。它处理大数据集的能力很强,但前提是参数给对。先看我实际用的一套配置。

tippecanoe -f \ -Z11 \ -z15 \ -l building \ --drop-densest-as-needed \ --maximum-tile-bytes=500000 \ -o ./output/buildings.mbtiles \ /tmp/buildings_enriched.geojson

参数含义从字面能猜个大概,但有几个细节值得展开说。最大zoom我限制在15,再往上对白模来说性价比极低,数据量却会指数级增长。最小zoom设为11,比这更小的缩放级别直接不加载,避免浪费请求。关于zoom级联,等一下单独讲。--drop-densest-as-needed表示当某个zoom的数据量超出瓦片容量时,自动从密度最大的区域开始丢弃要素,保证城市中心不至于因为要素过多而卡死。

5.2 zoom分级:宏观看体量,微观看楼栋

白模在不同缩放级别下需要展示的信息量完全不同。在省域视角,你只需要看到城市建成区的整体轮廓和核心区的高层体块;到了城市街道视角,才需要逐栋显示建筑轮廓。如果所有zoom都输出全量建筑,瓦片数量和单瓦片体积都会爆炸。

实际操作中,最小zoom到最大zoom之间要让tippecanoe自己去做渐进抽稀。在低zoom级别,通过密度抽稀自然会把大量低矮建筑丢掉,保留高层和大面积建筑;随着zoom升高,细节逐步还原,最终在15级展示全部轮廓。

如果发现某个区域在中间zoom级别仍然过密,可以单独加一个文件做分级控制,但多数场景下--drop-densest-as-needed已经能应付。默认参数通常表现稳定,不推荐一上来就加一堆精细调优参数,先把基本流程跑通,再针对数据分布慢慢调整。

5.3 性能、内存与输出文件的坑

处理一个省级数据包,耗时通常在十几分钟到半小时之间,具体取决于建筑要素数量和机器的磁盘性能。tippecanoe是单机多核工具,不需要额外配置分布式环境,但要注意磁盘剩余空间。中间文件加输出瓦片,建议预留至少两倍于输入数据体积的空间。

如果内存比较紧张,可以在tippecanoe参数里加临时目录的配置,把中间结果放到SSD上。我曾经在机械硬盘上直接跑,整个切片过程因为io瓶颈拖了两小时,换成SSD之后时间缩短了一半多。输出文件是mbtiles格式,本质是一个SQLite文件,部署时可以直接托管,也可以用工具转成pmtiles格式,方便用常规HTTP服务直接分发。


6. 白模渲染落地:从瓦片到浏览器里的一上下效果

6.1 图层名对不上,瓦片全白

切片之后,接下来是把它加载进前端三维引擎。我现在主要用MapLibre GL配置fill-extrusion图层来渲染白模。

map.addSource('buildings', { type: 'vector', tiles: ['https://your-cdn/tiles/{z}/{x}/{y}.pbf'] }); map.addLayer({ id: '3d-buildings', type: 'fill-extrusion', source: 'buildings', 'source-layer': 'building', paint: { 'fill-extrusion-color': [ 'interpolate', ['linear'], ['get', 'height'], 0, '#e8e8e8', 15, '#c8b28a', 40, '#a67c52' ], 'fill-extrusion-height': ['get', 'height'], 'fill-extrusion-base': ['get', 'min_height'], 'fill-extrusion-opacity': 0.85 } });

这里有个最容易踩的坑,source-layer必须和切片时生成的图层名完全一致,也就是tippecanoe命令里-l building中的building。如果source-layer写错,控制台不报错但屏幕上一片空白,很多人会在这里卡很久。排查方法很简单,先删掉所有过滤条件直接渲染,确认图层名没问题再继续调样式。

6.2 配色与透明度:让白模看起来更“专业”

纯白色虽然不会错,但观感很平。更常见的做法是让颜色随高度渐变,低层建筑偏灰白,高层建筑偏暖色,超高层用深色强调。透明度也要控制好,完全不透明会让密集城区的楼层互相遮挡,完全看不出街区结构。0.8到0.9的透明度是比较安全的区间。

is_estimated字段在这里很实用。对于估算高度的建筑,可以在渲染样式里单独用半透明或虚线描边方式区分,既不会干扰真实数据,也让数据可信度可见。有真实height的建筑和估算高度的建筑混在一起渲染时,没有这个字段很难定位异常数据。

6.3 更新数据与继续深化的方向

白模数据更新时,重新拉取PBF然后跑一遍完整脚本即可,中间文件全部覆盖。如果只是某个城市局部更新,可以在过滤PBF前先按地理范围切出局部数据,这样能大幅缩短处理时间。省域级别的全量更新,一天一刷不太现实,但每周或每月刷新一次完全可行。

想进一步增强白模效果,可以让building:part支持的分层形态真正生效,一栋塔楼的裙楼和塔体会有不同高度,在城市尺度上呈现错落有致的体块关系。


最后分享一个自己的体会:这套流程看起来很长,但真正容易出问题的地方往往不是命令,而是数据认知。绝大多数翻车都发生在tag理解、关系处理、坐标系这三个环节上。与其拿到数据急着切片,不如先把中间GeoJSON打开仔细看几轮,确认建筑轮廓和高度都符合直觉再进行下一步。这一步慢不是浪费时间,是省时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询