Land Cover Change Analysis with Python and GDAL - Tutorial

LandCoverChange.png

Satellite imagery brought us the capacity to see the land surface on recent years but we haven’t been so successful to understand land cover dynamics and the interaction with economical, sociological and political factors. Some deficiencies for this analysis were found on the use of GIS commercial software, but there are other limitations in the way we apply logical and mathematical processes to a set of satellite imagery. Working with geospatial data on Python gives us the capability to filter, calculate, clip, loop, and export raster or vector datasets with an efficient use of the computational power providing a bigger scope on data analysis.

This tutorial shows the complete procedure to create a land cover change raster from a comparison of generated vegetation index (NDVI) rasters by the use of Python and the Numpy and GDAL libraries. Contours of land cover change where generated with some tools of GDAL and Osgeo and an analysis of deforestation were done based on the output data and historical images from Google Earth.

Tutorial

Python code

This is the Python code for the tutorial:

Import requires libraries

This tutorial uses just Python core libraries, Scipy (Numpy, Matplotlib and others) and GDAL. For Windows users, the most effective way is to download the GDAL Wheel from https://www.lfd.uci.edu/~gohlke/pythonlibs/ and install through pip.

from osgeo import ogr, gdal, osr
import numpy as np
import os
import matplotlib.pyplot as plt

Setting up the input and output files

We declare the path for the raster bands and the output NDVI and land cover change rasters. The land cover change contour shape path is also defined.

#Input Raster and Vector Paths

#Image-2019
path_B5_2019="../Image20190203clip/LC08_L1TP_029047_20190203_20190206_01_T1_B5_clip.TIF"
path_B4_2019="../Image20190203clip/LC08_L1TP_029047_20190203_20190206_01_T1_B4_clip.TIF"

#Image-2014
path_B5_2014="../Image20140205clip/LC08_L1TP_029047_20140205_20170307_01_T1_B5_clip.TIF"
path_B4_2014="../Image20140205clip/LC08_L1TP_029047_20140205_20170307_01_T1_B4_clip.TIF"

#Output Files

#Output NDVI Rasters 
path_NDVI_2019 = '../Output/NDVI2019.tif'
path_NDVI_2014 = '../Output/NDVI2014.tif'
path_NDVIChange_19_14 = '../Output/NDVIChange_19_14.tif'

#NDVI Contours
contours_NDVIChange_19_14 = '../Output/NDVIChange_19_14.shp'

Open the Landsat image bands with GDAL

In this part we open the red (Band 4) and near infrared NIR (Band 5) with commands of the GDAL library and then we read the images as matrix arrays with float numbers of 32 bits.

#Open raster bands
B5_2019 = gdal.Open(path_B5_2019)
B4_2019 = gdal.Open(path_B4_2019)
B5_2014 = gdal.Open(path_B5_2014)
B4_2014 = gdal.Open(path_B4_2014)

#Read bands as matrix arrays
B52019_Data = B5_2019.GetRasterBand(1).ReadAsArray().astype(np.float32)
B42019_Data = B4_2019.GetRasterBand(1).ReadAsArray().astype(np.float32)
B52014_Data = B5_2014.GetRasterBand(1).ReadAsArray().astype(np.float32)
B42014_Data = B4_2014.GetRasterBand(1).ReadAsArray().astype(np.float32)

Compare matrix sizes and geotransformation parameters

The operation in between Landsat 8 bands depends that the resolution, size, src, and geotransformation parameters are the same for both images. In case these caracteristics do not coincide a warp, reproyection, scale or any other geospatial process would be necessary.

print(B5_2014.GetProjection()[:80])
print(B5_2019.GetProjection()[:80])
if B5_2014.GetProjection()[:80]==B5_2019.GetProjection()[:80]: print('SRC OK')

