X
7246 Rate this article:
No rating

Introducing the ENVIHydratable Interface

Anonym

A while ago I wrote about how you could study the signatures of the ENVIRaster virtual functions and deterministically build the nested JSON objects needed to communicate the raster chain to ESE.  I’m here today to let you know that in ENVI 5.3 SP1 we’ve added some functionality to the ENVI API to make this much easier and automatic for you.

This is accomplished via two new facilities – the ENVIHydratable interface class and the ENVIHydrate() factory function.  Before I get into them in detail I will digress about their names.  While the logical assumption might be to call them ENVISerializable and ENVISerialize(), the term “serialization” implies a conversion to or from a text or byte array.  We debated the names of these capabilities, and went with Dehydrate/Hydrate as the final names as they imply the symmetry and invertibility we need while not implying the intermediate representation that serialize does.  If you look at the way these new features work, the ENVIHydratable::Dehydrate() method returns an IDL Hash object, and ENVIHydrate() expects a Hash object as well.  This decision was made to allow us the flexibility of not tying the ENVI API  to a particular transport stream or storage format.  The current ESE tasks use JSON, but a few years ago XML would have been the logical choice, and a few years from now who knows what the hot new technology might be.  So rather than couple the API to JSON right now, we kept it as IDL primitive data types, which are easily convertible to and from JSON (via JSON_Serialize() and JSON_Parse()), but that’s not the only option.  If someone wanted to use XML, they can write a serialize/deserialize module to do that.  Even with JSON, you can still use one of the encoders I blogged about last time before serialization so that you can guarantee full IEEE precision on your floating point parameters.

As I stated before, the ENVIHydratable class is an interface class – it defines the signature of the Dehydrate() method and provides for polymorphic identification via the ISA() function.  This class does not implement the Dehydrate() method, it just throws an error message, it is the responsibility of any class that inherits from it to override and provide a type specific implementation to return an appropriate  Hash.  In the ENVI 5.3 SP1 release, every class in the API that could be considered part of the data model inherits from ENVIHydratable and implements Dehydrate(): ENVIRaster, ENVIMosaicRaster, ENVIRasterSeries, ENVIVector, ENVIROI, ENVIGCPSet, ENVITiePointSet, ENVISpectralLibrary, ENVITime, ENVICoordSys, ENVIPsuedoRasterSpatialRef, ENVIRPCRasterSpatialRef, ENVIStandardRasterSpatialRef, ENVIGridDefinition, and ENVIRasterMetadata.  Since all the virtual raster functions return ENVIRaster objects, all of them documented here are also covered.

What does one of these Dehydrate Hashes look like?  Each Hash has a “factory” key, which is the name of the class/factory function with the “ENVI” prefix.  Readers of my original blog post will note that there I only mentioned ENVI::OpenRaster()  as the factory function for plain rasters, but now there is the ENVIURLRaster() function that effectively replaces ENVI::OpenRaster().  The rest of the keys in the Hash are the keywords of the factory function that have defined values.  The values for these keys have to be IDL primitives: scalar numbers or strings, arrays of numbers or strings, or List or Hash objects composed of primitives.  This leads to nest Hashes of Hashes when you have virtual raster chains.

The ENVIHydrate() function is the ultimate generic factory function.  It takes in a Hash, which must have a “factory” key in it.  That key must have a scalar string value associated with it, which is the name of the factory function to invoke after prepending “ENVI” to it.  Each of the other Hash keys are treated as keywords when invoking the factory function, and any value that is itself a Hash will have ENVIHydrate() called recursively on it before invoking the outer factory function.  This leads to a depth first traversal of the nested Hash structure, which correlates to the order in which the objects were created before being Dehydrated.

So let’s put this all in action and show how it works.  This example is a bit contrived, but illustrates the power of ENVIHydrateable and ENVIHydrate().  I start by loading qb_boulder_msi into ENVI, but then decide that I don’t like a bunch of its metadata.  So I create a new ENVIStandardRasterSpatialRef that moves it from UTM Zone 13N to 18N, an ENVIRasterMetadata object to reverse the order of the bands’ wavelengths, and an ENVITime object to set the acquisition time to now.  I then close the raster, and reopen it with all these overrides.  I then spatially subset the raster to the upper left quadrant, and calculate the NDVI from it, which is very different since it uses the new wavelengths to select band 2 as red and band 1 as near IR.  Lastly, I call Dehydrate() on the final raster, so we can see its pedigree.  This was all done in a headless ENVI session, which is then closes, along with all the rasters to prove there is nothing up my sleeve.  I then launch ENVI in UI mode, pass the dehydrated Hash into ENVIHydrate(), and use it to create  new layer in the view.

  e = ENVI(/HEADLESS)
 
  file = FilePath('qb_boulder_msi', ROOT=e.Root, SUBDIR='data')
 
  raster = e.OpenRaster(file)
  spatRef = raster.SpatialRef
 
  newSpatRef = ENVIStandardRasterSpatialRef(COORD_SYS_CODE=32618, $
    PIXEL_SIZE=spatRef.Pixel_Size, $
    TIE_POINT_MAP=spatRef.Tie_Point_Map, $
    TIE_POINT_PIXEL=spatRef.Tie_Point_Pixel)
 
  raster.Close
 
  metaOverride = ENVIRasterMetadata()
  metaOverride['wavelength'] = [ 830.0, 660.0, 560.0, 485.0 ]
 
  timeOverride = ENVITime(UNIX_SECONDS=SysTime(1))
 
  raster = e.OpenRaster(file, SPATIALREF_OVERRIDE=newSpatRef, $
                        METADATA_OVERRIDE=metaOverride, $
                        TIME_OVERRIDE=timeOverride)
 
  subRaster = ENVISubsetRaster(raster, $
                          SUB_RECT=[0, 0, 511, 511])
 
  ndvi = ENVISpectralIndexRaster(subRaster, INDEX='NDVI')
 
  hydratedForm = ndvi.Dehydrate()
  print, hydratedForm, /IMPLIED
 
  ndvi.Close
  subRaster.Close
  raster.Close
  e.Close
 
 
  e = ENVI()
 
  newRaster =  ENVIHydrate(hydratedForm)
 
  v = e.GetView()

  l = v.CreateLayer(newRaster)

Here is the implied print output of the dehydrated Hash (I did reorder some of the keys here for readability, but it uses a Hash not an OrderedHash so the order is dictated by the hashing function, not the order the keys were added by the objects) :

{
    "factory": "SpectralIndexRaster",
    "input_raster": {
        "factory": "SubsetRaster",
        "input_raster": {
            "factory": "URLRaster",
            "url": "C:\\Program Files\\Exelis\\ENVI53\\data\\qb_boulder_msi",
            "spatialref_override": {
                "factory": "StandardRasterSpatialRef",
                "coord_sys_code": 32618,
                "pixel_size": [2.7999999999999998, 2.7999999999999998],
                "rotation": 0.00000000000000000,
                "tie_point_map": [480267.20000000001, 4428978.4000000004],
                "tie_point_pixel": [0.00000000000000000, 0.00000000000000000]
            },
            "time_override": {
                "factory": "Time",
                "acquisition": "2016-01-21T17:07:45.987Z"
            },
            "metadata_override": {
                "factory": "RasterMetadata",
                "WAVELENGTH": [830.00000000000000, 660.00000000000000, 560.00000000000000, 485.00000000000000]
            }
        },
        "sub_rect": [0, 0, 511, 511]
    },
    "index": "NDVI"

}