Bootstrap

GEE必须会教程——Savitzky-Golay(S-G)滤波器在Sentinel影像时间序列数据的应用

1964年,发表在《Analytical Chemistry》上的一篇名为“Smoothing and Differentiation of Data by Simplified Least Squares Procedures”引起了学界关注,该篇论文针对当时时间序列数据的分析和处理中面临的普遍需求,即“如何在保留信号特征的同时平滑数据和减少噪声”,提出了一种切实有效的方法,为化学光谱数据的平滑和微分处理开辟了新路径。Paper的作者为Abraham Savitzky 和 Marcel J. E. Golay,后世将这种时间序列数据的平滑和去噪方法称之为Savitzky-Golay滤波方法,这就是S-G滤波的由来。

 尽管S-G滤波提出之初,主要是针对化学光谱数据的,但在分析遥感数据时,大部分时候,我们主要也是针对影像的波段光谱数据展开分析,因此S-G滤波器同样也适用于遥感影像的时间序列数据分析。今天,跟着小编来看看GEE上怎么实现S-G滤波分析。

1 S-G滤波器概要

在进行遥感影像处理时,常常需要处理大量的数据。这些数据通常包含噪声,这些噪声可能来自遥感平台自身误差、大气扰动或者地物周围环境的干扰。因此,在实际应用过程中需要一种方法来平滑数据,提取方便研究者开展分析的光谱信息特征,同时去除噪声。

在S-G滤波提出之前,常用的平滑方法包括移动平均滤波加权平均滤波,大家或多或少在进行数据处理的时候都用过。这些方法虽然能够平滑数据,但往往会模糊数据中的重要特征(如峰值和谷值),导致信号失真。

S-G滤波方法提出后,受到学界欢迎的原因就是,他真的太好用了!!在平滑数据的同时,能够很好地保留信号中的局部特征,如峰值和谷值。这在光谱分析、时间序列分析等领域尤为重要。

2 S-G滤波原理

S-G滤波基于最小二乘法的平滑方法进行。

首先,定义滑动窗口的大小,在每个滑动窗口内,使用一个多项式函数来拟合数据点。这个多项式函数的阶数可以根据具体需求进行选择(通常是二次或三次多项式) ,接着,使用最小二乘法确定多项式的系数,以便拟合曲线与滑动窗口内的数据点尽可能匹配。拟合后的多项式值用于替代窗口中心点的原始数据值。最后,滤波器在整个数据序列上以固定长度的窗口进行滑动,对每个窗口内的数据点重复进行多项式拟合和平滑处理,完成整个时间序列数据的处理。

如果想进一步了解S-G滤波的原理,请看大佬文章(我是没看懂hhhh,大家看懂了教教我~~~):

http://t.csdnimg.cn/bJ1HA

http://t.csdnimg.cn/cpr2c

3 应用S-G滤波器的一般步骤

一般而言,Savitzky-Golay滤波在遥感影像时间序列分析中的应用主要包括以下几个步骤:

第一步 数据准备:获取并预处理遥感影像数据,计算时间序列指标。该部分需要基于GEE平台访问影像数据,并根据想要构建的目标指数(NDVI、EVI)等合成时间序列。

第二步 滤波参数选择:选择适当的窗口大小和多项式阶数。窗口的大小和多项式阶数会影像滤波的效果,前者决定了覆盖的时间范围,且窗口越大,平滑效果越强,但是容易造成数据丢失;后者越大,表明拟合关系越复杂,但精度更高

第三步 应用S-G滤波:使用第二步选择的参数对时间序列数据进行S-G滤波。

第四步 结果分析:对比原始和平滑后的时间序列数据,提取其中的趋势和周期性变化进行进一步分析。

4 代码示例

接下来,我们以江西庐山某地区为例,一起来看看如何应用S-G滤波器对该区的NDVI时间序列数据展开分析。

Ⅰ生成NDVI时间序列

由于Sentinel影像在NIR和RED波段的空间分辨率较高,均为10m,因此我们在庐山地区选择了一小块区域进行实验,以免出现计算时内存溢出。

