编码最佳实践

本文档介绍了一些编码做法,旨在最大限度地提高复杂或耗时的 Earth Engine 计算成功的几率。此处介绍的方法适用于交互式(例如 Code Editor)和批处理 (Export) 计算,但通常长时间运行的计算应在批处理系统中运行。

避免将客户端函数和对象与服务器函数和对象混用

Earth Engine 服务器对象是指构造函数以 ee 开头(例如 ee.Imageee.Reducer)的对象,并且此类对象上的任何方法都是服务器函数。任何未以这种方式构建的对象都是客户端对象。客户端对象可以来自 Code Editor(例如 MapChart)或 JavaScript 语言(例如 DateMath[]{})。

此处此处此处所述,为避免意外行为,请勿在脚本中混用客户端和服务器函数。如需深入了解 Earth Engine 中的客户端与服务器,请参阅此页面和/或此教程。以下示例说明了混合使用客户端和服务器功能的危险性:

错误 - 此代码无效!

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');

// Won't work.
for(var i=0; i<table.size(); i++) {
  print('No!');
}

您能发现错误吗?请注意,table.size() 是服务器对象上的服务器方法,不能与客户端功能(例如 < 条件)搭配使用。

您可能需要使用 for 循环的情况是界面设置,因为 Code Editor ui 对象和方法位于客户端。(详细了解如何在 Earth Engine 中创建界面)。例如:

使用客户端函数进行界面设置。

var panel = ui.Panel();
for(var i=1; i<8; i++) {
  panel.widgets().set(i, ui.Button('button ' + i))
}
print(panel);

反之,map() 是服务器函数,客户端功能无法在传递给 map() 的函数内运行。例如:

错误 - 此代码无效!

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');

// Error:
var foobar = table.map(function(f) {
  print(f); // Can't use a client function here.
  // Can't Export, either.
});

如需对集合中的每个元素执行操作,请对集合 map() 一个函数并 set() 一个属性:

使用 map()set()

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');
print(table.first());

// Do something to every element of a collection.
var withMoreProperties = table.map(function(f) {
  // Set a property.
  return f.set('area_sq_meters', f.area())
});
print(withMoreProperties.first());

您还可以根据计算属性或现有属性filter()集合,并print()结果。请注意,您无法输出包含 5,000 多个元素的集合。如果您收到“Collection query aborted after accumulating over 5000 elements”错误,请在输出之前对集合执行 filter()limit()

避免不必要地转换为列表

Earth Engine 中的集合会使用优化进行处理,但将集合转换为 ListArray 类型会破坏这些优化。除非您需要对集合元素进行随机访问(即需要获取集合的 i 个元素),否则请对集合使用过滤器来访问各个集合元素。以下示例展示了使用类型转换(不推荐)和过滤(推荐)来访问集合中元素之间的区别:

请勿不必要地转换为列表!

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');

// Do NOT do this!!
var list = table.toList(table.size());
print(list.get(13)); // User memory limit exceeded.

请注意,如果不必要地将集合转换为列表,则很容易触发错误。更安全的方法是使用 filter()

使用 filter()

print(table.filter(ee.Filter.eq('country_na', 'Niger')).first());

请注意,您应尽早在分析中使用过滤条件

避免使用 ee.Algorithms.If()

请勿使用 ee.Algorithms.If() 来实现分支逻辑,尤其是在映射函数中。如以下示例所示,ee.Algorithms.If() 可能会占用大量内存,因此不建议使用:

请勿使用 If()

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');

// Do NOT do this!
var veryBad = table.map(function(f) {
  return ee.Algorithms.If({
    condition: ee.String(f.get('country_na')).compareTo('Chad').gt(0),
    trueCase: f,      // Do something.
    falseCase: null   // Do something else.
  });
}, true);
print(veryBad); // User memory limit exceeded.

// If() may evaluate both the true and false cases.

请注意,map() 的第二个实参为 true。这意味着映射的函数可能会返回 null,而这些 null 将在生成的集合中被丢弃。这可能很有用(无需 If()),但在这里,最简单的解决方案是使用过滤器:

