This tutorial will guide you through the process of creating a custom pixel-wise processing routine with Geomatica and Python. In this context, pixel-wise processing is considered to be running a routine on an pixel location and returning a result, then move on to the next pixel location and repeat the same routine, until all pixels are processed. For example, in the graphic below, the developer created a function that checks each pixel position in two images for the color of the pixel in each image. If they match, the output pixel is colored purple. Otherwise the pixel is colored yellow.
Many algorithms or workflows need to evaluate the value of a pixel at a specific location against itself or against pixel values from other images or channels at the same location. This tutorial will teach you how to create completely custom routines for processing every pixel in an image one at a time.
- Geomatica 2016 or later installed
- Python 101 (basic understanding of python coding concepts)
- How to access modules, classes, attributes and methods
- How to call a function
- How to use with statements
- How to create if-statements
- Some experience programming with Geomatica (recommended)
- Some experience with numpy (recommended)
- Understanding how to use a decorator (recommended)
- Click Here to download the data required for this tutorial
To demonstrate how to build custom pixel-wise processes, we will expand on the Raster Arithmetic tutorial. Specifically, we will calculate an NDVI (normalized Difference Vegetation Image) from a Landsat-8 image and then create a custom pixel-wise process that evaluates the NDVI value at each pixel. The pixel-wise process will evaluate whether a pixel contains high vegetation cover, moderate vegetation cover or no vegetation.
1. Import necessary modules & setup inputs/outputs
The first step is to import the Geomatica modules we will be calling in this tutorial and setting up the variables that will point to our input file(s) and output file(s).
In the above code block, line 2 is used to import numpy, which is an open source library for storing and performing computations on large arrays. Ultimately, the custom calculations will be performed in numpy. Line 3 imports gobs and datasource classes from Geomatica’s platform api. These classes are used to read in and write out the raster pixels, geocoding information and coordinate system.
On line 6, working_dir is a variable that contains the path to the directory where your tutorial data is stored. You will need to change this to the directory on your computer where your tutorial data is saved.
2. Open the dataset and read the dataset
The second step is to open the dataset, access the required channels and pixels (you can subset at this step), and access/store the input coordinate system and geocoding information in variables for use later in the script.
On line 11 we will use a with statement to call the ds.open_dataset(raw_l8) method, passing the raw_l8 variable as it’s argument, which holds the path to the raw Landsat-8 MS image. On line 12, you create an object called reader_l8 from the ds.BasicReader() class, which is initialized by passing the dataset variable as the first argument and a list of the channels you wish to open as the second argument. In this case, we are opening channel 4 (red) and channel 5 (NIR).
On lines 13 and 14, you will call the crs and geocoding methods via the reader_l8 object. The crs and geocode methods will return the coordinate system information and geocoding information from the raw landsat-8 image to the variables in_coord_sys and in_geocode, respectively.
Finally, on line 15 you create a raster object called in_raster by calling the method read_raster(), where the arguments define the dimensions of the raster. The first two arguments define the upper-left offset and are set to zero (0), which means no offset (start reading from the upper-leftmost pixel). The last two arguments define the width and height of the raster that should be read in. In this case, we set them equal to the width and height of the input file by setting the reader_l8.width and reader_l8.height attributes. This paragraph is really just a complicated way of saying that line 15 reads in all pixels in the channels.
3. Define NumPy arrays for each input channel
In this step, we will define the numpy arrays for the red and NIR channels and convert them to 32-bit float. The reason we want to convert them to 32-bit float when we perform the calculation, the values in the output array will be use the same data type as the input arrays. The NDVI results will range between +1.0 and -1.0, therefore, it is important that we retain the decimal points.
On line 18, we access the first element in the in_raster object (in_raster.data[:, :, 0]), which is the red channel and assign it to the red_chan variable. On line 19 we convert the red_chan numpy array to a 32-bit float, using astype. Line 20 and 21 do the same thing as above, but for the NIR channel.
Note: Instead of using astype, consider using vectorize to optimize performance
4. Calculating NDVI in NumPy
The next step is to perform an arithmetic operation with the numpy arrays that are holding the pixels for the red channel and the NIR channel, in order to calculate NDVI.
On line 25, we use a with-statement to invoke np.errstate in order to handle division errors in the case where background pixels are set to zero (0). Many orthorectified images have background pixels set as 0 and without handling this division error, your code will exit with an error. On line 26, we perform the NDVI calculation and assign the resulting array to the ndvi_array variable.
5. Create function with pixel-wise process to identify vegetation cover
In this step we will create a decorator for numpy’s np.vectorize function and then create the custom function that will define how we should process each pixel.
On line 29 we create a decorator with the @ character for the np.vectorize function, which will take the custom function that directly follows (find_veg) and run that function on each value in a numpy array (each pixel).
On line 30, we create the custom function find_veg(ndvi):, which takes in the ndvi _array as its only argument (array to process).
Lines 31 to 36 contain the custom rules for determining if a pixel is high vegetation cover, moderate vegetation cover or no vegetation cover.
Line 42 converts the output array, veg_id_array, to an 8bit unsigned integer. Line 43 creates a raster object for the veg_id_int array called veg_id_raster, by calling the gobs.array_to_raster() method, which formats the array so that it can be written to an output file by Geomatica’s platform API.
Note: array_to_raster() does not copy the values and is optimized for performance
6. Writing the output vegetation map to file
In this final step, we will write the results of our vegetation analysis to an output file. In this case, .pix (PCIDSK), but other formats, such as GeoTIFF are also available.
On line 46, we Use the with statement to call the ds.new_dataset() function and pass the variable veg_out, which contains the path to the output file we want written as the first argument, ‘PCIDSK’ as the second argument, which defines the file format and an empty string ‘’ as the third argument, which could be used to define addition file options. We then use the as-statement to set all this information to the write_dataset variable.
On line 47, we create an object called writer by instantiating the ds.BasicWriter() class and passing the write_dataset variable as its only argument. On line 50 we call the create() method from the writer object to create the file on disk based on the size and datatype of the raster object ndvi_raster. On line 51, we call the write_raster method to write the pixel values to the file. And finally, on lines 54 and 55, we call the writer object’s crs and geocoding methods to write the coordinate system and geocoding information from the input file (held in in_coord_sys and in_geocode variables) to the output file.