Cesium三维热力图:克里金插值从散点到连续渲染 简介本资源面向从事三维地理信息可视化与气象数据展示的开发者提供基于 Cesium 与克里金插值算法实现温度场渲染的完整工程源码。Cesium 作为开源 JavaScript 三维地球库支持地形、影像与矢量叠加克里金插值则通过变异函数分析离散温度测点估算未知位置数值从而将零散观测数据平滑为连续温度场适合气象监测、环境分析与空间插值学习等场景。压缩包共 585 个文件约 7.27MB以 321 个 js 脚本为核心逻辑辅以 141 个 png、53 个 jpg 图片资源、29 个 json 配置、27 个 css 样式及 svg、wasm、html 等文件结构完整可直接运行调试。目前已有 167 人学习下载读者可借此掌握克里金插值在 Cesium 中的集成方式、温度数据渲染流程与三维可视化实现思路快速复用到自身项目中。1. 从一堆散点温度到一张热力图这个项目到底在干什么手头有一批气象站传回来的温度数据几十个到几百个点不等每个点就一个经纬度加一个摄氏度数值。想把它铺到 Cesium 的三维地球上让整片区域都有颜色渐变而不是孤零零几个标注点。这个项目解决的就是这件事——用克里金插值Kriging把离散点推成连续栅格再贴到 Cesium 的 imagery 图层上渲染出来。它适合两类人一类是手里有散点观测数据、想在三维场景里做面状可视化的开发者另一类是已经会用 Cesium 加载影像和地形、但卡在“怎么把业务数据变成一张可叠加的图”这一步的人。技术栈不复杂核心就三块Cesium 负责三维渲染kriging.js 负责插值计算Canvas 负责把插值结果画成图片再交给 Cesium 当图层用。整条链路跑通之后换任何标量数据湿度、PM2.5、噪声都只是改个字段名的事。2. 技术选型与整体链路为什么是 Kriging 加 Canvas 而不是直接画点2.1 插值算法的选择逻辑散点数据要变成面绕不开插值。常见的反距离加权IDW实现简单但它是纯几何加权不考虑数据本身的空间相关性容易出现“牛眼”伪影——每个观测点周围一圈同心圆。克里金不一样它基于变异函数建模会先分析数据在半变异函数上的分布再决定各点权重。代价是计算量更大但对温度这种空间连续性强、又有局部波动的数据成图质量明显更稳。这个项目用的是 kriging.js一个纯 JavaScript 实现的轻量库不依赖后端。它暴露的核心接口就一个kriging.train传入观测值、x 坐标、y 坐标、变异函数模型和参数返回一个模型对象再用kriging.grid在指定网格上求值。变异函数模型一般选exponential或gaussian前者适合变化平缓的温度场后者适合有局部峰值的场景。2.2 从插值结果到 Cesium 图层的完整链路整条链路分四步走每一步都有明确的输入输出数据准备把原始观测数据整理成三个平行数组——values温度值、lons经度、lats纬度。模型训练调用kriging.train得到变异函数模型这一步决定了后续插值的空间权重分配。网格求值在目标区域的经纬度范围内生成规则网格用kriging.grid逐格计算插值结果得到一个二维数组。渲染上屏把二维数组映射成颜色画到离屏 Canvas 上再通过 Cesium 的SingleTileImageryProvider或Rectangle范围贴到地球上。这里有个关键决策为什么不直接用 Cesium 的 Entity 画点、用材质做渐变因为 Entity 的渐变材质是屏幕空间的不随地理坐标变化缩放后颜色分布会错位。而 Canvas 方案生成的是地理配准的图片贴上去之后无论怎么缩放旋转颜色和地理位置始终对齐。2.3 坐标范围与网格密度的权衡网格密度直接决定成图精度和计算耗时。假设目标区域经度跨度 10 度、纬度跨度 8 度如果按 0.01 度一个格就是 1000×800 共 80 万个格子。kriging.js 在每个格子上都要做一次权重计算80 万次下来浏览器主线程会卡死。实际项目中一般控制在 200×200 到 500×500 之间配合 Canvas 的像素尺寸做映射。// 网格参数配置根据区域范围和性能预算调整 const gridConfig { lonMin: 116.0, // 区域最小经度 lonMax: 117.0, // 区域最大经度 latMin: 39.5, // 区域最小纬度 latMax: 40.5, // 区域最大纬度 gridWidth: 300, // 横向网格数对应 Canvas 宽度 gridHeight: 300 // 纵向网格数对应 Canvas 高度 }; // 生成网格坐标数组kriging.grid 需要这种扁平结构 function buildGrid(config) { const xGrid []; const yGrid []; const lonStep (config.lonMax - config.lonMin) / config.gridWidth; const latStep (config.latMax - config.latMin) / config.gridHeight; for (let i 0; i config.gridWidth; i) { xGrid.push(config.lonMin i * lonStep); } for (let j 0; j config.gridHeight; j) { yGrid.push(config.latMin j * latStep); } return { xGrid, yGrid }; }这段代码里gridWidth和gridHeight是性能旋钮。300×300 在普通笔记本上大约 200 到 400 毫秒完成插值500×500 会到 1 秒以上。如果数据点超过 500 个建议先做抽稀或者分块计算否则训练阶段就会明显卡顿。3. 核心模块拆解从 train 到 grid 再到 Canvas 上色3.1 变异函数模型训练与参数调优kriging.train的签名是train(values, x, y, model, sigma2, alpha)。前三个参数是数据后三个是变异函数参数。model选exponential时sigma2是基台值sillalpha是变程range。如果不想手动调可以传exponential和两个 0kriging.js 内部会做简单的自动拟合但效果一般。实际项目里我一般会先用自动拟合跑一版看趋势再根据成图效果微调。// 假设已有三个数组temps温度、lons经度、lats纬度 // 数据量建议在 30 到 500 之间太少插值不稳太多训练慢 const model exponential; const sigma2 0; // 传 0 让库自动估计基台值 const alpha 0; // 传 0 让库自动估计变程 const krigingModel kriging.train(temps, lons, lats, model, sigma2, alpha); // 训练完成后可以打印模型参数用于判断拟合是否合理 console.log(变异函数模型:, krigingModel.model); console.log(基台值:, krigingModel.sigma2); console.log(变程:, krigingModel.alpha);sigma2和alpha这两个参数是玄学重灾区。alpha太小插值结果会过度平滑丢掉局部细节太大又会退化成类似 IDW 的效果每个点周围出现同心圆。经验值是让alpha大约等于区域对角线长度的 1/3 到 1/2。如果自动拟合出来的alpha明显偏离这个范围成图大概率有问题需要手动干预。3.2 网格插值与二维数组生成训练完模型后用kriging.grid在网格上求值。这个函数接收网格坐标数组和模型对象返回一个二维数组第一维是 x 方向第二维是 y 方向。注意 kriging.js 的网格坐标是扁平数组不是二维矩阵所以生成时要按顺序 push。// 接上面的 buildGrid 函数 const { xGrid, yGrid } buildGrid(gridConfig); // 执行网格插值返回二维数组 const gridResult kriging.grid( [xGrid, yGrid], // 网格坐标注意是数组的数组 krigingModel, // 训练好的模型 1 // 并行计算的块大小一般传 1 即可 ); // gridResult 的结构gridResult[i][j] 对应 xGrid[i], yGrid[j] 处的插值温度 // 遍历时注意外层是 x内层是 y console.log(网格尺寸:, gridResult.length, x, gridResult[0].length);kriging.grid的第三个参数是分块大小传 1 表示逐点计算。如果网格很大可以传更大的值让库内部做批量处理但实测提升有限主要瓶颈还是在 JavaScript 的单线程计算上。如果确实需要处理超大网格常见做法是放到 Web Worker 里跑避免阻塞 UI 线程。3.3 Canvas 颜色映射与 Cesium 图层挂载拿到二维数组后下一步是把它画成图片。核心逻辑是遍历每个格子根据温度值在色带上的位置取颜色填充对应像素。色带可以用线性渐变或者分段色阶温度场一般用蓝到红的渐变低温蓝、高温红。// 创建离屏 Canvas尺寸与网格一致 const canvas document.createElement(canvas); canvas.width gridConfig.gridWidth; canvas.height gridConfig.gridHeight; const ctx canvas.getContext(2d); // 计算温度范围用于归一化 const flatValues gridResult.flat(); const minTemp Math.min(...flatValues); const maxTemp Math.max(...flatValues); // 逐像素上色 for (let i 0; i gridConfig.gridWidth; i) { for (let j 0; j gridConfig.gridHeight; j) { const value gridResult[i][j]; // 归一化到 0-1 const ratio (value - minTemp) / (maxTemp - minTemp); // 色带映射蓝(0) - 青 - 绿 - 黄 - 红(1) const r Math.round(255 * Math.min(1, ratio * 2)); const g Math.round(255 * Math.min(1, 2 - ratio * 2)); const b Math.round(255 * Math.max(0, 1 - ratio * 2)); ctx.fillStyle rgb(${r},${g},${b}); // 注意 Canvas 的 y 轴向下而纬度向上需要翻转 ctx.fillRect(i, gridConfig.gridHeight - 1 - j, 1, 1); } } // 转成 dataURL交给 Cesium const dataUrl canvas.toDataURL(image/png);这里有个容易翻车的点Canvas 的 y 轴是向下的而地理纬度是向上的。如果不做翻转成图会上下颠倒高温区跑到南边去。上面代码里gridConfig.gridHeight - 1 - j就是干这个的。挂载到 Cesium 时用SingleTileImageryProvider把 dataUrl 贴到指定矩形范围const rectangle Cesium.Rectangle.fromDegrees( gridConfig.lonMin, gridConfig.latMin, gridConfig.lonMax, gridConfig.latMax ); viewer.imageryLayers.addImageryProvider( new Cesium.SingleTileImageryProvider({ url: dataUrl, rectangle: rectangle, tileWidth: gridConfig.gridWidth, tileHeight: gridConfig.gridHeight }) );rectangle必须和插值时的经纬度范围完全一致否则图片会被拉伸错位。tileWidth和tileHeight设成网格尺寸保证像素和格子一一对应不会出现模糊。4. 避坑与排查那些让我加班到凌晨的细节4.1 成图全黑或全白现象Canvas 画出来一片纯色没有任何渐变。原因通常是minTemp和maxTemp相等或者gridResult里全是NaN。如果观测数据里有null或undefinedkriging.js 不会报错但插值结果会污染成NaN。解决在训练前过滤掉无效值并检查maxTemp - minTemp是否大于 0。4.2 颜色分布和实际数据对不上现象明明高温点集中在东边成图却显示西边红。原因多半是经纬度顺序搞反了。kriging.js 不关心你的坐标是经纬度还是投影坐标它只按数组顺序计算。如果lons和lats传反了插值结果的 x/y 方向就互换了。解决打印前几个点的坐标确认和网格生成时的顺序一致。4.3 页面卡死或浏览器崩溃现象数据点超过 300 个、网格超过 400×400 时页面直接无响应。原因是 kriging.js 的train和grid都是同步计算跑在主线程上。解决把整个插值流程放进 Web Worker主线程只负责接收结果和渲染。如果不想用 Worker至少把网格降到 200×200 以下或者对数据点做抽稀。4.4 Cesium 图层叠加后颜色被地形遮挡现象热力图贴上去之后部分区域被地形挡住或者和底图混在一起看不清。原因是SingleTileImageryProvider默认贴在地表如果地形起伏大低洼处的图层会被山体遮挡。解决把viewer.terrainProvider设为EllipsoidTerrainProvider关闭地形或者调整图层的alpha做半透明叠加。4.5 缩放后图片模糊现象放大到一定程度后热力图变成马赛克。原因是SingleTileImageryProvider把整张图当一张瓦片放大就是像素插值。解决要么提高网格密度代价是计算变慢要么改用WebMapTileServiceImageryProvider做多级瓦片但后者需要后端切片复杂度高不少。折中方案是把网格控制在 500×500 左右配合 Cesium 的maximumLevel限制缩放层级。5. 二次开发与改造从温度场到任意标量场的通用化5.1 把插值逻辑抽成独立模块原始代码里插值、上色、挂载是揉在一起的改一个参数要翻好几处。我一般会把它拆成三个函数trainModel(data, config)、renderToCanvas(gridResult, colorStops)、addToCesium(viewer, dataUrl, rectangle)。这样换数据源时只动第一个换色带时只动第二个换渲染方式时只动第三个。// 通用插值入口传入观测点数组和配置返回 dataUrl async function interpolateToImage(points, config) { // points: [{ lon, lat, value }, ...] const values points.map(p p.value); const lons points.map(p p.lon); const lats points.map(p p.lat); const model kriging.train(values, lons, lats, exponential, 0, 0); const { xGrid, yGrid } buildGrid(config); const gridResult kriging.grid([xGrid, yGrid], model, 1); return renderToCanvas(gridResult, config, config.colorStops); }这个函数把数据格式统一成{ lon, lat, value }不管原始数据是 CSV、JSON 还是接口返回先转成这个结构再进来。colorStops是一个颜色断点数组比如[{ pos: 0, color: [0,0,255] }, { pos: 1, color: [255,0,0] }]渲染时做线性插值。这样换色带不用改渲染代码。5.2 性能优化的几个实操手段如果数据点超过 1000 个训练阶段会明显变慢。我试过几个手段按性价比排序手段效果代价数据抽稀网格采样训练时间降 50% 以上丢失局部细节Web Worker 异步计算UI 不卡顿代码复杂度增加降低网格密度计算量线性下降成图精度降低缓存模型对象重复渲染时省去训练数据变化时需重建抽稀的做法是按一定经纬度间隔保留一个点比如每 0.05 度保留一个。对于温度场这种空间连续性强的数据抽稀到 200 个点左右成图质量基本看不出差别。5.3 验证插值结果是否靠谱插值这东西没有绝对的对错但有几个快速验证方法。一是留出法随机扣掉 10% 的观测点用剩下的训练模型再看扣掉的点上预测值和实际值差多少。如果平均误差超过数据标准差的 30%说明变异函数参数没调好。二是目视检查把原始观测点叠加到热力图上看颜色分布是否和点的数值趋势一致。如果某个高温点周围却是蓝色大概率是坐标顺序或者色带映射出了问题。从那以后我每次做完插值都强制走一遍留出法验证哪怕只是快速跑个脚本。这个习惯帮我拦住了好几次“图很好看但数据是错的”的翻车现场。希望帮到你。本文还有配套的精品资源点击获取