使用 filter()

print(table.filter(ee.Filter.eq('country_na', 'Chad')));

本教程所示,使用过滤器的函数式编程方法是将一种逻辑应用于集合的某些元素,将另一种逻辑应用于集合的其他元素的正确方法。

避免使用 reproject()

除非绝对必要,否则请勿使用重新投影。您可能想要使用 reproject() 的一个原因是,强制在特定规模下进行 Code Editor 计算,以便您能够在所需的分析规模下检查结果。在下一个示例中,系统会计算热点补丁,并计算每个补丁中的像素数。运行该示例,然后点击其中一个补丁。请注意,经过重新投影的数据与未经过重新投影的数据之间的像素数不同。

var l8sr = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2');
var sf = ee.Geometry.Point([-122.405, 37.786]);
Map.centerObject(sf, 13);

// A reason to reproject - counting pixels and exploring interactively.
var image = l8sr.filterBounds(sf)
    .filterDate('2019-06-01', '2019-12-31')
    .first();

image = image.multiply(0.00341802).add(149);  // Apply scale factors.
Map.addLayer(image, {bands: ['ST_B10'], min: 280, max: 317}, 'image');

var hotspots = image.select('ST_B10').gt(317)
  .selfMask()
  .rename('hotspots');
var objectSize = hotspots.connectedPixelCount(256);

Map.addLayer(objectSize, {min: 1, max: 256}, 'Size No Reproject', false);

// Beware of reproject!  Don't zoom out on reprojected data.
var reprojected = objectSize.reproject(hotspots.projection());
Map.addLayer(reprojected, {min: 1, max: 256}, 'Size Reproject', false);

出现差异的原因在于,分析的缩放比例由代码编辑器的缩放级别设置。通过调用 reproject(),您可以设置计算的规模,而不是通过 Code Editor 设置。请务必谨慎使用 reproject(),原因详见此文档

先过滤和 select()

一般而言,请先按时间、地理位置和/或元数据过滤输入集合,然后再对集合执行任何其他操作。先应用选择性更高的过滤条件,再应用选择性较低的过滤条件。空间过滤器和/或时间过滤器通常更具选择性。例如,请注意 select()filter() 是在 map() 之前应用的:

var images = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED');
var sf = ee.Geometry.Point([-122.463, 37.768]);

// Expensive function to reduce the neighborhood of an image.
var reduceFunction = function(image) {
  return image.reduceNeighborhood({
    reducer: ee.Reducer.mean(),
    kernel: ee.Kernel.square(4)
  });
};

var bands = ['B4', 'B3', 'B2'];
// Select and filter first!
var reasonableComputation = images
    .select(bands)
    .filterBounds(sf)
    .filterDate('2018-01-01', '2019-02-01')
    .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 1))
    .aside(print) // Useful for debugging.
    .map(reduceFunction)
    .reduce('mean')
    .rename(bands);
var viz = {bands: bands, min: 0, max: 10000};
Map.addLayer(reasonableComputation, viz, 'reasonableComputation');

使用 updateMask(),而不要使用 mask()

updateMask()mask() 之间的区别在于,前者会对参数(新遮罩)和现有图片遮罩执行逻辑 and(),而 mask() 只会将图片遮罩替换为参数。后者的危险在于,您可能会无意中揭露像素。在此示例中,目标是掩盖海拔高度小于或等于 300 米的像素。如您所见(缩小),使用 mask() 会导致许多像素(不属于感兴趣的图片的像素)取消遮罩:

var l8sr = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2');
var sf = ee.Geometry.Point([-122.40554461769182, 37.786807309873716]);
var aw3d30 = ee.Image('JAXA/ALOS/AW3D30_V1_1');

Map.centerObject(sf, 7);

var image = l8sr.filterBounds(sf)
    .filterDate('2019-06-01', '2019-12-31')
    .first();

image = image.multiply(0.0000275).subtract(0.2);  // Apply scale factors.
var vis = {bands: ['SR_B4', 'SR_B3', 'SR_B2'], min: 0, max: 0.3};
Map.addLayer(image, vis, 'image', false);

