Global-scale MODIS NDVI time series analysis (with interpolation)

A material for the presentation in FOSS4G-Hokkaido on 1st July 2017.

Copyright © 2017 Naru. Tsutsumida (naru@kais.kyoto-u.ac.jp)

Summary

Using Google Earth Engine, Annually averaged MODIS MOD13Q1 NDVI data (250m spatial resolution, 16 days intervals) at a global scale are summarised during 2001-2016, then timeseries trend is calculated by Mann-kendall analysis.
Cloud masking and interpolation is not applied but code can be shown.

Prerequisite

1. Initial setting


In [1]:
from IPython.display import Image, display, HTML

%matplotlib inline

from pylab import *
import datetime
import math
import time

import ee
ee.Initialize()

2. Functions for MODIS MOD13Q1 NDVI

MODIS QA mask to filter out low quality pixels. several approaches for QA masks can be applied (but used only maskSummaryQA function)


In [2]:
def getQABits(image, start, end, newName):
    #Compute the bits we need to extract.
    p = 0
    for i in range(start,(end+1)):
        p += math.pow(2, i)

    # Return a single band image of the extracted QA bits, giving the band
    # a new name.
    return image.select([0], [newName])\
                  .bitwiseAnd(p)\
                  .rightShift(start)

#A function to mask out cloudy pixels.
def maskClouds(img):
  # Select the QA band.
    QA = img.select('DetailedQA')
  # Get the MOD_LAND_QA bits
    internalCloud = getQABits(QA, 0, 1, 'MOD_LAND_QA')
  # Return an image masking out cloudy areas.
    return img.mask(internalCloud.eq(0))

##originally function for landsat
#https://groups.google.com/forum/#!searchin/google-earth-engine-developers/python$20bitwiseAnd%7Csort:relevance/google-earth-engine-developers/OYuUMjFr0Gg/GGtYWh4CAwAJ
def maskBadData(image):
    invalid = image.select('DetailedQA').bitwiseAnd(0x6).neq(0)
    clean = image.mask(invalid.Not())
    return(clean)

def maskSummaryQA(img):
    QA = img.select('SummaryQA').eq(0)
    best = img.mask(QA)
    return(best)

# function to add system time band
def addTimeBand(image):
    return image.addBands(image.metadata('system:time_start').rename(["time"]))

3. Pre-processing Input Data (MODIS MOD13Q1 NDVI in 2009)

3.1 Input data

in this demonstration the quality assurance (QA) mask is applied. Only the best quality pixel (QA=0) is picked up.


In [3]:
modisNDVI = ee.ImageCollection('MODIS/006/MOD13Q1') \
.select(['NDVI', "SummaryQA"]) \
.filterDate('2001-01-01', '2016-12-31') \
.sort('system:time_start') 

count = modisNDVI.size().getInfo()  ## total number of selected images
print('count:', count)


('count:', 368)

In [4]:
filteredMODIS = modisNDVI \
.map(maskSummaryQA) \
.select('NDVI')

3.2 see detailed information

toList(1,X). X=0,1,... first image as X=0, second as X=1....
check the info of first image


In [5]:
img1 = ee.Image(filteredMODIS.toList(1,0).get(0))
scale = img1.projection().nominalScale().getInfo()
props = img1.getInfo()['properties']
date = props['system:time_start']
system_time = datetime.datetime.fromtimestamp((date / 1000) - 3600) 
date_str = system_time.strftime("%Y_%m_%d")

img1 = img1.set('bands', date_str)

##check metadata
print('scale:', scale)   ##spatial resolution
print('DATE:', date_str) ##first date


('scale:', 231.65635826395828)
('DATE:', '2001_01_01')

3.3 Smoothing data


In [6]:
## This field contains UNIX time in milliseconds.
timeField = 'system:time_start'
join = ee.Join.saveAll('images')

interval = 72 ##72 days

##ee.Filter.maxDifference:  
##Creates a unary or binary filter that passes if the left and right operands, both numbers, are within a given maximum difference. If used as a join condition, this numeric difference is used as a join measure.
diffFilter =  ee.Filter.maxDifference(difference = (1000 * 60 * 60 * 24) * interval,
                                      leftField = timeField, 
                                      rightField = timeField)

NeighborJoin = join.apply(primary = filteredMODIS, 
                               secondary = filteredMODIS, 
                               condition = diffFilter)

def smooth_func(image): 
    collection = ee.ImageCollection.fromImages(image.get('images'))
    return ee.Image(image).addBands(collection.mean().rename(['smooth']))


smoothed = ee.ImageCollection(NeighborJoin.map(smooth_func))

See a map of whole mean NDVI in 2001-2016.
Note that all data is in GEE server, not in this running environment.


In [8]:
regionfilter = ee.Geometry.Polygon([-170, 80, 0, 80, 170, 80, 170, -80, 10, -80, -170, -80]).toGeoJSON()

