I wrote some time ago two posts about raster classification and how to colorize a raster using GDAL. Both examples work well, but they are terribly inefficient and won't process a really big raster.
In this post, I'll show how to work properly with rasters, specially with big rasters or when the processing time matters, such as on the fly raster generation processes.
Before continuing, I recommend to read this course about Geoprocessing by Chris Garrard, that helped me a lot.
As usual, you can find all the code at GitHub
The results for this file are:
to test the time I ahve just run the script with using time:
time python classification_original.py
So the biggest improvement comes from using NumPy arrays instead of reading the file pixel by pixel. Then, using blocks instead of reading the whole array, also improves the performance more than a 40%, since the file is quite big (69MB)
Using the maximum and minimum can change the efficiency depending of the raster range.
But to see the importance of using blocks, I have merged four images from the same source, using:
gdal_merge.py -o big.tiff srtm*.tif
So the resulting GeoTIFF is 275MB, too big for my old computer to read it at once. The results are:
And using all the improvements, the time is still less than the half it lasted the original script to process a raster 75% smaller!
Next post, I will explain how to colour the rasters using color tables or SciPy to generate PNG ths fast way.
In this post, I'll show how to work properly with rasters, specially with big rasters or when the processing time matters, such as on the fly raster generation processes.
Before continuing, I recommend to read this course about Geoprocessing by Chris Garrard, that helped me a lot.
![]() |
| The classified raster seen on QGIS |
The original code
To learn about the major errors in my former posts, the best is to look at the code:
#! /usr/bin/python
#Change the value with your raster filename here
raster_file = 'w001001.adf'
output_file = 'classified.tiff'
classification_values = [0,500,1000,1500,2000,2500,3000,3500,4000] #The interval values to classify
classification_output_values = [10,20,30,40,50,60,70,80,90] #The value assigned to each interval
from osgeo import gdal
from osgeo.gdalconst import *
import numpy
import struct
#Opening the raster file
dataset = gdal.Open(raster_file, GA_ReadOnly )
band = dataset.GetRasterBand(1)
#Reading the raster properties
projectionfrom = dataset.GetProjection()
geotransform = dataset.GetGeoTransform()
xsize = band.XSize
ysize = band.YSize
datatype = band.DataType
#Reading the raster values
values = band.ReadRaster( 0, 0, xsize, ysize, xsize, ysize, datatype )
#Conversion between GDAL types and python pack types (Can't use complex integer or float!!)
data_types ={'Byte':'B','UInt16':'H','Int16':'h','UInt32':'I','Int32':'i','Float32':'f','Float64':'d'}
values = struct.unpack(data_types[gdal.GetDataTypeName(band.DataType)]*xsize*ysize,values)
#Now that the raster is into an array, let's classify it
out_str = ''
for value in values:
index = 0
for cl_value in classification_values:
if value <= cl_value:
out_str = out_str + struct.pack('B',classification_output_values[index])
break
index = index + 1
#Once classified, write the output raster
#In the example, it's not possible to use the same output format than the input file, because GDAL is not able to write this file format. Geotiff will be used instead
gtiff = gdal.GetDriverByName('GTiff')
output_dataset = gtiff.Create(output_file, xsize, ysize, 4)
output_dataset.SetProjection(projectionfrom)
output_dataset.SetGeoTransform(geotransform)
output_dataset.GetRasterBand(1).WriteRaster( 0, 0, xsize, ysize, out_str )
output_dataset = None
- Reading the raster without using the ReadAsArray function (lines 26 to 29). Is more inefficient and really more complicated.
- Iterating pixel by pixel to calculate the classified values instead of doing it at once using the NumPy arrays capabilities.
- Classifying all the possible values, instead of looking for the maximum and minimum values of the raster and classify it only in this interval
- Not reading the raster by block size, which is much more efficient in big rasters, and indispensable for the really big ones.
New code
The following code has all the changes together, but at the GitHub you can find different versions applying them one by one:import numpy
from numpy import zeros
from numpy import logical_and
from osgeo import gdal
classification_values = [0,500,1000,1500,2000,2500,3000,3500,4000,4500,5000,5500,6000,6500,7000,7500,8000] #The interval values to classify
classification_output_values = [10,20,30,40,50,60,70,80,90,100,110,120,130,140,150,160,170] #The value assigned to each interval
in_file = "YourInputFileHere.tif"
ds = gdal.Open(in_file)
band = ds.GetRasterBand(1)
block_sizes = band.GetBlockSize()
x_block_size = block_sizes[0]
y_block_size = block_sizes[1]
xsize = band.XSize
ysize = band.YSize
max_value = band.GetMaximum()
min_value = band.GetMinimum()
if max_value == None or min_value == None:
stats = band.GetStatistics(0, 1)
max_value = stats[1]
min_value = stats[0]
format = "GTiff"
driver = gdal.GetDriverByName( format )
dst_ds = driver.Create("original_blocks.tif", xsize, ysize, 1, gdal.GDT_Byte )
dst_ds.SetGeoTransform(ds.GetGeoTransform())
dst_ds.SetProjection(ds.GetProjection())
for i in range(0, ysize, y_block_size):
if i + y_block_size < ysize:
rows = y_block_size
else:
rows = ysize - i
for j in range(0, xsize, x_block_size):
if j + x_block_size < xsize:
cols = x_block_size
else:
cols = xsize - j
data = band.ReadAsArray(j, i, cols, rows)
r = zeros((rows, cols), numpy.uint8)
for k in range(len(classification_values) - 1):
if classification_values[k] <= max_value and (classification_values[k + 1] > min_value ):
r = r + classification_output_values[k] * logical_and(data >= classification_values[k], data < classification_values[k + 1])
if classification_values[k + 1] < max_value:
r = r + classification_output_values[k+1] * (data >= classification_values[k + 1])
dst_ds.GetRasterBand(1).WriteArray(r,j,i)
dst_ds = None
- At line 46, you can see how ReadAsArray works. No conversion to NumPy array has to be done, and no types are needed to be specified. The example works using blocks, but you can read the whole array just by changing the parameters to ReadAsArray (0, 0, xsize, ysize).
- This is the main change.
- At line 47, an empty array is created, specifying that the output will be an array of unsigned bytes (the most efficient for classifications up to 256 values)
- For every possible value in the classification (line 49), the input raster is evaluated, using the NumPy function logical_and. This creates an array with a boolean value set to true for each pixel that meets the condition. Then, the array is multiplied to the output classification value and added to the output array.
When this is done for each value in the scale, the output classified raster is get. - To see better how to do it, take a look at the file classification_numpy_arrays.py, since working by blocks makes things a bit more difficult to understand.
- At lines 21 to 27, the maximum and minimum values are calculated. Note the if block. Some formats store these values, and others, like GeoTIFF, don't. But GDAL can calculate them, and stores the values in a file with the extension .aux.xml, so the next time the calculation is not needed.
All this takes a time, but then, at line 50, the if can be done, and the process improves a lot if there are many values to classify out of the image range.
Performance improvements
To see how does the new code affect the performance I needed bigger rasters than at the old example. The example files I have downloaded are from the CGIAR SRTM elevation data. I have chosen a region from the Himalaya (srtm_54_05.tif).The results for this file are:
- classification_original.py: 2m22.047s
- classification_numpy_array.py: 0m32.359s
- classification_blocks.py: 0m17.900s
- classification_blocks_minmax.py: 0m14.223s
to test the time I ahve just run the script with using time:
time python classification_original.py
So the biggest improvement comes from using NumPy arrays instead of reading the file pixel by pixel. Then, using blocks instead of reading the whole array, also improves the performance more than a 40%, since the file is quite big (69MB)
Using the maximum and minimum can change the efficiency depending of the raster range.
But to see the importance of using blocks, I have merged four images from the same source, using:
gdal_merge.py -o big.tiff srtm*.tif
So the resulting GeoTIFF is 275MB, too big for my old computer to read it at once. The results are:
- classification_original.py: 4m41.438s + Memory Error
- classification_numpy_array.py: 1m5.858s + Memory Error
- classification_blocks.py: 1m11.053s
- classification_blocks_minmax.py: 0m50.891s
And using all the improvements, the time is still less than the half it lasted the original script to process a raster 75% smaller!
Next post, I will explain how to colour the rasters using color tables or SciPy to generate PNG ths fast way.





