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,大家看懂了教教我~~~):
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,咱们下期再会啦!!