var mask = aw3d30.select('AVE').gt(300);
Map.addLayer(mask, {}, 'mask', false);

// NO!  Don't do this!
var badMask = image.mask(mask);
Map.addLayer(badMask, vis, 'badMask');

var goodMask = image.updateMask(mask);
Map.addLayer(goodMask, vis, 'goodMask', false);

组合 reducer

如果您需要从单个输入(例如图片区域)获取多个统计信息(例如平均值和标准差),则组合使用多个 reducer 会更高效。例如,如需获取图片统计信息,请按如下方式组合 reducer:

var image = ee.Image(
  'COPERNICUS/S2_HARMONIZED/20150821T111616_20160314T094808_T30UWU');

// Get mean and SD in every band by combining reducers.
var stats = image.reduceRegion({
  reducer: ee.Reducer.mean().combine({
    reducer2: ee.Reducer.stdDev(),
    sharedInputs: true
  }),
  geometry: ee.Geometry.Rectangle([-2.15, 48.55, -1.83, 48.72]),
  scale: 10,
  bestEffort: true // Use maxPixels if you care about scale.
});

print(stats);

// Extract means and SDs to images.
var meansImage = stats.toImage().select('.*_mean');
var sdsImage = stats.toImage().select('.*_stdDev');

请注意,在此示例中,平均值缩减器与标准差缩减器相结合,并且 sharedInputs 为 true,以便对输入像素进行单次传递。在输出字典中,会将 reducer 的名称附加到组名称。如需获取均值和标准差图片(例如对输入图片进行标准化处理),您可以将这些值转换为图片,然后使用正则表达式分别提取均值和标准差,如示例所示。

使用 Export

对于在代码编辑器中导致“用户内存用量超限”或“计算超时”错误的计算,使用 Export 可能可以成功执行相同的计算。这是因为在批处理系统(导出操作运行的位置)中运行时,超时时间更长,允许的内存占用量更大。(您可能需要先尝试其他方法,详情请参阅调试文档)。继续上例,假设字典返回了错误。您可以通过执行以下操作来获取结果:

var link = '86836482971a35a5e735a17e93c23272';
Export.table.toDrive({
  collection: ee.FeatureCollection([ee.Feature(null, stats)]),
  description: 'exported_stats_demo_' + link,
  fileFormat: 'CSV'
});

请注意,该链接会嵌入到资源名称中,以便重现。另请注意,如果您想导出 toAsset,则需要提供几何图形,它可以是任何内容,例如计算量小且便宜的图片重心。(即,如果不需要复杂的几何图形,请勿使用)。

如需查看使用 Export 解决计算超时并发汇总过多的示例,请参阅调试页面。如需详细了解导出功能,请参阅此文档

如果您不需要剪辑,请勿使用 clip()

不必要地使用 clip()增加计算时间。除非您的分析需要,否则请避免使用 clip()。如果您不确定,请勿剪辑。剪辑的错误使用示例:

请勿不必要地剪裁输入!

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');
var l8sr = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2');

var belgium = table.filter(ee.Filter.eq('country_na', 'Belgium')).first();

// Do NOT clip unless you need to.
var unnecessaryClip = l8sr
    .select('SR_B4')                          // Good.
    .filterBounds(belgium.geometry())         // Good.
    .filterDate('2019-01-01', '2019-04-01')   // Good.
    .map(function(image) {
      return image.clip(belgium.geometry());  // NO! Bad! Not necessary.
    })
    .median()
    .reduceRegion({
      reducer: ee.Reducer.mean(),
      geometry: belgium.geometry(),
      scale: 30,
      maxPixels: 1e10,
    });
print(unnecessaryClip);

可以完全跳过剪裁输入图片,因为区域是在 reduceRegion() 调用中指定的:

请指定输出区域!

var noClipNeeded = l8sr
    .select('SR_B4')                           // Good.
    .filterBounds(belgium.geometry())          // Good.
    .filterDate('2019-01-01', '2019-12-31') // Good.
    .median()
    .reduceRegion({
      reducer: ee.Reducer.mean(),
      geometry: belgium.geometry(), // Geometry is specified here.
      scale: 30,
      maxPixels: 1e10,
    });
