
简介这是一份以雨季Sentinel-2影像为基础、面向城市土地覆盖制图的Google Earth Engine实操资源适合具备遥感与GIS基础的研究人员、技术从业者尤其是对随机森林分类和对象化影像分析感兴趣的群体。资源以PDF文档形式呈现共1个文件压缩包大小751KB内容围绕基于对象的随机森林分类全流程展开包括边界与训练数据导入、Sentinel-2云掩膜与雨季合成、超像素聚类SNIC、结合DEM与坡度的变量组合、模型训练验证及土地覆盖图导出等关键环节。同时提供额外练习引导读者尝试多季节对比。全文包含完整代码示例与参数说明可支撑读者从数据准备到结果输出的独立实践。目前该资源已有85人学习下载适合希望系统掌握GEE对象化分类流程、提升Sentinel-2处理能力的读者作为参考。1. 对象化随机森林雨季 Sentinel-2 城市分类的完整链路做遥感城市土地覆盖分类的人大多听过一句话基于像素的分类在城区里会被屋顶、树影和裸土之间的光谱混杂搞到怀疑人生。这份资源给的是另一条路——用 Google Earth Engine 把雨季 Sentinel-2 影像先切成超像素对象再喂给随机森林分类器从对象层面判断每个图斑是建筑、裸地、农田、草地、林地还是水体。整套流程从边界导入、云掩膜、SNIC 分割、特征堆叠到模型训练、精度输出和云端导出全部在 GEE 里闭环跑通适合已经有 GIS 基础、想从栅格分类转向对象化分类的从业者。我拆完这份实验室材料后最大的感受是它不只是代码清单而是把 GEE 里最容易出问题的数据对齐、样本采样和导出参数都摊开讲了。2. 数据准备与云掩膜边界、训练样本和雨季影像的预处理链路2.1 边界与训练数据的 SHP 导入流程GEE 的逻辑和其他遥感软件不太一样数据不是上传到某个盘里而是作为 Asset 存进你的 Earth Engine 账户。第一步是在 Code Editor 左侧的 Assets 面板里点 New然后选 Shapefile 上传。这里支持 .shp、.zip、.dbf、.prj、.shx 等格式实际操作中我一般直接打一个 zip 包传上去GEE 会自动解压并识别全套文件比单独传 .shp 再补投影文件省事。上传完成后注意点一下 Assets 面板的 Refresh否则新上传的文件不会出现在下拉列表里。导入后 GEE 会自动生成一个 table 变量这一步有两个命名习惯建议直接沿用边界数据改成 boundary训练数据改成 training_areas。名字后面会直接写进代码提前改好能少踩一半的坑。还有一个细节值得留意导入训练数据后要检查它的属性表。本资源的训练数据里有一个 Cl_Id 字段用来记录每个多边形的地类编号0 到 5 分别对应建成区、裸地、农田、草地、林地和水体。后面做栅格化样本时用的就是它如果字段名不一致reduceToImage 那一步会直接报错。2.2 Sentinel-2 雨季影像筛选与 QA60 云掩膜影像源选择 COPERNICUS/S2_SR也就是 Level-2A 地表反射率产品不需要再做大气校正。这个集合里的每一景影像都带一个 QA60 波段它是个位掩码波段第 10 位记录不透明云第 11 位记录卷云。代码里的 maskS2clouds 函数是整条链路的第一个关键点// 用 Sentinel-2 QA60 波段构建云掩膜 function maskS2clouds(image) { var qa image.select(QA60); // Bits 10 和 11 分别对应云和卷云 var cloudBitMask ee.Number(2).pow(10).int(); var cirrusBitMask ee.Number(2).pow(11).int(); // 两个位标志都应为 0表示晴空 var mask qa.bitwiseAnd(cloudBitMask).eq(0).and( qa.bitwiseAnd(cirrusBitMask).eq(0)); // 返回掩膜后的数据并除以 10000 转为反射率 return image.updateMask(mask).divide(10000); }这段代码的逻辑是用 bitwiseAnd 把 QA60 波段里的第 10 位和第 11 位分别取出来判断是否为 0。为 0 说明没有云保留为 1 说明有云直接掩掉。最后除以 10000是因为 Sentinel-2 的 SR 数据是以 10000 为缩放系数存储的整数值。这里有一个新手容易忽略的点GEE 是按位操作不是按数值操作。cloudBitMask 是 2 的 10 次方转成整数后是 1024cirrusBitMask 是 2048。如果你直接写 qa.eq(0)那会因为 QA60 里还包含其他位的信息而几乎过滤掉所有影像。2.3 雨季合成影像从选片到 median 合成云掩膜函数定义好之后下一步就是筛选雨季影像并合成。本资源选的时间窗口是 2020 年 1 月 1 日到 2020 年 3 月 30 日这是津巴布韦布拉瓦约的雨季植被状态和光谱特征相对稳定。// 按日期、云量和云掩膜函数筛选雨季影像 var rs_composite s2.filterDate(2020-01-01, 2020-03-30) .filter(ee.Filter.lt(CLOUDY_PIXEL_PERCENTAGE, 20)) .map(maskS2clouds) .select(B2, B3, B4, B5, B6, B7, B8, B11, B12); // 生成雨季中值合成影像并裁剪到边界 var S2_RS rs_composite.median(); var rainyComposite S2_RS.clip(boundary);这里有几个参数值得细说。CLOUDY_PIXEL_PERCENTAGE 小于 20 是按影像元数据里的整体云量百分比做初筛它是个粗筛真正精细的云剔除靠的是 maskS2clouds。两个手段叠加才能保证进入合成的都是有效像元。波段选择方面资源选了 B2、B3、B4 做可见光B5、B6、B7、B8A 做红边B8 做近红外B11、B12 做短波红外。红边波段对植被和城市地物的区分度比较高短波红外对裸土和含水量敏感这两个系列在土地覆盖分类里几乎不能省。分辨率方面B2/B3/B4/B8 是 10 米其余是 20 米但后面 SNIC 和分类统一在 20 米尺度上做不用额外重采样。合成方式用的 median 而不是 mean这是个很实用的选择。雨季影像里即使做了云掩膜仍然可能有薄云残留或阴影中值能把这些离群值压掉比均值稳健得多。如果你换成 mean会在水体边缘和建筑物阴影处出现很多伪变化。显示合成影像时用真彩色组合 {B4, B3, B2}拉伸范围取 0 到 0.3。注意这是反射率值不是原始 DN 值因为前面已经除以 10000 了。3. SNIC 超像素分割五个参数决定对象形状和质量3.1 为什么要先分割再做分类像素级随机森林分类的思路是把每个像元当成独立样本逐像元预测类别。城市区域的问题在于一个 10 米像元里可能同时混着屋顶、树冠和阴影光谱值被平均得面目全非。对象化思路是先分割后分类把影像切成一个个光谱均匀的对象对每个对象提取均值、纹理、几何等特征再拿这些特征去做分类。这样不仅降低了椒盐噪声还让分类结果在空间上有实际意义。SNIC 是 Simple Non-Iterative Clustering 的缩写可以理解成一种超像素分割算法。它比 SLIC 快而且不需要预先指定迭代次数。GEE 里直接调用 ee.Algorithms.Image.Segmentation.SNIC() 就能完成这也是我认为这份资源里最值得反复调试的部分。在调用 SNIC 之前资源先做了一步高斯卷积这是很多人忽略的细节// 高斯核平滑抑制单像元噪点 var kernel ee.Kernel.gaussian(3); var imageSeg2 rainyComposite.convolve(kernel); var imageSeg2 imageSeg2.clip(boundary);kernel 是 3 像素半径的高斯核卷积的作用是把相邻像元的光谱值做加权平均。这样做的好处是减少分割时产生的碎片对象代价是会稍微模糊边缘。在 20 米尺度上做城市分类高斯半径我建议不要超过 3否则建筑物边界会被磨掉。3.2 SNIC 参数逐一拆解SNIC 的调用参数有五个每一个都直接决定对象的大小、形状和后续分类的输入质量参数取值作用调参建议imageimageSeg2输入影像建议先平滑不要传原始影像分割会很碎compactness0控制形状紧凑度0 表示纯光谱聚合城市用 0农田可调到 1 到 2connectivity8像元邻接方式8 表示八邻域保持默认 84 会导致对象狭长neighborhoodSize64搜索半径影响超像素大小越大对象越大内存占用越高size3最小对象尺寸单位是像元20 米分辨率下 3 比较合理seedsseedGrid(6)种子点间隔间隔越小对象越碎越小越碎seeds 这里用的是 seedGrid(6)表示每 6 个像元放一颗种子。意思是初始对象大约 36 个像元大小对应 20 米分辨率下约 120 米乘以 120 米的实际面积。如果你的研究区地物很破碎比如城中村区域把 6 改成 4 会让对象更贴合建筑边界如果是大范围农田可以放宽到 8 或 10。这里有个比较关键的工程细节SNIC 输出的是带 _mean 后缀的波段比如 B2_mean、B3_mean这些才是后续特征输入要用的波段名。很多人在分类时报错说找不到波段就是因为直接在原始波段名上做训练而分类器拿到的是分割后的均值波段。// 执行 SNIC 超像素分割 var seeds ee.Algorithms.Image.Segmentation.seedGrid(6); var snic_rs ee.Algorithms.Image.Segmentation.SNIC({ image: imageSeg2, compactness: 0, connectivity: 8, neighborhoodSize: 64, size: 3, seeds: seeds }).reproject({ crs: EPSG:4326, scale: 20 });reproject 这一步容易被忽略但很重要。它强制 SNIC 结果在 EPSG:4326 和 20 米尺度下输出保证后续和 DEM、坡度以及训练样本的像元对齐。如果不做这一层投影约束GEE 会根据当前 Map 的缩放级别动态决定输出尺度导致特征影像的分辨率不确定最终分类结果的几何精度很难交代。3.3 从 DEM 和坡度看地形变量的作用SNIC 处理完光谱影像后资源引入了 SRTM 高程和坡度。这一步的原理不难理解建成区、草地和林地在光谱上可能混淆但在地形上分布有规律比如林地一般分布在坡度较大的区域水体分布在低洼处。把这些地形变量作为额外特征能有效提升随机森林的区分能力。// 加载 SRTM 高程并裁剪 var SRTM ee.Image(USGS/SRTMGL1_003); var elevation SRTM.clip(boundary); // 从 SRTM 提取坡度 var slope ee.Terrain.slope(SRTM).clip(boundary);ee.Terrain.slope() 会在内部计算高程梯度输出的坡度单位是度。这里不需要自己写坡度公式GEE 已经封装好了。需要留意的是 SRTM 的原始分辨率为 30 米而前面 SNIC 的输出尺度是 20 米GEE 在 addBands 时会自动重采样对齐但高程数据的有效分辨率仍然是 30 米这一点在论文或报告中要如实说明。4. 特征堆叠与样本工程把分割对象、地形和训练标签绑成一张表4.1 用 addBands 把预测变量拼成一张影像分类前的最后一步数据准备是把所有预测变量组合到一个影像对象里。这一步的核心方法是 addBands它能把不同来源的栅格数据合并成同一个多波段影像。// 组合所有预测变量SNIC 对象均值 高程 坡度 var input_features snic_rs.addBands(elevation.addBands(slope).clip(boundary));这里有个嵌套写法需要讲清楚先执行 elevation.addBands(slope)得到包含高程和坡度两个波段的影像再整体 clip 到边界最后与 snic_rs 合并。执行完这行代码后input_features 的波段列表里应该有 9 个 SNIC 均值波段B2_mean 到 B12_mean加上 elevation 和 slope一共 11 个波段。打印 input_features 到 Console 检查波段名这是我每次都会做的一步。波段名拼错了后面训练时 Classifier.train 会直接报 inputProperties 不存在的错误。4.2 训练样本栅格化与类号对齐训练数据是面状多边形格式是矢量随机森林需要的是点状样本。资源用 reduceToImage 把训练多边形转成栅格// 把训练多边形栅格化用 Cl_Id 字段的值作为像元值 var training_rasterized training_areas.reduceToImage({ properties: [Cl_Id], reducer: ee.Reducer.first() }).toInt() .remap([0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5]);reduceToImage 传了两个关键参数properties 指定用哪个属性字段reducer 用 first 表示当多个多边形重叠时取第一个。remap 的作用是确保类别编号从 0 开始连续排列。如果训练数据里的 Cl_Id 原本是从 1 到 6或者中间缺了某个编号这里就必须映射成 0 到 5否则后面 stratifiedSample 的 classBand 会读取到缺失的类别导致某些类没有样本。这行代码跑完后把栅格化的类别影像 addBands 到 input_features 上命名为 class// 把类别标签作为波段加入特征影像 input_features input_features.addBands(training_rasterized.toInt().rename(class));4.3 分层采样与训练验证划分样本不是直接从影像里逐像元全量取的而是用 stratifiedSample 做分层随机采样。分层的意义是保证每个类别的样本量大致均衡避免建成区样本多到把模型带偏// 按类别分层采样每类最多 10000 个像元 var training_dataset input_features.stratifiedSample({ numPoints: 10000, classBand: class, region: boundary, scale: 20 });numPoints 的语义是每个类最多取多少个点不是总样本数。如果你某个类别面积特别大比如裸地占了半个研究区它会被截断到 10000 个像元而小类别可能只有两三千个像元这正好是分层采样的目的。在把样本送进分类器之前需要用随机列的方式做训练集和验证集拆分// 添加随机列并固定种子保证结果可复现 training_dataset training_dataset.randomColumn(random, 50); // 70% 训练30% 验证 var split 0.7; var Sample_training training_dataset.filter(ee.Filter.lt(random, split)); var Sample_validation training_dataset.filter(ee.Filter.gte(random, split));randomColumn 的第二个参数是随机种子。固定种子后每次运行这行代码生成的随机数序列都一样这对复现实验结果至关重要。如果你不设种子两次运行得到的训练集和验证集不同模型精度会有小幅波动在写报告时就会遇到说不清楚的差异。拆分后用 Sample_training.size() 确认训练样本量。资源里给出的结果是 17979 个训练像元和 7685 个验证像元这个比例大致符合 7 比 3。5. 避坑手册从 OOB 误差到导出任务的七条实战记录5.1 训练样本过密导致采样报错现象stratifiedSample 或后续分类时返回 User memory limit exceeded 错误任务在 Console 里直接变红。原因GEE 对单次请求的内存有上限。训练多边形覆盖面积大且 numPoints 设得过高时采样请求需要处理的候选像元数量会超出限制。尤其当你把区域边界设成整个城市范围时这个问题极易触发。解决把 numPoints 从 10000 降到 5000 或 3000或者把研究区按网格切成小块分批采样。资源里 10000 能跑通是因为它的训练多边形本身分布比较稀疏。如果你复制代码到自己的研究区发现报错优先检查 numPoints不要先动分类器参数。5.2 训练数据投影和导出投影不一致现象分类结果在 Map 上显示正常但导出到 Drive 后用 QGIS 打开发现影像位置偏移几十米或者和底图对不上。原因GEE 的 Map 显示默认用 Web Mercator 投影而 Export.image.drive 里的 crs 参数写的是 EPSG:32735这是 UTM 35S 投影。如果训练样本和边界的原始投影不是 UTM 35S而导出时强制指定了这个坐标系GEE 会做重投影重投影过程中如果像元对齐方式处理不当就会出现偏移。解决导出前先确认研究区的 UTM 分带。布拉瓦约在西经 28 度到 30 度之间属于 UTM 35S所以资源里用 EPSG:32735 是合理的。换研究区时要先用 ee.Image.getInfo() 或直接查 utmZone 来确定正确的投影代号不要照搬。5.3 CLOUDY_PIXEL_PERCENTAGE 过滤失效现象即使过滤条件设成小于 10合成影像上仍然有大片云影。原因CLOUDY_PIXEL_PERCENTAGE 是整景影像的云量估计不代表研究区上空就是晴的。一景影像整体云量 5%但正好研究区在云下面这种情况很常见。另外GEE 的 CLOUDY_PIXEL_PERCENTAGE 本身用的是场景级元数据各个时期的算法不完全一致。解决把 filter 阈值从严设到 10 或 15同时依赖 maskS2clouds 做像元级剔除。更稳妥的做法是看一眼合成影像的真彩色显示如果某个区域明显发白或发暗再对照原始影像检查。雨季影像里云是常态多选几个时间窗口交叉验证比不断调阈值更有效。5.4 numberOfTrees 设太大导致训练超时现象把 numberOfTrees 从 100 改成 500 后训练任务长时间不结束甚至报 Computation timed out。原因随机森林的每棵树都要在全部训练样本上做有放回抽样和特征选择树的数量越多训练时间线性增长。GEE 的单次请求有 5 分钟左右的超时上限树太多就撑爆了。解决把树的棵数保持在 100 到 250 之间。随机森林的 OOB 误差在树超过一定数量后会趋于平稳继续加树带来的精度提升很有限收益不如增加训练样本或调整特征组合。如果实在想要更强的模型优先调 compactness 或加波段而不是堆树。5.5 分类结果里出现大面积椒盐状误分现象分类图上草地和林地之间出现大量交替的零散像元视觉上非常碎。原因输入的 SNIC 对象太碎或者分类时没有把特征影像统一到同一个尺度。常见做法里很多人会跳过卷积平滑或者把 seedGrid 间隔调得太小导致对象数量巨大但每个对象的样本代表性不足。另一个原因是 addBands 时没有 clip 边界边界外的高程值参与了训练。解决确认 input_features 里所有波段都经过了 clip(boundary)其次把 seedGrid 间隔从 6 调到 8 或 10增加对象面积最后检查 size 参数如果设成 1 很容易产生碎片对象建议至少 3。6. 导出前的一小时用验证集给每类地物做体检模型训练完OOB 误差只有 3%很容易让人觉得万事大吉。但做遥感分类的人应该都知道 OOB 误差是所有样本的平均表现它会掩盖个别类别的严重混淆。我习惯在导出前花一个小时用验证集算一次混淆矩阵把每一类的生产者精度和用户精度拉出来看。// 用验证集对分类器做预测 var validation Sample_validation.classify(classifier); // 计算混淆矩阵 var confusionMatrix validation.errorMatrix(class, classification); // 打印总体精度和 Kappa 系数 print(Overall Accuracy:, confusionMatrix.accuracy()); print(Kappa:, confusionMatrix.kappa()); // 打印逐类精度 print(Confusion Matrix:, confusionMatrix);这段代码里errorMatrix 的第二个参数 classification 是 classify 之后自动生成的预测结果波段名。accuracy() 返回总体精度kappa() 返回 Kappa 系数。逐类的生产者精度和用户精度可以通过 get 方法从混淆矩阵里提取具体索引以你训练数据里的类别顺序为准。我通常会在 Console 里重点看建成区和水体这两类的用户精度。建成区容易和裸地混淆水体容易和阴影混淆。如果某一类的用户精度低于 0.8我会回到 SNIC 参数那里调整 compactness 或 seedGrid而不是继续加树。这个动作看似绕路实际上比任何后处理滤波都有效。导出任务本身也有几个值得盯的参数// 导出分类影像到 Google Drive Export.image.drive({ image: classified, description: Bulawayo_RS_S2_RF_LC_Jan_Oct20a, scale: 20, crs: EPSG:32735, maxPixels: 6756353855, region: boundary });maxPixels 设成 67 亿是为了覆盖大范围研究区。如果你的研究区比布拉瓦约小很多这个值可以适当调低避免导出任务在计算配额上卡住。scale 必须和训练特征影像的尺度一致这里统一是 20 米。从那以后我每次做 GEE 分类导出前都强制自己跑一遍验证集混淆矩阵把逐类精度记录到表格里再点 Run。这样一个简单的习惯能省掉后面在 GIS 里反复改图的半天时间。这份资源的价值正在于此它给的是一套可以照搬的完整工作流而不是一个单点技巧希望你照着跑一遍雨季影像分类后能把手上的城市制图项目往前推进一大步。本文还有配套的精品资源点击获取