Overview
This is a sample Python script that utilizes Opticks and the Python Scripting Extension for Opticks. This script allows you to load an image into Opticks and then generate the Sobel edge detection results for one or more bands of the image you loaded.
If you want to execute this script in Opticks while learning about it, see the Quick Start.
What Will I Learn?You will learn the following about the Python scripting extension for Opticks: |
What Do I Need?
|
The Script
Let's discuss this script as four parts:
- Get the image data for the current window into a NumPy array
- Perform a Sobel edge detection using the built-in algorithms provided by NumPy
- Push the Sobel edge detection results back into Opticks
- Create a new window in Opticks to display the results
Before you continue, let me offer the following reading tips:
- You can type every single line of Python code you see below directly into the "Python" tab of the Scripting Window. Just make sure you have loaded the image first.
- You should also take a look at the Quick Start if you want to be able to run this script before or while you are reading the below explanation.
Part 1. Get the image data for the current window into a NumPy array
The first thing we need to do is get a reference to the image data from the active view. This is accomplished with the following Python code:
Now, that we have a reference to the image data, we can determine the size of the image, with the following Python code:
So, in order to generate the Sobel edge detection result we need to fetch one band of the image data into a NumPy array. We do this using the getDataPointerArray function of the RasterElement object. This function returns a NumPy array containing the image data you request. You use a DataPointerArgs object to specify which rows, columns and bands you want. In our case we want to fetch a one full band, so we want all the columns, all the rows and then just one single band. See the Python code required to fetch one full band into a NumPy array below:
Ok, so now that reviewed the above code you probably have some questions. What's the opticks.data.types.InterleaveFormat.BIP argument to the DataPointerArgs object all about? It specifies the order of the dimensions in the NumPy array. Still confused, check the listing below:
- opticks.data.types.InterleaveFormat.BIP - the NumPy array will have the following shape: [rows, columns, bands]
- opticks.data.types.InterleaveFormat.BSQ - the NumPy array will have the following shape: [bands, rows, columns]
- opticks.data.types.InterleaveFormat.BIL - the NumPy array will have the following shape: [rows, bands, columns]
Now that I've cleared that up, the next question you might have is why does getDataPointer() return two objects? And what are they? The first object returned, data in the above example, is the NumPy array object. It is a standard NumPy array with the shape and size of what you requested in the opticks.data.types.DataPointerArgs object. The second object returned, deleter in the above example, is a memory management object. Before you cry and say, "I use Python because I want to stay away from such things" hear me out. Opticks is designed to work with very large images, images that are so big they can't be loaded entirely into your computers RAM. The other day, I was playing around with a 16 GB image. In order to support this kind of processing, we have to expose a memory management object into the Python scripting extension. Sorry. But here are the basic rules, make sure to keep the deleter object around as long as you want to use the NumPy array (e.g. data object). And if you are planning on using your Python script on a large image make sure to release the deleter and data object as soon as you are done using them. You can release these objects in three ways:
- Create them as local variables within a scoped Python statement (for loop, if statement, function). Python will release them when execution of the script leaves the scoped statement.
- Manually release them using the delete built-in function. So, for example: "del data, deleter"
- Manually release them by setting them equal to None. So, for example: "data = None"
Hopefully, I calmed your fears.
Now, if you thought to yourself, "How unfortunate that I have to hard-code the band number of the band I want into my Python script", then I have something for you to look at. You can in fact pop-up a modal dialog to ask the user a question. We aren't going to use any built-in Opticks capability to do this. We are going to use the PyQt library which is Python wrapper of the Qt library. I choose this because Opticks also uses the Qt library. You just need to download and install the PyQt library into your Python install and then type the two lines of Python code below:
The code above pops up a modal dialog with a spin box for the user to select a number. This dialog is nice enough to restrict the user to a specified range of numbers, which is why we pass 0 and di.numBands-1 as the last two arguments to the function. The band_num variable will be set to the integer value they selected. And the accepted variable will be True if they closed the dialog using the "OK" button and False if they canceled the dialog.
On to part 2.
Part 2. Perform a Sobel edge detection using the built-in algorithms provided by NumPy
Now that we have a NumPy array containing all the values from a single band of the image in the active view, we need to perform our Sobel edge detection. This as it turns out is the easiest part of this whole script thanks to the SciPy library. You simply call the scipy.ndimage.sobel() function which takes a NumPy as input and returns the Sobel edge detection result as a new NumPy array. However, before we use this wonderful creation, we are going to convert our 3-dimension NumPy array into a 2-dimension NumPy array. We do this using the numpy.reshape function, telling it to return a NumPy array with a new shaped based upon the original shape of the array but omitting the last dimension. If you remember we requested this NumPy array from Opticks using an interleave of BIP, which means our original shape is [rows, columns, bands], so we are simply omitting the bands dimension. This is completely fine, because you also remember that we only requested one band. And everyone knows that omitting a dimension of size 1 is no problem at all. You'll see a little bit later why we wanted to remove that 3rd dimension. And for your reading pleasure, Python code for everything we've been talking about:
Now, that we have our Sobel edge detection results in the sobel variable, we don't need to use the NumPy array we requested from Opticks anymore. And since I know we all strive to be responsible users of system memory, we should allow Opticks to reclaim our NumPy array and the memory it is using. And if you remember from our earlier discussion when we retrieved the NumPy array from Opticks, you can easily do this using the del statement. Behold the highly interesting piece of Python required:
Ok, so now we have our Sobel edge detection and we've demonstrated our commitment to being responsible users of system memory. Let's get to part 3, where we put these results back into Opticks.
Part 3. Push the Sobel edge detection results back into Opticks
In oder to put the data back into Opticks, we need to construct an instance of opticks.data.raster.RasterElement. Normally to create a RasterElement, we need two pieces of information: the name of the RasterElement and a description of the size of the RasterElement.
For the name, we will simply construct a string using the name of the original image and appending the text "-SobelForBand" onto the end. We can easily do this using the Python string substitution syntax.
For the description of the size of the RasterElement, we need to use a org.data.types.RasterElementArgs object. The RasterElementArgs object lets you describe the number of rows, columns and bands we want in the RasterElement we are going to create. This object also requires an interleave, if you remember our earlier discussion (BIP, BSQ and BIL), that describes how the rows, columns and bands are ordered relative to each other. This object also requires an encoding type, which specifies the size and type of each pixel in the image, for example integer or float. That sounds like a bunch of stuff to set doesn't it. Well, if you have a 2-D NumPy array, you can use the fromNumpyArray function and it will automatically set all of above by querying the NumPy array. Remember, when I said I had a good reason for converting our original 3-D NumPy array into a 2-D NumPy array, well now you see what it was.
If you have all the data you want populated into the RasterElement as a NumPy array you can provide that as the third argument to the RasterElement constructor. Luckily for us, we do, so we'll pass sobel along to the constructor. That's all you need to push a NumPy array back into Opticks. See the Python code below:
Creating a RasterElement object only makes the image data available to Opticks. You can still do some useful things like export it to a file, but if you want to display the image you'll need to read the next section.
Part 4. Create a new window in Opticks to display the results
So, now we want to create a new window to display our image data. We simply provide a name for the view, along with a reference to a opticks.data.raster.RasterElement and then a bool to indicate we want to create a new view instead of fetching a reference to an existing view. It requires one line of Python code, see below:
Let's take it a step further
So, now we've learned how to calculate the sobel edge detection result for a single band of an input image. Let's take it a step further. Let's calculate the sobel edge detection result for every band in the input image and generate one new image to contain all of the results. Now, let's break this down into three parts:
- Prepare the input image and the output image
- Iterate over each band in the input image and generate the sobel edge detection results into the output image
- Create a new window in Opticks to display the results
Part 1. Prepare the input image and the output image
I'll assume you've read the earlier part of this sample. However, I will repeat some Python code samples just to make it easier to follow this more advanced example from start to end.
We need to first to fetch a reference to the image data in the active view, which we will use as our input image. We've done this earlier in the basic version of this script, so here's a repeat of the necessary Python code:
Since we are going to create a Sobel edge detection result for every band in the input image, we need to create an output image that is the same size as the input image. If you remember our earlier discussion you specify the size of a RasterElement using the RasterElementArgs object. In our case, we have to set every member of the RasterElementArgs object. But this is pretty easy because we query most of the information from the input image using a opticks.data.types.DataInfo object.
Now, the only piece of information we don't want to use from the input image is the interleaveFormat. Since we are going to be calculating the Sobel edge detection result a band at a time, it's going to be more efficient if all the information for a single band is tightly packed together. This is exactly what the BSQ interleave does, since it arranges the dimensions in the order of [bands, rows, columns]. So, we'll make sure to create our output input image using an interleave of opticks.data.types.InterleaveFormat.BSQ. Here is the relevant Python code:
Now, in part two we are going to loop over all of the bands in the input image, generate the sobel edge detection result, and then write it to the output image. The read request from the input image and the write request to the output image both require a opticks.data.types.DataPointerArgs object. So, we will construct and initialize this object with as much information as we can.
Notice, the only part we left out was the start band and stop band. This was intentional since we will provide this information in the for loop. So, now we've done all the setup and we can move onto part 2 and the for loop.
Part 2. Iterate over each band in the input image and generate the sobel edge detection results into the output image
Let's first look at the for loop:
Ok, now if you've read the basic version of this script above everything in this for loop should familiar and make sense except for this line:
So, let's explain this line. In the basic version of this script we provided all the data for the output RasterElement when we created it. We couldn't do that here because we had to create the output RasterElement before we started looping over the bands of the input image. So, we need someway to write the single band sobel edge detection results to our output image. This is the purpose of the copyArray function. You provide a NumPy array as the first argument and then you provide a description of where in the output image to write the contents of that NumPy array. You provide a description of where to write the NumPy array using a opticks.data.types.DataPointerArgs object. In our case we can re-use the DataPointerArgs object we used to fetch a NumPy array from the input image.
Ok, so now we are done processing the input image and done populating the output image with the sobel edge detection results. So, onto the last part.
Part 3. Create a new window in Opticks to display the results
You've seen this all before in part 4 of the basic version of this script. So, I will spare you the explanation and just provide the Python code so you don't have to scroll back up.
Congrats! We are done.
Quick Start
- Download and install everything listed under "What Do I Need?"
- Download the Sobel Edge Detection script bundled into an Opticks extension and install it. See How To Install an Extension for details on installing.
- Start up Opticks and display the "Python Samples" toolbar if it isn't already visible.
- Load an image to calculate the Sobel Edge Detection for. Look under "What Do I Need?" for an ideal image to use. You can load the image by dragging and dropping the file onto the Opticks application window. Or you can load the image using the "File->Import..." menu item.
- Once the image is loaded, select the "Python Samples->Sobel Edge Detect One Band From Current Image" from the "Python Samples" toolbar to execute the Python script. This will prompt you for which band in the currently selected image you would like to calculate the sobel edge detection results. It will then generate a new image and window with the sobel edge detection results.
- If you are looking to execute the "Advanced" portion of this script, then you should select the "Python Samples->Sobel Edge Detect All Bands From Current Image" from the "Python Samples" toolbar. This will create a new image and window where each band in the new image corresponds to the sobel edge detection result for the corresponding band in the current image window you have selected.
- You can find the Python script in "OPTICKS_INSTALL\SupportFiles\site-packages\sobel.py", assuming you installed the script as detailed in step 2.