Source code for cedar.sensors.landsat

""" Functions for dealing with Landsat data on GEE
"""
import datetime as dt
import logging
import warnings

import ee

from ..exceptions import EmptyCollectionError
from . import common

logger = logging.getLogger(__name__)

# Renaming stuff
BANDS_COMMON = ['blue', 'green', 'red', 'nir',
                'swir1', 'swir2', 'thermal', 'pixel_qa']

BANDS_LT4 = ['B1', 'B2', 'B3', 'B4', 'B5', 'B7',  'B6', 'pixel_qa']
BANDS_LT5 = BANDS_LT4.copy()
BANDS_LE7 = ['B1', 'B2', 'B3', 'B4', 'B5', 'B7',  'B6', 'pixel_qa']
BANDS_LC8 = ['B2', 'B3', 'B4', 'B5', 'B6', 'B7', 'B10', 'pixel_qa']

BANDS = {
    'COMMON': BANDS_COMMON,
    'LANDSAT/LT04/C01/T1_SR': BANDS_LT4,
    'LANDSAT/LT05/C01/T1_SR': BANDS_LT5,
    'LANDSAT/LE07/C01/T1_SR': BANDS_LE7,
    'LANDSAT/LC08/C01/T1_SR': BANDS_LC8,
}

#: dict[str, Number]: NoDataValues for Landsat collections
NODATA = {
    'LANDSAT/LT04/C01/T1_SR': -9999,
    'LANDSAT/LT05/C01/T1_SR': -9999,
    'LANDSAT/LE07/C01/T1_SR': -9999,
    'LANDSAT/LC08/C01/T1_SR': -9999,
}


_T1_SR_METADATA = [
    'CLOUD_COVER',
    'CLOUD_COVER_LAND',
    'EARTH_SUN_DISTANCE',
    'ESPA_VERSION',
    'GEOMETRIC_RMSE_MODEL',
    'GEOMETRIC_RMSE_MODEL_X',
    'GEOMETRIC_RMSE_MODEL_Y',
    'LANDSAT_ID',
    'LEVEL1_PRODUCTION_DATE',
    'SATELLITE',
    'SENSING_TIME',
    'SOLAR_AZIMUTH_ANGLE',
    'SOLAR_ZENITH_ANGLE',
    'SR_APP_VERSION',
    'WRS_PATH',
    'WRS_ROW',
    'system:id',
    'system:time_start',
    'system:version'
]
_T1_SR_METADATA_LT04 = ['IMAGE_QUALITY']
_T1_SR_METADATA_LT05 = ['IMAGE_QUALITY']
_T1_SR_METADATA_LE07 = ['IMAGE_QUALITY']
_T1_SR_METADATA_LC08 = ['IMAGE_QUALITY_OLI']
METADATA = {
    'LANDSAT/LT04/C01/T1_SR': _T1_SR_METADATA + _T1_SR_METADATA_LT04,
    'LANDSAT/LT05/C01/T1_SR': _T1_SR_METADATA + _T1_SR_METADATA_LT05,
    'LANDSAT/LE07/C01/T1_SR': _T1_SR_METADATA + _T1_SR_METADATA_LE07,
    'LANDSAT/LC08/C01/T1_SR': _T1_SR_METADATA + _T1_SR_METADATA_LC08,
}


[docs]def create_ard(collection, tile, date_start, date_end, filters=None, validate=False): """ Create an ARD :py:class:`ee.Image` Parameters ---------- collection : str GEE image collection name tile : stems.gis.grids.Tile STEMS TileGrid tile date_start : dt.datetime Starting period date_end : dt.datetime Ending period filters : Sequence[ee.Filter], optional Additional filters to apply over image collection validate : bool, optional Perform validity checks at cost of submission speed (runs ``.getInfo`` on metadata, requiring us to wait on client-server communication) Returns ------- ee.Image "ARD" image from collection with all observations within period Sequence[dict] Metadata, one dict per image """ # TODO: convert system:time_start to datetime/strftime assert isinstance(date_start, dt.datetime) assert isinstance(date_end, dt.datetime) # Get collection if isinstance(collection, ee.ImageCollection): imgcol = collection collection = imgcol.get('system:id').getInfo() else: imgcol = ee.ImageCollection(collection) if not collection in BANDS.keys(): raise KeyError(f'Image collection "{collection}" is unsupported') # Find images in tile imgcol = common.filter_collection_tile(imgcol, tile) # For each unique date of imagery in this image collection covering the tile imgcol = common.filter_collection_time(imgcol, date_start, date_end) # Apply additional filters if filters: logger.debug(f'Applying {len(filters)} filters over collection') imgcol = imgcol.filter(filters) # Select and rename bands # TODO: specify what bands get ordered band_names = BANDS['COMMON'] imgcol = imgcol.select(BANDS[collection], band_names) # Find number of unique observations (or, uniquely dated) imgcol_udates = common.get_collection_uniq_dates(imgcol) n_images = len(imgcol_udates) if n_images == 0: warnings.warn(f'Found 0 images for "{collection}" between ' f'{date_start}-{date_end}') # Loop over unique dates, making mosaics to eliminate north/south if needed logger.debug(f'Creating ARD for {n_images} images') prepped = [] for udate in sorted(imgcol_udates): # Prepare and get metadata for unique date img, meta = _prep_collection_image(imgcol, collection, udate, validate=validate) # Add image and metadata prepped.append((img, meta)) # Unpack if prepped: images, image_metadata = list(zip(*prepped)) else: images, image_metadata = [], [] # Re-create as collection and turn to bands (n_image x bands_per_image) tile_col = ee.ImageCollection.fromImages(images) tile_bands = tile_col.toBands().toInt16() # Remove mask nodata = NODATA[collection] tile_bands_unmasked = tile_bands.unmask(nodata) # Get all image metadata at once (saves time back and forth) images_metadata_ = list(ee.List(image_metadata).getInfo()) # Create overall metadata metadata = { 'bands': band_names, 'nodata': nodata, 'images': images_metadata_ } return tile_bands_unmasked, metadata
def _imgcol_metadata(imgcol, keys): """ Return metadata for Landsat image collection """ def inner(img, previous): meta = common.object_metadata(img, keys) previous_ = ee.List(previous) return ee.List(previous_.add(meta)) meta = imgcol.iterate(inner, ee.List([])) return meta def _prep_collection_image(imgcol, collection, date, validate=False): """ Prepare an image for and ``date`` from an ImageCollection """ # Filter for this date (day <-> day+1) date_end = (date + dt.timedelta(days=1)) imgcol_ = common.filter_collection_time(imgcol, date, date_end) if validate: # Check to make sure just 1 unique date _ = common.get_collection_uniq_dates(imgcol_) assert len(_) == 1 # Prepare all images in this collection (i.e., 1 or 2, depending on overlap) img = imgcol_.mosaic() # Get metadata from each image in new, potentially mosaiced ``img`` keys = METADATA[collection] meta = _imgcol_metadata(imgcol_, keys) return img, meta