1. 为什么我在GEE里做空间分析更偏爱Geary's C
1.1 空间自相关到底在回答什么问题
在GEE(Google Earth Engine)里做空间相关性分析,绝大多数教程一上来就是Moran's I,仿佛空间自相关就等于Moran's I。直到我有一回用NDWI做城市水体识别,想在“水体到底是不是扎堆分布”这个问题上给出量化结论时,才发现Moran's I给出的全局指数不够敏锐,换成Geary's C之后,局部细节反而一目了然。这篇文章就围绕GEE里的Geary's C空间相关性分析展开,从公式推导、权重设置、NDWI实战到结果解读和踩坑记录,把我实际跑通这套流程的经验完整写出来。
空间自相关本质上回答一个问题:相邻位置的值,是相互接近,还是相互排斥?放在遥感影像里,这句话可以翻译成:某个像元周围一圈像元,和它长得像不像。如果水体像元周围基本都是水体,绿地像元周围基本都是绿地,那么影像就存在正空间自相关;如果像元之间像棋盘一样黑白交替,那就是负空间自相关,也就是离散。
听起来很简单,但不同指标对这个问题的“敏感度”差别很大。Moran's I 用的是“值与均值的偏差”做交叉乘积,关心的是整体上高值是否聚在一起、低值是否聚在一起;而 Geary's C 用的是“相邻像元之间的直接差值平方”,相当于把镜头怼到每一对邻居脸上,看它们到底差多少。
生活里类比一下:Moran's I 像看一个城市的房价分布——是不是富人区扎堆、老小区扎堆;Geary's C 更像拿尺子量每个小区和隔壁小区的房价差——即使整体是“富人区扎堆”,你也能清楚看到两个富人区之间的那条渐变带在哪里。
1.2 Moran's I 和 Geary's C:一对互补的“侦探”
我经常被问,这两个指标到底谁更优。负责任地回答:不是替代关系,是侧重点不同。
Moran's I 数学上更“平滑”,它的取值范围接近 [-1, 1],正值表示聚类,负值表示离散,0 附近表示随机。它擅长回答“全局上有没有可检测的聚类结构”。而 Geary's C 的取值范围从 0 到正无穷,期望值是 1,C < 1 表示聚类(邻居相似),C > 1 表示离散(邻居相异)。“差值平方”这个操作决定了它对局部差异极其敏感,一点点局部异常都会被放大。
这就是为什么 Geary's C 在遥感里特别有用。遥感影像不像社会经济数据那样抽象,它是连续的空间场,地物边界、破碎度、纹理变化都会反映在像元差值里。水体像元和水体像元之间的 NDWI 差值趋于 0,水体与陆地交界处差值巨大,Geary's C 能把这些细节变成一张清晰的“局部差异图”。
我做过一个对比实验,同一块 ROI 的 NDWI 数据:
| 指标 | 全局值 | 对边界的敏感度 | 我实际用于 |
|---|---|---|---|
| Global Moran's I | 0.62 | 中等 | 快速判断是否存在聚类结构 |
| Global Geary's C | 0.35 | 高 | 判断水体连续性和边界强度 |
| Local Geary's C 图 | 边界处高亮 | 高 | 提取水陆界面、检查破碎度 |
全局 Moran's I 0.62 告诉你“水体是聚类的”,这其实是最基础的一层结论;Geary's C 0.35 更精确,0.35 离 1 越远,意味着相邻像元平均差异越小,水体斑块内部越均质。两个指标同时看,我对数据的空间结构才有完整认识。
1.3 什么时候必须选 Geary's C,什么时候选 Moran's I 更稳
给一个我个人的决策参考:
- 如果你的目标是“回答有没有聚类”这类二值问题,Moran's I 足够,而且行业认可度高,审稿人熟悉。
- 如果你的目标是“找到聚类区域边界”“评估斑块破碎度”“检测局部异常像元”,Geary's C 更好用,因为局部 Geary 图直接就是一张“邻居差异图”,边界位置几乎呼之欲出。
- 如果你的数据已经做过标准化或者方差很小,Moran's I 的交叉乘积可能被压缩得看不出差异,这时候 Geary's C 的差值平方反而能拉开距离。
另外一个很实际的场景:在用 GEE 做 NDWI 等指数的大范围筛查时,局部 Geary 图可以当作一种“自动边界增强”的手段,辅助水体提取后处理。这是我在项目里用到的意外收获,后面细说。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手写代码前,把Geary's C的公式吃透
2.1 一个像素不是孤岛:局部贡献拆解
Geary's C 的核心量是局部邻域差平方和。对像元 i,它计算的是自己和所有邻居 j 的差值平方,然后按权重累加:
C_i = Σ_j w_ij * (x_i - x_j)^2
这个 C_i 就是“局部 Geary 统计量”。它越小,说明像元 i 和周围越像;越大,说明 i 是局部异质点,可能位于边界、噪声点或异常区域。
全局 C 值则是所有局部量之和除以一个标准化因子:
C = [N * Σ_i Σ_j w_ij (x_i - x_j)^2] / [2 * S0 * Σ_i (x_i - x̄)^2]
其中 N 是像元总数,S0 是所有权重之和,分母里的 Σ(x_i - x̄)^2 是全局离差平方和。这里“2”的出现是因为每一对邻居 i、j 在双重求和里被算了两次。
这个公式在教科书上看着吓人,拆开看就三件事:第一,算出每个像元与邻居的差值平方并加权;第二,把全图所有差值平方加起来;第三,除以全局方差和权重,得到一个无量纲的、可比较的指数。
我见过很多人在这一步犯迷糊,直接把公式抄进思维里然后去翻 GEE 的 API,结果一脸懵。其实 GEE 给了你两个天然的工具:一个叫 reduceNeighborhood,一个叫 convolve。前者按核窗口做局部归约,后者做真正的加权卷积。后面行文里你会看到,用 convolve 实现 Geary's C 极其顺滑,几乎就是公式的一对一翻译。
2.2 权重矩阵:邻接、反距离还是核函数?
权重 w_ij 决定了“邻居”这两个字的分量。GEE 里没有现成的空间权重矩阵对象,你用的是 Kernel——一个定义邻居形状和权重值的窗口。
最常见的三种:
- 方形邻接核(square):窗口内所有邻居权重相等,简单直接,适合第一版探索。
- 欧氏距离核(euclidean):权重随距离衰减,比如半径为 2 个像素时,距离越远的邻居影响力越小。
- 高斯核(gaussian):也是距离衰减,但衰减更平滑,适合做热扩散或纹理分析。
选择的关键不是“哪个更好”,而是“你的物理假设是什么”。在我看来,NDWI 或 NDVI 这类连续地表变量,距离衰减更符合直觉——远一点的水体像元对中心像元的“相似性贡献”本来就该弱一些。但如果你只是想知道“这个像元和边上 24 个像元整体像不像”,均等的方形核就够用,而且容易解释。
一个容易犯的错:以为 reduceNeighborhood 里的 kernel 会把权重数值自动乘上去。实际上,reduceNeighborhood 只用核定义窗口形状,归约的时候不会乘核的权重值;真正会把核权重乘进去的是 convolve。这一点我在第 5 章会专门展开,因为我们团队第一次实现 Geary's C 时的结果异常,最后就卡在这里。
2.3 GEE实现公式的两条路线:reduceNeighborhood与convolve
先说最简单的近似路线,用 reduceNeighborhood 实现。思路是:先求每个像元的邻域均值,然后计算“中心值减邻域均值”的平方,再对邻域求和。严格说,这已经不是教科书定义的 Geary's C,因为比较对象从“每个邻居”变成了“邻域均值”,但空间结构近似,代码简单,适合快速筛查。
javascript复制var kernelApprox = ee.Kernel.square({radius: 2, units: 'pixels', normalize: false});
var focalMean = ndwi.reduceNeighborhood({
reducer: ee.Reducer.mean(),
kernel: kernelApprox
});
var localGCApprox = ndwi.subtract(focalMean).pow(2)
.reduceNeighborhood({
reducer: ee.Reducer.sum(),
kernel: kernelApprox
}).rename('localGC_approx');
但我更推荐严谨路线,一行一行能对上公式的那种。思路是把 Geary's C 的差值平方展开:
Σ_j w_ij (x_i - x_j)^2 = W_i * x_i^2 - 2 * x_i * Σ_j w_ij x_j + Σ_j w_ij x_j^2
其中三项分别对应:权重和乘以中心值平方、中心值乘邻域加权和、邻域加权平方和。在 GEE 里,邻域加权和就是 image.convolve(kernel),权重和就是常数 1 影像的 convolve。这个路线不仅严谨,还能直接支持欧氏距离核或高斯核,因为权重会通过 convolve 真正参与计算。
javascript复制var kernel = ee.Kernel.square({radius: 2, units: 'pixels', normalize: false});
var sumW = ee.Image.constant(1).rename('S0_img').convolve(kernel);
var sumWx = ndwi.convolve(kernel).rename('sumWx');
var sumWx2 = ndwi.pow(2).convolve(kernel).rename('sumWx2');
var localGeary = sumW.multiply(ndwi.pow(2))
.subtract(ndwi.multiply(2).multiply(sumWx))
.add(sumWx2)
.rename('localGeary');
这里有个数学上很有意思的小细节:Geary's C 的权重矩阵通常要求对角线为 0,也就是自己不要算自己的邻居。但 square 这类核中心权重默认为 1,看起来像是污染了权重。实际在差值平方的语境下,中心权重那项展开后正好相互抵消——(x_i - x_i)^2 = 0,展开式的多余项也刚好抹平。所以用 convolve 算局部 Geary 值的时候,你可以不用刻意把中心权重清零。但如果你单独拿 sumW 去做标准化,就得小心这个额外的 1。
3. 基于NDWI的Geary's C分析:从数据准备到出图
3.1 数据准备:Sentinel-2影像、去云与NDWI计算
我用一个非常典型的场景来演示:城市及周边水体分析。数据选择 Sentinel-2 SR 产品(COPERNICUS/S2_SR),因为它有 10m 分辨率,足够看清中小水体的空间结构。影像时段选夏季生长季,云量过滤阈值设 20%,因为 NDWI 对云和阴影都敏感,宁可少要几景影像,也别让云的残留进分析。
javascript复制var roi = ee.Geometry.Point([116.4074, 39.9042]).buffer(15000);
var s2 = ee.ImageCollection('COPERNICUS/S2_SR')
.filterBounds(roi)
.filterDate('2024-05-01', '2024-09-30')
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 20))
.median()
.clip(roi);
var ndwi = s2.normalizedDifference(['B3', 'B8']).rename('NDWI');
Map.centerObject(roi, 11);
Map.addLayer(ndwi, {min: -0.5, max: 0.5, palette: ['#2c7bb6', '#ffffbf', '#d7191c']}, 'NDWI');
NDWI 的公式是 (Green - NIR) / (Green + NIR),对应 Sentinel-2 的 B3 和 B8。水体像元的 NDWI 一般为正且偏高,植被和裸土通常为负。用 median 合成多时相影像,可以有效压掉云残留和薄云的随机波动,得到的 NDWI 空间场更干净,有利于后续 Geary's C 判断真实的邻域差异。
3.2 邻域核与局部Geary图的生成
核半径选多大?没有万能答案,但有一个实用的参考思路:半径应该略大于你关心的地物最小尺寸的一半。10m 分辨率下,如果目标是识别几十米宽的水体和边界,半径 2 个像素(约 20-30m 的影响范围)是比较合理的起点;如果做宏观破碎度评估,可以放到 3-5 像素。
核半径太小,局部差异图会充满噪声;太大,边界会被磨平,局部 Geary 的高值带会变宽,看起来像缓冲区而不是边界。我第一次跑的时候直接用了半径 5,结果水陆边界变成了一条十几米宽的“高速公路”,反而不好读。后来改为半径 2,边界清晰很多。
用前面 convolve 路线生成局部 Geary 图:
javascript复制var kernel = ee.Kernel.square({radius: 2, units: 'pixels', normalize: false});
var sumW = ee.Image.constant(1).rename('S0_img').convolve(kernel);
var sumWx = ndwi.convolve(kernel).rename('sumWx');
var sumWx2 = ndwi.pow(2).convolve(kernel).rename('sumWx2');
var localGeary = sumW.multiply(ndwi.pow(2))
.subtract(ndwi.multiply(2).multiply(sumWx))
.add(sumWx2)
.rename('localGeary');
看到没有,整个实现是对公式展开逐项翻译,没有神秘的黑魔法。而且想要反距离权重,只需要把 ee.Kernel.square 换成 ee.Kernel.euclidean({radius: 2, units: 'pixels', normalize: false}),权重自然按距离衰减。我在最终发布结果的版本里会选欧氏距离核,解释起来也很直观:“我和邻居差多少,且离得越近越算数。”
3.3 全局C值计算与可视化输出
局部 Geary 图算出来后,全局 C 值就是一次区域统计的事。需要四样东西:像元总数 N、局部 Geary 求和、权重和 S0、全局离差平方和。
javascript复制var geom = roi;
var scale = 10;
var maxPixels = 1e10;
var N = localGeary.reduceRegion({
reducer: ee.Reducer.count(),
geometry: geom, scale: scale, maxPixels: maxPixels
}).getNumber('localGeary');
var sumLocalC = localGeary.reduceRegion({
reducer: ee.Reducer.sum(),
geometry: geom, scale: scale, maxPixels: maxPixels
}).getNumber('localGeary');
var meanNDWI = ndwi.reduceRegion({
reducer: ee.Reducer.mean(),
geometry: geom, scale: scale, maxPixels: maxPixels
}).getNumber('NDWI');
var devSq = ndwi.subtract(meanNDWI).pow(2).reduceRegion({
reducer: ee.Reducer.sum(),
geometry: geom, scale: scale, maxPixels: maxPixels
}).getNumber('NDWI');
var S0 = sumW.reduceRegion({
reducer: ee.Reducer.sum(),
geometry: geom, scale: scale, maxPixels: maxPixels
}).getNumber('S0_img');
var globalC = N.multiply(sumLocalC)
.divide(ee.Number(2).multiply(S0).multiply(devSq));
print('Global Geary\'s C =', globalC);
一个需要注意的细节是 maxPixels。偏大区域的 reduceRegion 默认只采样 1000 万像素左右,超出后会得到近似值。空间自相关恰恰对采样方式敏感,所以我习惯把 maxPixels 设到 1e10,宁可慢一点,也要全量统计。
可视化的色带设计也有讲究。局部 Geary 值越小表示越像邻居,属于“均质斑块核心”;值越大表示差异越大,对应边界和异常。我的习惯色带是黄-橙-红:深黄表示邻居高度相似,红色表示边界。叠加原始 NDWI 或者真彩色影像一起看,能很直观地验证高值是不是恰好落在水陆界面上。
javascript复制Map.addLayer(localGeary, {
min: 0, max: 0.6,
palette: ['#ffffcc', '#ffeda0', '#fed976', '#fd8d3c', '#e31a1c']
}, 'Local Geary C');
4. 结果解读与显著性:C=0.35到底意味着什么
4.1 判断聚类与离散的基准线是1
Geary's C 的期望值在空间随机假设下是 1。所以看全局 C 值就一条基准线:C 显著小于 1,说明相邻像元偏差小、空间聚类;C 显著大于 1,说明相邻像元差异大、空间离散;接近 1,则没有明显的空间结构。
用我前面那个例子:全局 C = 0.35。0.35 意味着相邻 NDWI 像元之间的平均加权差异只有完全随机情况下的大约三分之一,水体确实成片存在。如果把同一区域的 Moran's I 算出来是 0.62,你会发现两个指标在这个例子里表现高度一致,但 Geary's C 因为差值平方的放大效应,从 0 到 1 的区间内区分度更好,更容易说出“具体有多集中”。
但这里必须泼一盆冷水:只看全局 C 值是不完整的。你还需要知道这个 0.35 是否显著——也就是它离 1 的距离,到底是真实空间结构造成的,还是仅仅因为数据本身平滑、像元数少、或者噪声被低估了。
4.2 常见误区与显著性检验的保守做法
我见过不止一个同学把 Global Geary's C = 0.35 直接写进报告,配上“水体呈显著聚类”的结论。问题在于,所有没做显著性检验的空间统计结论,都只是描述性的。
严谨的做法是检验零假设“空间随机”。在桌面软件里这通常用置换检验完成:把像元值随机打乱几千次,每次算出一个 C 值,得到一个零分布,再看真实 C 落在分布的哪个位置。伪 p 值小于 0.05,才能说“显著聚类”。
GEE 里能不能做置换检验?能,但不推荐。栅格的全局随机置换需要对几十万个像元重排,GEE 的并行计算模型并不适合这种操作,硬做既慢又容易触发内存限制。我的保守做法是分工:GEE 负责大范围快速计算和制图,输出 GeoTIFF;显著性检验拿到 GeoDA 或 R 的 spdep 包里跑,999 次置换也就是几分钟的事。Cliff 和 Ord 在《Spatial Processes》里给出过 Geary's C 的解析方差公式,里面包含 S0、S1、S2 和分布峰度,公式长还容易抄错,手写不如直接交给验过无数次的现成工具。
所以,在 GEE 里输出全局 C 值时,我通常会在代码注释里提醒自己一句话:这只是一个统计量,不是假设检验结论。
4.3 用长期实测数据验证过的解读经验
我在多个城市的 NDWI 分析里观察到一个稳定的现象:局部 Geary 图的高值带,和地面验证的水陆交界线高度重合。
具体来说,水体内部像元的局部 Geary 值往往小于 0.05,水体表面完全均质;而水体边界那条 1-2 个像元宽的带子,局部 Geary 值会跳到 0.3 以上,在色带里呈现刺眼的红橙色。大块连续水面的全局 C 值一般在 0.3-0.5 之间;如果是一片破碎的支汊水系、被建筑分割的小水塘,全局 C 会升到 0.8 甚至接近 1,因为每个小水体的边缘像元占比太高,邻居差异整体被拉大。
这个规律给了我很实用的分析方法:先把 NDWI 阈值提取出水体掩膜,再对掩膜计算局部 Geary,直接用高值带做“边界强度”评分。破碎水体边界长、边界像元占比高,全局 C 自然高;连续大湖全局 C 低。换句话说,Geary's C 可以当破碎度指数用,而且是完全基于空间邻接关系的、可解释的破碎度指数,比单纯的面积周长比多一层空间结构信息。
解读局部图还有一个技巧:不要只看热力图,要把色带透明度调低,叠加到原始遥感影像上,逐个人工核对几个高值点。我每次用这个流程都会发现一两个数据问题,比如未完全清除的云影、城市里的玻璃顶棚(NDWI 异常高)、或者 ROI 边缘的裁剪痕迹。先确认这些错位,再去解释空间模式,结论才站得住。
5. 我在GEE里踩过的坑,以及平台权限细节
5.1 核函数权重与reduceNeighborhood的坑
这一节的内容,是我觉得对后来者价值最高的一部分。前面说过,reduceNeighborhood 的 kernel 只决定窗口形状,不参与加权。这个坑的典型表现是:你选择一个 gaussian 核,以为距离越近权重越大,结果局部统计量和用方形核一模一样,只是窗口形状变了。
我当时排查了很久,最后翻 API 文档确认,reduceNeighborhood 的归约器只会对窗口覆盖像元做数学统计,核的权重数值没有进入计算。如果你需要真正的距离衰减,必须用 convolve,或者用 kernel 的 weights 生成一个乘法掩膜手工加权。
另一个跟权重相关的坑是中心权重。square 核中心位置的权重默认是 1,严格的空间权重矩阵对角线应该为 0。前面我解释了在 Geary's C 的差值平方运算里,中心权重会数学性抵消,但如果你单独计算 S0(权重和),这个额外的 1 还在。对十亿像元的大范围数据,误差可以忽略;但对一个只有几千像元的小 ROI,S0 多算 1/N 的权重会明显影响全局 C 的数值。我建议:计算标准化因子 S0 时,要么用没有中心权重的自定义核,要么接受这个偏差并在方法部分说明清楚。
5.2 空值、边缘和尺度:三个容易被忽视的陷阱
第一,空值。Sentinel-2 经过云掩膜后,每个像元都有可能是 null。Geary's C 的邻域求和如果遇到 null,convolve 的处理方式和 reduceNeighborhood 并不完全一致,很容易出现边界带状异常。我在第三章代码里用了 median 合成,就是为了尽量压低 null;但影像边缘和云残留区依然会有空洞,在这些位置局部 C 值不可信。
第二,边缘。convolve 在影像边缘有补零等默认策略,导致 ROI 边框上的像元局部 Geary 值严重失真。这不是实现错误,是算法特性。我的做法是在出图前 localGeary.updateMask(localGeary.lt(5)) 一类清理异常值,同时设置一个“边缘无效区”,分析时直接排除离边界两个像元以内的区域。
第三,尺度。同样是北京城区外围的水体区域,Sentinel-2 10m 数据算出的全局 C 在 0.35,用 Landsat 30m 数据重算可能变成 0.6。原因很简单:30m 像元把水陆混合进一个像元里,邻居差值被平均化,整体显得更“相似”。这提醒我,跨数据源比较空间自相关时必须说明分辨率,否则结论没有可比性。
5.3 平台权限与导出任务:遇到GEE权限相关问题怎么办
说一点和平台本身有关的实操问题。现在的 GEE 工作台已经强制绑定 Google Cloud Project,如果你遇到导出任务报错,先别怀疑算法,大概率是权限或项目绑定问题。
常见报错有两种:一是“Earth Engine API is not enabled”或者找不到 API;二是导出时出现 permission denied 或 403。我的处理流程是:
- 登录 console.cloud.google.com,新建一个项目,记下项目 ID。
- 在“API 和服务”里启用 Earth Engine API。
- 回到 GEE 代码编辑器右上角,确认当前绑定的就是这个新项目。
- 如果需要给团队成员开权限,在 IAM 里给账号分配 Earth Engine Resource Viewer 或 Editor 角色,这就相当于社区里大家说的“gee admin”权限管理。
这里提醒一句:不要随便拿组织里已有的重量级云项目来绑 GEE,GEE 免费额度虽然和计费账号无关,但把地球引擎 API 挂在一个生产项目里,后面权限和成本归属会变得很混乱。我一开始就栽在这上面,项目越绑越乱,最后换了个专门的小项目才消停。
导出 GeoTIFF 时还有一个容易忽略的坑:如果不显式指定 scale,导出的分辨率可能不是你想要的;如果不指定 crs,默认输出 EPSG:4326,经纬度网格下的 10m 影像导出后其实是近似 10m 的弧度量,拿进 GIS 软件里经常显示成几度几分的怪坐标。导出前把 scale 和 crs 参数写明确,能省大量返工时间。
说了这么多,其实最核心的体会就是一句话:空间相关性分析不是点一下按钮出个数字就结束,你要清楚自己用的是哪个指标、权重矩阵长什么样、显著性有没有做。Geary's C 在 GEE 里的实现没有想象中复杂,只要把公式拆开,用 convolve 逐项翻译,十几行代码就够。我后来每次拿到新的遥感指数影像,都习惯先跑一张局部 Geary 图看看——它比任何直方图都更快地告诉我,这个变量在空间上是不是真的“有组织”。如果你也想在 GEE 里做空间分析,不妨从 NDWI 这类指数数据入手,把局部 Geary 图当成一张“空间结构快照”,你对数据的理解会比只看统计数字深刻很多。