接下来我们上代码,依旧包括了几个步骤,卫星数据访问、去云、选用B4,B3波段计算NDVI并添加波段。

var s2 = ee.ImageCollection("COPERNICUS/S2"); 
var geometry = ee.FeatureCollection(ee.Geometry.Polygon(
        [[[115.90071074526553,29.493898535399506],
          [116.07443205874209,29.493898535399506],
          [116.07443205874209,29.59425592477838],
          [115.90071074526553,29.59425592477838],
          [115.90071074526553,29.493898535399506]]], null, false));
var styling = {color:'red',fillColor:'00000000'}
Map.addLayer(geometry.style(styling), {}, 'Lushan')
Map.centerObject(geometry)
var geometry = geometry.geometry()
var startDate = ee.Date('2023-01-01')
var endDate = startDate.advance(1, 'year')

var filtered = s2
  .filter(ee.Filter.date(startDate, endDate))
  .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 30))
  .filter(ee.Filter.bounds(geometry))

// 定义一个去云函数
function maskS2clouds(image) {
  var qa = image.select('QA60')
  var cloudBitMask = 1 << 10;
  var cirrusBitMask = 1 << 11;
  var mask = qa.bitwiseAnd(cloudBitMask).eq(0).and(
             qa.bitwiseAnd(cirrusBitMask).eq(0))
  return image.updateMask(mask)//.divide(10000)
      .select("B.*")
      .copyProperties(image, ["system:time_start"])
}

var filtered = filtered.map(maskS2clouds)


// 定义一个计算NDVI的函数并添加波段
function addNDVI(image) {
  var ndvi = image.normalizedDifference(['B8', 'B4']).toFloat().rename('ndvi');
  return image.addBands(ndvi);
}


// 对数据集进行循环遍历
var withNdvi = filtered.map(addNDVI);

// 挑选NDVI波段
var ndviCol = withNdvi.select('ndvi')
print('Original Collection', ndviCol)

Ⅱ创建一个包含n天影像的空时间序列数据

//确定时间间隔,按照间隔插入空白影像
var n = 7;
var totalDays = endDate.difference(startDate, 'day');
var daysToInterpolate = ee.List.sequence(1, totalDays, n)

var initImages = daysToInterpolate.map(function(day) {
  var image = ee.Image().rename('ndvi').toFloat().set({
    'system:index': ee.Number(day).format('%d'),
    'system:time_start': startDate.advance(day, 'day').millis(),
    // 设置属性,以便分辨插值影像
    'type': 'interpolated'
  })
  return image
})

var initCol = ee.ImageCollection.fromImages(initImages)
print('Empty Collection', initCol)

此处创建一个包含n天(这里设置为一周时间)的时间序列数据,源于在处理哨兵-2卫星的遥感数据时,由于其两个传感器(Sentinel-2A 和 Sentinel-2B)每天可能会产生多景影像,所以我们需要对一定时间范围内的NDVI计算结果取平均。最后,原来的多景影像将减少:

Ⅲ使用Join连接前后图像

var mergedCol = ndviCol.merge(initCol)

var mergedCol = mergedCol.map(function(image) {
  var timeImage = image.metadata('system:time_start').rename('timestamp')
  var timeImageMasked = timeImage.updateMask(image.mask().select(0))
  return image.addBands(timeImageMasked)
})

// 指定时间窗口
// 设置改窗口以便我们在该时间段包含无云影像
var days = 30
var millis = ee.Number(days).multiply(1000*60*60*24)

var maxDiffFilter = ee.Filter.maxDifference({
  difference: millis,
  leftField: 'system:time_start',
  rightField: 'system:time_start'
})

var lessEqFilter = ee.Filter.lessThanOrEquals({
  leftField: 'system:time_start',
  rightField: 'system:time_start'
})


var greaterEqFilter = ee.Filter.greaterThanOrEquals({
  leftField: 'system:time_start',
  rightField: 'system:time_start'
})


var filter1 = ee.Filter.and(maxDiffFilter, lessEqFilter)
var join1 = ee.Join.saveAll({
  matchesKey: 'after',
  ordering: 'system:time_start',
  ascending: false})
  