ndvi_palette = 'FFFFFF, CE7E45, DF923D, F1B555, FCD163, 99B718, 74A901, 66A000, 529400, 3E8601, 207401, 056201, 004C00, 023B01, 012E01, 011D01, 011301'
vizParams = {'min': -2000, 
             'max': 10000, 
             'region':regionfilter,
             'palette': ndvi_palette}

img = smoothed.select('smooth').mean()
%config InlineBackend.figure_format = 'retina'    
print(img.getThumbUrl(vizParams))
Image(url=img.getThumbUrl(vizParams), width=900, unconfined=True)


https://earthengine.googleapis.com/api/thumb?thumbid=a6ea0d5cf576cf1b79e92b9de9bfd54c&token=b9e7905c88b8e2a974f0883ec739042e
Out[8]:

4. Trend analysis of annual average of NDVI in 2001-2016

After annual averaged NDVI datasets in 2001-2016 are calculated. Here Mann-Kendall trend test is applied.
Keep in mind as p-values are not calculated at this time on GEE (1st July 2017),
The statistically significant test is not available.

4.1 parameters settings


In [9]:
#create name list

yr = range(2001, 2017)
yr = map(str, yr)
num=len(range(2001, 2017))
yy = np.array(["Y"]*num)
years = np.core.defchararray.add(yy, yr)

st = np.array(["-01-01"]*num)
ed = np.array(["-12-31"]*num)

starts = np.core.defchararray.add(yr, st)
ends = np.core.defchararray.add(yr, ed)

4.2 Calculate annual NDVI average


In [10]:
y = 0
MODcoll = smoothed \
        .filterDate(starts[y], ends[y]) \
        .sort('system:time_start') \
        .select('smooth')
start = starts[y]
end = ends[y]
avg = MODcoll.mean().rename([years[y]])

for y in range(1, 16):
    MODcoll = smoothed \
        .filterDate(starts[y], ends[y]) \
        .sort('system:time_start') \
        .select('smooth')
    start = starts[y]
    end = ends[y]
    average = MODcoll.mean()
    avg = avg.addBands(average.rename([years[y]]))

In [11]:
info_bands = avg.getInfo()['bands']
#print('Dimensions:', info_bands[0]['dimensions'])
print('Number of bands:', len(info_bands))
##see band names
for ids in range(0,len(info_bands),1):
    print(info_bands[ids]['id'])


('Number of bands:', 16)
Y2001
Y2002
Y2003
Y2004
Y2005
Y2006
Y2007
Y2008
Y2009
Y2010
Y2011
Y2012
Y2013
Y2014
Y2015
Y2016

4.3 Mann-Kendall trend test


In [12]:
mk_ans = avg.reduce(ee.Reducer.kendallsCorrelation(1))

In [13]:
info_bands = mk_ans.getInfo()['bands']
#print('Dimensions:', info_bands[0]['dimensions'])
print('Number of bands:', len(info_bands))

##see bands
for ids in range(0,len(info_bands),1):
    print(info_bands[ids]['id'])


('Number of bands:', 2)
tau
p-value

4.4 Display the results


In [14]:
RdBu_palette = '#B2182B, #D6604D, #F4A582, #FDDBC7, #F7F7F7, #D1E5F0, #92C5DE, #4393C3, #2166AC'

mk_tau = ee.Image(mk_ans.select('tau')).multiply(10000).int16()
url = mk_tau.getThumbUrl({
                'min':-10000,
                'max':10000,
                'region':regionfilter,
                'crs': 'EPSG:4326',
                'palette': RdBu_palette
    })    

%config InlineBackend.figure_format = 'retina'    
print(url)
Image(url=url, width=900, unconfined=True)


https://earthengine.googleapis.com/api/thumb?thumbid=2b272f30266b5c9c6a8bef68538ead58&token=606e039654f13cd805d8259c94f27611
Out[14]:

4.5 Export Geotiff


In [20]:
globalfilter = ee.Geometry.Polygon([-170, 80, 0, 80, 170, 80, 170, -80, 10, -80, -170, -80])
globalfilter = globalfilter['coordinates'][0]

task_config = {
    'description': 'imageToDriveExample',
    'scale': 231.65635826395828,  
    'region': globalfilter,
    'maxPixels': 5e10
    }

task = ee.batch.Export.image(mk_tau, 'kenn_0116', task_config)
task.start()

In [21]:
time.sleep(10)
while task.status()['state'] == 'RUNNING':
    print 'Running...'
    time.sleep(100)
print 'Done.', task.status()


Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Running...
Done. {u'task_type': u'EXPORT_IMAGE', u'description': u'imageToDriveExample', u'output_url': [u'https://drive.google.com/', u'https://drive.google.com/', u'https://drive.google.com/', u'https://drive.google.com/', u'https://drive.google.com/', u'https://drive.google.com/', u'https://drive.google.com/', u'https://drive.google.com/'], u'update_timestamp_ms': 1498473239875, u'creation_timestamp_ms': 1498463963513, u'state': u'COMPLETED', u'start_timestamp_ms': 1498463966758, u'id': u'WSDUAHJQMMDN3S2KG2KSPQX3'}

In [ ]: