Introducing the ENVIHydratable Interface
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"
}