Wiki

Clone wiki

enmap-box-idl / Application Development Tutorial

The very short way

  • If not already done, add your source code root directory to your workbench and the IDL path.

image001.png

  • For an IDL application, open the console of your IDL Workbench and call:
IDL> enmapBoxDev_runApplicationWizard
  • For an R application, use the Rscript keyword:
IDL> enmapBoxDev_runApplicationWizard, /Rscript

This will create the basic structure of a hubAPI routine with a sample R script in the folder \%AppFolder\_lib\script.r

  • For a python application, use the Pythonscript keyword:
IDL> enmapBoxDev_runApplicationWizard, /Pythonscript

This will create the basic structure of a hubAPI routine with a sample python script in the folder \%AppFolder\_lib\script.py

  • Set the application name prefix, select the source code root folder and accept.

image002.png

  • The wizard creates the basic file structure of your new application.

image003.png

  • Run the applications make script:
IDL> myApp_make
  • Start the EnMAP-Box:
IDL> enmapBox
  • Now you can run your application from the EnMAP-Box menu bar via Applications > MyApp > MyApp Application

image004.png

  • … or open supporting documents via Help > MyApp

image005.png

  • Just modify the IDL code, manuals etc. to add your own application logic.
  • Call IDL> myApp_make again for compiling your application and copying *.sav files and supporting documents to the application folder:
    <EnMAP-Box>/enmapProject/applications/MyApp
  • For distributing your application you can either
    • Copy your Applications Source Code Folder to share your project with other EnMAP-Box developers.
    • Copy the <EnMAP-Box>/enmapProject/applications/MyApp folder to share it with other EnMAP-Box users. For installation they just need to put it into their own <EnMAP-Box>/enmapProject/applications/ directory and re-start their EnMAP-Box.
  • Finally some helpful IDL commands:
IDL CommandDescription
myApp_makeCompile application, create documentation, copy files to application folder (installation).
myApp_make, /cleanupRemove application folder (de-installation).
enmapBoxStart EnMAP-Box in IDL license mode.
enmapBoxVMStart EnMAP-Box in IDL virtual machine mode. This opens the EnMAP-Box using the *.sav files only without compilation of *.pro-files. Use this to check if your app runs correctly without development license.

General Hints

  • Use Shift+B to toogle breakpoints and stop your code for debugging.

image006.png

  • Create test routines with sample data to verify your routines, e.g. “test_myApp_processing”.
  • Use Ctrl + Space for auto-completion.

image007.png

  • Don’t forget: you need to re-compile your *.pro files to make changes visible.
  • Use IDLDoc to explain your code.
  • In case of strange IDL behavior: use the reset button.
  • In case of very strange IDL behavior: restart the IDL workbench.
  • Use the hubAPI documentation to get familiar with the basic concepts of application development.
  • Use Strg and click on methods to jump to their definition or hover over for a description.

A first EnMAP-Box application: MyNDVI

This tutorial shows you how to create an EnMAP-Box application using the EnMAP-Box and hubAPI programming interfaces. For this we want to create a small program that calculates the NDVI (NDVI = (NIR-Red)/(NIR + Red)). For a start our program should be able to:

  • ask the user for the image the NDVI should be calculated from
  • ask the user for the name of the output file with the NDVI values
  • calculate NDVI even for very, very big images that would not fit into your computer’s memory
  • give a small final report

Adding more sophisticated functionality to our program follows in Section 4 where we give examples how to

  • show the progress of our calculations
  • ask the user for the specific wavelength or bands he wants to use for the NDVI calculation
  • ask for an optional mask file to exclude specific areas (Masking)
  • show some basic statistics (avg, min, max) of our calculated NDVI values

Prepare the Basic Application

  • If not already part of your IDL workspace, add your personal source code folder to the project explorer.

image008.png

  • Use your IDL console to start the application wizard for an IDL app:
IDL> enmapBoxDev_runApplicationWizard

Alternatively, for an R App / Python App, use

IDL> enmapBoxDev_runApplicationWizard, /Rscript

Or

IDL> enmapBoxDev_runApplicationWizard, /Pythonscript
  • Add your application name and the source code directory.

image009.png

The wizard copies the template stored in <EnMAP-Box>/ SourceCode/applications/TEMPLATE and replaces the string “template” by your application name prefix “MyNDVI”.

  • Now your project explorer should look like this (if not, refresh folder):

image010.png


Now you know how to create a new application framework using the application wizard.


Install Your Application

By now the IDL Workbench will find all *.pro file stored within the application’s source code folder. New *.pro files will be added to the IDL Path automatically and can be called from the command line. However, this is not the case for other file types, as for example text, data or configuration files we want to use for our application.
For example the “enmap.men” file is used to describe how your application is added to the EnMAP-Box menu structure (see Section 4.5 for details). In order to get found by the EnMAP-Box it must lie within a subfolder of its installation directory. To ensure this and to add other required non-*.pro file resources to the EnMAP-Box installation we use a make-file (See http://en.wikipedia.org/wiki/Make_file)

  • Call IDL> MyNDVI_make to install your application to the EnMAP-Box.
  • Your browser should open the IDLDoc documentation of your application’s source code.

image011.png

  • As in the next figure, the make routine adds the folder “MyNDVI” to your EnMAP-Box installation. By default, the folders “_copyrights”, “_help”, “_resources” are copied to folder “MyNDVI” and renamed by removing the leading underscores.

image012.png

The main tasks of the make routine are to:

  • Create the application directory
  • Compile all IDL routines and store it in an IDL SAVE (*.sav) file
  • Copy the folders "_copyrights", "_help", "_lib" and "_resource" to the application directory (removes the leading underscore "_")
  • Create the IDLDoc code documentation
  • Now open (or restart) the EnMAP-Box: IDL> enmapBox and run your application using its new menu button.

image013.png

Calling your application from an EnMAP-Box menu button is possible because the make routine copied the enmap.men file from your applications source code folder to the final application installation folder.

  • A small widget dialog appears. Click “Accept” and see what happens until the applications ends.

image014.png

Keep in mind:

  • Changes of your source code (*.pro files) are automatically available to your IDL Environment.
  • Changes of non-source code files are not automatically available to your IDL Environment or the EnMAP-Box. They require a re-installation into the EnMAP-Box applications directory. This is usually done by calling the make routine. |

These commands might be helpful:

IDL CommandDescription
MyNDVI_makeCompile application, create documentation, copy files to application folder (installation).
MyNDVI_make, /cleanupRemove application folder (de-installation).
enmapBoxStart EnMAP-Box in IDL license mode.
enmapBoxVMStart EnMAP-Box in IDL virtual machine mode. This opens the EnMAP-Box using the *.sav files only without compilation of *.pro-files. Use this to check if your app runs correctly without development license.

Now you know how to:

  • use the make routine to create a final installation of your application.
  • use the make routine to de-install your application.
  • start your application from the EnMAP-Box or IDL-Command line.

EnMAP-Box Application Scheme

Now let’s have a look into the files created by the application wizard to see which parts an EnMAP-Box application (can) consist of.

  • Open the application file
IDL> .edit MyNDVI_application

This file is the “main” definition of our application. It describes the basic program flow.

pro MyNDVI_application, applicationInfo
; get global settings for this application
  settings = MyNDVI_getSettings()
; save application info to settings hash
  settings = settings + applicationInfo
  parameters = MyNDVI_getParameters(settings)
  if parameters['accept'] then begin
    reportInfo = MyNDVI_processing(parameters, settings)
    if parameters['showReport'] then begin
      MyNDVI_showReport, reportInfo, settings
    endif
  endif
end
  • The application follows this scheme:

image015.png

  • A widget program collects required input parameters, as for example filenames, numbers or text strings. This is realized by the function “MyNDVI_getParameters” that returns a hash containing the collected parameter values.
  • If the “accept” key of the parameter hash is true, the processing routine “MyNDVI_processing” is called. It implements the “core-logic” of the application and is the routine where we will implement the NDVI calculation.

Note: For later applications it can be useful to distinguish between image and data processing routines. This makes re-using code much easier, since the data processing routine does not care about things like “how to open an image and read the data”.


  • Finally a report routine gives feedback on the processing results. In our application it is called “MyNDVI_showReport” and creates the HTML file that will open in your web browser. The report routine can be used to present statistical measures, show occurred errors or just to say that processing is done.

Now you are familiar with:

  • the proposed workflow to design EnMAP-Box applications. For later applications it can be useful to distinguish between image and data processing routines. This makes re-using code much easier, since the data processing routine does not care about things like “how to open an image and read the data”.

Add Application Logic

In this section we use the framework created by the application wizard to add our own application logic. We will make use of the hubAPI routines, so please open its IDLDoc documentation which can be found via Help > EnMAP-Box > hubAPI

or inside the EnMAP-Box installation directory

<EnMAP-Box>\enmapProject\lib\hubAPI\help\idldoc\index.html

image016.png

Collect Parameters

  • Open the “myNDVI_getParameters” source code file.
IDL> .edit MyNDVI_getParameters
  • We use the hubAPI’s auto-managed-widgets (AMW) to collect the required parameters for our NDVI application. In general, these widgets can be defined between the lines
hubAMW_program , groupLeader, Title = settings['title']
;... and ...
parameters = hubAMW_manage()
  • Insert these lines into your IDL code and use the API documentation to find out what the different keywords are used for.
function MyNDVI_getParameters, settings
 
 groupLeader = settings.hubGetValue('groupLeader')
 
  hubAMW_program , groupLeader, Title = settings['title']
  hubAMW_label, 'This is '+settings['title']

  ; frame for output options & parameters
  hubAMW_frame , Title = 'Input'
  hubAMW_inputImageFilename, 'inputImage', Title='Image'

  ; frame for output options & parameters
  hubAMW_frame , Title = 'Output'
  hubAMW_outputFilename, 'outputImage' $
  , Title='NDVI file', Value='ndvi'

  hubAMW_checkbox, 'showReport', Title='Show Report', Value=1b

  parameters = hubAMW_manage()
 
 ; if required, perform some additional changes on the parameters hash

  return, parameters
end
  • Compile the file and call: IDL> test_MyNDVI_getParameters to see how your parameter dialog looks like. image017.png
  • Change your input parameters and Accept. The test routine will print the hash and its values (instead of passing it to other routines):
% No group leader defined via the groupLeader argument. A temporary top level base is created which serves as group leader.
outputImage: C:\Users\Username\AppData\Local\Temp\ndvi
showReport: 1
accept: 1
inputImage: D:\EnMAP-Box\enmapProject\lib\hubAPI\resource\test...

You can ignore the % No group leader message.

  • Repeat this with changed input values and look how the returned parameter hash will change. And of course, feel free to change our code!

Now you know how to write simple auto-managed-widget programs that collect input image filenames and output filenames.


Processing Routine

  • Open the MyNDVI_processing source code file:
IDL> .edit MyNDVI_processing
  • As you can see, the MyNDVI_processing() function’s first argument is the parameter-hash returned by MyNDVI_getParameters().
  • The second input argument is the settings hash which provides general application settings. It is used to provide values defined in an external settings file.
  • For clarity, we start defining the minimum of required parameters:
; check / define required parameters
tileLines = 100 ;image lines of a single image tile
wavelengthRed = 600.00 ; wavelength in nm
wavelengthNIR = 800.00

Variable tileLines contains the maximum number of image lines we will read in one step and calculate the NDVI values for. We use it to limit the number of pixels to be read from an image file. This avoids slowing down your system and allows to process even the largest (ok, maybe not the largest, but at least very large) images.

  • Now we initiate a hubIOImgInputImage object for reading the image raster data and a hubIOImgOutputImage object for writing the NDVI values.
; create the input and output image objects
inputImage = hubIOImgInputImage(parameters['inputImage'])
outputImage = hubIOImgOutputImage(parameters['outputImage'])
  • The hubIOImgInputImage offers access to the images’ meta data. For instance
IDL> print, 'bands:' + inputImage.getMeta('bands') 

prints the number of bands described in the header file.

  • The input images for our application might be different in wavelength and FWHM. It is likely that they do not match our NDVI wavelengths of choice exactly. So we chose those spectral bands with a center wavelength closest to our definition
;locate bandindices
bandIndexRed = inputImage.locateWavelength(wavelengthRed)
bandIndexNIR = inputImage.locateWavelength(wavelengthNIR) 

More details on header file meta information and image data formats used by EnMAP-Box can be found in the under Data Format Definition.

  • Now we initialize the data reader of the inputImage object.
;initialize the input image reader
inputImage.initReader, tileLines, /TileProcessing, /Slice $
          , SubsetBandPositions=[bandIndexRed, bandIndexNIR] $
          , DataType='float'

/TileProcessing and tileLines avoid overfilling of the memory (OutOfMemoryExceptions)
/Slice keyword specifies that pixels and its spectral values are read “in a row”
SubsetBandPosition allows us to limit the returned output to the spectral bands we are interested in
DataType='float' ensures that the reader converts all image values to float

  • Now the inputImage object is able to read in one step: 100 lines (or less if not available) and only the spectral values of the band closest to 600 and 800 nm.
  • For writing the NDVI values we need to define the dimensions of the final output file:
;initialize the NDVI image writer
outputImage.copyMeta, inputImage, /CopySpatialInformation


The copyMeta command helps you to define the spatial dimensions of the NDVI image. This is equivalent to:

outputImage.setMeta, 'lines', inputImage.getMeta('lines')
outputImage.setMeta, 'samples', inputImage.getMeta('samples')
outputImage.setMeta, 'map info', inputImage.getMeta('map info')


  • Now we initialize the writer
outputImage.initWriter, inputImage.getWriterSettings(SetBands=1)

getWriterSettings() returns the parameters how the data comes from the input file (number of tilelines, Slice mode, spatial/spectral size etc).
SetBands overwrites the number of bands to 1, since we just want to write one band.


  • The calculation of the NDVI is done in a while-loop over all image tiles read from the inputImage object:
; calculate the NDVI
while ~inputImage.tileProcessingDone() do begin
  data = inputImage.getData()
  ndviValues = (data[1,*] - data[0,*]) / $
  (data[1,*] + data[0,*])
  outputImage.writeData, ndviValues
endwhile

getData() returns an array of dimensions:
[2, image samples x tileLines], in other words
[Red + NIR band, total number of image tile pixels]
Each time getData() is called the reader shifts forward and is ready to read the next image tile.
writeData works similar and shifts to the corresponding physical byte positions of the output file.


  • All calculations are done, so we can close reader and writer.
; cleanup objects
inputImage.cleanup
outputImage.cleanup

This finishes all reading/writing operations, closes logical file units associated with the image files and destroys the objects.

  • Finally we return some results programmatically.
; store results to be reported inside a hash
result = hash() ;empty hash
result += parameters.hubGetSubHash(['inputImage','outputImage'])
return, result



The hubGetSubHash function extends the standard IDL hash and becomes very helpful when you want to copy values between hashes. Its call is equivalent to:

result['inputImage'] = parameters.hasKey('inputImage')? $
                        parameters['inputImage'] : !NULL
result['outputImage'] = parameters.hasKey('outputImage')? $
                         parameters['outputImage'] : !NULL

This helps you to get subsets of hashes, whether they are defined or not.



  • All in all, your processing routine should now look like this:
function MyNDVI_processing, parameters, settings
 
  ; check / define required parameters
  tileLines = 100 ;image lines of a single image tile
  wavelengthRed = 600.00 ; wavelength in nm
  wavelengthNIR = 800.00
 
  ; create the input and output image objects
  inputImage = hubIOImgInputImage(parameters['inputImage'])
  outputImage = hubIOImgOutputImage(parameters['outputImage'])
 
  ;locate bandindices
  bandIndexRed = inputImage.locateWavelength(wavelengthRed)
  bandIndexNIR = inputImage.locateWavelength(wavelengthNIR)

  ;initialize the input image reader
  inputImage.initReader, tileLines, /TileProcessing, /Slice $
            , SubsetBandPositions=[bandIndexRed, bandIndexNIR] $
            , DataType='float'
 
  ;initialize the NDVI image writer
  outputImage.copyMeta, inputImage, /CopySpatialInformation
  outputImage.initWriter, inputImage.getWriterSettings(SetBands=1)
 
  ;calculate the NDVI
  while ~inputImage.tileProcessingDone() do begin
    data = inputImage.getData()
    ndviValues = (data[1,*] - data[0,*]) / $
    (data[1,*] + data[0,*])
    outputImage.writeData, ndviValues
  endwhile

  ;cleanup objects
  inputImage.cleanup
  outputImage.cleanup

  ; store results to be reported inside a hash
  result = hash() ;empty hash
  result += parameters.hubGetSubHash(['inputImage','outputImage'])
  return, result

end

Now you know how to:

  • use the hubIOImgInputImage object to query image meta data
  • use the hubIOImgInputImage object to get image data
  • initialize & run a tile processing to save local memory
  • use the hubIOImgOutputImage object to create a new raster data set
  • use some of the new function to access values stored in hashes

Final report

  • Open your report routine MyNDVI_showReport and remove all lines between the initialization of the hubReport object and the call of its saveHTML() function.
  • The hubReport-object helps you to combine text, images or tables in a structured way. We take it to provide some information on the image file and the resulting NDVI image.
  • Start with a level 1 headline
report.addHeading, 'Files'
  • As a sub-headline of this we add the input image
report.addHeading, 'Input Image', 2
  • Now we add some file information with help of our hubIOImgInputImage object
inputImage = hubIOImgInputImage(reportInfo['inputImage'])
textArray = ['filename: ' + reportInfo['inputImage'] $
            ,'samples: ' + strtrim(inputImage.getMeta('samples'),2) $
            ,'lines: ' + strtrim(inputImage.getMeta('lines'),2) $
            ,'bands: ' + strtrim(inputImage.getMeta('bands'),2) $
            ]
report.addMonospace, textArray

Each string array element represents a new text line
addMonospace encloses all text lines in a html <pre><pre /> tag

  • Before closing the input image we use it to add a quicklook picture
report.addImage, inputImage.quicklook(/TrueColor) $
               , 'True Color Quicklook'
inputImage.cleanup
  • For showing the properties of the NDVI image we choose another way and use a list instead of an array
report.addHeading, 'NDVI Image', 2
ndviImage = hubIOImgInputImage(reportInfo['outputImage'])
textList = List()
  • Now we can add new text lines step by step
textList.add, 'filename: ' + reportInfo['outputImage']
  • To add the NDVI image’s meta information we use a more dynamic way. First we define the meta tags we like to show:
tags = ['lines', 'samples', 'bands', 'file type', 'interleave' $
, 'description', 'a non-existing tag']
  • Then we initiate a loop over these tags and look if they exist.
foreach tag, tags do begin
  if ndviImage.hasMeta(tag) then begin
    ;todo: do something with the tag value
  endif
endforeach
  • Replace the TODO comment as followed:
foreach tag, tags do begin
  if ndviImage.hasMeta(tag) then begin
    valueString = strtrim(ndviImage.getMeta(tag),2) + ':'
    textLine = string(format='(%"%-11s")', valueString)
    textList.add, textLine
  endif
endforeach

The variable textLine is filled with help of the string function and its mighty format string options. So we can ensure a fixed style for all textlines. The meaning of format string '(%"%-11s")' is to expand or trim the text valueString to a length of 11 characters with a left alignment


Try the following command line examples to understand why using format strings can be very helpful: Print text strings:

print, string(format='(%"%s")' , 'your Text')
print, string(format='(%"%5s")' , 'your Text')
print, string(format='(%"%10s")' , 'your Text')
print, string(format='(%"%-10s")', 'your Text')

Combine different format strings and text:

print, string(format='(%"Value1:%s and value2:%i")', 'foobar', 42)

Print integer (non-floating point) numbers:

print, string(format='(%"%i")', 1000)
print, string(format='(%"%3i")', 5) ;explicit string length
print, string(format='(%"%-3i")', 5) ;left alignment
print, string(format='(%"%10.5I")', 42) ;use leading zeros
print, string(format='(%"%i %i")' , 5, -5) ;no '+' prefix
print, string(format='(%"%+i %+i")', 5, -5) ;with '+' prefix
;but be aware of
;cutted decimal places (no rounding)
print, string(format='(%"%i")', 42.99)
;string length < significant digits
print, string(format='(%"%3i")', 1000)

Print floating point numbers (float, double, etc.):

print, string(format='(%"%f")', 42.2313)
;set total length to 5 characters with 2 decimal places
print, string(format='(%"%5.2f")', 42.2313)
; but be aware off
print, string(format='(%"%4.2f")', 42.23)
print, string(format='(%"%5.2f")', 42.23)
;(the point is a character too)
Print numbers and show its exponent:
print, string(format='(%"%10.3E")', 42.21) ;show exponent
print, string(format='(%"%10.3e")', 42.21) ;show exponent
; but again you need to ensure enough characters:
print, string(format='(%"%8.3e")', 42.21)
  • Now we add the textlist to our report, create a NDVI quicklook and close the image object
report.addMonospace, textList.toArray()
report.addImage, ndviImage.quicklook(), 'NDVI Quicklook'
ndviImage.cleanup
  • The final call of the hubReport objects saveHTML method produces a temporary HTML file, which should be opened by your default browser.
report.saveHTML, /Show
  • Your MyNDVI_showReport routine should look like this:
pro MyNDVI_showReport, reportInfo, settings

  report = hubReport(Title=settings['title'])
  report.addHeading, 'Files'
  report.addHeading, 'Input Image', 2

  inputImage = hubIOImgInputImage(reportInfo['inputImage'])
  textArray = ['filename: ' + reportInfo['inputImage'] $
              ,'samples: ' + strtrim(inputImage.getMeta('samples'),2) $
              ,'lines: ' + strtrim(inputImage.getMeta('lines'),2) $
              ,'bands: ' + strtrim(inputImage.getMeta('bands'),2) $
              ]
  report.addMonospace, textArray

  report.addImage, inputImage.quicklook(/TrueColor) $
               , 'True Color Quicklook'
  inputImage.cleanup

  report.addHeading, 'NDVI Image', 2
  ndviImage = hubIOImgInputImage(reportInfo['outputImage'])
  textList = List()

  textList.add, 'filename: ' + reportInfo['outputImage']
  tags = ['lines', 'samples', 'bands', 'file type', 'interleave' $
       , 'description', 'a non-existing tag']
  foreach tag, tags do begin
    if ndviImage.hasMeta(tag) then begin
      valueString = tag + ‘:’ strtrim(ndviImage.getMeta(tag),2)
      textLine = string(format='(%"%-25s")', valueString)
      textList.add, textLine
    endif
  endforeach

  report.addMonospace, textList.toArray()
  report.addImage, ndviImage.quicklook(), 'NDVI Quicklook'
  ndviImage.cleanup

  report.saveHTML, /Show

end
  • Start your application using the EnMAP-Box button or call (compile first)
IDL> test_MyNDVI_application
  • The calculated NDVI image appears in the EnMAP-Box file list and can be opened. Your browser should show the report.

image019.png image020.png

  • Call IDL> MyNDVI_make again to build your final distribution package (see Install your Application). Now you just need to copy the folder.
  • Will your application run on the IDL Virtual Machine, which uses *.sav files only? To check this, start the EnMAP-Box in Virtual Machine Mode by calling
IDL> enmapBoxVM

and try to run your application.


Now you know how to:

  • use the hubReport object to generate simple HTML pages without knowing anything about HTML
  • add headlines, images and normal text to the hubReport object
  • use lists to collect data in a dynamical way
  • use IDL C-style format strings
  • open the EnMAP-Box in Virtual Machine Mode

Distribute Your Application

Sharing your application with other EnMAP-Box Users is pretty simple. Just copy the folder MyNDVI between the application directories (<EnMAP-Box>/enmapProject/applications) of your EnMAP-Box and that you want to install it.
The same way you just need to copy your source code directory in case you want to share it with other Developers.

Extend your application

Progress Bar

Some parts of your application might need some more time to get finished. In these cases a progress bar is a good choice to show that your program is not stuck and still works.

  • Open your processing routine MyNDVI_processing
  • To create a new progress bar, use the following lines in your IDL code:
; create the input and output image objects
inputImage = hubIOImgInputImage(parameters['inputImage'])
outputImage = hubIOImgOutputImage(parameters['outputImage'])

;create a progressBar
progressBar = hubProgressBar(Title=settings.hubGetValue('title') $
              ;use GroupLeader if defined
              ,GroupLeader = settings.hubGetValue('groupLeader'))
progressBar.setInfo, 'Calculating NDVI...'
progressBar.setRange, [0,inputImage.getMeta('lines')]
progressDone = 0

The group leader is the ID of the parent IDL window, in our case the EnMAP-Box. If the group leader is defined, closing the parent window destroys the progress bar too.

  • Use the while-loop to measure the progress of your calculations.
;calculate the NDVI
while ~inputImage.tileProcessingDone() do begin
    data = inputImage.getData()
    ndviValues = (data[1,*] - data[0,*]) / $
                 (data[1,*] + data[0,*])
    outputImage.writeData, ndviValues

    progressDone += tileLines
    progressBar.setProgress, progressDone
endwhile

The variable ‘progressDone’ shows the total number of image lines processed.

  • Finally cleanup the progress bar object
;cleanup objects
inputImage.cleanup
outputImage.cleanup
progressBar.cleanup
  • Compile and start your application. When calculating the NDVI you should see this progress-bar:

image021.png

Selecting Image Bands

By now the wavelengths used for calculating the NDVI are defined in MyNDVI_processing using these lines:

wavelengthRed = 600.00 ; wavelength in nm
wavelengthNIR = 800.00

But maybe a user prefers other wavelengths or just doesn’t know the exact center wavelength of his image bands. So it’s a good idea to extend our application by two aspects:

  1. ask for the wavelength that is to be used for NDVI calculation or, alternatively,
  2. ask for the explicit band indices to be used.
  • For this we modify MyNDVI_getParameters()
; frame for output options & parameters
hubAMW_frame , Title = 'Input'
hubAMW_inputImageFilename, 'inputImage', title='Image'

; frame for advanced input parameters
hubAMW_frame, Title= 'Band Selection Mode', /Advanced

hubAMW_subframe, 'bandChoice', /Row, /SetButton $
   , title = 'Choose bands closest to wavelength'
hubAMW_parameter, 'wlRed', title='Red', value=600.00 $
   , Unit='nm ', /Float, IsGE=0
hubAMW_parameter, 'wlNIR', title='NIR', value=800.00 $
   , Unit='nm ', /Float, IsGE=0

hubAMW_subframe, 'bandChoice', title = 'Choose by band index', /Row
hubAMW_parameter, 'indexRed', title='Red', /Integer, IsGE=0
hubAMW_parameter, 'indexNIR', title='NIR', /Integer, IsGE=0

The hubAMW_frame with keyword /Advanced organizes the following widgets in a frame that will be shown after clicking on the Advanced button only.
The hubAMW_subframe widgets are used with the string argument ‘bandChoice’. So they are exclusive and a hash key ‘bandChoice’ will return the index of chosen subframe. This way a user can select either wavelength or band indices.
hubAMW_parameter widgets are used to collect user defined values for wavelength (wlRed and wlNIR) or band indices (indexRed and indexNIR).
Keyword IsGE=0 ensures that values below zero and therefore negative indices cannot be entered.
Keyword /Integer limits the input to natural numbers only.
Keyword value is used to provide default values for the wavelengths.

  • call IDL> test_MyNDVI_getParameters to open the modified input dialog. Press ‘Advanced’ to make the Band Selection Mode Menu visible.

image022.png

  • Now we can modify the MyNDVI_processing function to use the new hash-keys 'wlRed' and 'wlNIR', or 'indexRed' and 'indexNIR', respectively.
;locate band indices
if parameters['bandChoice'] eq 0 then begin
  ;choice = 0 -> use user defined wavelength
  bandIndexRed = inputImage.locateWavelength(parameters['wlRed'])
  bandIndexNIR = inputImage.locateWavelength(parameters['wlNIR'])
endif else begin
  ;choice = 1 -> use explicit band indices
  bandIndexRed = parameters['indexRed']
  bandIndexNIR = parameters['indexNIR']
endelse
  • Alternatively you can write the last code example without if-statements.
bandIndexRed = parameters.hubGetValue('indexRed' $
         , default = inputImage.locateWavelength(parameters['wlRed']))
bandIndexNIR = parameters.hubGetValue('indexNIR' $
         , default = inputImage.locateWavelength(parameters['wlNIR']))

The hubGetValue extension helps to avoid Key-does-not-exists Exceptions. In case a hash key is missing it will return !NULL or, if defined, a default value. The alternative code listing allows using different selection modes for the Red and NIR band. The hash-key “bandChoice” is not required anymore.



Now you know how to:

  • define an advanced settings menu
  • use exclusive subframes to offer different input modes
  • collect numbers or text values with help of the hubAMW_parameter widget
  • define and show default parameter values
  • use the hubGetValue hash method to write less code

Consistency Checks

In the previous section we allowed the user to specify the bands to be used in the NDVI calculation. But what happens in case the user enters non-valid values? This requires to perform consistency checks to avoid the processing routine is started with a configuration that could lead to exceptions or other errors.

By default a build-in consistency check is performed through each hubAMW_parameter widget after the user has pressed the Accept button. According to the keywords used each input value is verified. For instance an error will be thrown if a text-string is given instead of a number to specify the wavelength for our red band.

Another situation exists when a value can only be verified in the context of another value. In our example a valid band index is defined as 0 ≤ band index < number of bands (zero based indices). Providing a greater band index might result in an IDL “index out of range” exception, so we want to avoid this. But since we do not know the total number of bands of the user specified image, we can’t use a fixed limit for it.

But the hubAMW_manage() function offers to define a user defined consistency check function. The following code gives a short example how this is used in general. The widget dialog’s only task is to collect two non-equal integer numbers for parameter A and B.

function example_getParameters_Check, resultHash, Message=message
  message = 'A must be different to B.'
  isConsistent = resultHash['parA'] ne resultHash['parB']
  return, isConsistent
end

function example_getParameters
  hubAMW_program , Title = 'Consistency Check Example'
  hubAMW_label, 'Please specify two different integer values'
  hubAMW_parameter, 'parA', /Integer, title='A ='
  hubAMW_parameter, 'parB', /Integer, title='B ='
  parameters = hubAMW_manage(ConsistencyCheckFunction='example_getParameters_Check')
  return, parameters
end

If defined, hubAMW_manage() will call the ConsistencyCheckFunction example_getParametersCheck with the resultHash as input argument. If the value of parA is equal to the value in parB, the variable isConsistent and finally the return value of the check function will be set to false. Additionally the keyword Message is used to provide a description of the error.

image023.png

In our NDVI example we just need to modify myndvi_getparameters.pro:

  • Ensure that the consistency check function is defined before the MyNDVI_getParameters().
function MyNDVI_getParameters_Check, resultHash, Message=Message
  isConsistent = 1b ;true by default
  messages = List() ;empty error message list
  if resultHash['bandChoice'] eq 1 then begin
    ;check band indices
    inputImage = hubIOImgInputImage(resultHash['inputImage'])
    bands = inputImage.getMeta('bands')
    inputImage.cleanup

    ; check the red band index
    if resultHash['indexRed'] ge bands then begin
      isConsistent = isConsistent && 0b ;set on false
      messages.add, 'Red band index i must be 0 <= i < '+strtrim(bands-1,2)
    endif

    ; check the NIR band index
    if resultHash['indexNIR'] ge bands then begin
      isConsistant = isConsistent && 0b ;set on false
      messages.add, 'NIR band index i must be 0 <= i < '+strtrim(bands-1,2)
    endif
  endif
  message = messages.toArray()
  return, isConsistent
end
  • If you like, add the condition that the index or wavelength to be used as Red band must be lower than that for the NIR band.
  • Don’t forget to tell hubAMW_manage() to use the consistency check function:
parameters = hubAMW_manage(ConsistencyCheckFunction = $
'MyNDVI_getParameters_Check')

Now you know how to:

  • use the build-in consistency checks
  • specify user defined consistency checks
  • react dynamically on user specified input values

Mask Files

Sometimes we are interested in a spatial subset of our data set only, for example when we a calculation should be done for a specific area only. Such subsets can be defined with the help of a mask image, where all pixels with a value different to zero are those of interest.

For extending our NDVI application to consider mask images to requires the following steps:

  1. Modify MyNDVI_getParameters() to ask for an optional mask file
  2. Modify MyNDVI_processing() to consider the optionally defined mask file. This should be done in a time saving way. If an image tile contains masked pixels only, we do not need to perform any calculation.

Parameters Dialog

  • So let’s start with the parameters dialog and replace the hubAMW_inputImageFilename widget by:
hubAMW_inputSampleSet,'sampleSet', title='Image', /Masking $
                     , ReferenceTitle='Mask', /ReferenceOptional
  • Call IDL> test_MyNDVI_getParameters to see how your input dialog looks now. image024.png
  • Accept to print the returned hash

showReport: 1
accept: 1
sampleSet: <ObjHeapVar85239(HASH)>
wlRed: 600.00000
wlNIR: 800.00000
outputImage: C:\Users\User\AppData\Local\Temp\ndvi
bandChoice: 0

  • The hash-key sampleSet refers to another hash. Add
print, parameters['sampleSet']

to your test routine to print out:

labelFilename: !NULL
featureFilename: D:\EnMAP-Box\enmapProject\...\Hymap_Berlin-A_Image

We used the keyword /ReferenceOptional, so the mask file is an optional feature of our application. If not used, ‘labelFilename’ will return !NULL.

  • The hubAMW_sampleSet is used to specify an image, called “feature file” and an accompanying “label file”. While the feature file can have multiple bands, the label file is allowed to have one band only. Each label file must:
  1. have the same spatial dimensions as the feature file
  2. fit the requirements defined by one of the following keywords:
KeywordRequired File Settings
/Maskingbands = 1
/Classificationbands = 1,
file type = ENVI Classification
categorical label information
/Regressionbands = 1,
file type = ENVI Standard
continuous label information
/Densitybands = 1
  • To simplify the use of our parameter hash, e.g. within the MyNDVI_processing, we add the following lines to the end of our MyNDVI_getParameters() function
; if required, perform some additional changes on the parameters hash
; reform the parameters hash
parameters['inputImage'] = (parameters['sampleSet'])['featureFilename']
parameters['maskImage'] = (parameters['sampleSet'])['labelFilename']
;remove not-required hash-keys
parameters.hubRemove, 'sampleSet'

This allows to get the mask file name via parameters['maskImage'] instead of (parameters['sampleSet'])['labelFilename']

Processing Routine

  • Now we can modify MyNDVI_processing.pro to make use of our mask file. First we need to define a value that is assigned to pixels where no NDVI value is calculated for because they are ignored/masked.
dataIgnoreValue = -9
  • In case the mask exists, we create a hubIOImgInputImage pointing on it …
; create the input and output image objects
inputImage = hubIOImgInputImage(parameters['inputImage'])
outputImage = hubIOImgOutputImage(parameters['outputImage'])

if isa(parameters['maskImage']) then begin
  maskImage = hubIOImgInputImage(parameters['maskImage'], /Mask)
endif
  • … and initialize the reader
;initialize the input image reader
inputImage.initReader, tileLines, /TileProcessing, /Slice $
       , SubsetBandPositions=[bandIndexRed, bandIndexNIR]

;initialize the mask image reader
if isa(maskImage) then begin
  maskImage.initReader, tileLines, /TileProcessing, /Slice, /Mask
endif

The keyword /Mask ensures that returned values will be of type byte and 0 (masked) or 1 (non-masked) only.

  • The image tile loop is modified as followed
while ~inputImage.tileProcessingDone() do begin
  data = inputImage.getData()

  nTilePixels = n_elements(data[0,*])
  ;default ndvi values are set to data ignore value
  ndviValues = make_array(nTilePixels $
                         , value=dataIgnoreValue $
                         , /Float)
 
  if isa(maskImage) then begin
    ;which pixels are not masked?
    iPx = where(maskImage.getData() ne 0, /NULL)
  endif else begin
    ;no masking, all pixels are valid
    iPx = ulindgen(nTilePixels)
  endelse
  
  ;calculate the NDVI for non-masked pixels
  if isa(iPx) then begin
    ndviValues[iPx] = (data[1,iPx] - data[0,iPx]) / $
                      (data[1,iPx] + data[0,iPx])
  endif

  outputImage.writeData, ndviValues

  progressDone += tileLines
  progressBar.setProgress, progressDone
endwhile
  • Finally we cleanup the mask image object with
if isa(maskImage) then maskImage.cleanup

Here is the final code:

function MyNDVI_processing, parameters, settings
  ; check / define required parameters
  tileLines = 100 ;image lines of a single image tile
  wavelengthRed = 600.00 ; wavelength in nm
  wavelengthNIR = 800.00
  dataIgnoreValue = -9

  ; create the input and output image objects
  inputImage = hubIOImgInputImage(parameters['inputImage'])
  outputImage = hubIOImgOutputImage(parameters['outputImage'])
  if isa(parameters['maskImage']) then begin
    maskImage = hubIOImgInputImage(parameters['maskImage'], /Mask)
  endif

  ;create a progressBar
  progressBar = hubProgressBar(Title=settings.hubGetValue('title') $
              ;use GroupLeader if defined
              ,GroupLeader = settings.hubGetValue('groupLeader'))
  progressBar.setInfo, 'Calculating NDVI...'
  progressBar.setRange, [0,inputImage.getMeta('lines')]
  progressDone = 0

  ;locate band indices
  if parameters['bandChoice'] eq 0 then begin
    ;choice = 0 -> use user defined wavelength
    bandIndexRed = inputImage.locateWavelength(parameters['wlRed'])
    bandIndexNIR = inputImage.locateWavelength(parameters['wlNIR'])
  endif else begin
    ;choice = 1 -> use explicit band indices
    bandIndexRed = parameters['indexRed']
    bandIndexNIR = parameters['indexNIR']
  endelse
  

  ;initialize the input image reader
  inputImage.initReader, tileLines, /TileProcessing, /Slice $
           , SubsetBandPositions=[bandIndexRed, bandIndexNIR] $
           , DataType='float'
  
  ;initialize the mask image reader
  if isa(maskImage) then begin
    maskImage.initReader, tileLines, /TileProcessing, /Slice, /Mask
  endif

  ;initialize the NDVI image writer
  outputImage.copyMeta, inputImage, /CopySpatialInformation
  outputImage.initWriter, inputImage.getWriterSettings(SetBands=1)

  ;calculate the NDVI
  while ~inputImage.tileProcessingDone() do begin
    data = inputImage.getData()
    nTilePixels = n_elements(data[0,*])
    ;default ndvi values are set to data ignore value
    ndviValues = make_array(nTilePixels $
    , value=dataIgnoreValue $
    , /Float)
   
    if isa(maskImage) then begin
      ;which pixels are not masked?
      iPx = where(maskImage.getData() ne 0, /NULL)
    endif else begin
      ;no masking, all pixels are valid
      iPx = ulindgen(nTilePixels)
    endelse
    
    ;calculate the NDVI for non-masked pixels
    if isa(iPx) then begin
      ndviValues[iPx] = (data[1,iPx] - data[0,iPx]) / $
                        (data[1,iPx] + data[0,iPx])
    endif
    
    outputImage.writeData, ndviValues
    
    progressDone += tileLines
    progressBar.setProgress, progressDone
  endwhile

  ;cleanup objects
  inputImage.cleanup
  outputImage.cleanup
  if isa(maskImage) then maskImage.cleanup

  ; store results to be reported inside a hash
  result = hash() ;empty hash
  result += parameters.hubGetSubHash(['inputImage','outputImage'])
  return, result

end
  • Compile everything and test it. Calculate the NDVI for Hymap-Berlin-B-Image without and with using the mask Hymap-Berlin-B_Mask.

image025.png

As shown in the figure, providing a data ignore value to your output images helps to distinguish between correct calculated pixels and those with another semantic. Since the data ignore value is defined in the image header it might be evaluated by other routines that use your NDVI image. This ensures consistency and safety of your programs.


Now you know how to:

  • use the hubAMW_sampleSet widget to collect feature images and related label files
  • use the hubIOImgInputImage object to read a mask
  • use the data ignore value

Menu Buttons

To start your application from an EnMAP-Box menu button two conditions must be fulfilled:

  1. Your applications installation folder is located in the EnMAP-Box Application folder (APPDIR).
  2. Your applications installation folder contains a text file called enmap.men file that describes the buttons you want to add to the EnMAP-Box menu and the IDL event handlers they are connected to.

When the EnMAP-Box is started, it searches for all enmap.men files within its application folder:

image027.png

To add, remove or change a button you just need to modify the enmap.men file in your projects subfolder _resource and recall IDL> MyNDVI_make (as mentioned in Install your Application)

0 {Applications}

1 {MyNDVI}

2 {MyNDVI Application} {} {MyNDVI_event}

...

Each menu button is defined using the following syntax:
LEVEL {BUTTON NAME} [{ARGUMENT} {EVENT HANDLER PROCEDURE}] [{separator}]

  • This means you can create your own menu structure in this way

0 {My Own Main Menu}

1 {My Sub Menu}

2 {My Sub Sub Menu}
2 {My Second Sub Sub Menu}

1 {My Second Sub Menu}
1 {A separated Sub Menu} {separator}

image028.png

  • Connecting buttons with an event handler routine is done as followed:

...
1 {My Second Sub Menu}

2 {my event button} {} {my_event_routine}

...

The result looks like:

image029.png

  • By default the application wizard creates an event handler routine to call your application as in the following lines:
pro MyNDVI_event, event
     ; set up a default error handler
     
     @hubErrorCatch
     
     ; query information about the application (which is stored inside ...
     ; - note that menu buttons are defined inside the menu file '.\MyNDVI\_resource\enmap.men'
     ; - the following information is returned as a hash:
     ; applicationInfo['name'] = 'MyNDVI Application'
     ; applicationInfo['argument] = 'MyNDVI_ARGUMENT'
     ; applicationInfo['groupLeader'] = <EnMAP-Box top level base>

     applicationInfo = enmapBoxUserApp_getApplicationInfo(event)

     ; call the application main procedure
     ; if required, provide menu button information to it

     MyNDVI_application, applicationInfo

end

The @hubErrorCatch is used to insert the code defined in lib\hubAPI\hub\Error\huberrorcatch.pro. In case your application throws an error, it will be stopped here and shown to the user. This way your application is terminated only, but the EnMAP-Box keeps running. The error catch shows the stack trace, which helps you to locate the error’s origin and to debug your application.

  • You can use the button definition to pass string arguments to the applicationInfo-hash. For example

...
1 {My Second Sub Menu}

2 {my event button} {my info text} {my_event_routine}

...
and

applicationInfo = enmapBoxUserApp_getApplicationInfo(event)
print, application['argument']

will print out “my info text” to the command line.

  • This mechanism is used by some predefined API routines that help you to open specific files:

enmapBoxUserApp_showASCII_event to open ASCII or UTF encoded text files, e.g. by
1 {Information} {.../readme.txt} {enmapBoxUserApp_showASCII_event}

enmapBoxUserApp_showHTML_event to open local HTML files or URLs.
1 {Local} {.../index.html} {enmapBoxUserApp_showHTML_event}
1 {Online} {http://hu-berlin.de} {enmapBoxUserApp_showHTML_event}

enmapBoxUserApp_showPDF_event to open PDF files
1 {Tutorial} {.../tutorial.pdf} {enmapBoxUserApp_showPDF_event}

  • The keyword %APPDIR% will be replaced by the local path of the enmapProject/applications folder. 1 {About MyNDVI} {%appdir%/MyNDVI/copyrights/about.txt} {enmapBox...
    Using this way of relative path you do not need to know where a user installs the EnMAP-Box.

Progress Stop Watch

A hubProgressStopWatch can be used to stop the time required to run certain processes.

  • Add the respective line at the beginning of MyNDVI_processing:
function MyNDVI_processing, parameters, settings
watch = hubProgressStopWatch()
; check / define required parameters
tileLines = 100 ;image lines of a single image tile
  • It initializes the object and starts counting the time. Use the hubAPI documentation to inform yourself about taking split times during the process and stopping and starting the watch during processing.
  • Now use the method showResults at the end of your procedure.
; store results to be reported inside a hash
result = hash() ;empty hash
result += parameters.hubGetSubHash(['inputImage','outputImage'])
watch.showResults, title=settings['title'], description=['Calculation successful' $
,'No errors occurred']

image030.png

Application Settings

You can save settings of your application using a hub_setAppState. It requires the parameters application name, a key for your application state and its value. In the next run of your MyNDVI program, these application state/settings can be retrieved by hub_getAppState and saved to a variable. This variable is given over as parameter to the MyNDVI_getParameters and used as default value, including, in this example, the last input image and mask image.

  • Open your MyNDVI_application.pro and add the two lines with hub_getAppState and hub_setAppState:
pro MyNDVI_application, applicationInfo

  ; get global settings for this application
  settings = MyNDVI_getSettings()

  ; save application info to settings hash
  settings = settings + applicationInfo

  defaultValues = hub_getAppState('MyNDVI', 'stateHash_MyNDVI', default=hash())
  parameters = MyNDVI_getParameters(settings, defaultValues)

  if parameters['accept'] then begin

    reportInfo = MyNDVI_processing(parameters, settings)
  
    hub_setAppState, 'MyNDVI', 'stateHash_MyNDVI', parameters

    if parameters['showReport'] then begin
      MyNDVI_showReport, reportInfo, settings
    endif

  endif

end
  • Open your MyNDVI_getparameters.pro and add the defaultValues parameter to the function.
function MyNDVI_getParameters, settings, defaultValues
  • Now add to the hubAMW_inputSampleSet the default values by adding the following lines.
hubAMW_inputSampleSet, 'sampleSet', Title='Image', /Masking $
                     , ReferenceTitle='Mask', /ReferenceOptional $
           , Value=defaultValues.hubGetValue('inputImage') $
           , ReferenceValue=defaultValues.hubGetValue('maskImage')

Develop R and Python Applications

  • Use the application wizard to either create an R or a python application
enmapBoxDev_runApplicationWizard, /Rscript
enmapBoxDev_runApplicationWizard, /Pythonscript
  • Name it “RPlotApp”. This will create the basic structure of a hubAPI routine with a sample R/python script in the folder \%AppFolder\_lib\.
    The application should be able to collect parameters for an external R/python plotting routine. Therefore,
  • open the RPlotApp_getParameters routine and modify it such that the R plot parameters “col”, “xlab”, “ylab” and “type” are collected including the path of the created plot image:
hubAMW_program, groupLeader, Title=title
hubAMW_frame, Title='Parameters'
hubAMW_label, 'Color (e.g. black,red,blue)'
hubAMW_parameter, 'col', /STRING,TITLE=''
hubAMW_label, 'X axis label'
hubAMW_parameter, 'xlab', /STRING,TITLE=''
hubAMW_label, 'Y axis label'
hubAMW_parameter, 'ylab', /STRING,TITLE=''
hubAMW_label, 'Plot type (e.g. l,p)'
hubAMW_parameter, 'type', /STRING,TITLE=''
hubAMW_frame, Title='Output'
hubAMW_outputFilename,'plotFilename', TITLE='Path to image'
parameters = hubAMW_manage(/Dictionary)
  • open the RPlotApp_processing routine and modify it such that defined x and y vectors are passed to the R routine:
function RPlotApp_processing, parameters, Title=title, GroupLeader=groupLeader
  x = [-2*!PI : +2*!PI : 0.1]
  parameters['x'] = x
  parameters['y'] = cos(x)*x

  scriptFilename = filepath('script.r', ROOT_DIR=RPlotApp_getDirname(), SUBDIRECTORY='lib')
  scriptResult = hubR_runScript(scriptFilename, parameters, spawnResult, spawnError, Title=title, GroupLeader=groupLeader)
  
  ; store results to be reported inside a hash
  result = hash()
  hubHelper.openFile, parameters['plotFilename']
  
  return, result
  • Now open the RPlotApp_showreport routine and provide only a simple output example:
pro RPlotApp_showReport, reportInfo, Title=title
  report = hubReport(Title=title)
  report.addHeading, 'R_plot Output'
  report.saveHTML, /Show
end

This is the preparation for parameter collection from the EnMAP-Box. The sample R/python script located in \%AppFolder\_lib\ now has to be edited accordingly.

  • For our plot application, add the following lines to the script.r:
### user defined code ###

tiff(p$plotFilename, res=300, height=1500, width=1500)
plot(x=p$x, y=p$y, type=p$type, col=p$col,xlab=p$xlab, ylab=p$ylab)
dev.off()

#########################
  • Run RPlotApp_make and the application can be run via the GUI.

Updated