
简介这份资源面向需要在 Web 三维场景中呈现连续温度场的前端与 GIS 开发者围绕 Cesium 与克里金插值展开解决离散温度测点难以平滑表达的问题。包内共 585 个文件以 321 个 js 脚本、141 个 png 与 53 个 jpg 图像、29 个 json 配置、27 个 css 样式为主另有 svg、wasm、html 等辅助文件压缩包约 7.27MB整体为可直接运行的 Cesium 工程结构。资源将克里金插值用于把不规则分布的温度测量值转换为连续温度场使三维空间中的温度变化得以平滑展现读者可据此理解变异函数分析与插值流程并参考工程组织方式完成地形渲染、影像叠加与矢量展示等模块的整合。已有 167 人学习适合希望掌握空间插值可视化实现思路的开发者参考。1. 从一张“温度热力图”说起Cesium 里怎么把离散气象站数据铺成连续色带去年冬天帮一个做区域气象可视化的团队看代码需求很朴素地图上散着几十个自动站每个站有实时温度要在 Cesium 的三维地球上画出一张平滑的、随缩放跟着走的温度分布图。他们一开始用的是 IDW 反距离加权结果城区站点密集的地方还行山区站点稀疏的地方直接糊成一片等值线歪得没法看。后来换成克里金插值Kriging效果立刻不一样了——它不只是按距离加权还考虑了空间自相关性能给出更符合地理规律的连续曲面。这个项目做的就是这件事把 Cesium 的三维渲染能力和 Kriging 插值算法接起来让离散的温度点变成一张可交互的温度场。适合谁看手里有站点数据、想在三维地图上做连续场渲染的前端或 GIS 开发尤其是被 IDW 效果坑过、想上克里金但不知道从哪下手的人。2. 技术选型与整体架构为什么是 Cesium Kriging而不是别的组合2.1 渲染层选 Cesium 的硬理由三维地球场景里能同时处理地形、影像、矢量、自定义图元并且开源可商用的Cesium 基本是默认答案。这个项目需要温度场贴着地形起伏走还要能随相机缩放、旋转实时更新用二维 Canvas 叠上去会穿帮。Cesium 的Entity、Primitive、ImageryLayer三套体系里温度场这种“整片区域连续着色”的需求最稳的落法是生成一张栅格图挂成ImageryLayer或者贴到Rectangle图元上。这样既吃到了 Cesium 的瓦片调度又不用自己写 WebGL 着色器去逐像素算插值——插值在 CPU 端算一次出图交给 GPU 贴。2.2 插值层选 Kriging 的取舍Kriging 家族里常见的有普通克里金Ordinary Kriging、泛克里金、协同克里金。温度场这种没有明显全局趋势、局部平稳的变量普通克里金够用计算量也可控。它的核心是半变异函数semivariogram先根据样本点两两之间的距离和温度差拟合出一个模型球状、指数、高斯再用这个模型给每个待插值像素算权重。相比 IDW 的“距离越近权重越大”Kriging 多了一步“空间结构建模”这就是它在稀疏区不糊的关键。技术栈上前端是原生 JavaScript Cesium插值算法用成熟的 kriging.js一个轻量的 JS 克里金实现数据用 JSON 或 GeoJSON 喂进来。没有上框架是因为这类可视化项目往往要嵌进已有的 GIS 平台保持轻量反而好集成。2.3 一次渲染的完整链路把链路拆开看从数据到屏幕是这么走的// 1. 准备样本站点经纬度 温度值 const samples [ { lng: 116.40, lat: 39.90, value: 3.2 }, { lng: 116.52, lat: 39.95, value: 2.8 }, // ... 几十个站点 ]; // 2. 转成 kriging.js 需要的格式[x, y, value] const values samples.map(s [s.lng, s.lat, s.value]); // 3. 训练变差函数模型球状模型参数由 fit 自动拟合 const model spherical; const sigma2 0; // 块金效应先给 0 让 fit 自己估 const alpha 100; // 变程单位是度按区域尺度调 const krigingModel kriging.train(values, 0, 0, model, sigma2, alpha); // 4. 在网格上逐点预测生成栅格 const gridWidth 256, gridHeight 256; const canvas document.createElement(canvas); canvas.width gridWidth; canvas.height gridHeight; const ctx canvas.getContext(2d); const imageData ctx.createImageData(gridWidth, gridHeight); for (let y 0; y gridHeight; y) { for (let x 0; x gridWidth; x) { // 把像素坐标映射回经纬度 const lng minLng (maxLng - minLng) * (x / gridWidth); const lat minLat (maxLat - minLat) * (y / gridHeight); const t kriging.predict(lng, lat, krigingModel); // 温度映射到色带写入 RGBA const [r, g, b] tempToColor(t); const idx (y * gridWidth x) * 4; imageData.data[idx] r; imageData.data[idx 1] g; imageData.data[idx 2] b; imageData.data[idx 3] 200; // 半透明露出底图 } } ctx.putImageData(imageData, 0, 0); // 5. 挂到 Cesium 上贴到指定矩形范围 viewer.imageryLayers.addImageryProvider( new Cesium.SingleTileImageryProvider({ url: canvas.toDataURL(), rectangle: Cesium.Rectangle.fromDegrees(minLng, minLat, maxLng, maxLat), }) );这段代码是整个项目的骨架。kriging.train的四个关键参数值得说清楚model决定半变异函数形状温度场一般用spherical或exponentialsigma2是块金值表示微观尺度上的随机误差给 0 让算法自己拟合通常更稳alpha是变程意思是“超过这个距离两点就基本不相关了”单位跟你的坐标单位一致——这里用经纬度所以是度如果换成投影坐标就是米这个单位搞错是新手最常见的翻车点。kriging.predict返回的是该点的预测值逐像素算完写进ImageData最后用SingleTileImageryProvider贴图Cesium 会自动处理投影和缩放。2.4 目录结构长什么样这类项目不需要复杂工程化一个能直接打开的静态目录就够cesium-kriging-temp/ ├── index.html # 入口挂 Cesium viewer ├── js/ │ ├── main.js # 初始化场景、加载数据、触发渲染 │ ├── kriging.js # 克里金算法库第三方可替换 │ ├── interpolate.js # 封装 train/predict/出图逻辑 │ └── colorScale.js # 温度到颜色的映射 ├── data/ │ └── stations.json # 站点样本数据 └── lib/ └── Cesium/ # Cesium 静态资源interpolate.js是二次开发最常动的地方把“训练模型—网格预测—生成 canvas”三步封成一个函数main.js只负责调它。colorScale.js单独拆出来是因为色带方案经常要按业务换比如零下用蓝、零上用红拆开后改色不用碰插值逻辑。3. 核心模块拆解从样本训练到栅格出图的每一步3.1 半变异函数拟合Kriging 的“黑匣子”在这里很多人用 kriging.js 只会调train不知道里面发生了什么结果参数一错就出鬼图。train做的事是把所有样本点两两配对算出距离h和温度差的半方差γ(h) 0.5 * (z_i - z_j)^2按距离分箱求平均得到一张经验半变异函数散点图再用选定的模型去拟合这条曲线。拟合出来的三个参数——块金、基台、变程——决定了后续预测的权重分配。// kriging.js 内部拟合的简化逻辑帮助理解非源码逐行 // 经验半变异函数按距离分箱 const bins {}; for (let i 0; i n; i) { for (let j i 1; j n; j) { const h distance(values[i], values[j]); const gamma 0.5 * Math.pow(values[i][2] - values[j][2], 2); const bin Math.round(h / binSize); (bins[bin] bins[bin] || []).push(gamma); } } // 每个 bin 取平均得到 (h, γ) 点对再对模型做最小二乘拟合这段逻辑说明一件事样本点太少比如少于 10 个或者分布极不均匀时经验半变异函数根本拟合不出可靠曲线预测结果会剧烈抖动。我一般会在训练前先做个检查样本数低于 15 就退回 IDW 兜底别硬上 Kriging。3.2 网格分辨率与性能的平衡gridWidth和gridHeight直接决定计算量256×256 就是 65536 次predict调用每次 predict 要遍历所有样本算权重复杂度是 O(像素数 × 样本数)。几十个样本、256 网格现代浏览器一两秒能出图但如果网格拉到 1024样本上百就会卡到主线程冻结。常见做法是把网格控制在 256 到 512 之间视觉上已经足够平滑因为 Cesium 贴图本身会做双线性插值放大。真要更高精度就把预测挪到 Web Worker 里主线程只负责收结果贴图// worker 里跑插值避免阻塞渲染 const worker new Worker(js/interpolate.worker.js); worker.postMessage({ samples, bounds, gridSize: 512 }); worker.onmessage (e) { const { imageData } e.data; // 主线程拿到 ImageData转 dataURL 贴图 const canvas document.createElement(canvas); canvas.width imageData.width; canvas.height imageData.height; canvas.getContext(2d).putImageData(imageData, 0, 0); addTemperatureLayer(canvas.toDataURL()); };参数上bounds是插值区域的外包矩形[minLng, minLat, maxLng, maxLat]必须比样本范围略大一圈否则边缘站点外侧会出现一条没有数据的硬边。这个“外扩”我一般取样本跨度的 5% 到 10%。3.3 色带映射温度场好不好看一半靠它插值出的是数值人眼要看的是颜色。colorScale.js里做的是分段线性映射把温度区间切成若干档每档对应一个 RGB// 温度色带-10℃ 深蓝 → 0℃ 青 → 10℃ 黄 → 30℃ 红 const stops [ { t: -10, color: [30, 60, 180] }, { t: 0, color: [80, 180, 220] }, { t: 10, color: [250, 220, 90] }, { t: 30, color: [220, 60, 40] }, ]; function tempToColor(t) { // 夹到区间内防止极端值越界 t Math.max(stops[0].t, Math.min(stops[stops.length - 1].t, t)); for (let i 0; i stops.length - 1; i) { const a stops[i], b stops[i 1]; if (t a.t t b.t) { const k (t - a.t) / (b.t - a.t); return [ Math.round(a.color[0] (b.color[0] - a.color[0]) * k), Math.round(a.color[1] (b.color[1] - a.color[1]) * k), Math.round(a.color[2] (b.color[2] - a.color[2]) * k), ]; } } return stops[stops.length - 1].color; }这里有个容易忽略的点alpha 通道别给满。温度场是覆盖层全不透明会把底图地形和路网全盖死给 180 到 210 之间既看得清色带又能透出地理信息。另外色带断点最好跟业务对齐比如农业场景关心 0℃ 霜冻线那 0℃ 那一档就要卡得准别用等间距硬切。3.4 贴图挂载与范围对齐最后一步是把 canvas 变成 Cesium 能认的图层。SingleTileImageryProvider适合单张整图rectangle参数必须和插值时用的bounds完全一致否则色带会跟站点位置错位——这是排查时最隐蔽的 bug图看着没问题但某个站点的颜色跟它的实际温度对不上八成就是这里差了一点。const provider new Cesium.SingleTileImageryProvider({ url: canvas.toDataURL(image/png), rectangle: Cesium.Rectangle.fromDegrees( bounds[0], bounds[1], bounds[2], bounds[3] ), tileWidth: 512, tileHeight: 512, }); viewer.imageryLayers.addImageryProvider(provider);tileWidth/tileHeight设成和 canvas 一致避免 Cesium 内部再做一次重采样导致模糊。如果温度场要随时间刷新比如逐小时数据记得在加新图层前把旧图层 remove 掉不然图层会越叠越多内存一路涨。4. 避坑与排查这几个坑我踩过你别再踩4.1 现象整张图一片纯色没有任何渐变原因通常是alpha变程参数给得离谱。如果坐标用经纬度度变程给成 100意味着“100 度以内都强相关”整个区域被当成一个点预测值全趋近样本均值自然一片纯色。解决变程按实际区域尺度给几十公里量级的区域经纬度变程大约 0.5 到 2 度先用样本跨度的 1/3 试再微调。4.2 现象边缘出现锯齿状硬边或空白条原因是插值bounds和贴图rectangle不一致或者 bounds 没外扩。样本最外圈站点之外没有数据支撑预测值会快速衰减。解决bounds 在样本外包矩形基础上外扩 5% 到 10%并且保证插值和贴图用的是同一个 bounds 变量别在两处各写一遍。4.3 现象页面卡死几秒缩放都卡原因是主线程同步跑了高分辨率网格的逐点预测。解决网格降到 256 起步或者把插值搬进 Web Worker另外predict内部每次都要重算样本权重矩阵样本多时可以在训练后缓存模型别每次刷新都重新train。4.4 现象温度值明显异常比如出现 -999 或远超合理范围原因是样本数据里混进了缺测标记气象数据常见 -999、9999 这类哨兵值直接喂进train会把半变异函数带偏。解决训练前做一次清洗过滤掉超出物理合理区间的值缺测站点直接剔除别用 0 填充——0℃ 在温度场里是有意义的值填充会污染插值。4.5 现象切换数据源后色带对不上同一温度颜色变了原因是色带的 min/max 写死了换区域后温度区间变了映射关系就错位。解决色带断点要么按业务固定如霜冻线要么每次根据当前样本的 min/max 动态生成但动态生成时要把图例一起更新否则用户看到的颜色没有参照。5. 二次开发把温度场换成任意标量场以及几个实用改造这套代码的插值部分其实跟“温度”无关任何带经纬度的标量数据都能套PM2.5 浓度、土壤湿度、噪声分贝、降雨量改的只是colorScale.js的断点和单位。我一般会先把interpolate.js抽成一个不依赖 Cesium 的纯函数模块输入samples bounds gridSize输出ImageData这样同一套插值逻辑既能给 Cesium 用也能给二维地图或导出图片用复用性立刻上来。第二个改造点是时间序列。气象数据往往是逐小时的与其每次重新train不如把模型训练和网格预测拆开模型在数据更新时训练一次缓存起来预测按需触发。下面这个结构我用了很多次稳// 模型缓存同一批样本只训练一次 let cachedModel null; let cachedSampleHash ; function getModel(samples) { const hash JSON.stringify(samples.map(s [s.lng, s.lat, s.value])); if (hash cachedSampleHash cachedModel) return cachedModel; const values samples.map(s [s.lng, s.lat, s.value]); cachedModel kriging.train(values, 0, 0, spherical, 0, estimateAlpha(samples)); cachedSampleHash hash; return cachedModel; } // 变程按样本跨度自适应省去手调 function estimateAlpha(samples) { const lngs samples.map(s s.lng); const lats samples.map(s s.lat); const span Math.max( Math.max(...lngs) - Math.min(...lngs), Math.max(...lats) - Math.min(...lats) ); return span / 3; }estimateAlpha这个自适应变程是我被手调参数折磨之后加的样本跨度除以 3 是个经验值大多数区域气象场景直接能用省掉一轮试错。验证方法也简单拿几个没参与训练的站点做留一交叉验证看预测值和实测值的平均绝对误差误差明显偏大就说明变程或模型选得不对换exponential再试一轮。还有一个容易被忽略的改造图例。温度场贴上去之后用户不知道颜色对应多少度得在角落放一个渐变色条加刻度。这个用普通 DOM 叠在 Cesium 容器上就行别用 Cesium 的 HTML 覆盖物去折腾简单直接。图例的断点必须和colorScale.js里的stops同源我一般让colorScale.js导出一个getLegendStops()图例和渲染共用一份配置改一处两边同步。最后说个血泪经验这类插值可视化最耗时间的从来不是算法本身而是坐标系和范围对齐。经纬度、投影坐标、Cesium 的弧度三套单位混着用稍不留神就错位。从那以后我每次接新数据源都强制先把样本点用 Cesium 的PointPrimitive打在地图上肉眼确认位置对了再开插值——这一步花两分钟能省掉后面两小时的排查。希望帮到你。本文还有配套的精品资源点击获取