print(noClipNeeded);

如果此计算超时,请按此示例所示对其执行 Export 操作。

如果您需要使用复杂集合进行剪辑,请使用 clipToCollection()

如果您确实需要剪裁某些内容,并且您要用于剪裁的几何图形位于集合中,请使用 clipToCollection()

var ecoregions = ee.FeatureCollection('RESOLVE/ECOREGIONS/2017');
var image = ee.Image('JAXA/ALOS/AW3D30_V1_1');

var complexCollection = ecoregions
    .filter(ee.Filter.eq('BIOME_NAME',
                         'Tropical & Subtropical Moist Broadleaf Forests'));
Map.addLayer(complexCollection, {}, 'complexCollection');

var clippedTheRightWay = image.select('AVE')
    .clipToCollection(complexCollection);
Map.addLayer(clippedTheRightWay, {}, 'clippedTheRightWay', false);

请勿对大型和/或复杂的集合使用 featureCollection.geometry()featureCollection.union(),因为它们可能需要更多内存。

请勿将复杂集合用作 reducer 的区域

如果您需要进行空间缩减,以便缩减器将 FeatureCollection 中的多个区域的输入汇总到一起,请勿将 featureCollection.geometry() 作为缩减器的 geometry 输入提供。而是使用 clipToCollection() 和足以涵盖集合边界的区域。例如:

var ecoregions = ee.FeatureCollection('RESOLVE/ECOREGIONS/2017');
var image = ee.Image('JAXA/ALOS/AW3D30_V1_1');

var complexCollection = ecoregions
    .filter(ee.Filter.eq('BIOME_NAME', 'Tropical & Subtropical Moist Broadleaf Forests'));

var clippedTheRightWay = image.select('AVE')
    .clipToCollection(complexCollection);
Map.addLayer(clippedTheRightWay, {}, 'clippedTheRightWay');

var reduction = clippedTheRightWay.reduceRegion({
  reducer: ee.Reducer.mean(),
  geometry: ee.Geometry.Rectangle({
    coords: [-179.9, -50, 179.9, 50],  // Almost global.
    geodesic: false
  }),
  scale: 30,
  maxPixels: 1e12
});
print(reduction); // If this times out, export it.

使用非零 errorMargin

对于可能需要大量计算的几何图形运算,请根据计算所需的精度使用尽可能大的误差范围。误差边界指定了在对几何图形执行操作(例如重新投影)期间允许的最大误差(以米为单位)。指定较小的误差范围可能会导致需要对几何图形(含坐标)进行重合,这可能会占用大量内存。最好为计算指定尽可能大的误差范围:

var ecoregions = ee.FeatureCollection('RESOLVE/ECOREGIONS/2017');

var complexCollection = ecoregions.limit(10);
Map.centerObject(complexCollection);
Map.addLayer(complexCollection);

var expensiveOps = complexCollection.map(function(f) {
  return f.buffer(10000, 200).bounds(200);
});
Map.addLayer(expensiveOps, {}, 'expensiveOps');

请勿将 reduceToVectors() 与极小的比例搭配使用

如果您想将光栅图像转换为矢量图像,请使用适当的比例。指定非常小的比例可能会大幅增加计算开销。将比例设置得尽可能高,以提供所需的精度。例如,如需获取表示全球陆地的多边形,请执行以下操作:

var etopo = ee.Image('NOAA/NGDC/ETOPO1');

// Approximate land boundary.
var bounds = etopo.select(0).gt(-100);

// Non-geodesic polygon.
var almostGlobal = ee.Geometry.Polygon({
  coords: [[-180, -80], [180, -80], [180, 80], [-180, 80], [-180, -80]],
  geodesic: false
});
Map.addLayer(almostGlobal, {}, 'almostGlobal');

