7992 Rate this article:

Extending ENVI Extensions


One of my favorite parts of being able to extend ENVI with IDL is that I have the ability to add custom buttons into ENVI's toolbox. These buttons are called extensions and I have made many of them during my time with NV5 Geospatial. I have created buttons for our tradeshow demos, examples for customers, and even for the Precision Ag Toolkit. Extensions in ENVI can you anything that you want them to. Most of the time this is using ENVI's dynamic UI for creating simple widgets that allow a user to select all the inputs and outputs for a task, but you can also just call IDL directly and instantiate some other GUI based application if you want.

While extensions are very easy to make (I just copy/paste old code I had and tweak it), they can be cumbersome when you have many extensions to create all at once. In addition to this, if you have some addition that you want added to all your extensions, then you have to go through each one by hand to change them. This is where getting creative with IDL programming comes in really handy because IDL can be used to automate the generation of buttons in ENVI. For this example, I decided to finally take a crack at dynamically creating X number of buttons from ENVI tasks without needing to write a separate extension for every one.

The basic idea is that I wanted a dynamic extension that would allow me to pass in any task, create a button, run the right task when I press the button, and offer the ability to display any and all results from the task that are EVNIRasters, ENVIROIs, or ENVIVectors (or shapefiles). If you're unfamiliar how to add extensions to ENVI, then here is a link to the docs which can provide some background information


Following the example in the docs, the way that I decided to proceed was to use the UVALUE for the buttons to pass in information about which task I wanted to create a dynamic UI for. I decided to use an orderedhash for my data structure with: the key is the task name, the value is a two element string array that represents the folder that we want the task to appear in the name of the button that you will see in ENVI's toolbox. Here is an example of what the data structure looked like:

;create an orderedhash of buttons that we want to create
buttons = orderedhash() 
buttons['ROIMaskRaster'] = ['/Regions of Interest', 'ROI Mask Raster']

At this point it just came down to looping over the hash correctly so that the buttons would be made in the right space. I chose to use the foreach loop because it makes it easy to get the key and value from hashes or orderedhashes all at the same time. Here is what that code looks like:

foreach val, buttons, key do begin
  e.AddExtension, val[1], 'better_extensions', $
    PATH = '/Better Extensions' + val[0], $
    UVALUE = key 

That short code block will dynamically go through every entry in my orderedhash and then create the buttons with the right placement and task to be executed when clicked on. Note that I created a subfolder called 'Better Extensions' that would contain all the fancy buttons that the extension would create. Once I had this code figured out, I just needed to look at the actual procedure which would run the extension (I called it "better_extensions")

You can see the complete code for better_extensions below, but there are a few key points to mention about the code:

  • To get the task name for the button that was clicked, when creating the extension I used the UVALUE keyword to store the name. Then, in the better_extensions procedure, I get the event as an argument and can get the uvalue for the button using widget_control.

  • To create the task UI all I needed to do was the following:

        ;create a dialogue for the rest of the task
        ok = e.UI.SelectTaskParameters(task)
  • After that, I just needed to go through each task parameter, check for INPUT or OUTPUT and, if it was output, check for ENVIRaster, ENVIVector, or ENVIROI. I then saved these and displayed them in ENVI's current view if they were found in the output.

The main goal here was to show that, with a little bit of IDL programming, you can take your ENVI analytics to the next level. The code for the extension can be found below. Cheers!

Also, keep an eye out for my next blog where I'm going to talk about giving an old extension (from the online extensions library) a much needed update!

; Add the extension to the toolbox. Called automatically on ENVI startup.
pro better_extensions_extensions_init

  ; Set compile options
  compile_opt idl2

  ; Get ENVI session
  e = envi(/CURRENT)
  ;create an orderedhash of buttons that we want to create
  buttons = orderedhash() 
  ;sample ROI tools
  buttons['ROIMaskRaster'] = $
    ['/Regions of Interest', 'ROI Mask Raster']
  bttons['ROIToClassification'] = $
    ['/Regsions of Interest', 'Classification Image from ROis']
  ;classification tools
  buttons['ISODataClassification'] = $
    ['/Classification/Unsupervised Classification', 'ISOData Classification']
  buttons['ClassificationAggregation'] = $
    ['/Classification/Post Classification', 'Classification Aggregation']
  buttons['ClassificationClumping'] = $
    ['/Classification/Post Classification', 'Clump Classes']
  buttons['ClassificationSieving'] = $
    ['/Classification/Post Classification', 'Sieve Classes']
  buttons['ClassificationSmoothing'] = $
    ['/Classification/Post Classification', 'Classification Smoothing']
  buttons['ClassificationToShapefile'] = $
    ['/Classification/Post Classification', 'Classification to vector']
  ;radiometric correction tools
  buttons['DarkSubtractionCorrection'] = $
    ['/Radiometric Correction', 'Dark Subtraction']
  buttons['ApplyGainOffset'] = $
    ['/Radiometric Correction', 'Apply Gain and Offset']

  buttons['BitErrorAdaptiveFilter'] = $
    ['/Filters', 'Bit Errors Filter']
  buttons['GaussianHighPassFilter'] = $
    ['/Filters', 'Gaussian High Pass Filter']
  buttons['GaussianLowPassFilter'] = $
    ['/Filters', 'Gaussian Low Pass Filter']
  buttons['HighPassFilter'] = $
    ['/Filters', 'High Pass Filter']
  buttons['LowPassFilter'] = $
    ['/Filters', 'Low Pass Filter']
  ;add the buttons
  foreach val, buttons, key do begin
    e.AddExtension, val[1], 'better_extensions', $
      PATH = '/Better Extensions' + val[0], $
      UVALUE = key 


; ENVI Extension code. Called when the toolbox item is chosen.
pro better_extensions, event

  ; Set compile options
  compile_opt idl2

  ;Get ENVI session
  e = envi(/CURRENT)
  if (e eq !NULL) then begin
    e = envi()

  CATCH, Error_status
  IF Error_status NE 0 then begin
    catch, /CANCEL
    help, /LAST_MESSAGE, output = err_txt
    p = dialog_message(err_txt)

  ;get the directory that our extension lives in
  WIDGET_CONTROL, event.id, GET_UVALUE = taskName
  ;create the task object
  task = ENVITask(taskname)
  ;add parameter to ask if we want to displayt he results
  displayParam = ENVITaskParameter(NAME='DISPLAY_RESULT', $
    DISPLAY_NAME = 'Display Result',$
    TYPE='bool', $
    DIRECTION='input', $
    REQUIRED = 1)
  ;replace old parameter
  task.AddParameter, displayParam

  ;create a dialogue for the rest of the task
  ok = e.UI.SelectTaskParameters(task)

  ;user selected OK
  if (ok eq 'OK') then begin
    ;get rid of dummy result
    display = task.DISPLAY_RESULT
    task.RemoveParameter, 'DISPLAY_RESULT'

    ;run the task

    ;check if we want to display things
    if display then begin
      ;things to display
      rasters = list()
      rois = list()
      shapefiles = list()
      ;check for output datatypes that are rasters, vectors, or rois
      foreach paramName, task.ParameterNames() do begin
        param = task.parameter(paramName)
        if (param.direction eq 'OUTPUT') then begin
          if (param.TYPE eq 'ENVIRASTER') then begin
            e.data.add, param.VALUE
            rasters.add, param.VALUE
          if (param.TYPE eq 'ENVIROI') then begin
            e.data.add, param.VALUE
            rois.add, param.VALUE
          if (param.TYPE eq 'ENVIVECTOR') then begin
            e.data.add, param.VALUE
            shapefiles.add, param.VALUE
      ;get envi's view
      View1 = e.GetView()
      ;disable refresh
      e.refresh, /DISABLE
      ;check for rasters and ROIs
      if (n_elements(rasters) gt 0) then begin
        ;display each raster in the view
        foreach raster, rasters do begin
          rasterLayer = View1.CreateLayer(raster)
        ;only display roi is there is a rasterlayer
        foreach roiarr, rois do begin
          ;handle arrays of rois
          foreach roi, roiarr do begin
            roilayer = rasterlayer.AddROI(roi)
      ;check for vectors
      ;only display vectors if ROI is present
      foreach vector, shapefiles do begin
        ; Create a vector layer
        vectorLayer = view.CreateLayer(vector)
      ;refresh display again