GEE实战02期:从卫星波段开始:利用 Sentinel-2 自己计算 NDVI

上一期,我们已经了解了NDVI指数的基本含义,学习了其如何服务于植被覆盖监测,并学习了绘制NDVI地图的基本方法。但实际遥感研究中,我们经常需要从卫星原始波段出发,自行计算植被指数。与上一期相比,本期最大的变化不是换了一套数据,而是开始学习一个非常重要的 GEE 思维:定义一个处理函数,并通过循环遍历方法应用到 ImageCollection 中的每一景影像。

GEE实战01期:制作北京及周边地区夏季 NDVI 分布图-CSDN博客

**本期关注:**如何利用 Sentinel-2 地表反射率数据,经过影像筛选、云掩膜和逐景 NDVI 计算,得到北京及周边地区 2024 年夏季的高分辨率 NDVI 分布?

**核心流程:**调用影像数据,进行影像云掩膜处理,根据近红外波段和红光波段计算NDVI指数,绘制地图并进行导出。

1.数据准备

本期选用Sentinel-2的Level-2A 地表反射率数据:

哨兵数据的特点就是空间分辨率比较高 ,10m左右,比上一期我们用到的分辨率高了很多,可以看到更加细致的城市、农田和山地植被空间结构。它的重访周期是5天,我们需要关注B8-近红外波段和B4-红光波段,后续计算需要用到。

在上一期我们已经知道计算NDVI的公式是:

换到Sentinel-2的影像后,我们的公式转换为:

此外,我们还需要关注Q~系列的质量控制波段,因为遥感影像并不是每一景都很好,有时候会受到云的影像,故需要进行一定的筛选,将质量不好的影像剔除出去。

需要注意的是,该产品包含多个光谱波段,并以 10000 为比例因子保存地表反射率,因此后续需要有一个比例因子恢复的过程。

2. 研究步骤

(1)定义研究区

首先仍然使用北京及周边地区作为研究区域,定义过程的第一期的是一样的。所以,GEE 学习并不是每一期都重新开始,而是不断把之前学过的操作组合起来。

javascript 复制代码
// 1. 定义研究区
// 1. 研究区
var roi = ee.Geometry.Rectangle([115.7, 39.4, 117.4, 41.1]);
Map.centerObject(roi, 8);

// 显示研究区域
Map.addLayer(
  roi, {color: 'red'},'Study Area', false
);

定义的研究区域如下:

(2)读取Sentinel-2影像

首先借助ee.ImageCollection()来访问特定的影像,数据产品的代码调用可以从GEE的官方数据集介绍中找到。一般的基本流程是,调用-根据研究区筛选-根据时间筛选-根据云覆盖分数筛选。

javascript 复制代码
// 2. Sentinel-2影像筛选
var s2 = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
  .filterBounds(roi)
  .filterDate('2024-06-01', '2024-09-01')
  .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 20));

print('Image count:', s2.size());

这些筛选过程分为:

**根据研究区筛选:filterBounds,**只保留覆盖研究区的影像;

**根据时间筛选:filterDate,**获取 2024 年夏季的影像;

**根据筛选器筛选:filter,**只保留整景云覆盖比例低于 20% 的影像。

注意,使用CLOUDY_PIXEL_PERCENTAGE进行云初筛**,**注意是在筛影像,而不是去云。比如一景影像云量为 15%,它仍然有一部分像元被云覆盖,只是整景云量还没有高到需要直接丢弃。

这些关键的属性,后面还需要做逐像元云掩膜。从这里可以发现,2024年夏季一共有91景影像符合我们的筛选标准,这91景影像是我们后续分析的基础。

(3)利用 QA60 做逐像元云掩膜

这里的QA60是 Sentinel-2 自带的质量控制波段。

javascript 复制代码
// 3. QA60云掩膜
function maskS2clouds(image) {
  var qa = image.select('QA60');
  var cloud = 1 << 10;
  var cirrus = 1 << 11;
  var mask = qa.bitwiseAnd(cloud).eq(0)
    .and(qa.bitwiseAnd(cirrus).eq(0));
  return image.updateMask(mask);
}

我们来看官网对Q60的介绍,我们主要使用其中两个标志:Bit10代表不透明云,Bit11代表卷云,当这两个标志都为 0 时,就保留这个像元。image.updateMask(mask)表示的是把云像元遮掉,只让满足条件的晴空像元参与后续计算。

具体的处理原理大家可以看如下的示意图了解标记过程:

相信这个原理大家应该比较清楚了。

(4)计算NDVI数据

