GEE, Sentinel 2 cloud and shadow

GEE在2021年8月30日左右,发布了Sentinel 2 去云的新教程,利用Probability产品和 CDI指数来去云。官方教程代码如下:
https://code.earthengine.google.com/a7d6d6defee0ae0d0fdfe4d4c5011306
或者在Examples中也可以找到

image.png

以下内容是自己的笔记

目录

  • 知识点
  • 官方代码讲解
  • 实际应用

一、知识点

  • CDI
    CDI是David Frantz在2018年创造的指数,这个指数是用来探测Sentinel 2中的云,并且还可以区分高亮云和建筑物,在GEE中的调用是ee.Algorithms.Sentinel2.CDI 。由于CDI指数是使用Sentinel 2 Level 1C 产品生成的,所以用这个算法计算CDI时,要用到Sentinel 2 Level 1C 。想要更加了解这个指数的可以看David Frantz在RSE上发表的文章Improvement of the Fmask algorithm for Sentinel-2 images: Separating clouds from bright surfaces based on parallax effects

  • ee.Join函数
    关于这部分内容,推荐去看知乎上的一个作者,想要了解Join函数就必须先了解Filter,更加推荐大家去看视频版,也就12分钟。
    第10节 GEE的参数类型 (Filter,Join) - 知乎 (zhihu.com)

二、官方代码讲解

1. 调用三个数据集

// 调用 Sentinel 2 L1C产品,来计算CDI
var s2 = ee.ImageCollection('COPERNICUS/S2');
// 调用 Probability产品
var s2c = ee.ImageCollection('COPERNICUS/S2_CLOUD_PROBABILITY');
// 调用 Sentine 2 L2A产品
var s2Sr = ee.ImageCollection('COPERNICUS/S2_SR');

2. 筛选

// ROI
var roi = ee.Geometry.Point([-122.4431, 37.7498]);
Map.centerObject(roi, 11);

// 定义时间范围
var start = ee.Date('2019-03-01');
var end = ee.Date('2019-09-01');

// 选出CDI计算中要用到的波段
s2 = s2.filterBounds(roi).filterDate(start, end)
    .select(['B7', 'B8', 'B8A', 'B10']);

s2c = s2c.filterDate(start, end).filterBounds(roi);
// 这里对Sentinel 2 L2A产品的波段选择没有要求,也可以选择其他波段,或者全选,但注意和L1C区分
s2Sr = s2Sr.filterDate(start, end).filterBounds(roi)
    .select(['B2', 'B3', 'B4', 'B5']);

3. 定义函数:将两个数据集合并
下面这个函数,实际上是将collectionB 按照时间属性,加入到collectionA中。比如:有2个数据集,一个是EVI,一个是NDVI,它们都有一年的时间序列,我想按照时间整合两个数据集到一个数据集当中,以便于分析。
注意:ee.Join.saveFirst 返回的是左边数据集collectionA,只是将 collectionB当作一个属性添加到collectionA中的属性当中而已

// propertyName 是一个MatchKey,由用户自定义,可以是任何字符串
//为了后面的map函数,ee.ImageCollection 一定要加,因为ee.Join函数返回的是Join对象
function indexJoin(collectionA, collectionB, propertyName) {
  var joined = ee.ImageCollection(ee.Join.saveFirst(propertyName).apply({
    primary: collectionA,
    secondary: collectionB,
    condition: ee.Filter.equals({
      leftField: 'system:index',
      rightField: 'system:index'})
  }));
  // 通过get函数获取右边数据集的影像,再通过addBands合并成一个ImageCol
  return joined.map(function(image) {
    return image.addBands(ee.Image(image.get(propertyName)));
  });
}

4. 定义函数:去云
这一步才是真正的开始去云以及去掉阴影,前面都是数据集的准备

function maskImage(image) {
  // 计算CDI,下面的B10是Sentinel2 L1C的波段
  var cdi = ee.Algorithms.Sentinel2.CDI(image);
  var s2c = image.select('probability');
  var cirrus = image.select('B10').multiply(0.0001);

 // 阈值设定,满足下列条件的都是云,这里的阈值是官方给的
  var isCloud = s2c.gt(65).and(cdi.lt(-0.5)).or(cirrus.gt(0.01));

  // 以下代码似乎是通过面积来判断,有知道的可以评论一下
  // Reproject is required to perform spatial operations at 20m scale.
  // 20m scale is for speed, and assumes clouds don't require 10m precision.
  isCloud = isCloud.focal_min(3).focal_max(16);
  isCloud = isCloud.reproject({crs: cdi.projection(), scale: 20});

  // Project shadows from clouds we found in the last step. This assumes we're working in
  // a UTM projection.
  var shadowAzimuth = ee.Number(90)
      .subtract(ee.Number(image.get('MEAN_SOLAR_AZIMUTH_ANGLE')));

  // With the following reproject, the shadows are projected 5km.
  isCloud = isCloud.directionalDistanceTransform(shadowAzimuth, 50);
  isCloud = isCloud.reproject({crs: cdi.projection(), scale: 100});

  isCloud = isCloud.select('distance').mask();
  return image.select('B2', 'B3', 'B4').updateMask(isCloud.not());
}

