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,还是进行云处理、温度计算、指数转换,本质上都可以沿用相同的思路。

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

相关推荐
大文说跨境1 小时前
亚马逊多账号管理浏览器怎么选?云登/紫鸟/AdsPower 对比
java·前端·自动化
CoderYanger1 小时前
Java EE 进阶:4.Spring Web MVC 入门(第一弹)
java·开发语言·前端·spring·面试·java-ee·mvc
顽疲1 小时前
java springboot vue 养老系统源码详解:请假 care_leave 销假与床位停费日扣实践
java·vue.js·spring boot
XIAEE2 小时前
一个即时通讯项目是怎么长出来的:(Spring Boot 3 + Vue 3 微前端 + 悟空 IM + LiveKit)
java·spring
n87652 小时前
Playwright Java WEB UI 自动化实现框架与逻辑
java·前端·ui
Java_AI工程师2 小时前
Spring4 / SpringBoot1.x 老项目想接入 AI?不用升级框架!3 套无痛落地方案
java·人工智能·spring boot
BBmmo2 小时前
Java Stream 的 filter 为什么看起来没运行?
java
VimorphTech2 小时前
Java 文件缓存实战:如何避免读到未下载完成的视频?
java·spring boot
白露与泡影2 小时前
后端 Java 面试核心八股汇总|高频面试题 + 解析,覆盖基础到进阶
java·开发语言·面试