此处定义一个函数用于计算NDVI,定义函数的目的在于方便后续批量处理,因整个夏季时段有很多景Sentinel-2影像。具体而言,对于每一景影像,我们使用B8和B4波段进行归一化计算,并将得到的波段结果命名为NDVI,再添加到影像的波段里面中。

javascript 复制代码
// 4. 计算NDVI
function addNDVI(image) {
  var ndvi = image
    .normalizedDifference(['B8', 'B4'])
    .rename('NDVI');
  return image.addBands(ndvi);
}

此处要注意B8和B4的顺序不能写反了,否则计算符号会相反。

使用addBands的目的在于不删除 Sentinel-2 原有波段,而是在原影像中增加一个 NDVI 波段。

(5)批量处理所有影像

此处使用了map,进行循环遍历。

javascript 复制代码
// 5. 批量处理所有影像
var processed = s2
  .map(maskS2clouds)
  .map(addNDVI);

我们此前定义的函数,只能处理一景影像,而S2是一个影像集合,一般使用map,表示对S2中的每一景影像都采用指定函数的操作,直到所有影像都处理完。你可以理解如下:

Image 1 → maskS2clouds()

Image 2 → maskS2clouds()

Image 3 → maskS2clouds()

......

Image N → maskS2clouds()

在GEE中,当我们要进行某项操作的时候,一般采用先写好处理单景影像,再进行循环遍历,处理所有影像的思路。

本次实验中,我们使用map,先批量去云,再批量计算每一景影像的NDVI。

(6)夏季NDVI中位数合成

经过上述操作,我们得到了2024年夏季该区域所有符合筛选规则下并去云后的影像的NDVI,但是还是有好多景影像,那么,究竟哪一景影像能够代表北京市周边区域NDVI的实际情况呢?

javascript 复制代码
// 6. 夏季NDVI中位数合成
var summerNDVI = processed
  .select('NDVI')
  .median()
  .clip(roi);

一般使用median,对同一个位置的所有有效观测值取中位数。上述代码的含义便是选择NDVI波段,进行median处理,并裁剪到研究区。注意,中位数指的是对多时相影像的同一个像元取中位数,用以代表观测,不是指的取这些影像的中位影像,之前我也有误解。下面这张图展示了具体的计算原理,相信你一看就知道。

有读者可能会问:为什么使用中位数,不用均值?

实际遥感影像中,即使经过云处理,也可能残留一些异常值、薄云或其他干扰,mean容易受到极端值干扰,而中位数通常对于少数异常值更加稳健,多时相遥感影像中经常使用中位数合成得到一幅代表性影像。

(7)观看真彩色影像

在显示NDVI之前,我们可以先展示一下影像,一般采用红,绿和蓝波段组合,正好对应人眼看到的 RGB 真彩色组合。

javascript 复制代码
// 7. 真彩色影像
var rgb = s2
  .map(maskS2clouds)
  .median()
  .clip(roi);

Map.addLayer(
  rgb,
  {bands: ['B4', 'B3', 'B2'], min: 0, max: 3000},
  'Sentinel-2 RGB'
);

这里同样采用了median思路,展示夏季的代表影像。这一步主要是为了方便对比:

城市建成区和山地 NDVI 有什么差异?

山区是不是形成连续的高 NDVI 区域?

农田、城区、水体在 NDVI 图中有什么不同?

通过来回切换影像,对比原始影像和NDVI,能够得到上述图。

(8)显示计算的NDVI

这里我们定义了NDVI的最大最小值,colorbar,要显示的波段是我们之前计算得到的SummerNdvi影像,并将其命名为Summer NDVI 2024。

javascript 复制代码
// 8. NDVI显示
var ndviVis = {
  min: 0,
  max: 0.8,
  palette: [
    'a6611a',
    'dfc27d',
    'f5f5b5',
    'a6d96a',
    '1a9850',
    '006837'
  ]
};
Map.addLayer(
  summerNDVI,
  ndviVis,
  'Summer NDVI 2024'
);

配色的显示原理:

最终得到的计算结果如下:

(9)导出数据

导出数据的代码比较固定,基本包含:

image:要导出的影像名称;description:描述影像;folder:文件夹;

filenamePrefix:文件名称;region:导出的区域;scale:影像空间分辨率;

maxPixels:最大像素处理量;fileFormat:文件格式;

javascript 复制代码
// 9. 导出GeoTIFF
Export.image.toDrive({
  image: summerNDVI,
  description: 'Beijing_S2_NDVI_2024',
  folder: 'GEE_Export',
  fileNamePrefix: 'Beijing_S2_NDVI_2024',
  region: roi,
  scale: 10,
  maxPixels: 1e10,
  fileFormat: 'GeoTIFF'
});