5. 合并与去云
官方这里给的是无云中值合成案例

// 将Probability 加入到 Sentinel2 L2A中
var withCloudProbability = indexJoin(s2Sr, s2c, 'cloud_probability');

// 将Sentinel2 L1C 加入到 withCloudProbability 中
var withS2L1C = indexJoin(withCloudProbability, s2, 'l1c');

// 对最后合并的数据集进行去云
var masked = ee.ImageCollection(withS2L1C.map(maskImage));

// 中值合成,下面设置8是为了避免memory errors
var median = masked.reduce(ee.Reducer.median(), 8);

三、实际应用

生成云掩膜的过程实际上只用到L1C产品和Probability产品,如果按照官方的步骤,把L2A产品和Probability产品先合并,那么L2A产品中的波段不能含有 ['B7', 'B8', 'B8A', 'B10'],因为后面还会和L1C产品合并,CDI只用到了L1C产品中的 ['B7', 'B8', 'B8A', 'B10']来计算。如果将L2A和L1C的['B7', 'B8', 'B8A', 'B10']都合并在一起,会出现波段命名不一样的情况,如下图所示,此时如果用maskImage,CDI计算用到的波段可能是L2A的。

image.png

而我们在使用过程中,B8这个波段对我们很重要,不能丢掉B8。因此,可以先生成掩膜文件,再通过Map循环来掩膜。
代码如下:

// 数据集
var s2 = ee.ImageCollection('COPERNICUS/S2');
var s2c = ee.ImageCollection('COPERNICUS/S2_CLOUD_PROBABILITY');
var s2Sr = ee.ImageCollection('COPERNICUS/S2_SR');

var roi = ee.Geometry.Point([-122.4431, 37.7498]);
Map.centerObject(roi, 11);

var start = ee.Date('2020-01-01');
var end = ee.Date('2021-01-01');

//筛选
s2 = s2.filterBounds(roi).filterDate(start, end)
    .select(['B7', 'B8', 'B8A', 'B10']);
s2c = s2c.filterDate(start, end).filterBounds(roi);

var s2Sr = s2Sr
            .filterBounds(roi)
            .filterDate(start,end)
            .filter(ee.Filter.lte('CLOUDY_PIXEL_PERCENTAGE',50))

// Join two collections on their 'system:index' property.
function indexJoin(collectionA, collectionB, propertyName) {
  var joined = ee.ImageCollection(ee.Join.saveFirst(propertyName).apply({
    primary: collectionA,
    secondary: collectionB,
    condition: ee.Filter.equals({
      leftField: 'system:index',
      rightField: 'system:index'})
  }));
  // Merge the bands of the joined image.
  return joined.map(function(image) {
    return image.addBands(ee.Image(image.get(propertyName)));
  });
}

// 生成一个云掩膜波段,并将这个掩膜波段命名为 CloudMask
function maskImage(image) {
  // Compute the cloud displacement index from the L1C bands.
  var cdi = ee.Algorithms.Sentinel2.CDI(image);
  var prob = image.select('probability');
  var cirrus = image.select('B10').multiply(0.0001);
  var isCloud = prob.gt(65).and(cdi.lt(-0.5)).or(cirrus.gt(0.01));

  isCloud = isCloud.focal_min(3).focal_max(16);
  isCloud = isCloud.reproject({crs: cdi.projection(), scale: 20});

  var shadowAzimuth = ee.Number(90)
      .subtract(ee.Number(image.get('MEAN_SOLAR_AZIMUTH_ANGLE')));

  isCloud = isCloud.directionalDistanceTransform(shadowAzimuth, 50);
  isCloud = isCloud.reproject({crs: cdi.projection(), scale: 100});

  isCloud = isCloud.select('distance').mask().rename('CloudMask');
  return isCloud.not();
}

// 将probability产品合并到s2中
var withCloudProbability = indexJoin(s2, s2c, 'cloud_probability');
// 生成掩膜文件
var masked = ee.ImageCollection(withCloudProbability.map(maskImage));
//将掩膜文件合并到L2A产品中
var s2SrWithMask = indexJoin(s2Sr,masked,'could_mask');

// 使用Map函数,开始掩膜
var S2_cloudMask = s2SrWithMask.map(function(img){
    var mask = img.select('CloudMask')
    return img.updateMask(mask)
              .divide(10000)
              .select('B.*')
              .toFloat()
              .copyProperties(img, ["system:time_start"])
})

print('S2_cloudMask:',S2_cloudMask)

你可能感兴趣的:(GEE, Sentinel 2 cloud and shadow)