After this general introduction to pandas, we come back to the geospatial domain and will talk about GDAL/OGR [1] a bit. GDAL is a raster and vector processing library that has been developed with a strong focus on supporting a large number of file formats, being able to translate between the different formats, and fostering data exchange. As we already mentioned, GDAL and OGR were originally two separate libraries focusing on raster data (GDAL) and vector data (OGR), respectively. These have now been merged and GDAL (‘Geospatial Data Abstraction Library’) is commonly used to refer to the combined library.
GDAL had its initial release in the year 2000 and originally was mainly developed by Frank Warmerdam. But, since version 1.3.2, responsibilities have been handed over to the GDAL/OGR Project Management Committee under the umbrella of the Open Source Geospatial Foundation [2] (OSGeo). GDAL is available under the permissiveX/MIT style free software license and has become one of the major open source GIS libraries, used in many open source GIS software, including QGIS. The GDAL Python package provides a wrapper around the GDAL C++ library that allows for using its functionality in Python. Similar support exists for other languages and it is also possible to use GDAL/OGR commands from the command line of your operating system. The classes and functions of the Python package are documented here [3]. In the following, we show a few examples illustrating common patterns of using the GDAL library with Python.
OGR and GDAL exist as separate modules in the osgeo package together with some other modules such as OSR for dealing with different projections and some auxiliary modules. As with previous packages you will probably need to install gdal (which encapsulates all of them in its latest release) to access these packages. So typically, a Python project using GDAL would import the needed modules similar to this:
1 2 3 | from osgeo import gdal from osgeo import ogr from osgeo import osr |
Let’s start with taking a closer look at the ogr module for working with vector data. The following code, for instance, illustrates how one would open an existing vector data set, in this case an Esri shapefile. OGR uses so-called drivers to access files in different formats, so the important thing to note is how we first obtain a driver object for ‘ESRI Shapefile’ with GetDriverByName(...) and then use that to open the shapefile with the Open(...) method of the driver object. The shapefile we are using in this example is a file with polygons for all countries in the world (available here) [4] and we will use it again in the lesson’s walkthrough. When you download it, you may still have to adapt the path in the first line of the code below.
1 2 3 | shapefile = r 'C:\489\L3\TM_WORLD_BORDERS-0.3.shp' drv = ogr.GetDriverByName( 'ESRI Shapefile' ) dataSet = drv. Open (shapefile) |
dataSet now provides access to the data in the shapefile as layers, in this case just a single layer, that can be accessed with the GetLayer(...) method. We then use the resulting layer object to get the definitions of the fields with GetLayerDefn(), loop through the fields with the help of GetFieldCount() and GetFieldDefn(), and then print out the field names with GetName():
1 2 3 4 | layer = dataSet.GetLayer( 0 ) layerDefinition = layer.GetLayerDefn() for i in range (layerDefinition.GetFieldCount()): print (layerDefinition.GetFieldDefn(i).GetName()) |
Output: FIPS ISO2 ISO3 UN NAME ... LAT
If you want to loop through the features in a layer, e.g., to read or modify their attribute values, you can use a simple for-loop and the GetField(...) method of features. It’s important that, if you want to be able to iterate through the features another time, you have to call the ResetReading() method of the layer after the loop. The following loop prints out the values of the ‘NAME’ field for all features:
1 2 3 | for feature in layer: print (feature.GetField( 'NAME' )) layer.ResetReading() |
Output: Antigua and Barbuda Algeria Azerbaijan Albania ....
We can extend the previous example to also access the geometry of the feature via the GetGeometryRef() method. Here we use this approach to get the centroid of each country polygon (method Centroid()) and then print it out in readable form with the help of the ExportToWkt() method. The output will be the same list of country names as before but this time, each followed by the coordinates of the respective centroid.
1 2 3 | for feature in layer: print (feature.GetField( 'NAME' ) + '–' + feature.GetGeometryRef().Centroid().ExportToWkt()) layer.ResetReading() |
We can also filter vector layers by attribute and spatially. The following example uses SetAttributeFilter(...) to only get features with a population (field ‘POP2005’) larger than one hundred million.
1 2 3 4 | layer.SetAttributeFilter( 'POP2005 > 100000000' ) for feature in layer: print (feature.GetField( 'NAME' )) layer.ResetReading() |
Output: Brazil China India ...
We can remove the selection by calling SetAttributeFilter(...) with the empty string:
1 | layer.SetAttributeFilter('') |
The following example uses SetSpatialFilter(...) to only get countries overlapping a given polygonal area, in this case an area that covers the southern part of Africa. We first create the polygon via the CreateGeometryFromWkt(...) function that creates geometry objects from Well-Known Text (WKT) strings [5]. Then we apply the filter and use a for-loop again to print the selected features:
1 2 3 4 5 6 | wkt = 'POLYGON ( (6.3 -14, 52 -14, 52 -40, 6.3 -40, 6.3 -14))' # WKT polygon string for a rectangular area geom = ogr.CreateGeometryFromWkt(wkt) layer.SetSpatialFilter(geom) for feature in layer: print (feature.GetField( 'NAME' )) layer.ResetReading() |
Output: Angola Madagascar Mozambique ... Zimbabwe
Access to all features and their geometries together with the geometric operations provided by GDAL allows for writing code that realizes geoprocessing operations and other analyses on one or several input files and for creating new output files with the results. To show one example, the following code takes our selection of countries from southern Africa and then creates 2 decimal degree buffers around the centroids of each country. The resulting buffers are written to a new shapefile called centroidbuffers.shp. We add two fields to the newly produced buffered centroids shapefile, one with the name of the country and one with the population, copying over the corresponding field values from the input country file. When you will later have to use GDAL in one of the parts of the lesson's homework assignment, you can use the same order of operations, just with different parameters and input values.
To achieve this task, we first create a spatial reference object for EPSG:4326 that will be needed to create the layer in the new shapefile. Then the shapefile is generated with the shapefile driver object we obtained earlier, and the CreateDataSource(...) method. A new layer inside this shapefile is created via CreateLayer(...) and by providing the spatial reference object as a parameter. We then create the two fields for storing the country names and population numbers with the function FieldDefn(...) and add them to the created layer with the CreateField(...) method using the field objects as parameters. When adding new fields, make sure that the name you provide is not longer than 8 characters or GDAL/OGR will automatically truncate the name. Finally, we need the field definitions of the layer available via GetLayerDefn() to be able to later add new features to the output shapefile. The result is stored in variable featureDefn.
1 2 3 4 5 6 7 8 9 10 11 | sr = osr.SpatialReference() # create spatial reference object sr.ImportFromEPSG( 4326 ) # set it to EPSG:4326 outfile = drv.CreateDataSource(r 'C:\489\L3\centroidbuffers.shp' ) # create new shapefile outlayer = outfile.CreateLayer( 'centroidbuffers' , geom_type = ogr.wkbPolygon, srs = sr) # create new layer in the shapefile nameField = ogr.FieldDefn( 'Name' , ogr.OFTString) # create new field of type string called Name to store the country names outlayer.CreateField(nameField) # add this new field to the output layer popField = ogr.FieldDefn( 'Population' , ogr.OFTInteger) # create new field of type integer called Population to store the population numbers outlayer.CreateField(popField) # add this new field to the output layer featureDefn = outlayer.GetLayerDefn() # get field definitions |
Now that we have prepared the new output shapefile and layer, we can loop through our selected features in the input layer in variable layer, get the geometry of each feature, produce a new geometry by taking its centroid and then calling the Buffer(...) method, and finally create a new feature from the resulting geometry within our output layer.
1 2 3 4 5 6 7 8 9 10 11 12 13 | for feature in layer: # loop through selected features ingeom = feature.GetGeometryRef() # get geometry of feature from the input layer outgeom = ingeom.Centroid(). Buffer ( 2.0 ) # buffer centroid of ingeom outFeature = ogr.Feature(featureDefn) # create a new feature outFeature.SetGeometry(outgeom) # set its geometry to outgeom outFeature.SetField( 'Name' , feature.GetField( 'NAME' )) # set the feature's Name field to the NAME value of the input feature outFeature.SetField( 'Population' , feature.GetField( 'POP2005' )) # set the feature's Population field to the POP2005 value of the input feature outlayer.CreateFeature(outFeature) # finally add the new output feature to outlayer outFeature = None layer.ResetReading() outfile = None # close output file |
The final line “outfile = None” is needed because otherwise the file would remain open for further writing and we couldn’t inspect it in a different program. If you open the centroidbuffers.shp shapefile and the country borders shapefile in a GIS of your choice, the result should look similar to the image below. If you check out the attribute table, you should see the Name and Populations columns we created populated with values copied over from the input features.
Centroid() and Buffer() are just two examples of the many availabe methods for producing a new geometry in GDAL. In this lesson's homework assignment, you will instead have to use the ogr.CreateGeometryFromWkt(...) function that we used earlier in this section to create a clip polygon from a WKT string representation but, apart from that, the order of operations for creating the output feature will be the same. The GDAL/OGR cookbook [6] contains many Python examples for working with vector data with GDAL, including how to create different kinds of geometries [7] from different input formats, calculating envelopes, lengths and areas of a geometry [8], and intersecting and combining geometries [9]. We recommend that you take a bit of time to browse through these online examples to get a better idea of what is possible. Then we move on to have a look at raster manipulation with GDAL.
To open an existing raster file in GDAL, you would use the Open(...) function defined in the gdal module. The raster file we will use in the following examples contains world-wide bioclimatic data and will be used again in the lesson’s walkthrough. Download the raster file here [10].
1 | raster = gdal. Open (r 'c:\489\L3\wc2.0_bio_10m_01.tif' ) |
We now have a GDAL raster dataset in variable raster. Raster datasets are organized into bands. The following command shows that our raster only has a single band:
1 | raster.RasterCount |
Output: 1
To access one of the bands, we can use the GetRasterBand(...) method of a raster dataset and provide the number of the band as a parameter (counting from 1, not from 0!):
1 | band = raster.GetRasterBand( 1 ) # get first band of the raster |
If your raster has multiple bands and you want to perform the same operations for each band, you would typically use a for-loop to go through the bands:
1 2 3 | for i in range ( 1 , raster.RasterCount + 1 ): b = raster.GetRasterBand(i) print (b.GetMetadata()) # or do something else with b |
There are a number of methods to read different properties of a band in addition to GetMetadata() used in the previous example, such as GetNoDataValue(), GetMinimum(), GetMaximum(), andGetScale().
1 2 3 4 | print (band.GetNoDataValue()) print (band.GetMinimum()) print (band.GetMaximum()) print (band.GetScale()) |
Output: -1.7e+308 -53.702073097229 33.260635217031 1.0
GDAL provides a number of operations that can be employed to create new files from bands. For instance, the gdal.Polygonize(...) function can be used to create a vector dataset from a raster band by forming polygons from adjacent cells that have the same value. To apply the function, we first create a new vector dataset and layer in it. Then we add a new field ‘DN’ to the layer for storing the raster values for each of the polygons created:
1 2 3 4 5 | drv = ogr.GetDriverByName( 'ESRI Shapefile' ) outfile = drv.CreateDataSource(r 'c:\489\L3\polygonizedRaster.shp' ) outlayer = outfile.CreateLayer( 'polygonized raster' , srs = None ) newField = ogr.FieldDefn( 'DN' , ogr.OFTReal) outlayer.CreateField(newField) |
Once the shapefile is prepared, we call Polygonize(...) and provide the band and the output layer as parameters plus a few additional parameters needed:
1 2 | gdal.Polygonize(band, None , outlayer, 0 , []) outfile = None |
With the None for the second parameter we say that we don’t want to provide a mask for the operation. The 0 for the fourth parameter is the index of the field to which the raster values shall be written, so the index of the newly added ‘DN’ field in this case. The last parameter allows for passing additional options to the function but we do not make use of this, so we provide an empty list. The second line "outfile = None" is for closing the new shapefile and making sure that all data has been written to it. The result produced in the new shapefile rasterPolygonized.shp should look similar to the image below when looked at in a GIS and using a classified symbology based on the values in the ‘DN’ field.
Polygonize(...) is an example of a GDAL function that operates on an individual band. GDAL also provides functions for manipulating raster files directly, such as gdal.Translate(...) for converting a raster file into a new raster file. Translate(...) is very powerful with many parameters and can be used to clip, resample, and rescale the raster as well as convert the raster into a different file format. You will see an example of Translate(...) being applied in the lesson’s walkthrough. gdal.Warp(...) is another powerful function that can be used for reprojecting and mosaicking raster files.
While the functions mentioned above and similar functions available in GDAL cover many of the standard manipulation and conversion operations commonly used with raster data, there are cases where one directly wants to work with the values in the raster, e.g. by applying raster algebra operations. The approach to do this with GDAL is to first read the data of a band into a GDAL multi-dimensional array object with the ReadAsArray() method, then manipulate the values in the array, and finally write the new values back to the band with the WriteArray() method.
1 2 | data = band.ReadAsArray() data |
If you look at the output of this code, you will see that the array in variable data essentially contains the values of the raster cells organized into rows. We can now apply a simple mathematical expression to each of the cells, like this:
1 2 | data = data * 0.1 data |
The meaning of this expression is to create a new array by multiplying each cell value with 0.1. You should notice the change in the output from +308 to +307. The following expression can be used to change all values that are smaller than 0 to 0:
1 2 3 | print (data. min ()) data [ data < 0 ] = 0 print (data. min ()) |
data.min() in the previous example is used to get the minimum value over all cells and show how this changes to 0 after executing the second line. Similarly to what you saw with pandas data frames in Section 3.8.6, an expression like data < 0 results in a Boolean array with True for only those cells for which the condition <0 is true. Then this Boolean array is used to select only specific cells from the array with data[...] and only these will be changed to 0. Now, to finally write the modified values back to a raster band, we can use the WriteArray(...) method. The following code shows how one can first create a copy of a raster with the same properties as the original raster file and then use the modified data to overwrite the band in this new copy:
1 2 3 4 5 | drv = gdal.GetDriverByName( 'GTiff' ) # create driver for writing geotiff file outRaster = drv.CreateCopy(r 'c:\489\newRaster.tif' , raster , 0 ) # create new copy of inut raster on disk newBand = outRaster.GetRasterBand( 1 ) # get the first (and only) band of the new copy newBand.WriteArray(data) # write array data to this band outRaster = None # write all changes to disk and close file |
This approach will not change the original raster file on disk. Instead of writing the updated array to a band of a new file on disk, we can also work with an in-memory copy instead, e.g. to then use this modified band in other GDAL operations such as Polygonize(...) . An example of this approach will be shown in the walkthrough of this lesson. Here is how you would create the in-memory copy combining the driver creation and raster copying into a single line:
1 | tmpRaster = gdal.GetDriverByName( 'MEM' ).CreateCopy('', raster, 0 ) # create in-memory copy |
The approach of using raster algebra operations shown above can be used to perform many operations such as reclassification and normalization of a raster. More complex operations like neighborhood/zonal based operators can be implemented by looping through the array and adapting cell values based on the values of adjacent cells. In the lesson’s walkthrough you will get to see an example of how a simple reclassification can be realized using expressions similar to what you saw in this section.
While the GDAL Python package allows for realizing the most common vector and raster operations, it is probably fair to say that it is not the most easy-to-use software API. While the GDAL Python cookbook [11] contains many application examples, it can sometimes take a lot of search on the web to figure out some of the details of how to apply a method or function correctly. Of course, GDAL has the main advantage of being completely free, available for pretty much all main operating systems and programming languages, and not tied to any other GIS or web platform. In contrast, the Esri ArcGIS API for Python discussed in the next section may be more modern, directly developed specifically for Python, and have more to offer in terms of visualization and high-level geoprocessing and analysis functions, but it is tied to Esri’s web platforms and some of the features require an organizational account to be used. These are aspects that need to be considered when making a choice on which API to use for a particular project. In addition, the functionality provided by both APIs only overlaps partially and, as a result, there are also merits in combining the APIs as we will do later on in the lesson’s walkthrough.
Links
[1] http://www.gdal.org/
[2] https://www.osgeo.org/
[3] https://gdal.org/api/index.html#python-api
[4] https://www.e-education.psu.edu/geog489/sites/www.e-education.psu.edu.geog489/files/downloads/TM_WORLD_BORDERS-0.3.zip
[5] https://en.wikipedia.org/wiki/Well-known_text
[6] https://pcjericks.github.io/py-gdalogr-cookbook/index.html
[7] https://pcjericks.github.io/py-gdalogr-cookbook/geometry.html#create-a-point
[8] https://pcjericks.github.io/py-gdalogr-cookbook/geometry.html#calculate-envelope-of-a-geometry
[9] https://pcjericks.github.io/py-gdalogr-cookbook/geometry.html#calculate-intersection-between-two-geometries
[10] https://www.e-education.psu.edu/geog489/sites/www.e-education.psu.edu.geog489/files/downloads/worldwide_bioclimatic_data.zip
[11] https://pcjericks.github.io/py-gdalogr-cookbook/