(10)完整代码

本期完整代码如下:

javascript 复制代码
// ========================================
// GEE实战入门 EP02
// Sentinel-2 夏季NDVI
// 核心:function + map()
// ========================================

// 1. 研究区
var roi = ee.Geometry.Rectangle([115.7, 39.4, 117.4, 41.1]);
Map.centerObject(roi, 8);

// 2. Sentinel-2影像筛选
var s2 = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
  .filterBounds(roi)
  .filterDate('2024-06-01', '2024-09-01')
  .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 20));

print('Image count:', s2.size());


// 3. QA60云掩膜
function maskS2clouds(image) {
  var qa = image.select('QA60');
  var cloud = 1 << 10;
  var cirrus = 1 << 11;

  var mask = qa.bitwiseAnd(cloud).eq(0)
    .and(qa.bitwiseAnd(cirrus).eq(0));

  return image.updateMask(mask);
}


// 4. 计算NDVI
function addNDVI(image) {
  var ndvi = image
    .normalizedDifference(['B8', 'B4'])
    .rename('NDVI');

  return image.addBands(ndvi);
}


// 5. 批量处理所有影像
var processed = s2
  .map(maskS2clouds)
  .map(addNDVI);


// 6. 夏季NDVI中位数合成
var summerNDVI = processed
  .select('NDVI')
  .median()
  .clip(roi);


// 7. 真彩色影像
var rgb = s2
  .map(maskS2clouds)
  .median()
  .clip(roi);

Map.addLayer(
  rgb,
  {bands: ['B4', 'B3', 'B2'], min: 0, max: 3000},
  'Sentinel-2 RGB'
);


// 8. NDVI显示
var ndviVis = {
  min: 0,
  max: 0.8,
  palette: [
    'a6611a',
    'dfc27d',
    'f5f5b5',
    'a6d96a',
    '1a9850',
    '006837'
  ]
};

Map.addLayer(
  summerNDVI,
  ndviVis,
  'Summer NDVI 2024'
);


// 9. 导出GeoTIFF
Export.image.toDrive({
  image: summerNDVI,
  description: 'Beijing_S2_NDVI_2024',
  folder: 'GEE_Export',
  fileNamePrefix: 'Beijing_S2_NDVI_2024',
  region: roi,
  scale: 10,
  maxPixels: 1e10,
  fileFormat: 'GeoTIFF'
});

这一期,我们没有继续直接调用现成的 NDVI 产品,而是从 Sentinel-2 的红光和近红外波段出发,自己完成了云处理、NDVI 计算和多时相合成。

相比最终得到一张 NDVI 地图,更重要的是理解了 GEE 中一个非常核心的处理模式:

定义函数,function(image),map() 批量处理整个ImageCollection。

理解这一点以后,后续无论是计算 NDVI、NDWI、NDBI,还是进行云处理、温度计算、指数转换,本质上都可以沿用相同的思路。

好了,本期内容暂时分享到这里,创作不易,如果对你有帮助,欢迎关注,支持,转发!

相关推荐
学长毕设11 小时前
基于SpringBoot的野生动物公益保护平台设计与实现-附源码
java·spring boot·后端
SL_staff11 小时前
离散制造排程破局:JVS-APS 四步闭环落地实践(开发者视角)
java·产品·全栈
这个昵称不容易11 小时前
SpringBoot 美容院管理系统:232个Java文件315个接口,5套角色从预约到发货一整条链路
java·springboot·毕设
装不满的克莱因瓶12 小时前
【SpringAI Alibaba】ReactAgent记忆管理MessageTrimmingHook与MessageDeletionHook
java·人工智能·ai·ai编程·springai·springaialibaba
文丑颜不良啊12 小时前
LeetCode2241~2260题目解析
java·leetcode
文丑颜不良啊12 小时前
LeetCode2321~2340题目解析
java·leetcode
驭渊的小故事13 小时前
Spring AOP 核心概念与通知类型详解
java·后端·spring
vipxieliang13 小时前
ValidX vs 手写验证代码:开发效率对比实测
java·spring boot·后端
vipxieliang13 小时前
虚拟线程与 GraalVM:Spring Boot 3.x 云原生性能跃迁
java·spring boot·后端
Rain的Java大神之路13 小时前
面试官最烦的答案:“我用 Redis + Token”!接口幂等 8 种方案 + 四层防御,把重复扣款工单干到 0
java·后端·面试