Source code for espatools.raster

__all__ = [
    'Band',
    'ColorSchemes',
    'RasterSet',
]

import properties
import numpy as np

from .meta import *


[docs]class Band(properties.HasProperties): """Contains raster metadata and data for a single band.""" # metadata attributes name = properties.String('Name of the band') data_type = properties.String('Band data type') nlines = properties.Integer('number of lines') nsamps = properties.Integer('number of samples') product = properties.String('Data product') # Not required app_version = properties.String('app version', required=False) production_date = properties.String('production date', required=False) resample_method = properties.String('resample method', required=False) category = properties.String('Band category', required=False) source = properties.String('Band source', required=False) qa_description = properties.String('QA description', required=False) # TODO: class_values percent_coverage = properties.Float('percent coverage', required=False) # metadata: All required short_name = properties.String('Short name') long_name = properties.String('Long display name') file_name = properties.String('Original file name') pixel_size = properties.Instance('The pixel size', PixelSize) # data information fill_value = properties.Integer('fill value', default=-9999) saturate_value = properties.Integer('Saturate value', required=False) add_offset = properties.Float('Add offset', required=False) data_units = properties.String('Data units', required=False) scale_factor = properties.Float('Scaling factor', required=False) valid_range = properties.Instance('The valid data range', ValidRange, required=False) radiance = properties.Instance('The radiance', Lum, required=False) reflectance = properties.Instance('The reflectance', Lum, required=False) thermal_const = properties.Instance('The thermal const', ThermalConst, required=False) bitmap_description = properties.Dictionary( 'band bitmap description (not always present)', required=False, key_prop=properties.String('Key value'), value_prop=properties.String('Bitmap value description') ) # TODO: data validation causes a MAJOR slowdown. WAAAAYYY faster to not set # the data as a `properties` attribute. # data = properties.Array( # 'The band data as a 2D NumPy data', # shape=('*','*'), # ) data = None
[docs]class ColorSchemes(object): """A class to hold various RGB color schemes fo reference. These color schemes are defined on the `USGS website`_. .. _USGS website: https://www.usgs.gov/faqs/what-are-band-designations-landsat-satellites?qt-news_science_products=0#qt-news_science_products """ LOOKUP_TRUE_COLOR = dict( LANDSAT_8=['sr_band4', 'sr_band3', 'sr_band2'], LANDSAT_7=['sr_band3', 'sr_band2', 'sr_band1'], LANDSAT_5=['sr_band3', 'sr_band2', 'sr_band1'], LANDSAT_4=['sr_band3', 'sr_band2', 'sr_band1'], ) LOOKUP_INFRARED = dict( LANDSAT_8=['sr_band5', 'sr_band4', 'sr_band3'], LANDSAT_7=['sr_band4', 'sr_band3', 'sr_band2'], LANDSAT_5=['sr_band4', 'sr_band3', 'sr_band2'], LANDSAT_4=['sr_band4', 'sr_band3', 'sr_band2'], ) LOOKUP_FALSE_COLOR_A = dict( LANDSAT_8=['sr_band6', 'sr_band5', 'sr_band4'], LANDSAT_7=['sr_band5', 'sr_band4', 'sr_band3'], LANDSAT_5=['sr_band5', 'sr_band4', 'sr_band3'], LANDSAT_4=['sr_band5', 'sr_band4', 'sr_band3'], ) LOOKUP_FALSE_COLOR_B = dict( LANDSAT_8=['sr_band7', 'sr_band6', 'sr_band4'], LANDSAT_7=['sr_band7', 'sr_band5', 'sr_band3'], LANDSAT_5=['sr_band7', 'sr_band5', 'sr_band3'], LANDSAT_4=['sr_band7', 'sr_band5', 'sr_band3'], ) LOOKUP_FALSE_COLOR_C = dict( LANDSAT_8=['sr_band7', 'sr_band5', 'sr_band3'], LANDSAT_7=['sr_band7', 'sr_band4', 'sr_band2'], LANDSAT_5=['sr_band7', 'sr_band4', 'sr_band2'], LANDSAT_4=['sr_band7', 'sr_band4', 'sr_band2'], )
[docs]class RasterSet(properties.HasProperties): """The main class to hold a set of raster data. This contains all of the bands for a given set of rasters. This is generated by the ``RasterSetReader``. """ version = properties.String('version', required=False) global_metadata = properties.Instance('Raster metadata', RasterMetaData) # Bands bands = properties.Dictionary('A dictionary of bands for the swath', key_prop=properties.String('Band name'), value_prop=Band ) nlines = properties.Integer('The number of lines') nsamps = properties.Integer('The number of samples') pixel_size = properties.Instance('The pixel size', PixelSize) RGB_SCHEMES = dict( true=ColorSchemes.LOOKUP_TRUE_COLOR, infrared=ColorSchemes.LOOKUP_INFRARED, false_a=ColorSchemes.LOOKUP_FALSE_COLOR_A, false_b=ColorSchemes.LOOKUP_FALSE_COLOR_B, false_c=ColorSchemes.LOOKUP_FALSE_COLOR_C, )
[docs] def get_rgb(self, scheme='infrared', names=None): """Get an RGB color scheme based on predefined presets or specify your own band names to use. A given set of names always overrides a scheme. Note: Available schemes are defined in ``RGB_SCHEMES`` and include: - ``true`` - ``infrared`` - ``false_a`` - ``false_b`` - ``false_c`` """ if names is not None: if not isinstance(names, (list, tuple)) or len(names) != 3: raise RuntimeError('RGB band names improperly defined.') else: lookup = self.RGB_SCHEMES[scheme] names = lookup[self.global_metadata.satellite] # Now check that all bands are available: for nm in names: if nm not in self.bands.keys(): raise RuntimeError('Band (%s) unavailable.' % nm) # Get the RGB bands r = self.bands[names[0]].data g = self.bands[names[1]].data b = self.bands[names[2]].data # Note that the bands should already be masked from read. # If casted then there are np.nans present r = ((r - np.nanmin(r)) * (1/(np.nanmax(r) - np.nanmin(r)) * 255)).astype('uint8') g = ((g - np.nanmin(g)) * (1/(np.nanmax(g) - np.nanmin(g)) * 255)).astype('uint8') b = ((b - np.nanmin(b)) * (1/(np.nanmax(b) - np.nanmin(b)) * 255)).astype('uint8') return np.dstack([r, g, b])
[docs] def GetRGB(self, *args, **kwargs): return self.get_rgb(*args, **kwargs)
[docs] def validate(self): b = self.bands.get(list(self.bands.keys())[0]) ny, nx = b.nlines, b.nsamps dx, dy = b.pixel_size.x, b.pixel_size.y for name, band in self.bands.items(): if band.nlines != ny or band.nsamps != nx: raise RuntimeError('Band size mismatch.') if band.pixel_size.x != dx or band.pixel_size.y != dy: raise RuntimeError('Pixel size mismatch.') self.nlines = ny self.nsamps = nx self.pixel_size = b.pixel_size return properties.HasProperties.validate(self)
[docs] def to_pyvista(self, z=0.0): """Create a :class:`pyvista.UniformGrid` of this raster. Use the ``z`` argument to control the dataset's Z spatial reference. """ try: import pyvista as pv except ImportError: raise ImportError("Please install PyVista.") # Build the spatial reference output = pv.UniformGrid() output.dimensions = self.nsamps, self.nlines, 1 output.spacing = self.pixel_size.x, self.pixel_size.y, 1 corner = self.global_metadata.projection_information.corner_point[0] output.origin = corner.x, corner.y, z # Add data arrays clean = lambda arr: np.flip(arr, axis=0) for name, band in self.bands.items(): output[name] = clean(band.data).ravel() for scheme in list(self.RGB_SCHEMES.keys()): output[scheme] = clean(self.get_rgb(scheme=scheme)).reshape((-1,3)) # Add an array for the mask try: mask = clean(~band.data.mask).ravel() output["valid_mask"] = mask except NameError: pass # Return the dataset return output