在 Google Earth Engine(GEE)中导出大数据集时,尤其是高分辨率影像或大范围区域的数据,往往会超出系统的导出任务限制。
对大规模导出进行分块(Tiling)处理是绕过这些限制的常用方法。
从 GEE导出大规模栅格数据时,建议将导出数据分割为若干较小的分块。在本文中,我将分享在目标投影中创建分块导出的最佳实践,以确保这些分块在后期拼接时不会出现像素间的缝隙或重叠。
核心概念是利用 crsTransform 参数,确保每个分块都位于相同的像素网格上。
在导出全球或国家尺度的高分辨率影像时,任务可能需要数小时甚至数天才能完成。为了加快超大规模导出任务的处理速度,推荐的做法是将影像划分为较小的分块,并将每个分块作为单独的任务进行导出。
正确执行上述操作需要对 Earth Engine 中投影的细节有深入理解,并确保所有与投影相关的操作在 GEE 中保持一致。以下是将大规模影像分割为多个分块的推荐工作流程:
-
使用 Earth Engine 中的
coveringGrid()
函数,在所需的坐标参考系(CRS)中创建一个分块网格。
-
计算一个 CRS Transform,用于定义目标像素网格,并确保每个导出分块使用相同的像素网格。
-
对像素进行重新采样或聚合操作(Resample 或 Aggregate Pixels)。
-
设置一个 NoData 值,标识无效数据区域。
-
使用相同的 CRS Transform 导出每个分块。
接下来,我们将通过一个示例及相关代码来详细展示这一流程。示例中,我们将使用 ESA WorldCover 土地覆盖数据集,导出覆盖整个爱沙尼亚国家范围的影像分块。
对于这类需要创建和启动多个导出任务的批量导出操作,建议优先使用 Earth Engine 的 Python API.
我们选择一个国家,并为该区域创建裁剪后的 ESA WorldCover 2020 分类数据。随后,我们使用 geemap 可视化显示该影像。
worldcover = ee.ImageCollection("ESA/WorldCover/v100")
lsib = ee.FeatureCollection("USDOS/LSIB_SIMPLE/2017")
country = lsib.filter(ee.Filter.eq('country_na', 'Estonia'))
geometry = country.geometry()
image = worldcover.first().clip(geometry)
m = geemap.Map(width=800)
m.addLayer(image, {}, 'Input Image');
m.centerObject(country, 7)
m
我们创建了一个网格并计算了 CRS Transform 的相关参数。网格中的每个分块将按照选定的像素网格导出为单独的影像。如果导出的影像尺寸超过 10,000 x 10,000 像素,Earth Engine 会将输出自动分割为多个文件。为避免这种情况,建议将分块尺寸设置为不超过 10,000 像素。由于我们的输入影像像素分辨率为 10 米,因此我们将网格大小设置为 100,000 x 100,000 米。
crs = 'EPSG:3301'
pixelSize = 10
tileSize = 10000
gridSize = tileSize * pixelSize
bounds = geometry.bounds(**{
'proj': crs, 'maxError': 1
})
grid = bounds.coveringGrid(**{
'proj':crs, 'scale': gridSize
})
m.addLayer(grid, {'color': 'blue'}, 'Grid')
m
计算 CRS Transform(坐标参考系变换参数)
当栅格数据被重投影到另一个坐标参考系(CRS)时,会生成一个目标像素网格,并根据输入栅格的像素值计算每个输出像素的值。输出像素网格由 crsTransform 参数决定,该参数指定网格的起始点(即左上角像素的坐标)和每个像素的大小(分辨率)。在 Google Earth Engine 中,可以在所有与投影相关的函数中指定 crsTransform 参数,以确保所有计算在指定的像素网格上进行。请记住,在 Google Earth Engine 中,通常无需担心影像的输入投影。您可以直接混合和组合具有不同投影的影像,而无需先对它们进行重投影。只有在执行计算或导出数据时,才需要指定输出投影。GEE 会在内部将所有输入数据自动重投影到指定的目标投影,以确保计算的正确性和一致性。
bounds = grid.geometry().bounds(**{
'proj': crs, 'maxError': 1
});
coordList = ee.Array.cat(bounds.coordinates(), 1)
xCoords = coordList.slice(1, 0, 1)
yCoords = coordList.slice(1, 1, 2)
xMin = xCoords.reduce('min', [0]).get([0,0])
yMax = yCoords.reduce('max', [0]).get([0,0])
transform = ee.List([
pixelSize, 0, xMin, 0, -pixelSize, yMax]).getInfo()
print(transform)
默认情况下,影像会使用最近邻方法(Nearest Neighbor)重新采样到目标像素网格。这种方式适用于大多数影像类型,但在某些操作中,您可能需要更改这种行为。
何时需要重采样像素?
当原始像素大小与目标像素大小相近时,重采样是适合的选择。
何时需要聚合像素?
当目标像素的大小远大于原始像素时,需要使用适当的统计函数将输入像素聚合成更大的像素。
-
对于离散栅格
,使用众数(mode)聚合,例如
ee.Reducer.mode()
。
-
对于连续栅格
,使用平均值(mean)聚合,例如
ee.Reducer.mean()
。
-
对于人口栅格
,使用求和(sum)聚合,例如
ee.Reducer.sum()
。
重采样和聚合像素的相关内容可以参考 GEE 用户指南,或者参考我们关于聚合人口栅格的文章以获取更多详细信息。
在我们的例子中,由于输入的是像素大小相同的离散栅格,因此这一步骤无需额外处理。
这是一个重要的步骤。如果影像中存在被掩膜的像素,导出的分块可能会出现尺寸不一致的情况。为了确保每个分块具有相同的尺寸且没有像素间的缝隙或重叠,应对所有被掩膜的像素使用 unmask() 方法,并设置一个 NoData 值(无效数据值)。
noDataValue = 0
exportImage = image.unmask(**{
'value':noDataValue,
'sameFootprint': False
})
现在,我们可以使用网格图层将影像导出为分块数据。通过遍历每个网格要素,获取其几何信息,并使用指定的投影坐标系(CRS)和 CRS Transform 参数导出影像。利用 ee.batch.Export API,可以为每个分块自动创建并启动导出任务。
tile_ids = grid.aggregate_array('system:index').getInfo();
print('Total tiles', len(tile_ids))
for i, tile_id in enumerate(tile_ids):
feature = ee.Feature(grid.toList(1, i).get(0))
geometry = feature.geometry()
task_name = 'tile_' + tile_id.replace(',', '_')
task = ee.batch.Export.image.toDrive(**{
'image': exportImage,
'description': f'Image_Export_{task_name}',
'fileNamePrefix': task_name,
'folder':'earthengine',
'crs': crs,
'crsTransform': transform,
'region': geometry,
'maxPixels': 1e10
})
task.start()
print('Started Task: ', i+1)
您可以查看完整的代码,其中包含将大影像分割为分块并单独导出每个分块的完整可复现示例。生成的影像分块完美对齐,没有缝隙或像素重叠。
Google Colab Notebook:
https://colab.research.google.com/github/spatialthoughts/projects/blob/master/ee-python/large_gridded_exports.ipynb
GEE-Javascript:
https://code.earthengine.google.co.in/022063d17caeb0868d806ee2ca457e93