var vectors = bounds.selfMask().reduceToVectors({
  reducer: ee.Reducer.countEvery(),
  geometry: almostGlobal,
  // Set the scale to the maximum possible given
  // the required precision of the computation.
  scale: 50000,
});
Map.addLayer(vectors, {}, 'vectors');

在前面的示例中,请注意使用非测地多边形在全局求和中进行求值。

请勿将 reduceToVectors()reduceRegions() 搭配使用

请勿将 reduceToVectors() 返回的 FeatureCollection 用作 reduceRegions() 的输入。请改为在调用 reduceToVectors() 之前添加要缩减的频段:

var etopo = ee.Image('NOAA/NGDC/ETOPO1');
var mod11a1 = ee.ImageCollection('MODIS/006/MOD11A1');

// Approximate land boundary.
var bounds = etopo.select(0).gt(-100);

// Non-geodesic polygon.
var almostGlobal = ee.Geometry.Polygon({
  coords: [[-180, -80], [180, -80], [180, 80], [-180, 80], [-180, -80]],
  geodesic: false
});

var lst = mod11a1.first().select(0);

var means = bounds.selfMask().addBands(lst).reduceToVectors({
  reducer: ee.Reducer.mean(),
  geometry: almostGlobal,
  scale: 1000,
  maxPixels: 1e10
});
print(means.limit(10));

请注意,在另一个图像的区域内减少一个图像的像素的其他方法包括 reduceConnectedCommponents() 和/或分组 reducer

使用 fastDistanceTransform() 执行邻域操作

对于某些卷积运算,fastDistanceTransform() 可能比 reduceNeighborhood()convolve() 更高效。例如,如需对二进制输入进行侵蚀和/或膨胀,请执行以下操作:

var aw3d30 = ee.Image('JAXA/ALOS/AW3D30_V1_1');

// Make a simple binary layer from a threshold on elevation.
var mask = aw3d30.select('AVE').gt(300);
Map.setCenter(-122.0703, 37.3872, 11);
Map.addLayer(mask, {}, 'mask');

// Distance in pixel units.
var distance = mask.fastDistanceTransform().sqrt();
// Threshold on distance (three pixels) for a dilation.
var dilation = distance.lt(3);
Map.addLayer(dilation, {}, 'dilation');

// Do the reverse for an erosion.
var notDistance = mask.not().fastDistanceTransform().sqrt();
var erosion = notDistance.gt(3);
Map.addLayer(erosion, {}, 'erosion');

使用 reduceNeighborhood() 中的优化

如果您需要执行卷积,但无法使用 fastDistanceTransform(),请使用 reduceNeighborhood() 中的优化。

var l8raw = ee.ImageCollection('LANDSAT/LC08/C02/T1_RT');
var composite = ee.Algorithms.Landsat.simpleComposite(l8raw);

var bands = ['B4', 'B3', 'B2'];

var optimizedConvolution = composite.select(bands).reduceNeighborhood({
  reducer: ee.Reducer.mean(),
  kernel: ee.Kernel.square(3),
  optimization: 'boxcar' // Suitable optimization for mean.
}).rename(bands);

var viz = {bands: bands, min: 0, max: 72};
Map.setCenter(-122.0703, 37.3872, 11);
Map.addLayer(composite, viz, 'composite');
Map.addLayer(optimizedConvolution, viz, 'optimizedConvolution');

不要采样超出所需的数据

不要不必要地增加训练数据集的大小。虽然在某些情况下,增加训练数据量是一种有效的机器学习策略,但这也可能会增加计算开销,而准确性却没有相应提高。(如需了解何时增加训练数据集大小,请参阅此参考文档)。以下示例演示了如何请求过多训练数据可能会导致令人恐惧的“计算值过大”错误:

请勿抽取太多数据!

var l8raw = ee.ImageCollection('LANDSAT/LC08/C02/T1_RT');
var composite = ee.Algorithms.Landsat.simpleComposite(l8raw);
var labels = ee.FeatureCollection('projects/google/demo_landcover_labels');

// No!  Not necessary.  Don't do this:
labels = labels.map(function(f) { return f.buffer(100000, 1000); });

var bands = ['B2', 'B3', 'B4', 'B5', 'B6', 'B7'];

var training = composite.select(bands).sampleRegions({
  collection: labels,
  properties: ['landcover'],
  scale: 30
});

var classifier = ee.Classifier.smileCart().train({
  features: training,
  classProperty: 'landcover',
  inputProperties: bands
});
print(classifier.explain()); // Computed value is too large

更好的方法是先使用适量的数据,然后调整分类器的超参数,以确定能否达到所需的准确性:

调整超参数!

var l8raw = ee.ImageCollection('LANDSAT/LC08/C02/T1_RT');
var composite = ee.Algorithms.Landsat.simpleComposite(l8raw);
var labels = ee.FeatureCollection('projects/google/demo_landcover_labels');

// Increase the data a little bit, possibly introducing noise.
labels = labels.map(function(f) { return f.buffer(100, 10); });

var bands = ['B2', 'B3', 'B4', 'B5', 'B6', 'B7'];

var data = composite.select(bands).sampleRegions({
  collection: labels,
  properties: ['landcover'],
  scale: 30
});

// Add a column of uniform random numbers called 'random'.
data = data.randomColumn();

// Partition into training and testing.
var training = data.filter(ee.Filter.lt('random', 0.5));
var testing = data.filter(ee.Filter.gte('random', 0.5));

// Tune the minLeafPopulation parameter.
var minLeafPops = ee.List.sequence(1, 10);

var accuracies = minLeafPops.map(function(p) {
  var classifier = ee.Classifier.smileCart({minLeafPopulation: p})
      .train({
        features: training,
        classProperty: 'landcover',
        inputProperties: bands
      });

  return testing
    .classify(classifier)
    .errorMatrix('landcover', 'classification')
    .accuracy();
});

print(ui.Chart.array.values({
  array: ee.Array(accuracies),
  axis: 0,
  xLabels: minLeafPops
}));

在此示例中,分类器已经非常准确,因此不需要进行太多调整。您可能需要选择一个仍能达到所需准确度的尽可能小的树(即最大的 minLeafPopulation)。

Export 中间结果

假设您的目标是从相对复杂的计算图像中提取样本。通常,Export 图片 toAsset()、加载导出的图片,然后进行抽样,这样更高效。例如:

var image = ee.Image('UMD/hansen/global_forest_change_2018_v1_6');
var geometry = ee.Geometry.Polygon(
    [[[-76.64069800085349, 5.511777325802095],
      [-76.64069800085349, -20.483938229362376],
      [-35.15632300085349, -20.483938229362376],
      [-35.15632300085349, 5.511777325802095]]], null, false);
var testRegion = ee.Geometry.Polygon(
    [[[-48.86726050085349, -3.0475996402515717],
      [-48.86726050085349, -3.9248707849303295],
      [-47.46101050085349, -3.9248707849303295],
      [-47.46101050085349, -3.0475996402515717]]], null, false);

// Forest loss in 2016, to stratify a sample.
var loss = image.select('lossyear');
var loss16 = loss.eq(16).rename('loss16');

// Scales and masks Landsat 8 surface reflectance images.
function prepSrL8(image) {
  var qaMask = image.select('QA_PIXEL').bitwiseAnd(parseInt('11111', 2)).eq(0);
  var opticalBands = image.select('SR_B.').multiply(0.0000275).add(-0.2);
  var thermalBands = image.select('ST_B.*').multiply(0.00341802).add(149.0);
  return image.addBands(opticalBands, null, true)
      .addBands(thermalBands, null, true)
      .updateMask(qaMask);
}

var collection = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
    .map(prepSrL8);

// Create two annual cloud-free composites.
var composite1 = collection.filterDate('2015-01-01', '2015-12-31').median();
var composite2 = collection.filterDate('2017-01-01', '2017-12-31').median();

// We want a strtatified sample of this stack.
var stack = composite1.addBands(composite2)
    .float(); // Export the smallest size possible.

