简介:本资源面向从事三维地理信息可视化与气象数据展示的开发者,提供基于 Cesium 与克里金插值算法实现温度场渲染的完整工程源码。Cesium 作为开源 JavaScript 三维地球库,支持地形、影像与矢量叠加;克里金插值则通过变异函数分析离散温度测点,估算未知位置数值,从而将零散观测数据平滑转换为连续温度场,在三维空间中直观呈现温度变化趋势。压缩包共 585 个文件,以 321 个 js 脚本、141 个 png 与 53 个 jpg 图像、29 个 json 配置、27 个 css 样式为主,另含 svg、wasm、html 等,整体约 7.27MB,结构完整、依赖清晰。已有 167 人学习下载,适合需要掌握空间插值与 Cesium 集成渲染的中高级前端或 GIS 开发者参考,可据此理解插值参数配置、图层叠加与可视化调优思路。
1. 克里金插值:为什么气象站点数据能铺满整个地球
气象数据可视化里有个绕不开的矛盾:真实观测数据永远是离散的。全国几千个气象站,每个站一个温度值,但用户想看到的是整个地图上连续变化的温度色带。把几千个点直接画在地图上,得到的是一堆孤立的圆点;想让它变成一张连续的热力图,就必须在点与点之间"猜"出中间的值——这就是空间插值要解决的问题。
克里金插值(Kriging)和反距离加权(IDW)最大的区别在于:IDW 只按距离远近加权,离得近的点权重大,离得远的权重小,规则简单粗暴;而克里金会先分析已知点之间的空间自相关性——也就是"距离多远的两个点,它们的值还有关联"——再根据这个规律去预测未知位置。换句话说,IDW 假设"近的就相似",克里金则先算出"多近才算相似"。这个差异在气象场景下非常关键,因为温度场往往存在明显的空间结构,不是简单的距离衰减。
在 Cesium 里做这件事,难点不在插值算法本身,而在于插值结果是栅格数据,Cesium 的渲染管线吃的是纹理和几何体。怎么把克里金算出来的网格变成 Cesium 能渲染的东西,才是真正需要搞清楚的机制。
2. 从站点到网格:克里金插值的数学骨架
2.1 半变异函数:克里金的核心不是距离,是空间结构
克里金插值的起点不是直接算权重,而是先构建半变异函数(semivariogram)。它的定义很直白:对于所有距离约为 h 的点对,计算它们值之差的平方的一半,再取平均。
γ(h) = (1/2N(h)) * Σ [Z(xi) - Z(xj)]²其中 N(h) 是距离为 h 的点对数量,Z(xi) 和 Z(xj) 是两个点的观测值。这个函数描述的是:随着距离增大,两个点的值差异如何变化。距离很近时,差异应该很小;距离远到一定程度后,差异趋于稳定,说明两个点已经完全不相关了。
实际计算时不会对所有距离逐一算,而是把距离分成若干区间(lag),每个区间内取平均。得到一组 (h, γ(h)) 散点后,再用理论模型去拟合。常用的模型有三种:
| 模型 | 表达式 | 适用场景 |
|---|---|---|
| 球状模型 | γ(h) = C0 + C·(1.5h/a - 0.5(h/a)³) | 空间自相关随距离先增后稳,最常见 |
| 指数模型 | γ(h) = C0 + C·(1 - exp(-h/a)) | 自相关衰减较慢,适合平滑场 |
| 高斯模型 | γ(h) = C0 + C·(1 - exp(-h²/a²)) | 极平滑的场,如气压 |
C0 是块金值(nugget),代表距离趋近于零时的差异,通常来自测量误差或微观变异;C 是拱高(sill 减去 nugget),a 是变程(range),表示超过这个距离后空间自相关基本消失。这三个参数决定了整个插值的"性格"。
2.2 权重求解:为什么克里金能给出最优无偏估计
有了半变异函数模型,接下来就是求解权重。假设要预测位置 x0 的值,用周围 n 个已知点加权:
Z*(x0) = Σ λi · Z(xi)克里金要求这个估计满足两个条件:无偏(权重之和为 1)和方差最小。用拉格朗日乘数法推导后,得到克里金方程组:
[γ(x1,x1) γ(x1,x2) ... γ(x1,xn) 1] [λ1] [γ(x1,x0)] [γ(x2,x1) γ(x2,x2) ... γ(x2,xn) 1] [λ2] [γ(x2,x0)] [ ... ... ... ... ] [..] = [ ... ] [γ(xn,x1) γ(xn,x2) ... γ(xn,xn) 1] [λn] [γ(xn,x0)] [ 1 1 ... 1 0] [μ ] [ 1 ]左边矩阵中 γ(xi,xj) 是把两点距离代入半变异函数模型算出来的值,右边向量是待预测点到各已知点的半变异值。解这个线性方程组,就得到每个已知点的权重 λi 和拉格朗日乘数 μ。
这里有个容易被忽略的点:矩阵左边只跟已知点之间的空间关系有关,跟待预测点无关。如果要对大量网格点做插值,左边矩阵只需要求一次逆(或者做一次分解),右边向量每个网格点不同,但求解过程可以复用分解结果。这是克里金在工程实现上能跑得动的重要原因。
2.3 从数学到代码:一个可运行的克里金实现
下面是一个简化但完整的普通克里金实现,去掉了第三方库依赖,方便理解每一步在做什么:
// 普通克里金插值核心实现 // points: [{x, y, value}] 已知站点 // target: {x, y} 待预测位置 // model: 半变异函数模型参数 {nugget, sill, range, type} function krigingPredict(points, target, model) { const n = points.length; // 构建 (n+1) x (n+1) 的克里金矩阵 const A = []; for (let i = 0; i < n; i++) { A[i] = []; for (let j = 0; j < n; j++) { const d = distance(points[i], points[j]); A[i][j] = semivariance(d, model); } A[i][n] = 1; // 拉格朗日约束行 } A[n] = new Array(n + 1).fill(1); A[n][n] = 0; // 构建右侧向量:待预测点到各已知点的半变异值 const b = []; for (let i = 0; i < n; i++) { const d = distance(points[i], target); b[i] = semivariance(d, model); } b[n] = 1; // 解线性方程组 A * weights = b const weights = solveLinearSystem(A, b); // 加权求和得到预测值 let result = 0; for (let i = 0; i < n; i++) { result += weights[i] * points[i].value; } return result; } // 球状模型半变异函数 function semivariance(h, model) { const { nugget, sill, range } = model; if (h === 0) return nugget; if (h >= range) return sill; const ratio = h / range; return nugget + (sill - nugget) * (1.5 * ratio - 0.5 * ratio ** 3); }这段代码里,solveLinearSystem可以用高斯消元或 LU 分解实现。关键参数说明:nugget控制近距离的随机性,设太大会让插值结果变得"毛糙";range决定影响半径,设太小会导致远处网格点没有足够已知点参与插值;sill是总方差上限,通常取样本方差的某个倍数。
实际工程中不会对每个网格点都重新构建矩阵 A,而是先对 A 做一次 LU 分解,然后对每个网格点只做前代和回代。当站点数超过几百个时,还可以用局部克里金——只取待预测点周围一定范围内的站点参与计算,把矩阵规模从几千降到几十。
3. Cesium 渲染管线:插值结果怎么变成地图上的颜色
3.1 为什么不能直接画网格:Cesium 的渲染对象是什么
克里金算完之后,得到的是一张二维网格,比如 500×500 的浮点数组。这张网格在 Cesium 里没有直接的对应物。Cesium 的渲染体系里,能贴在地形上的东西主要有三类:Entity(点、线、面、模型)、Primitive(底层几何体)、ImageryLayer(影像图层)。
最自然的思路是把插值结果做成一张图片,然后作为 ImageryLayer 贴上去。但这里有个坐标系问题:克里金网格是基于经纬度矩形范围算的,而 Cesium 的影像图层需要的是标准瓦片或单张图片。如果直接把网格画成 canvas 再转成图片,需要处理经纬度到像素的映射,还要考虑 Cesium 的投影方式。
另一种思路是用 Primitive 构建自定义几何体,每个网格单元作为一个矩形面,顶点颜色根据插值值映射。这种方式灵活但性能差,500×500 就是 25 万个面,浏览器扛不住。
常见的工程做法是走 canvas 路线:把插值网格渲染到离屏 canvas 上,用颜色映射表把数值转成 RGBA,然后通过 Cesium 的 SingleTileImageryProvider 或自定义 ImageryProvider 加载。这样只需要处理一张图片,性能可控。
3.2 颜色映射:从温度值到 RGBA 的转换逻辑
温度数据的颜色映射不是简单的线性渐变。气象上常用的色带往往是非线性的,比如零度附近颜色变化剧烈,高温段变化平缓。实现时通常定义一个颜色停靠点数组:
// 温度色带定义:从低温到高温 const colorStops = [ { value: -30, color: [ 49, 54, 149] }, // 深蓝 { value: -10, color: [ 69, 117, 180] }, // 蓝 { value: 0, color: [171, 217, 233] }, // 浅蓝 { value: 10, color: [255, 255, 191] }, // 浅黄 { value: 25, color: [253, 174, 97] }, // 橙 { value: 40, color: [215, 48, 39] }, // 红 ]; // 根据温度值查找对应颜色,线性插值 function valueToColor(value, stops) { if (value <= stops[0].value) return stops[0].color; if (value >= stops[stops.length - 1].value) return stops[stops.length - 1].color; for (let i = 0; i < stops.length - 1; i++) { const a = stops[i], b = stops[i + 1]; if (value >= a.value && value <= b.value) { const t = (value - a.value) / (b.value - a.value); return a.color.map((c, idx) => Math.round(c + t * (b.color[idx] - c)) ); } } }这段逻辑的关键在于:色带停靠点的分布决定了视觉效果的"层次感"。如果停靠点均匀分布,温度变化在视觉上也是均匀的;如果某些区间停靠点密集,那个区间的颜色变化就会更细腻。气象可视化里通常会在零度附近加密停靠点,因为零度是雨雪分界线,视觉上需要突出。
3.3 网格到纹理:canvas 绘制的性能考量
把 500×500 的网格逐像素画到 canvas 上,用fillRect逐格绘制会非常慢。更高效的方式是直接用ImageData操作像素缓冲区:
// grid: Float32Array,长度 width * height // 直接操作 ImageData 像素缓冲区,避免逐格 fillRect function gridToImageData(grid, width, height, stops) { const imageData = new ImageData(width, height); const data = imageData.data; for (let i = 0; i < grid.length; i++) { const value = grid[i]; // 处理无效值(站点覆盖不到的区域) if (isNaN(value)) { data[i * 4 + 3] = 0; // 透明 continue; } const [r, g, b] = valueToColor(value, stops); data[i * 4] = r; data[i * 4 + 1] = g; data[i * 4 + 2] = b; data[i * 4 + 3] = 200; // 半透明,保留底图可见性 } return imageData; }这里有个容易翻车的细节:ImageData的像素顺序是行优先,而克里金网格的坐标系可能是纬度从南到北递增,而 canvas 的 y 轴是从上到下递增。如果直接按数组顺序填充,地图上的温度分布会上下颠倒。解决方式是在填充时翻转 y 索引,或者在生成网格时就按屏幕坐标顺序存储。
另外,alpha 通道的设置直接影响叠加效果。设成 255 完全不透明,底图地形就看不见了;设成 100 以下又太淡,温度色带不明显。通常取 180 到 220 之间,具体值需要根据底图明暗调整。
4. 避坑与排查:克里金插值在 Cesium 里最容易翻车的几个点
4.1 插值结果出现"牛眼"伪影
现象:插值后的温度图上,某些站点周围出现同心圆状的异常高值或低值区域,看起来像牛眼。
原因:这通常是半变异函数参数设置不当导致的。当 nugget 设得过小、range 设得过大时,克里金方程组会变得病态,某些站点的权重被过度放大。另一个常见原因是站点数据中存在重复坐标或极近距离的站点,导致矩阵中出现近似奇异的行。
解决:先检查站点数据,合并距离小于阈值的重复站点。然后调整 nugget 值,一般设为样本方差的 5% 到 15%。如果仍然出现,改用局部克里金,限制参与插值的站点数量在 20 到 50 个之间,避免远距离站点的权重干扰。
4.2 地图上温度色带整体偏移
现象:插值结果叠加到 Cesium 地图后,温度分布与地形、城市位置对不上,整体偏移了一段距离。
原因:经纬度到像素的映射关系搞错了。常见的情况是网格生成时经度范围用了 [minLon, maxLon],但 canvas 绘制时 x 轴方向反了,或者纬度方向没有翻转。Cesium 的经纬度坐标系是经度向东递增、纬度向北递增,而 canvas 是 x 向右递增、y 向下递增。
解决:在生成 ImageData 时显式做坐标变换。如果网格是按纬度从南到北存储的,填充像素时需要把行索引翻转:const row = height - 1 - Math.floor(y)。同时检查 Cesium 的 Rectangle 参数顺序是 [west, south, east, north],不要写反。
4.3 大数据量站点导致页面卡死
现象:站点数量超过 500 个后,插值计算耗时超过 10 秒,浏览器主线程阻塞,页面无响应。
原因:全局克里金的矩阵规模是 (n+1)×(n+1),n=500 时矩阵有 25 万个元素,LU 分解的时间复杂度是 O(n³),单次分解就需要数秒。如果对每个网格点都重新分解,计算量完全不可接受。
解决:改用局部克里金,每个网格点只取最近的 K 个站点(K 通常取 16 到 32),矩阵规模降到 33×33 以下,单点求解在毫秒级。同时把插值计算放到 Web Worker 里,避免阻塞 UI 线程。如果站点分布不均匀,可以用 k-d 树加速最近邻查询。
4.4 色带在零度附近出现明显断层
现象:温度色带在 0°C 附近出现一条突兀的分界线,两侧颜色跳跃过大。
原因:色带停靠点在零度附近设置过密,或者两侧颜色差异过大。比如 -1°C 是浅蓝,+1°C 是浅黄,中间没有过渡色,视觉上就会形成断层。
解决:在零度附近增加过渡色停靠点,比如 -2°C 到 +2°C 之间插入 3 到 4 个中间色。或者改用连续色带,用 HSL 插值代替 RGB 插值,避免 RGB 空间中的颜色跳跃。
4.5 透明区域边缘出现黑边
现象:插值网格中无效值区域(alpha=0)与有效值区域的交界处,出现一圈黑色或深色边缘。
原因:canvas 在缩放或纹理过滤时,透明像素的 RGB 值(通常是 0,0,0)会与相邻有效像素混合,导致边缘变暗。这是预乘 alpha 的经典问题。
解决:在设置无效像素时,不要只把 alpha 设为 0,同时把 RGB 也设为与相邻有效像素相近的值,或者统一设为白色。另一种方式是在 Cesium 的 ImageryProvider 中关闭纹理过滤的 mipmap,减少混合带来的边缘效应。
5. 设计取舍:为什么没有一种插值方案能通吃所有场景
克里金插值在气象可视化里之所以被广泛采用,核心原因是它把"空间结构"显式地建模进了算法。IDW 也能出图,但它的权重只跟距离有关,遇到站点稀疏区域会直接产生明显的同心圆伪影;克里金通过半变异函数先学习空间自相关规律,在站点分布不均匀时表现更稳健。但这个优势是有代价的:半变异函数的拟合需要人工干预,模型选择、参数调整都依赖经验,自动化程度不如 IDW。
在 Cesium 里落地时,另一个取舍是渲染方式。canvas 转图片的方案实现简单、性能可控,但分辨率受限于 canvas 尺寸,放大后会出现模糊。用 Primitive 逐网格构建几何体的方案理论上可以做到无限分辨率,但顶点数量随网格密度平方增长,实际项目中很难超过 200×200 的网格规模。还有一种折中方案是用 Cesium 的 CustomShader 在片元着色器里实时计算颜色映射,把插值网格作为纹理传入,这样既保留了图片方案的性能,又能在着色器里做更灵活的颜色处理。
参数调优上,我一般会遵循一个原则:先看数据,再调模型。把站点值按距离做散点图,观察半变异函数的大致形状,再决定用球状还是指数模型。range 的初始值可以设为研究区域对角线长度的三分之一到二分之一,nugget 从样本方差的 10% 开始试。如果插值结果出现明显的"过平滑"——所有网格点的值都趋近于平均值——说明 range 设得太大了;如果出现大量孤立的高值斑点,说明 range 设得太小。
从那以后我每次做空间插值,都会先把半变异函数的散点和拟合曲线画出来看一眼,确认模型形状跟数据对得上,再往 Cesium 里灌。这个习惯帮我省掉了大量反复调参的时间。希望帮到你。
本文还有配套的精品资源,点击获取