谷歌地球引擎(Google Earth Engine,简称GEE)处理土地利用分类,我这两年几乎每个月都要用上几次。无论是做城市扩张分析、农业种植结构提取,还是给生态评估项目出基础底图,GEE加监督分类这套组合基本上是绕不开的标配。这篇博文我打算把整个流程从数据准备、样本设计、分类器选型到精度验证、结果导出完整串一遍,重点放在那些官方文档里不会明说的判断逻辑和容易踩坑的细节上。不管你是刚接触GEE的遥感方向研究生,还是已经能用filterDate、clip但还不太确定样本怎么布、分类器参数怎么调的从业者,这篇内容都能给你一个可以直接照做的方案。
先说结论:GEE做监督分类,核心不在代码,而在样本和特征。代码翻来覆去就是sample、train、classify这三板斧,真正的差距在于你喂给分类器的训练样本够不够准、够不够均衡,以及你给模型的波段特征能不能把地类区分开。这篇文章会顺着这个逻辑展开,把每一步拆开讲清楚。
1. 整体思路:GEE为什么适合做土地利用监督分类
1.1 监督分类的基本逻辑
监督分类说白了就是“教机器认地物”。你先在影像上圈出几块你确定是水体的地方、确定是农田的地方、确定是城市的地方,告诉算法“这些像素分别属于哪一类”,算法从这些样本像素上学习光谱特征的分布规律,然后拿这个规律去推测整幅影像上每一个像素属于哪一类。
这里面有个关键概念叫特征空间。每个像素不只是一个颜色值,它是很多波段数值组成的一个向量。比如Landsat-8有蓝、绿、红、近红外、两个短波红外这6个反射率波段,那每个像素就是一个6维向量。水体在可见光波段反射率低、近红外波段更低,而植被在近红外波段反射率高,这些差异就是分类器做决策的依据。
监督分类跟非监督分类的区别在于,非监督分类(比如K-Means、ISODATA)是算法自己找数据中的聚类结构,跑完你再给每个类贴标签;监督分类是你要先给出带标签的样本,算法学的是“某某标签对应的像素长什么样”。前者适合你对区域完全不了解的探索阶段,后者适合你有明确分类体系、需要出精确成果的应用场景。土地利用分类绝大多数情况下是有明确体系的任务,所以监督分类是主流的做法。
1.2 GEE在这个任务里解决了什么核心痛点
传统的土地利用分类流程是这样的:先下载遥感影像(可能几十个场景),然后在一个能跑得动的桌面GIS软件里做大气校正、影像镶嵌、样本勾选、分类、后处理。这个过程最大的问题是数据管理和计算效率。如果研究区域跨好几景影像,光是下载、拼接、裁剪就会消耗大量时间和硬盘空间;如果做年度时序分析,时间维度上的数据量更是天文数字。
GEE的核心价值在于它把“数据存储、计算资源、算法库”三者捆在了一起。所有影像数据都在云端,你的脚本直接在服务器端跑,不需要把数据下载到本地。处理一整个省份的Landsat影像,GEE在几秒到几十秒内就能完成。你写的是JavaScript或Python代码,调用的却是谷歌数据中心里成千上万的CPU核心。这种方式让“大规模、多时相、长时序”的土地利用分类变成了一件门槛很低的事情。
对于监督分类这个具体任务,GEE还有一个优势:你可以在交互式地图上直接勾选训练样本,所见即所得,样本做完直接喂给分类器,反馈非常快。这种“边看边改”的交互方式,比传统软件里“样本表和影像分离”的流程高效太多。
1.3 整体技术路线:从数据到成图的七个环节
我们这次要做的完整流程,我习惯拆成七个环节来规划:
- 确定研究区域和时间范围:分类的目标区域在哪,用什么时间窗口的影像。
- 数据源选择与预处理:选Landsat-8还是Sentinel-2,做云掩膜、影像合成。
- 特征构建:在原始波段基础上加NDVI、NDWI等指数,提升地类区分度。
- 训练样本采集:人工目视解译或者基于辅助数据生成带标签的样本点。
- 样本拆分与分类器训练:按一定比例拆成训练集和验证集,训练随机森林等分类器。
- 全图分类与后处理:把训练好的模型应用到整个研究区,必要时做平滑滤波。
- 精度评估与结果导出:用独立验证样本计算混淆矩阵、Kappa系数,然后导出分类结果。
这个流程看着常规,但每个环节的选择都会直接影响最终精度。后面我按这个顺序详细展开。
2. 数据源与特征构建:分类精度的地基
2.1 Landsat-8还是Sentinel-2:尺度、时间与波段的取舍
做土地利用分类,第一步要选对影像源。现在最常用的就是Landsat-8/9和Sentinel-2,两者各有适用场景。我从几个维度对比一下:
| 对比维度 | Landsat-8/9 | Sentinel-2 |
|---|---|---|
| 空间分辨率 | 多光谱30米,全色15米 | 可见光近红外10米,红边和短波红外20米 |
| 重返周期 | 16天 | 5天(双星) |
| 时间跨度 | 2013年至今(L8)、2021年至今(L9) | 2015年至今 |
| 波段设置 | 4个可见光/近红外+2个短波红外,有热红外 | 4个10米波段+6个20米波段,有3个红边波段,无热红外 |
| 适合场景 | 中尺度区域、长时序分析、温度相关研究 | 小尺度精细分类、农业监测、植被参数反演 |
我的经验是:如果你做的是省级以上大范围、或者要做2015年以前的历史回溯,选Landsat系列,因为它的存档时间最长且连续;如果你的研究区不大(比如一个县或者一个流域),又特别在意空间细节,用Sentinel-2的10米分辨率做出来的图会干净很多。这篇文章的实操部分我用Landsat-8 Collection 2 Tier 1 Surface Reflectance数据来演示,因为它在GEE里处理最简单、计算快,而且对新手理解流程最友好。需要换Sentinel-2的话,把影像集合的ID换掉,特征波段做相应调整即可。
2.2 光谱波段与指数的组合:不要只用原始反射率
很多教程跑到分类这一步就直接把原始6个波段丢进分类器。这样做能出结果,但精度往往会差一口气。原因很简单:有些地类的区分不能靠单一波段的绝对值,而要靠波段之间的比值关系。
举个例子,水体和阴影在可见光波段的反射率都很低,单看蓝、绿波段很难区分,但水体在近红外波段的吸收比阴影更强,这就让归一化差异水体指数(NDWI)成为区分水和阴影的利器。再比如植被覆盖度高的农田和自然林地,原始波段形态相似,但增强型植被指数(EVI)对高植被覆盖区更敏感,加上它对大气和土壤背景的抗干扰能力,经常能让两类分开。
我在实际项目中常用的特征组合是:
- 6个原始反射率波段:蓝、绿、红、近红外、短波红外1、短波红外2。
- 3个光谱指数:NDVI(绿度)、NDWI(湿度/水体)、EVI(抗饱和植被指数)。
- 2个地形因子:高程和坡度,来自SRTM DEM数据。
为什么加入地形因子?因为土地利用分类不只是看植被和水体,还要区分山地林地和山脚农田,甚至阴坡和阳坡的植被长势完全不同。加上高程和坡度后,分类器可以利用“地形位置”这个极强先验信息。在山区项目里,加不加地形因子,分类精度能差5到10个百分点。
2.3 影像合成:用“一年中最清爽的一帧”来分类
还有一个容易忽略的问题:到底用哪一天的影像分类?单景影像往往有云、有阴影,而且同一种地类在不同季节长相完全不同。我的做法是选一个物候相对稳定的时间窗口,把窗口内的所有影像做中值合成。
中值合成的意思是,对每个像素,取这个时间窗口内所有有效观测值的中位数作为最终值。这样既去掉了云和异常噪声,又保留了这个时间段地物的普遍光谱状态。比如做华北平原的农耕区分类,我通常会选5月到9月这个窗口,因为这个时段植被覆盖度高、农田作物长势明显,各地类光谱差异最大。
代码实现上,filterDate之后先map一个云掩膜函数,再median()合成,就能得到一幅干净的无云影像。如果没有做云掩膜直接median,云和云的阴影虽然会被中值削弱,但还是会有一部分残留在结果里,所以云掩膜这一步不能省。
3. 训练样本设计与分类器选型:决定精度上限的关键
3.1 样本点怎么采集才不坑自己
这是整个监督分类流程里最需要耐心的环节。很多人把分类精度不高归结于算法不行,实际上八成是样本出了问题。我梳理了几个必须遵守的原则。
第一,样本必须有代表性。每个类别的样本要覆盖该类别在整个研究区内的光谱变异范围。比如“林地”这个类别,如果研究区里有针叶林、阔叶林、混交林,它们的反射率差异明显,你就不能只在一片针叶林里取样本,必须在不同林分类型里都取。从这个意义上说,样本的“面”比样本的“量”更重要。
第二,样本量要够但不要畸多。经验值是每个类别至少50到100个样本点,类别多或者光谱变异大的地区要更多。样本太多也未必好,如果同类样本之间高度相关,相当于给分类器灌入了冗余信息,反而可能造成过拟合。
第三,样本要尽量均匀分布。不要只集中在研究区一角,应该铺满整个研究区。分类模型学的是光谱特征,而同一个地类在不同区域因为土壤背景、大气状况、地形坡向的差异,光谱会有漂移,样本覆盖不全会导致没被覆盖到的地方分类出错。
第四,验证样本和训练样本必须独立。这一点我要特别强调。很多人图省事,把同一批样本既拿去训练又拿去验证,算出来的精度高得吓人(整体精度动辄90%以上),但一到真实应用就严重拉胯。正确的做法是先把样本整体做一次随机打乱,比如按70%和30%的比例拆成训练集和验证集。如果样本量充裕,更好的做法是训练样本和验证样本分别独立采集,互不关联。
3.2 分类器怎么选:CART、随机森林、SVM的取舍逻辑
GEE里内置了好几种监督分类器,最常用的就是CART(决策树)、随机森林(Random Forest)和支持向量机(SVM)。三者的区别可以从学习方式上理解。
CART就是一棵决策树,它通过一系列“如果某波段大于某阈值就分到某类”的规则把特征空间切分出来。优点是训练速度极快、结果可解释,缺点是单棵树的稳定性差——训练数据稍微变一点,树的结构就变,分类结果就会跟着波动。
随机森林是很多棵决策树的集成,每棵树用训练样本的一个随机子集和特征的一个随机子集来训练,最后让所有树投票决定类别。它对噪声和过拟合的抗性比单棵树强很多,训练速度也不算慢,是GEE里做土地分类性价比最高的选择。
SVM的思路是在特征空间里找一个“间隔最大”的超平面把不同类别分开。它对小样本、高维特征的表现很好,但缺点是训练时间长,对参数(比如核函数类型、惩罚系数)敏感,调参成本高。
我的使用经验是:默认优先用随机森林,它在绝大多数土地利用分类任务里精度和稳定性的平衡最好。CART可以用来做快速实验、看哪个特征对分类贡献大,但出正式成果我不会用它。SVM只有在样本量不大(比如几千个点以内)且对精度有极致要求时才考虑,样本量一大训练时间会让人崩溃。
3.3 随机森林关键参数:为什么是300棵树
GEE的随机森林算法对应函数是ee.Classifier.smileRandomForest(numberOfTrees)。官方的默认参数比较保守,实际使用中我一般手动设置几个关键参数。
树的数量(numberOfTrees):我常用300。这个数值不是拍脑袋定的。理论上树越多模型越稳定,但超过一定数量后边际收益急速递减,训练时间和内存开销却线性增长。我用100、300、500做过对比,100棵时精度略低一点,300和500的精度几乎没有差别,所以300是个性价比很好的折中值。
其它几个参数也很关键:
minLeafPopulation:叶子节点最少包含的样本数,默认1。设得太小容易过拟合,我一般设3或5。bagFraction:每棵树用的样本比例,默认0.5。这个参数让每棵树看到的样本不完全一样,是随机森林能抗过拟合的关键,一般不用动。maxNodes:单棵树的最大节点数,默认null也就是不限制。数据量大、特征多时可以限制一下,比如设到200,能明显加快训练速度。
需要明白的是,这些参数不是越复杂越好。随机森林本身就是“保持简单、依靠集成”的算法,多数情况下用合理的默认值就能拿到不错的结果。
3.4 特征重要性:让模型告诉你哪些特征在干活
GEE的分类器里有一个很实用的方法叫explain(),它能输出分类器在训练时用到的各项配置。但如果我们用的是随机森林,更值得看的其实是特征重要性。在GEE里可以用classifier.explain()获得,也可以基于训练好的模型输出每个特征的重要性属性。
我在项目里的做法是:第一次训练跑完之后,先去查看特征重要性排序,如果发现某个指数或波段的贡献度长期垫底(比如归一化差异水体指数在完全干旱区可能作用很小),就会考虑把它从特征列表里去掉。这个做法的意义在于减少特征维度,让模型更简洁、训练更快,有时候反而能提升精度。
这个环节不要省,它能让你的分类项目从“能用”进步到“好用”。
4. 完整代码实操:一步步跑通GEE监督分类
4.1 环境准备:区域、影像与云掩膜
打开 GEE Code Editor(code.earthengine.google.com),左边的搜索框可以搜索区域、数据、示例脚本。这一段我们先把研究区、影像集合、云掩膜函数准备好。
研究区域我这里先用一个点加缓冲区的方式演示,方便直接运行。如果是实际项目,你可以在左侧几何图形工具里画一个多边形,或者上传自己的Shapefile(通过Assets),然后读取ee.FeatureCollection("你的资产ID")。
// 定义研究区(示例:某地附近10公里范围) var studyArea = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017') .filterMetadata('country_na', 'equals', 'China') .geometry() .centroid() .buffer(20000); // 实际使用请换成自己的矢量范围 Map.centerObject(studyArea, 10); Map.addLayer(studyArea, {color: 'red'}, '研究区');然后是Landsat-8 Collection 2 Level-2地表反射率数据集的引入和云掩膜。Collection 2 Level-2的影像自带QA_PIXEL波段,里面有云、云阴影、冰雪等质量标记,我们通常只保留像素质量干净的部分。
var l8 = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2'); function maskL8Clouds(image) { var qa = image.select('QA_PIXEL'); var cloudBitMask = 1 << 3; // 云 var cloudShadowBitMask = 1 << 4; // 云阴影 var snowBitMask = 1 << 5; // 雪 var mask = qa.bitwiseAnd(cloudBitMask).eq(0) .and(qa.bitwiseAnd(cloudShadowBitMask).eq(0)) .and(qa.bitwiseAnd(snowBitMask).eq(0)); return image.updateMask(mask); }这段代码里的位运算可能对不熟悉二进制的人来说有点绕。简单解释:QA_PIXEL是一个16位的整数,每一位用0或1表示一种质量标志。比如第3位(bit 3)表示是否云,第4位是否云阴影,第5位是否雪。1 << 3是把1左移3位,得到一个只有第3位是1的数字,跟QA值做 bitwiseAnd 之后,如果结果等于0,就说明这一位没有被置1,也就是没有云。三个条件用 and 串起来,就得到了“无云无阴影无雪”的掩膜。
接下来选择时间窗口并合成影像。这里我选择植被生长季的5月到9月,并加上一个简单的缓冲区(一年前后各多出一定天数,让数据更充分)。
var startDate = '2023-05-01'; var endDate = '2023-10-01'; var image = l8 .filterBounds(studyArea) .filterDate(startDate, endDate) .map(maskL8Clouds) .map(function(img) { // Collection 2 Level-2 地表反射率带有缩放系数 var scale = 0.0000275; var offset = -0.2; var optical = img.select('SR_B[2-7]').multiply(scale).add(offset).copyProperties(img, ['system:time_start']); return optical; }) .median() .clip(studyArea);在这里我先把SR_B2到SR_B7六个反射率波段做了定标处理。Collection 2 Level-2的原始DN值需要经过缩放才能变成实际的反射率(范围大约0到1)。这个线性缩放不影响随机森林分类器的相对比较,但如果后面你要做阈值提取(比如NDVI大于0.5算植被),就必须先做这一步,不然阈值毫无意义。
4.2 光谱指数与地形特征:组合出最终的特征列表
接着构建NDVI、NDWI、EVI三个指数,并引入SRTM地形数据。
var ndvi = image.normalizedDifference(['SR_B5', 'SR_B4']).rename('NDVI'); var ndwi = image.normalizedDifference(['SR_B3', 'SR_B5']).rename('NDWI'); var evi = image.expression( '2.5 * ((NIR - RED) / (NIR + 6 * RED - 7.5 * BLUE + 1))', { 'NIR': image.select('SR_B5'), 'RED': image.select('SR_B4'), 'BLUE': image.select('SR_B2') }).rename('EVI'); var dem = ee.Image('USGS/SRTMGL1_003').select('elevation'); var slope = ee.Terrain.slope(dem).rename('slope'); // 合成最终特征影像 var featureImage = image .addBands(ndvi) .addBands(ndwi) .addBands(evi) .addBands(dem) .addBands(slope); var featureBands = [ 'SR_B2', 'SR_B3', 'SR_B4', 'SR_B5', 'SR_B6', 'SR_B7', 'NDVI', 'NDWI', 'EVI', 'elevation', 'slope' ]; Map.addLayer(featureImage, {bands: ['SR_B4', 'SR_B3', 'SR_B2'], min: 0, max: 0.3}, '真彩色影像');有一个细节要注意:normalizedDifference函数里两个波段的顺序。比如NDVI是(近红外 - 红)/(近红外 + 红),所以要写成['SR_B5', 'SR_B4'];NDWI常见定义是用绿光和近红外,即(绿 - 近红外)/(绿 + 近红外),所以是['SR_B3', 'SR_B5']。写反了结果完全不对,这类小错误非常容易在熬夜写代码时出现,建议把指数和波段对应关系先写在注释里。
4.3 训练样本:手工勾选变体与自动生成示例
训练样本在GEE里有两种常见获取方式。一种是你直接在左侧地图工具里手动画点,然后每个点通过属性赋类别标签;另一种是用随机点生成工具,再人工目视判别。
我这里用第二种方式做一个可复现的示例。它的逻辑是:在研究区内随机撒点,然后基于高分辨率影像(这里用Sentinel-2做参考)和你的经验,给每个点指定类别。当然,实际项目更推荐你直接动笔圈画多边形,把每个类别的地块完整勾出来再转成点,这样样本更纯、边界更干净。
// 方式1:手工sampling——在Map上手动添加点,并在属性里填class // 示例中用随机点近似(实际使用请删掉这段随机点逻辑,改为你的真实样本) var samples = ee.FeatureCollection([ ee.Feature(ee.Geometry.Point([116.391, 39.907]), {class: 0}), // 水体 ee.Feature(ee.Geometry.Point([116.458, 39.931]), {class: 1}), // 建设用地 ee.Feature(ee.Geometry.Point([116.312, 39.883]), {class: 2}), // 农田 ee.Feature(ee.Geometry.Point([116.215, 39.975]), {class: 3}), // 林地 ee.Feature(ee.Geometry.Point([116.178, 40.021]), {class: 4}) // 裸地 ]);除了手工点,我在项目里还有一种效率很高的做法:先用非监督分类(如K-Means)跑一版初步结果,然后基于初步结果来“指导”样本采集。比如非监督分类把一块区域分成5个光谱簇,你对照高分辨率影像逐个确认这些簇分别对应什么地类,然后把这些簇的代表性点直接吸收为监督分类的训练样本。这种方法比纯手工撒点快很多,而且样本在光谱特征空间的代表性更均匀。
无论用哪种方式,最终都要确保每个样本点都带有class这个数值型属性。类别编号建议从0开始连续编号(0, 1, 2, ...),因为GEE的分类器对类别编号有连续性要求,缺号会导致混淆矩阵显示不完整。
4.4 拆分训练集/验证集与训练分类器
样本准备好之后,用randomColumn方法给每个样本分配一个0到1之间的随机数,然后按0.7和0.3的比例拆分。
var samplesWithRandom = samples.randomColumn('random', 0); var trainingSet = samplesWithRandom.filter(ee.Filter.lt('random', 0.7)); var validationSet = samplesWithRandom.filter(ee.Filter.gte('random', 0.3));这里拆分的意义是:训练集用来训练模型,验证集用来评估模型在“没见过”的数据上的表现。如果验证精度和训练精度差距过大,说明模型过拟合了。注意randomColumn如果不给随机种子(第二个参数),每次运行时生成的随机数不同,结果会略有波动。如果你希望结果可复现,就把种子固定下来,比如randomColumn('random', 42)。
接下来训练随机森林分类器。
var classifier = ee.Classifier.smileRandomForest({ numberOfTrees: 300, minLeafPopulation: 3, bagFraction: 0.5, seed: 42 }).train({ features: trainingSet, classProperty: 'class', inputProperties: featureBands });这一步是整个流程里最核心的“学习”环节。.train()会遍历训练集里每一个样本点,提取对应位置上的特征波段值,跟样本的类别标签放在一起,交给随机森林算法训练出一组决策规则。训练完成后,这个分类器对象就包含了全部的规则信息,可以随时拿来对任意影像做分类。
4.5 执行分类与结果可视化
训练完成之后,分类就简单了,一句classify搞定。
var classified = featureImage.classify(classifier); var palette = [ '1d6fd9', // 0 水体 'd91d1d', // 1 建设用地 'f0d94c', // 2 农田 '2d8a3e', // 3 林地 '9c7c5b' // 4 裸地 ]; Map.addLayer(classified, {min: 0, max: 4, palette: palette}, '监督分类结果');这里有一个可视化上的小细节:分类结果的调色板,min和max一定要设置成类别编号的最小值和最大值,不要用默认拉伸。如果不设,GEE会按影像的直方图自动拉伸颜色,结果就是不同类别显示的颜色完全错乱,看起来一团糟。
4.6 精度评估:混淆矩阵、Kappa与各类精度
分类结果做完之后,最重要的一步是拿验证集算精度。做法是:把验证集样本对应的实际类别和分类结果类别放在一起,生成混淆矩阵。
var validationClassified = classified.sampleRegions({ collection: validationSet, properties: ['class'], scale: 30 }); var testAccuracy = validationClassified.errorMatrix('class', 'classification'); print('混淆矩阵', testAccuracy); print('总体精度', testAccuracy.accuracy()); print('Kappa系数', testAccuracy.kappa()); print('用户精度', testAccuracy.consumersAccuracy()); print('生产者精度', testAccuracy.producersAccuracy());混淆矩阵怎么读?行通常代表参考类别(真实类别),列代表分类结果类别。对角线上的数字是分类正确的样本数,非对角线上的数字是混淆的样本数。两类之间的混淆在矩阵里一目了然,这是排查分类问题的第一手信息。
几个精度指标的含义也需要理解清楚:
- 总体精度(Overall Accuracy):所有样本中分类正确的比例。
- Kappa系数:排除了随机一致性的影响之后的一致性度量,一般0.8以上算很好,0.6到0.8算可用。
- 生产者精度(Producer's Accuracy):某个真实类别被正确识别的比例,漏分类别的严重程度看它。
- 用户精度(Consumer's Accuracy):某个分类结果在真实世界里确实属于该类的比例,误分类别的严重程度看它。
实际项目中,我会特别关注混淆矩阵里哪两类在互相混淆。比如农田和林地经常混,那多半是物候期选择不当;建设用地和裸地经常混,那多半是光谱太接近,需要考虑加纹理特征或者更换数据源。
4.7 导出分类结果到本地方便制图
分类结果在GEE里可以直接截图,但正式成果需要导出到本地方便后续制图和分析。用Export.image.toDrive可以导出为GeoTIFF。
Export.image.toDrive({ image: classified.int16(), description: 'LandUse_Classified_2023', folder: 'GEE_Export', scale: 30, crs: 'EPSG:4326', region: studyArea, maxPixels: 1e13 });导出的几个参数要注意:classified.int16()是把分类结果的浮点标签转成整型,文件体积会小很多,也更符合分类产品的存储习惯;scale要根据你的数据源分辨率设置,Landsat是30米,Sentinel-2如果是用10米波段分类就写10;crs如果研究区在特定投影带内,建议用当地的高斯-克吕格投影或UTM投影,面积和形状畸变更小,不过对于普通的分类图导出,EPSG:4326也能用,主要看下游制图软件的需求。
启动导出任务后,到右侧的Tasks面板点击Run,等待处理完成后文件就会出现在你的Google Drive对应文件夹里。
5. 常见问题与排查技巧实录
5.1 分类结果“椒盐效应”明显怎么办
椒盐效应就是分类结果图里的地类像撒了一层盐和胡椒,明明是一大片连续农田,结果图里却密密麻麻混杂着零星的林地、裸地像素。这个问题的本质是像素级分类忽略了空间上下文信息——每个像元独立判断类别,没有考虑周围像元的类别。
有两种解决方案。第一种是结果后处理,对分类结果做众数滤波,也就是用一个窗口计算中心像元周围像元的众数,把中心像元的类别替换成众数类别。GEE里可以直接用focalMode:
var smoothed = classified.focalMode(30, 'square', 'average');需要注意的是,滤波窗口的大小要跟你允许的最小图斑面积匹配。30米分辨率影像用90米窗口做一次focalMode,基本能抹掉大部分小碎斑,但如果你要保留很小的地物(比如宽度只有30米的水渠),就不要用太大窗口。
第二种更彻底的方案是在分类前使用面向对象的分割方法,先对影像做分割,把像素分成一个个对象,再对每个对象做分类。这不仅解决椒盐效应,还能保留地物边界,但GEE里做分割计算量较大,不是所有场景都适合。如果只是出一个基础图,先用focalMode就够了。
5.2 为什么我的分类结果里建设用地和裸地分不开
建设用地和裸地的光谱特征确实非常接近——两者在可见光和短波红外波段都是高反射,NDVI都很低。这个问题我经常遇到,尤其在干旱半干旱区域。
解决办法从两个方向入手。第一个方向是增加特征:加入纹理特征,比如用灰度共生矩阵(GLCM)计算纹理,或者加入夜间灯光数据(NPP/VIIRS),因为建设用地有夜间灯光而裸地没有。第二个方向是改时间窗口:如果研究区有季节性的植被变化,建设用地全年不变,而裸地可能在雨季长出稀疏植被,用多个时相的影像做分类可以利用“时间轨迹”来区分它们。比如加入一个雨季NDVI和旱季NDVI的差值特征,往往能显著改善。
如果已经出了结果图且混分严重,还可以在分类后加一个规则处理:对“裸地”和“建设用地”这个两个类别,叠加夜间灯光数据,凡是灯光亮度超过一定阈值的裸地,就重编码为建设用地。这种“分类后处理”的思路在实践中非常有效。
5.3 训练时报错或内存溢出
GEE有一个“用户内存限制”的概念,每个计算任务可用内存是有限制的。当你处理大区域或者超高分辨率影像时,很容易遇到User memory limit exceeded错误。这个报错信息对新手来说很吓人,但解决办法其实不复杂。
常见诱因和解决办法:
- 研究区太大,特征影像的分辨率太高(比如直接用Sentinel-2的10米处理整个省)。解决:缩小研究区,或者先对影像做降采样,或者分块处理后再拼接。
- 导出时
maxPixels设置太小。解决:显式设置maxPixels: 1e13。 - 样本数量太大,导致训练过程内存暴涨。解决:适当减少样本量,或者在训练前用
stratifiedSample对样本做分层抽样,让每个类别的样本量控制在几百个以内。 - 分类器训练时带入了太多波段或数据维度。解决:精简特征,用特征重要性排序删除贡献低的波段。
遇到User memory limit exceeded,我的排错顺序是:先缩小region,再降分辨率,再减样本量,最后才考虑换更简单的模型。80%的情况都能在前两步解决。
5.4 验证精度虚高,结果却不对
这是一个比报错还隐蔽的问题。我接手过不少项目,对方说“我们精度95%呢”,但结果图明显不对——一条河道在中间断掉,一个山头被划成荒地。问题几乎都出在同一个地方:训练样本和验证样本不独立。
前面说过,如果验证样本就是从训练样本里随机抽出来的一部分,那分类器“见过”这部分样本了,验证精度自然虚高。这种情况下的总体精度没有参考意义。检验方法很简单:看一下你的混淆矩阵里每个类别是不是都高得离谱(比如都在95%以上),如果是,基本可以怀疑样本独立性出了问题。
正确的做法是:训练样本和验证样本分别采集,各用各的。如果样本量不够,至少也要用不同的地块分别采训练点和验证点,不要在同一块地里面既采样做训练又采样做验证。
5.5 不同年份分类结果不可比的坑
如果你的项目要做多期土地利用变化分析,还有一个大坑:不同年份的影像由于太阳高度角、大气条件、物候差异,反射率并不严格可比。这会导致你独立训练的两个年份分类器对“同一地物”的判断标准不一致,变化分析的结论就不可靠。
我的建议是:如果做多年份对比,尽量使用相同的影像源(比如都用Landsat-8),时间窗口尽量对齐(比如都用6月到9月),并且做相对辐射归一化,让两年影像的反射率分布匹配到同一基准。更进一步的做法是把多年份影像放在同一个分类器里训练,也就是把2018年、2021年、2023年的样本都放进同一个模型里,让模型学会“跨年份不变的光谱特征”。这个策略在多期制图项目中效果显著。
结尾一点个人体会
做GEE土地利用分类这几年,我最深的感触是:这个任务的技术门槛其实在快速降低,GEE把计算和数据都打包好了,任何人都能靠教程在半天内跑出一个结果。但真正的分水岭不在写代码,而在你对地物光谱、物候特征、区域背景的理解深度。你采样时有没有想过阴影里的水体怎么标?你选时间窗口时知不知道这个区域的作物什么时候成熟?这些知识才是决定成果质量的东西。所以我的建议是,在GEE里跑流程的同时,多花时间去看真彩色影像、看样本点的光谱曲线、跑完之后去野外或者用更高分辨率的影像抽查几个点。这个习惯比多调几次参数有用得多。如果你也在做类似的项目,欢迎拿这篇里的流程做底子,然后根据你的研究区特征去调整特征、样本和时间窗口——很快你就会有自己的一套成熟pipeline。