// Export the image.  This block is commented because the export is complete.
/*
var link = '0b8023b0af6c1b0ac7b5be649b54db06'
var desc = 'Logistic_regression_stack_' + link;
Export.image.toAsset({
  image: stack,
  description: desc,
  assetId: desc,
  region: geometry,
  scale: 30,
  maxPixels: 1e10
})
*/

// Load the exported image.
var exportedStack = ee.Image(
  'projects/google/Logistic_regression_stack_0b8023b0af6c1b0ac7b5be649b54db06');

// Take a very small sample first, to debug.
var testSample = exportedStack.addBands(loss16).stratifiedSample({
  numPoints: 1,
  classBand: 'loss16',
  region: testRegion,
  scale: 30,
  geometries: true
});
print(testSample); // Check this in the console.

// Take a large sample.
var sample = exportedStack.addBands(loss16).stratifiedSample({
  numPoints: 10000,
  classBand: 'loss16',
  region: geometry,
  scale: 30,
});

// Export the large sample...

请注意,在此示例中,图像以浮点值导出。除非绝对必要,否则请勿以双精度导出。进行此导出操作时,请注意,为了实现可重现性,文件名中嵌入了 Code Editor 链接(在导出前立即获取)。

导出完成后,重新加载资源并继续从中抽样。请注意,系统会先运行一小部分测试区域中的一小部分样本,以进行调试。当系统显示导出成功后,请获取较大的样本并进行导出。此类大型样本通常需要导出。请勿期望在未先导出这些示例的情况下,能够以交互方式(例如通过 print())使用或使用这些示例(例如将其用作分类器的输入)。

联接与映射-过滤

假设您想根据时间、位置或某个元数据属性联接合集。通常,使用联接是最有效的方式。以下示例在 Landsat 8 和 Sentinel-2 集合之间执行时空联接:

var s2 = ee.ImageCollection('COPERNICUS/S2_HARMONIZED')
    .filterBounds(ee.Geometry.Point([-2.0205, 48.647]));

var l8 = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2');

var joined = ee.Join.saveAll('landsat').apply({
  primary: s2,
  secondary: l8,
  condition: ee.Filter.and(
    ee.Filter.maxDifference({
      difference: 1000 * 60 * 60 * 24, // One day in milliseconds
      leftField: 'system:time_start',
      rightField: 'system:time_start',
    }),
    ee.Filter.intersects({
      leftField: '.geo',
      rightField: '.geo',
    })
  )
});
print(joined);

虽然您应先尝试联接(如有需要,请使用 Export),但有时 map() 中的 filter() 也能发挥作用,尤其是对于非常大的集合。

var s2 = ee.ImageCollection('COPERNICUS/S2_HARMONIZED')
    .filterBounds(ee.Geometry.Point([-2.0205, 48.647]));

var l8 = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2');

var mappedFilter = s2.map(function(image) {
  var date = image.date();
  var landsat = l8
      .filterBounds(image.geometry())
      .filterDate(date.advance(-1, 'day'), date.advance(1, 'day'));
  // Return the input image with matching scenes in a property.
  return image.set({
    landsat: landsat,
    size: landsat.size()
  });
}).filter(ee.Filter.gt('size', 0));
print(mappedFilter);

reduceRegion()reduceRegions() 与 for 循环

使用非常大或非常复杂的 FeatureCollection 作为输入调用 reduceRegions() 可能会导致令人恐惧的“计算值过大”错误。一种可能的解决方案是改为将 reduceRegion() 映射到 FeatureCollection。另一种可能的解决方案是使用(惊恐)for 循环。虽然我们强烈建议您不要在 Earth Engine 中这样做(如此处此处此处所述),但您可以在 for 循环中实现 reduceRegion() 以执行大量求交运算。

假设您的目标是为 ImageCollection 中的每张图片获取 FeatureCollection 中的每个地图项中的像素均值(或任何统计信息)。以下示例比较了前面介绍的三种方法:

// Table of countries.
var countriesTable = ee.FeatureCollection("USDOS/LSIB_SIMPLE/2017");
// Time series of images.
var mod13a1 = ee.ImageCollection("MODIS/006/MOD13A1");

// MODIS vegetation indices (always use the most recent version).
var band = 'NDVI';
var imagery = mod13a1.select(band);

// Option 1: reduceRegions()
var testTable = countriesTable.limit(1); // Do this outside map()s and loops.
var data = imagery.map(function(image) {
  return image.reduceRegions({
    collection: testTable,
    reducer: ee.Reducer.mean(),
    scale: 500
  }).map(function(f) {
    return f.set({
      time: image.date().millis(),
      date: image.date().format()
    });
  });
}).flatten();
print(data.first());

// Option 2: mapped reduceRegion()
var data = countriesTable.map(function(feature) {
  return imagery.map(function(image) {
    return ee.Feature(feature.geometry().centroid(100),
        image.reduceRegion({
          reducer: ee.Reducer.mean(),
          geometry: feature.geometry(),
          scale: 500
        })).set({
          time: image.date().millis(),
          date: image.date().format()
        }).copyProperties(feature);
  });
}).flatten();
print(data.first());

// Option 3: for-loop (WATCH OUT!)
var size = countriesTable.size();
// print(size); // 312
var countriesList = countriesTable.toList(1); // Adjust size.
var data = ee.FeatureCollection([]); // Empty table.
for (var j=0; j<1; j++) { // Adjust size.
  var feature = ee.Feature(countriesList.get(j));
  // Convert ImageCollection > FeatureCollection
  var fc = ee.FeatureCollection(imagery.map(function(image) {
    return ee.Feature(feature.geometry().centroid(100),
        image.reduceRegion({
          reducer: ee.Reducer.mean(),
          geometry: feature.geometry(),
          scale: 500
        })).set({
          time: image.date().millis(),
          date: image.date().format()
        }).copyProperties(feature);
  }));
  data = data.merge(fc);
}
print(data.first());

请注意,系统会输出每个集合中的 first() 实体,以便进行调试。您不应期望能够以交互方式查看完整结果:您需要执行 Export。另请注意,应谨慎使用 for 循环,并且只应在万不得已的情况下使用。最后,for 循环需要手动获取输入集合的大小,并将其硬编码到适当的位置。如果您对上述任何内容感到不清楚,请勿使用 for 循环。

对时间邻居使用正向差分

假设您有一个按时间排序的 ImageCollection(即时间序列),并且您想将每张图片与前一张(或下一张)图片进行比较。为此,使用基于数组的前向差分可能比使用 iterate() 更高效。以下示例使用此方法对 Sentinel-2 集合进行去重,其中重复项定义为同一日期的图片:

var sentinel2 = ee.ImageCollection('COPERNICUS/S2_HARMONIZED');
var sf = ee.Geometry.Point([-122.47555371521855, 37.76884708376152]);
var s2 = sentinel2
    .filterBounds(sf)
    .filterDate('2018-01-01', '2019-12-31');

var withDoys = s2.map(function(image) {
  var ndvi = image.normalizedDifference(['B4', 'B8']).rename('ndvi');
  var date = image.date();
  var doy = date.getRelative('day', 'year');
  var time = image.metadata('system:time_start');
  var doyImage = ee.Image(doy)
      .rename('doy')
      .int();
  return ndvi.addBands(doyImage).addBands(time)
      .clip(image.geometry()); // Appropriate use of clip.
});

var array = withDoys.toArray();
var timeAxis = 0;
var bandAxis = 1;

var dedupe = function(array) {
  var time = array.arraySlice(bandAxis, -1);
  var sorted = array.arraySort(time);
  var doy = sorted.arraySlice(bandAxis, -2, -1);
  var left = doy.arraySlice(timeAxis, 1);
  var right = doy.arraySlice(timeAxis, 0, -1);
  var mask = ee.Image(ee.Array([[1]]))
      .arrayCat(left.neq(right), timeAxis);
  return array.arrayMask(mask);
};

var deduped = dedupe(array);

// Inspect these outputs to confirm that duplicates have been removed.
print(array.reduceRegion('first', sf, 10));
print(deduped.reduceRegion('first', sf, 10));

检查打印的照片集,确认已移除重复照片。