PROJCS["WGS 84 / UTM zone 13N",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84
PROJCS["WGS 84 / UTM zone 13N",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84
SRC OK

print(B52014_Data.shape)
print(B52019_Data.shape)
if B52014_Data.shape==B52019_Data.shape: print('Array Size OK')

(610, 597)
(610, 597)
Array Size OK

print(B5_2014.GetGeoTransform())
print(B5_2019.GetGeoTransform())
if B5_2014.GetGeoTransform()==B5_2019.GetGeoTransform(): print('Geotransformation OK')

(652500.0, 29.98324958123953, 0.0, 2166000.0, 0.0, -30.0)
(652500.0, 29.98324958123953, 0.0, 2166000.0, 0.0, -30.0)
Geotransformation OK

Get geotransformation parameters

Since we have compared that rasters bands have same array size and are in the same position we can get the some spatial parameters

geotransform = B5_2014.GetGeoTransform()

originX,pixelWidth,empty,finalY,empty2,pixelHeight=geotransform
cols =  B5_2014.RasterXSize
rows =  B5_2014.RasterYSize

projection = B5_2014.GetProjection()

finalX = originX + pixelWidth * cols
originY = finalY + pixelHeight * rows

Compute the NDVI and store them as a different file

We can compute the NDVI as a matrix algebra with some Numpy functions. The result can be exported as a raster using GDAL and the transformation parameters. In order to have a more effective and clear code we create a function to export rasters.

ndvi2014 = np.divide(B52014_Data - B42014_Data, B52014_Data+ B42014_Data,where=(B52014_Data - B42014_Data)!=0)
ndvi2014[ndvi2014 == 0] = -999

ndvi2019 = np.divide(B52019_Data - B42019_Data, B52019_Data+ B42019_Data,where=(B52019_Data - B42019_Data)!=0)
ndvi2019[ndvi2019 == 0] = -999

def saveRaster(dataset,datasetPath,cols,rows,projection):
    rasterSet = gdal.GetDriverByName('GTiff').Create(datasetPath, cols, rows,1,gdal.GDT_Float32)
    rasterSet.SetProjection(projection)
    rasterSet.SetGeoTransform(geotransform)
    rasterSet.GetRasterBand(1).WriteArray(dataset)
    rasterSet.GetRasterBand(1).SetNoDataValue(-999)
    rasterSet = None

saveRaster(ndvi2014,path_NDVI_2014,cols,rows,projection)

saveRaster(ndvi2019,path_NDVI_2019,cols,rows,projection)

Plot NDVI Images

We create a function to plot the resulting NDVI images with a colobar

extentArray = [originX,finalX,originY,finalY]
def plotNDVI(ndviImage,extentArray,vmin,cmap):
    ndvi = gdal.Open(ndviImage)
    ds2019 = ndvi.ReadAsArray()
    plt.figure(figsize=(20,15))
    im = plt.imshow(ds2019, vmin=vmin, cmap=cmap, extent=extentArray)#
    plt.colorbar(im, fraction=0.015)
    plt.xlabel('Este')
    plt.ylabel('Norte')
    plt.show()

plotNDVI(path_NDVI_2014,extentArray,0,'YlGn')
output_21_0.png
plotNDVI(path_NDVI_2019,extentArray,0,'YlGn')
output_22_0.png

Create a land cover change image

We create a land cover change image based on the differences in NDVI from 2019 and 2014 imagery. The image will be stored as a separate file.

ndviChange = ndvi2019-ndvi2014
ndviChange = np.where((ndvi2014>-999) & (ndvi2019>-999),ndviChange,-999)
ndviChange

array([[-9.9900000e+02,  3.5470784e-02,  3.7951261e-02, ...,
         3.1590372e-02,  3.8002759e-02,  2.6692629e-02],
       [-9.9900000e+02,  3.7654012e-02,  5.8923483e-02, ...,
        -5.8691800e-03,  1.8922418e-02,  1.9506305e-02],
       [-9.9900000e+02,  3.2184020e-02,  3.7395060e-02, ...,
        -6.3773081e-02, -3.0675709e-02,  3.8942695e-02],
       ...,
       [-9.9900000e+02, -1.6597062e-02,  1.1402190e-02, ...,
         2.7218461e-03,  2.5526762e-02,  6.6639006e-02],
       [-9.9900000e+02,  5.9944689e-03, -4.0770471e-03, ...,
         5.7501763e-02,  4.6757758e-03,  8.4484324e-02],
       [-9.9900000e+02, -9.9900000e+02, -9.9900000e+02, ...,
        -9.9900000e+02, -9.9900000e+02, -9.9900000e+02]], dtype=float32)

saveRaster(ndviChange,path_NDVIChange_19_14,cols,rows,projection)

plotNDVI(path_NDVIChange_19_14,extentArray,-0.5,'Spectral')
output_26_0.png

Create Contourlines

Finally, the countour lines from the NDVI values are generated. This is done with tool from the GDAL library.

Dataset_ndvi = gdal.Open(path_NDVIChange_19_14)#path_NDVI_2014
ndvi_raster = Dataset_ndvi.GetRasterBand(1)

ogr_ds = ogr.GetDriverByName("ESRI Shapefile").CreateDataSource(contours_NDVIChange_19_14)

prj=Dataset_ndvi.GetProjectionRef()#GetProjection()

srs = osr.SpatialReference(wkt=prj)#
#srs.ImportFromProj4(prj)

contour_shp = ogr_ds.CreateLayer('contour', srs)
field_defn = ogr.FieldDefn("ID", ogr.OFTInteger)
contour_shp.CreateField(field_defn)
field_defn = ogr.FieldDefn("ndviChange", ogr.OFTReal)
contour_shp.CreateField(field_defn)
#Generate Contourlines
gdal.ContourGenerate(ndvi_raster, 0.1, 0, [], 1, -999, contour_shp, 0, 1)
ogr_ds = None

Input data

You can download the input files from this link.

Comment

Saul Montoya

Saul Montoya es Ingeniero Civil graduado de la Pontificia Universidad Católica del Perú en Lima con estudios de postgrado en Manejo e Ingeniería de Recursos Hídricos (Programa WAREM) de la Universidad de Stuttgart con mención en Ingeniería de Aguas Subterráneas y Hidroinformática.

Smiley face

Subscribe to our free e-newsletter for tutorials, articles, webminars, courses and more.