干我们这行的都知道,GEE里坑最多的不是复杂的波段计算,反而是样本提取这种“看着很简单”的环节。特别是做全球尺度的森林损失研究,你不可能人工去逐点标注几十万个验证样点,也没法用简单的随机抽样指望稀疏的损失像元能被抽中。去年我做全球森林变化产品精度评估时,被stratifiedSample这个函数卡了整整三天,反复研究后才发现问题往往不在函数本身,而在你对“分层”这件事的理解。这篇文章就把我从数据准备、类别设计到分层样本提取、质量检查的全过程完整拆出来,每一步都给可直接复用的代码和参数,希望能让你少走几趟弯路。
这个内容的核心是:基于Hansen全球森林变化数据集,用GEE内置的stratifiedSample分层抽样方法,在“森林损失”这个类别分布极不均衡的目标上,科学地提取分层样本点。它解决的痛点非常明确——如何在全球动辄几十亿像元的影像中,让稀有类别(比如某一年发生损失的像元)也能被充分抽样,同时保持样本的空间代表性和时间完整性。无论你是做森林变化监测、土地覆被产品精度验证,还是想学GEE分层抽样的原理,这套方案都值得完整看一遍。
1. 这个需求背后到底在解决什么问题
1.1 为什么偏偏选Hansen全球森林变化数据
做森林损失量分析,第一步就是选底图。GEE里不缺森林相关的数据集,有MODIS的年度森林盖度、有ESA的全球土地覆被,还有各种局部区域的林龄产品。但Hansen全球森林变化数据集(UMD/hansen/global_forest_change)几乎成了默认选项,原因无非三点。
第一,它的空间分辨率是30米,而MODIS那种250米到500米的产品对森林损失这种“碎片化”事件来说太粗了。一条道路建设或一次小规模皆伐,在MODIS影像上可能只体现为一个混合像元,根本没法数清损失了多少。Hansen的30米分辨率可以捕捉到大多数非法的、小规模的森林采伐。
第二,它提供了从2000年基线到逐年的损失年份(lossyear)信息。这个“逐年”属性特别关键——你不仅能知道哪里发生了损失,还能知道是哪一年损失的,这让“全球森林损失量”的分析从静态制图变成了时间序列分析。我在做数据产品精度评价时,最看重的就是这一点。
第三,它在GEE中是以固定资产形式托管的,访问速度快、不用自己做复杂的预处理。你要做的只是select出需要的波段,剩下的几何校正、大气校正全都不必操心。
但这里有个极其重要的实操细节,很多人忽略了:Hansen的lossyear波段里,0代表“2000年至数据版本年份之间未检测到损失”,1到数据版本年份才代表对应的损失年份。当你把它直接用于分层抽样时,如果不做任何处理,0类像元可能占据99%以上的面积,你抽取的样点会被无损失像元淹没,损失年份样本根本抽不到几条。这正是需要引入“分层”概念的直接原因。
1.2 分层抽样 vs 简单随机抽样:差在哪
说句实话,如果目标类别分布是均匀的,stratifiedSample和普通的sample没什么太大区别。但在森林损失这个场景下,二者差异是决定性的。简单随机抽样是在整个目标区域内完全随机地撒点,类别分布和像元分布保持一致。问题在于,如果我们研究的区域里只有0.5%的像元属于“2015年发生损失”,那么随机抽10000个点,理论上抽中2015年损失层的只有50个,这50个点根本不足以支撑任何统计分析。
分层抽样(stratified sampling)的思路则是先把研究区按类别规则切成若干个互不重叠的“层”(strata),然后在每一层内部独立地随机抽样。这样无论2015年损失层有多么稀疏,只要你指定给它抽200个点,它就一定会给你抽出200个。这就保证了每个类别都有“最低样本保障”,同时每一层的样本又保持了随机性。
用一个生活类比来说:如果你想了解整个城市不同收入人群的消费习惯,简单随机抽样就好比从电话簿上随机挑人,结果高收入人群可能一个都没被抽到;分层抽样则是先把市民按收入分成高、中、低三个名单,再从每个名单里分别随机挑人。你所付出的额外成本是必须先知道“怎么分层”,但换来的回报是每个层级都有足够的样本数量。
在GEE里,stratifiedSample函数做的就是这件事。它允许你指定一个分类波段作为分层的依据,每一个独特的整数值视为一层,然后在每一层内部执行随机采样。它内部会高效地做分桶统计和随机定位,比你自己写reduceRegions然后拼接多个filter结果要可靠得多。
2. stratifiedSample的核心机制与参数门道
2.1 函数签名与每个参数的真实含义
直接看GEE里stratifiedSample的调用方式,API文档写得比较简洁,实际含义得靠用才能体会。基本形式如下:
var samples = image.stratifiedSample({ numPoints: 100, classBand: 'class', region: roi, scale: 30, geometries: true, seed: 20240601, classValues: [0, 1, 2, 3] });每个参数的真实含义:
numPoints是每一层内计划抽取的样本点数。注意是“每一层”都抽这个数,不是总共抽这个数。如果你的分类波段有5个类别值,则总样本数大约是5乘以numPoints(实际可能因某个类别像元不足而少于该值)。
classBand是分层依据的波段名称。它必须是整数类型(Int或Byte),因为分层就是按整数值归类。如果波段是浮点型,你需要先重编码成整数,否则函数会报错或者行为不符合预期。
region定义了采样范围。可以是一个ee.Geometry、ee.FeatureCollection或ee.Feature。不传的话默认用影像全幅范围,但全球30米分辨率逐类采样,计算压力极大,强烈建议传区域。
scale是采样时考虑的空间分辨率。这个值的取舍很值得讲。用30米意味着逐像元级别的采样,最精确但对计算资源的需求高;用1000米意味着采样的“定位”粒度较粗,适合全球快速预览。我的经验是,如果研究区是一个国家或大洲级别,且你的计算配额不算富余,先用250米跑通全流程,最后对关键区域再用30米做一次精细抽样。
geometries是一个布尔值,决定输出的FeatureCollection中每个样本是否携带点几何。如果设为false,返回的记录只有属性值,没有空间坐标;设为true则有GeoJSON点。你做精度验证、人工目视判读或后续叠加高分辨率影像,都必须设为true。
seed是随机种子。这个参数的意义超过多数人的想象。分层抽样使用了随机数生成器,如果不固定种子,每次运行生成的样本位置都不同,论文和报告的复现性就没法保证。在学术研究中,审稿人要求“样本结果可复现”时,你只需要亮出固定的seed即可。我建议用日期加版本号,比如20240601,清晰又不容易冲突。
classValues是可选参数,用于显式指定要采样的类别值列表。默认会对classBand中的全部类别分别抽样。如果你只关心特定年份的损失层,这个参数可以避免多余的计算。
2.2 分层变量该怎么构造:重新编码成类别波段
这是整个流程中最容易被低估的一步。很多人直接把Hansen的lossyear波段塞给classBand,然后发现样本数量巨大、运算超时,或者返回的结果里损失年份样本出奇地少。原因在于lossyear的取值并不适合直接作为分层依据。
Hansen的lossyear取值是0到20+的整数,但在某些版本和特定区域,2000年之前已经有森林覆盖但从未经历损失的大片区域全是0值。直接把0到20+每个值当作一层,会产生两个问题:一是层数太多,导致numPoints乘以层数的总量非常惊人;二是0层占绝对优势,你要重点关注的不同时期损失层被稀释在了过细的分层结构里。
解决思路不是放弃lossyear,而是在它基础上重新编码成一个“有意义的类别波段”。比如按时间窗口合并类别,将森林损失划分为“未损失”、“2001-2005年损失”、“2006-2010年损失”、“2011-2015年损失”、“2016-2020年损失”五类。这样既保留了时间维度上的层次,又不会造成层数过多。代码实现如下:
// 加载Hansen全球森林变化数据集 var hansen = ee.Image('UMD/hansen/global_forest_change_2020_v1_8'); // 选择关键波段 var treeCover = hansen.select('treecover2000'); var lossYear = hansen.select('lossyear'); // 构建分层类别波段 // 0 = 未损失森林 (treecover2000 > 30 且 lossyear == 0) // 1 = 2001-2005年间损失 // 2 = 2006-2010年间损失 // 3 = 2011-2015年间损失 // 4 = 2016-2020年间损失 var forestMask = treeCover.gte(30); var classBand = ee.Image(0) .where(forestMask.and(lossYear.gte(1).and(lossYear.lte(5))), 1) .where(forestMask.and(lossYear.gte(6).and(lossYear.lte(10))), 2) .where(forestMask.and(lossYear.gte(11).and(lossYear.lte(15))), 3) .where(forestMask.and(lossYear.gte(16)), 4) .clip(roi) .byte();你可能注意到了,我把森林定义成treecover2000大于等于30%。这不是拍脑袋定的。Hansen团队官方建议,森林/非森林的阈值常用30%,这也是许多联合国报告和森林资源评估采用的标准。当然你完全可以根据研究需要改写成10%或50%,但必须保持一致,并在论文或报告里明确声明阈值。
2.3 种子、尺度和几何输出的设计细节
在GEE里使用stratifiedSample时,还有几个容易踩的细节,提前说清楚能省很多调试时间。
关于scale的选择逻辑,我强烈建议你做一个“两级采样”设计。第一级,在全球或大区域尺度,用scale参数设置为1000米甚至5000米,跑一次分层抽样,目的是验证分类波段是否正确、各类别样本空间分布是否合理、总体样本量是否符合预期。这一步成本极低,一两分钟就能出结果。第二级,在确认分类没问题后,再对重点区域用scale: 30做精细抽样。这样可以把90%的调试时间花在低成本的第一级上,而不会因为一个低级错误浪费掉几十分钟的计算配额。
关于geometries参数,还有一个小坑。当你设置geometries: true时,每个样本点都带空间几何,这个集合如果在GEE的Map面板上直接显示,可能因为点数太多导致页面卡顿。建议先用limit或者按类别filter后再显示,或者干脆只在导出Data后本地查看。
另一个容易被忽略的点是,stratifiedSample返回的FeatureCollection不仅包含classBand指定的类别值,还会包含参与分层影像的所有波段信息。如果你的分层影像是一个包含多波段的Image(比如你把treecover、lossyear都addBands进去了),那么每个样本都会带上这些波段的像元值作为属性。这其实是一件好事——你在后续精度验证或者建模时,不需要再次去影像里提取属性,因为所有属性已经挂在样本点上了。但要注意,如果影像中包含了不必要的波段(比如原始Hansen数据中的数据质量波段),输出的属性表会变得臃肿,导出CSV后光字段理解就要花不少时间。所以采样前用select限定波段是个好习惯。
3. 全球森林损失样本提取的完整实操
3.1 准备数据与目标区域
先明确我们要做什么。以东南亚地区为例,我想提取覆盖五类森林状态(未损失、四个时期损失)的样本点,每个类别各抽取300个,用于后续的高分辨率影像目视判读和损失面积估算。东南亚是森林损失重灾区,但也是行政边界复杂、影像噪声较多的地区,用它来演示更有说服力。
数据准备代码:
// 行政边界导入,可以用FAO的GAUL数据集或全球行政区划数据集 var roi = ee.FeatureCollection('FAO/GAUL/2015/level0') .filter(ee.Filter.inList('ADM0_NAME', ['Malaysia', 'Indonesia', 'Thailand'])) .geometry(); // 加载Hansen数据 var hansen = ee.Image('UMD/hansen/global_forest_change_2020_v1_8'); var treeCover = hansen.select('treecover2000'); var lossYear = hansen.select('lossyear'); Map.centerObject(roi, 5); Map.addLayer(treeCover.updateMask(treeCover.gt(0)), {palette: ['#006400', '#00ff00']}, 'Tree Cover 2000');森林盖度图层添加到地图上,目的不是看热闹,而是确认目标区域内森林的空间分布是否符合预期。比如东南亚的婆罗洲、苏门答腊这些热点区域应该有明显的森林覆盖;如果发现城市中心也有大量森林盖度,说明数据或区域投影有问题,要及早排查。
3.2 构造分层类别:把连续损失变成离散层
接着,用之前提到的思路重编码分层类别,并把需要携带的属性波段一起放在一个影像里:
// 构建森林区域掩膜 var forestMask = treeCover.gte(30); // 构建分层类别波段 var classBand = ee.Image(0) .where(forestMask.and(lossYear.eq(0)), 0) .where(forestMask.and(lossYear.gte(1).and(lossYear.lte(5))), 1) .where(forestMask.and(lossYear.gte(6).and(lossYear.lte(10))), 2) .where(forestMask.and(lossYear.gte(11).and(lossYear.lte(15))), 3) .where(forestMask.and(lossYear.gte(16)), 4) .clip(roi); // 创建用于采样的多波段影像 // classCode是分层依据,lossYear和treeCover作为样本属性一并携带 var sampleImage = ee.Image.cat([ classBand.rename('classCode'), lossYear.rename('lossYear'), treeCover.rename('treeCover') ]).byte(); // 给classCode波段起名为class,因为stratifiedSample默认以该波段分层 var sampleImage = sampleImage.select( ['classCode', 'lossYear', 'treeCover'], ['class', 'lossYear', 'treeCover'] );这里有两个细节。第一,我用ee.Image.cat把多个波段拼接成一个多波段影像,然后重命名。classCode重命名为class,这样stratifiedSample里classBand: 'class'一目了然。第二,所有波段最后都转成了byte(8位无符号整数)。Hansen的lossyear和treecover取值都在0到100之间,byte足够存储,减少一半以上的内存和网络传输消耗。这在全球尺度的计算中是非常可观的优化。
3.3 执行stratifiedSample并导出检查
核心语句来了:
// 设置抽样参数 var numPointsPerClass = 300; // 执行分层抽样 var samples = sampleImage.stratifiedSample({ numPoints: numPointsPerClass, classBand: 'class', region: roi, scale: 30, geometries: true, seed: 20240601, classValues: [0, 1, 2, 3, 4] }); print('Sample FeatureCollection', samples); print('Size of samples', samples.size());运行后在Console里观察:如果一切正常,输出集合大小应该在1500附近(5类乘以300),但实际情况可能因为某些类别在区域内像元不足而少于1500。比如2001-2005年东南亚的损失相对较少,抽取300个点时可能只能抽到280-290个,这是正常的,说明该类别在给定区域内的有效像元数不够。
为了快速检查各类别实际抽到了多少点,可以用下面的代码分组统计:
// 按类别统计样本数量 var classCounts = samples.reduceColumns({ reducer: ee.Reducer.frequencyHistogram(), selectors: ['class'] }); print('Class counts', classCounts);如果某个类别抽到0个样本,而你的研究又必须包含该类别,就需要放宽region范围,或者下调numPoints之外的硬约束。比如可以用多期Hansen版本拼接数据。但多数情况下,出现某个类别完全没有样本的情况,几乎总是因为分层波段构建逻辑有误。
3.4 样本可视化与快速质检
拿到样本后,第一件事不要急着导出,先在地图上可视化检查空间分布。我习惯的配色方案是:未损失层用深绿色,四个损失时期分别用橙黄红紫四色,这样一眼就能看出是否均匀分布在校准框内。
// 将样本按类别分割开显示 var palette = ['#006400', '#ff9900', '#ff3300', '#ff00ff', '#0000ff']; Map.addLayer(samples, {color: '000000'}, 'All samples (black)');更实用的做法是,把样本属性表导出为CSV,用统计软件快速查看每类的空间坐标范围、lossyear分布直方图。具体导出代码如下:
// 导出样本点 Export.table.toDrive({ collection: samples, description: 'SEAsia_forest_loss_samples', fileFormat: 'CSV', folder: 'GEE_exports', selectors: ['class', 'lossYear', 'treeCover', '.geo'] });导出后我习惯先在Excel里打开,确认几个关键字段:
class的取值是否只在0到4之间;lossYear的取值是否和class相互匹配(比如class=1时,lossYear应该在1到5之间);- 检查是否存在坐标为(0,0)或位于研究区之外的异常点。
这一步虽然简单,但往往能立刻暴露问题。我在一次实验中发现class=4(2016-2020损失)的点里混进了lossYear=0的记录,追根溯源发现是lossyear构建时分类波段和损失年份的掩膜顺序写反了,重新调整处理逻辑后一切正常。
4. 实战中一定会遇到的坑
4.1 “每个类别的像元数不够”怎么解
stratifiedSample最让人头疼的运行报错之一,是每个类别要求抽取numPoints个点,但某些层实际像元数少于numPoints。常见的提示是Number of pixels in strata < number of requested samples。这事我遇到过太多次,尤其是当研究区小、且某个损失年份特别稀疏时。
解决办法有几种,按优先级排列:
第一种,调低numPoints,让每个类别的期望样本数小于最少类别的有效像元数。你可以先用reduceRegion或classBand.reduceFrequencyHistogram快速统计各层像元数量,再据此设定numPoints。
第二种,扩大region,让稀有类别获得更大的空间范围来补充有效像元。比如把研究区从单一州郡扩大到整个省。
第三种,调整scale。scale从30米改成100米或250米,意味着每个“采样像元”覆盖更大的地面范围,类别有效像元数也随之增多。代价是采样点的定位精度下降,所以它更适合全球尺度的粗略研究,不适合需要精细点位的研究。
第四种,如果实在无法满足,就拆分区域。把研究区分成几个子区域,分别执行分层抽样,最后用merge合并所有样本。这个方法多花一点代码和时间,但能保留30米的分辨率,也是我处理非洲多国联合分析时最常用的一招。
// 拆分区域后分别抽样再合并 var subregions = ['regionA', 'regionB', 'regionC']; // 每个子区域都要有对应几何 var allSamples = ee.FeatureCollection([]); subregions.forEach(function(name) { var subRegion = ee.FeatureCollection(rois).filter(ee.Filter.eq('name', name)).geometry(); var subSamples = sampleImage.stratifiedSample({ numPoints: numPointsPerClass, classBand: 'class', region: subRegion, scale: 30, geometries: true, seed: 20240601, classValues: [0, 1, 2, 3, 4] }); allSamples = allSamples.merge(subSamples); });需要注意的是,GEE的forEach不是同步执行的,合并时最好用ee.FeatureCollection的flatten方式,或者在ui模式下改用map。上面的代码只是示意,实际使用推荐写成函数式合并。
4.2 采样点太密,内存溢出与地图卡顿
一次性生成几千个带几何的样本点,在GEE的Map面板上直接显示,十有八九会卡顿甚至白屏。这不是程序出bug,而是客户端渲染压力过大。我的处理方式是把样本点写到自己的资产里,或者导出GeoJSON到本地ArcGIS/QGIS查看。
如果必须在GEE里预览部分样本,可以这样:
// 显示每类前50个点,共250个点,足够预览 var previewSamples = samples.limit(250); Map.addLayer(previewSamples, {color: 'red'}, 'Preview samples (250)');另一个内存陷阱是想把样本点和高分辨率遥感影像叠加做目视判读时,直接addLayer高分辨率影像同样会让浏览器崩溃。解决方案是使用getThumbURL生成缩略图,或者只给单个样本点周围创建一个小缓冲区域再显示。实际操作中我更倾向于用GEE Code Editor的Export.map.toDrive把注意力转到导出任务,不在线上做深度交互。
4.3 不同版本的Hansen数据波段差异
GEE上的Hansen数据集更新过多个版本,有v1_7、v1_8、v1_10等。版本之间虽然核心波段设计一致,但也有细微差异。比如较新版本增加了datamask、gain等波段,损失年份的最大值也不同(v1_8损失年份到2020年,v1_10则可以到2022年甚至更晚)。这意味着如果你在前面代码里写死了lossYear.gte(16),在v1_10中会覆盖所有2020年之后的损失,得到的结果维度完全变了。
我的建议是研究开始前明确使用哪个版本,并检查对应版本的最大损失年份。代码里用动态方式获取版本上限,比写死数值更健壮:
var lossYear = hansen.select('lossyear'); var maxLossYear = lossYear.reduceRegion({ reducer: ee.Reducer.max(), geometry: roi, scale: 1000, maxPixels: 1e9 }).getNumber('lossyear'); print('Max loss year in ROI:', maxLossYear);这条命令可以快速确认你所选择的数据版本在目标区域内最晚的损失年份。许多同学跑完整套流程后才发现版本内损失年份的上限和预期不符,不得不重算一遍,费时费力。在第一步就确认版本和最大年份是一个好习惯。
4.4 常见问题速查表
为了更直观,我把使用stratifiedSample时常见的坑整理成一张速查表,建议收藏:
| 常见问题 | 可能原因 | 解决方案 |
|---|---|---|
| 报错No valid pixels in strata | classBand在ROI内全为掩膜值 | 检查classBand的掩膜范围,确保每个类别在ROI内有有效像元 |
| 某个类别样本数为0 | 类别编码冲突或区域太小 | 检查recode条件,扩大ROI或调低scale |
| 两个类别的样本点重合 | 分层波段取值定义重叠 | 检查.where条件之间是否有边界重叠,逐层用mask验证 |
| 导出CSV字段过多 | 未限定采样波段 | 采样前做select,只保留必要波段 |
| 每次运行结果不同 | 未设置seed | 固定seed,确保结果可复现 |
| Map显示卡死 | 样本点数过多 | 用limit预览,或导出本地查看 |
| 损失年份最大年份异常 | 使用了不同版本Hansen | 动态读取maxLossYear,并检查数据集ID |
5. 样本拿到之后还能干什么
5.1 用样本做面积估算与精度验证
分层样本的价值绝不只是“抽出来了就完事”。在森林损失研究中,最常见的后续工作是面积估算和分类精度验证。
以面积估算为例,Olofsson等人提出的良好实践指南里,推荐使用分层抽样(stratified random sampling)得到的样本,结合每层的像元数权重来修正面积估计。如果你把整个区域按“未损失”和“损失”两层抽样,那么损失面积的无偏估计可以写成:
损失面积估计 = 各类别损失像元比例乘以各类别总面积,再对各层加权合并。具体公式为:
A_hat = Σ (W_h × A_total × p_h)其中W_h是第h层像元占比,p_h是第h层中样本里判定为“损失”的比例,A_total是整个区域总面积。通过这样的计算,你得到的面积估计值比单纯的“损失像元乘以30×30平方米”要更稳健,尤其适合样本量不大、类别不平衡的数据。
在GEE中,你可以先用reduceRegion拿到每个类别的像元数量和面积权重,再用样本点和高分辨率影像人工判读的结果去更新p_h。这两个东西组合起来就能算出带置信区间的损失面积。具体代码实现较多,不在这里展开,但你要知道stratifiedSample抽出的样本是衔接“遥感分类结果”和“地面真实面积”的桥梁。
另外,如果你后续要训练一个自定义分类器(比如区分火灾、砍伐、风暴导致的损失),stratifiedSample产出的样本点还可以作为训练样本。做法很简单:把样本点叠加到高分辨率影像切片上,人工标注出损失原因,再把标注后的点集灌回分类器。这时候,你在分层抽样时保留的lossYear和treeCover属性就派上了大用场——它们可以作为辅助特征一起参与模型训练。
5.2 结合高分辨率影像做人工审核的流程
分层样本落到具体使用中,尤其是做精度评估时,光靠Hansen的30米分辨率是不够的,你需要用更高分辨率的影像(Planet、Maxar、Sentinel-2的10米)来确认每个点真实的地表状态。
实际操作中我的流程是这样的:先把stratifiedSample导出的样本在GEE里和Sentinel-2无云影像合成叠加,然后导出为PNG切片,再放到本地影像管理软件中人工判读。更高效的做法是直接在GEE Code Editor中逐一查看样本点的真彩色影像:
// 以其中一个样本点为中心,显示Sentinel-2真彩色影像 var pointSample = ee.Feature(samples.first()); Map.centerObject(pointSample, 15); Map.addLayer(s2Composite, {bands: ['B4', 'B3', 'B2'], min: 0, max: 3000}, 'S2 RGB'); // 查看该样本点的属性 print('Sample attributes:', pointSample);当你人工审核一批样本点后,如果发现某些点被Hansen标为“2015年损失”,但在Sentinel-2影像里一直是森林状态,就说明Hansen在那个区域存在漏检或误检。你可以把所有这类误判样本的坐标记录下来,反过来重新评估Hansen数据在特定区域的可信度。这正是“分层样本点”作为独立参考数据的关键价值——它不仅是被动的抽样工具,更是主动的质量监测工具。
不过,人工审核高分辨率影像也有耗时陷阱。我在处理全球尺度样本时,几千个样本点逐一点开查看往往要耗费数天时间。后来我用Google Earth Engine的Export.image.toDrive批量导出每个样本点的局部影像快照,然后用FastStone之类的看图软件快速浏览,效率提升明显。推荐你也把样本点分成小批次批量导出,再集中判读,既快又不容易漏。
结尾想说的几句实在话
分层抽样是整个GEE森林遥感分析中最容易被当作“配件”却又最影响结论质量的环节。我现在做任何全球尺度的森林面积或变化分析,都会把stratifiedSample放在处理流程的核心位置,而不是等分类都做完之后才想起来补样本。这背后的原因很简单:样本代表不了总体,后面的任何统计推断都是空中楼阁。建议你在跑正式实验前,先用小范围的测试区域把分类波段、numPoints和scale这三个参数全部调通,确认每层样本数和空间分布都符合预期,再放大到全研究区跑。宁可开头多花一个小时调参,也不要等到几万样本导出后才发现分层设计出了偏差,那才是真正的时间杀手。