var join1Result = join1.apply({
  primary: mergedCol,
  secondary: mergedCol,
  condition: filter1
})

var filter2 = ee.Filter.and(maxDiffFilter, greaterEqFilter)

var join2 = ee.Join.saveAll({
  matchesKey: 'before',
  ordering: 'system:time_start',
  ascending: true})
  
var join2Result = join2.apply({
  primary: join1Result,
  secondary: join1Result,
  condition: filter2
})

Ⅳ应用线性插值方法填充每个图像


var filtered = join2Result.filter(ee.Filter.eq('type', 'interpolated'))

// 设置插值函数
function interpolateImages(image) {
  var image = ee.Image(image)

  var beforeImages = ee.List(image.get('before'))
  var beforeMosaic = ee.ImageCollection.fromImages(beforeImages).mosaic()
  var afterImages = ee.List(image.get('after'))
  var afterMosaic = ee.ImageCollection.fromImages(afterImages).mosaic()

  var t1 = beforeMosaic.select('timestamp').rename('t1')
  var t2 = afterMosaic.select('timestamp').rename('t2')

  var t = image.metadata('system:time_start').rename('t')

  var timeImage = ee.Image.cat([t1, t2, t])

  var timeRatio = timeImage.expression('(t - t1) / (t2 - t1)', {
    't': timeImage.select('t'),
    't1': timeImage.select('t1'),
    't2': timeImage.select('t2'),
  })

  var interpolated = beforeMosaic
    .add((afterMosaic.subtract(beforeMosaic).multiply(timeRatio)))
  var result = image.unmask(interpolated)
  return result.copyProperties(image, ['system:time_start'])
}

var interpolatedCol = ee.ImageCollection(
  filtered.map(interpolateImages)).select('ndvi')
print('Interpolated Collection', interpolatedCol)

 

Ⅴ应用S-G滤波方法

var oeel=require('users/OEEL/lib:loadAll');

// Use the same maxDiffFilter we used earlier
var maxDiffFilter = ee.Filter.maxDifference({
  difference: millis,
  leftField: 'system:time_start',
  rightField: 'system:time_start'
})

// Use the default distanceFunction
var distanceFunction = function(infromedImage, estimationImage) {
  return ee.Image.constant(
      ee.Number(infromedImage.get('system:time_start'))
      .subtract(
        ee.Number(estimationImage.get('system:time_start')))
        );
  }

// Apply smoothing of order=5
var order = 5;
var smoothed = oeel.ImageCollection.SavatskyGolayFilter(
  interpolatedCol, 
  maxDiffFilter,
  distanceFunction,
  order)

// Select the d_0_ndvi band and rename it
var smoothed = smoothed.select(['d_0_ndvi'], ['smoothed'])

Ⅵ生成可视化图表


// 对指定地点生成可视化图表
var title = 'Savitsky-Golay smoothing' +
  '(order = '+ order + ', window_size = ' + days + ')'

// 生成原时间序列和平滑后的时间序列图表
var chart = ui.Chart.image.series({
  imageCollection: ndviCol.merge(smoothed),
  region: geometry,
  reducer: ee.Reducer.mean(),
  scale: 10
}).setOptions({
      lineWidth: 1,
      title: title,
      interpolateNulls: true,
      vAxis: {title: 'NDVI'},
      hAxis: {title: '', format: 'YYYY-MMM'},
      series: {
        0: {color: 'blue', lineWidth: 1, 
          lineDashStyle: [1, 1], pointSize: 1,
          }, // Original NDVI
        1: {color: 'red', lineWidth: 2 }, // Smoothed NDVI
      },

    })
print(chart)

好了,经过上面的一通操作,我们得到了NDVI数据的S-G滤波结果,基本保留了原时间序列数据的形状、趋势,将高低频噪声进行有效平滑后,我们更能看出NDVI时间序列的变化趋势和大致的周期,是不是很有用呢。 

最后,节目最后,发发疯:

 

很难嘛,难也要继续学啊!明天还是一条好汉!加油加油!

好了,今天的分享到这里就结束了,更多内容欢迎同步关注小编的公众号——梧桐GIS,咱们下期再会啦!!

;