__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