2025年5月27日

利用 Google Earth Engine 查看 NDVI 歷史變化

 最近教到群集的消長變化時,突然想到可以用Google Earth Engine來做NDVI的分析。

以前做這個東西,真的花很多時間,但是現在AI協作,幫忙在GGE平台上寫程式,一下就可以做出來了。

這是程式連結

NDVI(Normalized Difference Vegetation Index,正規化植生指數)是最常被用來評估綠意程度的指標之一。

NDVI 是由衛星量測紅光(Red)與近紅外光(NIR)反射率計算而來的簡單比值,用來表示地表上綠色植物的生長情況。

它是衡量植物生長狀況的指標,根據植被對近紅外光與紅光的反射特性計算而得。指數值範圍從 -1 到 1,數值越高代表綠意越旺盛,常用來分析森林覆蓋變化、農作物生長週期,甚至是氣候變遷影響。


NDVI = (NIR - RED) / (NIR + RED)

NDVI 越高,代表綠色植物越茂盛。

一般數值範圍是 -1 到 1(實際上通常落在 0 ~ 0.8)。

例如:

NDVI ≈ 0.6:森林或健康農作物

NDVI ≈ 0.2:草地或乾燥灌木

NDVI ≈ 0.0 或負值:裸土、城市、雲、積雪或水體

這段程式使用的衛星資料來自MODIS。MODIS 是美國 NASA Terra 與 Aqua 衛星搭載的儀器,從 2000 年起每天環繞地球,觀察大氣、海洋與陸地變化。MOD13Q1 是 MODIS 的 NDVI 資料產品,每 16 天提供一次雲遮減少、經過處理的高品質 NDVI 值,解析度約為 250 公尺。


在這程式上使用者只需要:

  1. 在地圖上圈選感興趣的區域(例如一塊山坡地或農田)
  2. 選擇想查看的時間範圍(例如 2015 到 2024 年)
  3. 選擇時間(年、季、月,依照需求)


系統會自動下載 MODIS NDVI 影像、進行正規化、計算區域平均值,並畫出時間變化圖表。還可以透過滑桿查看每一張影像的空間分布。

可以做哪些應用?

森林變化監測:觀察保護區十年來 NDVI 是否穩定?是否有非法砍伐跡象?
農業分析:農田的 NDVI 是否逐年穩定?是否有氣候影響?
都市擴張:城市邊緣 NDVI 是否逐年下降,代表綠地消失?
生態恢復追蹤:林火或颱風後,NDVI 是否逐年回升?


下圖就是十八尖山區域的NDVI變化


以下是操作的影片說明



--
程式內容

// ===[ 1. 使用者設定區塊 ]===
var config = {
  startYear: 2000,
  endYear: 2024,
  scale: 250, // MODIS 的空間解析度
  timeScale: 'yearly' // 可選:'yearly', 'quarterly', 'monthly', 'original'
};

// ===[ 2. 幾何區域:使用你畫的範圍 ]===
Map.centerObject(geometry, 12);  // geometry 為互動選取區域
Map.addLayer(geometry, {color: 'red'}, '分析範圍');

// ===[ 3. 載入 MODIS NDVI 並正規化 ]===
var modis = ee.ImageCollection("MODIS/006/MOD13Q1")
  .filterBounds(geometry)
  .filterDate(config.startYear + '-01-01', config.endYear + '-12-31')
  .select('NDVI');

var scaled = modis.map(function(img) {
  return img.multiply(0.0001)
            .copyProperties(img, ['system:time_start']);
});

// ===[ 4. 時間尺度函式 ]===
var timeReduced;
if (config.timeScale === 'yearly') {
  var years = ee.List.sequence(config.startYear, config.endYear);
  timeReduced = ee.ImageCollection.fromImages(
    years.map(function(y) {
      var start = ee.Date.fromYMD(y, 1, 1);
      var end = ee.Date.fromYMD(y, 12, 31);
      return scaled.filterDate(start, end).mean()
        .set('system:time_start', start.millis())
        .set('label', y.toString());
    })
  );
} else if (config.timeScale === 'quarterly') {
  var quarters = ee.List.sequence(0, ee.Number(config.endYear).subtract(config.startYear).multiply(4).subtract(1));
  timeReduced = ee.ImageCollection.fromImages(
    quarters.map(function(q) {
      var start = ee.Date(config.startYear + '-01-01').advance(q, 'quarter');
      var end = start.advance(1, 'quarter');
      return scaled.filterDate(start, end).mean()
        .set('system:time_start', start.millis())
        .set('label', start.format('YYYY Q'));
    })
  );
} else if (config.timeScale === 'monthly') {
  var months = ee.List.sequence(0, ee.Date(config.endYear + '-12-01').difference(ee.Date(config.startYear + '-01-01'), 'month').subtract(1));
  timeReduced = ee.ImageCollection.fromImages(
    months.map(function(m) {
      var start = ee.Date(config.startYear + '-01-01').advance(m, 'month');
      var end = start.advance(1, 'month');
      return scaled.filterDate(start, end).mean()
        .set('system:time_start', start.millis())
        .set('label', start.format('YYYY-MM'));
    })
  );
} else if (config.timeScale === 'original') {
  timeReduced = scaled;
} else {
  throw new Error('Unsupported timeScale: ' + config.timeScale);
}

// ===[ 5. 繪製圖表 ]===
var chart = ui.Chart.image.series({
  imageCollection: timeReduced.select('NDVI'),
  region: geometry,
  reducer: ee.Reducer.mean(),
  scale: config.scale,
  xProperty: 'system:time_start'
}).setOptions({
  title: 'NDVI 時間序列變化(' + config.timeScale + ')',
  hAxis: {title: '時間'},
  vAxis: {title: 'NDVI(正規化)'},
  lineWidth: 2,
  pointSize: 3
});
print(chart);

// ===[ 6. 顯示最新 NDVI 圖層 ]===
var latest = timeReduced.sort('system:time_start', false).first();
Map.addLayer(latest, {min: 0, max: 1, palette: ['white', 'green']}, '最新 NDVI (' + config.timeScale + ')');
// ===[ 7. 滑桿與時間標籤 UI ]===
var timeLabels = timeReduced.aggregate_array('label').getInfo();

// 建立時間標籤
var dateLabel = ui.Label('日期:' + timeLabels[timeLabels.length - 1]);

// 建立滑桿控制時間點
var slider = ui.Slider({
  min: 0,
  max: timeLabels.length - 1,
  step: 1,
  value: timeLabels.length - 1,
  style: {width: '400px'}
});

// 初始圖層
var currentImage = ee.Image(timeReduced.toList(timeReduced.size()).get(timeLabels.length - 1));
var currentLayer = ui.Map.Layer(currentImage, {min:0, max:1, palette:['white','green']}, 'NDVI ' + timeLabels[timeLabels.length - 1]);
Map.layers().set(0, currentLayer);

// 當滑桿變動時更新圖層與時間標籤
slider.onChange(function(index) {
  var label = timeLabels[index];
  dateLabel.setValue('日期:' + label);
  var img = ee.Image(timeReduced.toList(timeReduced.size()).get(index));
  Map.layers().set(0, ui.Map.Layer(img, {min:0, max:1, palette:['white','green']}, 'NDVI ' + label));
});

// 加入 UI 元件(先建立一個 panel)
var panel = ui.Panel({
  widgets: [dateLabel, slider],
  layout: ui.Panel.Layout.flow('vertical'),
  style: {position: 'bottom-left'}
});
Map.add(panel);