
做省级尺度的时候最折磨人的从来不是怎么画楼而是怎么把几十个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和tippecanoePython部分只涉及shapely和json处理都不复杂。整个流程跑通之后换一个省份的数据只需要把输入文件路径改一下剩下的全自动。2. 建筑要素筛选PBF包里的tag和关系才是真正的入口2.1 省域PBF包里到底有哪些数据类型PBF格式的地图数据里核心元素是节点、路径和关系。节点是经纬度点路径由节点序列组成关系则是组合规则。建筑在数据里的表现方式比较多样普通矩形住宅楼通常是一条闭合路径路径标签里有building字段而大型商场、机场、小区这类复杂建筑往往是一个关系。最容易被忽略的就是关系。一个multipolygon关系会包含外环成员和内环成员内环代表庭院、天井或者中庭。如果处理关系不当只是拿到最外圈轮廓直接生成多边形那这些楼就会变成没有洞的实心水泥块。所以在过滤和导出的每一步都要确认关系被正确解析成带孔洞的多边形而不是当成散装路径处理。另外PBF数据里建筑的几何精度总体不错但偶尔会有路径闭合不严、节点重复这类小问题。这些问题在宏观缩放时几乎不可见一旦进入高倍率的白模渲染就会出现楼体缺口、自相交导致的面撕裂。2.2 building、building:part和关系筛选的边界过滤建筑时第一个直觉是筛选building*。这个标签确实覆盖了大量普通建筑但只筛它会出现明显漏检。现代建筑里很多高塔、商业综合体、体育场馆的轮廓是由building:part*表达的buildingyes有可能挂在整栋建筑的关系上也有可能压根没标。稳妥的做法是同时匹配两类标签把完整轮廓和建筑部件都收入同一个图层。在属性里保留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-typespolygon -o /tmp/buildings_raw.geojson --overwrite第一步的作用不只是缩小体积更重要的是给第二步减负。如果不先过滤第二步就要对整个省的所有道路水系做几何解析既慢又浪费磁盘。第二步里的--geometry-typespolygon很关键它会把闭合路径和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, encodingutf-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_topologyTrue) 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-typespolygon -o $BUILDINGS_RAW --overwrite python3 enrich_buildings.py $BUILDINGS_RAW $BUILDINGS_ENRICHED tippecanoe -f -Z11 -z15 -l building -o $OUT_MBTILES $BUILDINGS_ENRICHEDbash脚本里的每一步都独立生成中间文件这样做的好处是方便排查。哪一步出了问题直接检查对应的中间文件就知道原因不需要从头跑一遍。第一次调试时尽量不要用管道把命令串起来等全部稳定了再考虑合并。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-bytes500000 \ -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打开仔细看几轮确认建筑轮廓和高度都符合直觉再进行下一步。这一步慢不是浪费时间是省时间。