From 2eab071ae05fb3ded27c96fb4d453b6f82bef910 Mon Sep 17 00:00:00 2001 From: Aaron Zuspan <50475791+aazuspan@users.noreply.github.com> Date: Thu, 3 Mar 2022 02:40:12 -0800 Subject: [PATCH] Use `panSharpen` and `matchHistogram` from `ee_extra` (#60) * Updated Awesome Spectral Indices 2021-10-02 00:24:24 UTC * Updated GEE STAC IDs list 2021-10-13 00:22:50 UTC * Updated GEE STAC scale and offset list 2021-10-13 01:07:54 UTC * Updated GEE STAC IDs list 2021-10-21 00:27:34 UTC * Updated GEE STAC scale and offset list 2021-10-21 01:13:55 UTC * Updated GEE STAC IDs list 2021-10-30 00:24:09 UTC * Updated GEE STAC scale and offset list 2021-10-30 01:02:00 UTC * Updated Awesome Spectral Indices 2021-11-07 00:26:33 UTC * Updated GEE STAC scale and offset list 2021-11-10 01:04:32 UTC * Updated GEE STAC scale and offset list 2021-11-11 01:05:27 UTC * Updated GEE STAC IDs list 2021-11-12 00:26:48 UTC * Updated GEE STAC scale and offset list 2021-11-12 01:05:57 UTC * Updated Awesome Spectral Indices 2021-11-18 00:26:39 UTC * Updated GEE STAC IDs list 2021-11-20 00:24:50 UTC * Updated Awesome Spectral Indices 2021-11-20 00:25:36 UTC * Updated GEE STAC scale and offset list 2021-11-20 01:03:44 UTC * Updated Awesome Spectral Indices 2021-12-02 00:26:38 UTC * Updated GEE STAC IDs list 2021-12-09 00:26:44 UTC * Updated GEE STAC scale and offset list 2021-12-09 01:07:40 UTC * Updated GEE STAC IDs list 2021-12-11 00:25:13 UTC * Updated GEE STAC IDs list 2021-12-12 00:28:57 UTC * Updated GEE STAC IDs list 2021-12-13 00:26:20 UTC * Updated GEE STAC IDs list 2021-12-14 00:27:39 UTC * Updated GEE STAC scale and offset list 2021-12-14 01:11:14 UTC * Updated GEE STAC IDs list 2021-12-17 00:26:46 UTC * Updated GEE STAC scale and offset list 2021-12-17 01:10:00 UTC * Updated GEE STAC scale and offset list 2021-12-18 01:12:04 UTC * Updated GEE STAC scale and offset list 2021-12-19 01:13:33 UTC * Updated Awesome Spectral Indices 2021-12-26 00:30:16 UTC * Updated GEE STAC IDs list 2022-01-12 00:30:28 UTC * Updated GEE STAC scale and offset list 2022-01-12 01:11:44 UTC * Updated Awesome Spectral Indices 2022-01-17 00:29:27 UTC * Updated Awesome Spectral Indices 2022-01-18 00:30:38 UTC * Updated GEE STAC IDs list 2022-01-20 00:30:21 UTC * Updated GEE STAC scale and offset list 2022-01-20 01:19:30 UTC * Updated Awesome Spectral Indices 2022-01-21 00:26:44 UTC * Updated GEE STAC scale and offset list 2022-01-21 01:05:15 UTC * Updated GEE STAC IDs list 2022-01-22 00:25:08 UTC * Updated GEE STAC scale and offset list 2022-01-22 01:04:29 UTC * Updated GEE STAC IDs list 2022-02-02 00:28:23 UTC * Updated Awesome Spectral Indices 2022-02-02 00:29:54 UTC * Updated GEE STAC scale and offset list 2022-02-02 01:08:41 UTC * Updated GEE STAC IDs list 2022-02-05 00:22:28 UTC * Updated GEE STAC scale and offset list 2022-02-05 00:58:29 UTC * Updated Awesome Spectral Indices 2022-02-08 00:28:56 UTC * Updated Awesome Spectral Indices 2022-02-10 00:29:37 UTC * Updated GEE STAC IDs list 2022-02-11 00:28:18 UTC * Updated Awesome Spectral Indices 2022-02-11 00:31:29 UTC * Updated GEE STAC scale and offset list 2022-02-11 01:14:12 UTC * Updated GEE STAC IDs list 2022-02-13 00:31:00 UTC * Updated GEE STAC scale and offset list 2022-02-13 01:14:46 UTC * Updated GEE STAC IDs list 2022-02-15 00:28:49 UTC * Updated GEE STAC scale and offset list 2022-02-15 01:18:06 UTC * Updated GEE STAC IDs list 2022-02-17 00:27:54 UTC * Updated GEE STAC scale and offset list 2022-02-17 01:05:07 UTC * Updated GEE STAC IDs list 2022-02-19 00:32:21 UTC * Updated GEE STAC scale and offset list 2022-02-19 01:12:42 UTC * Updated GEE STAC IDs list 2022-02-27 00:33:18 UTC * Updated GEE STAC scale and offset list 2022-02-27 01:17:00 UTC * Remove panSharpen and matchHistogram implementations and import from ee_extra instead (#57) Co-authored-by: GitHub Action --- eemont/common.py | 936 -------------------------------------- eemont/image.py | 16 +- eemont/imagecollection.py | 8 +- 3 files changed, 15 insertions(+), 945 deletions(-) diff --git a/eemont/common.py b/eemont/common.py index 0d7a1d13..b32aa030 100755 --- a/eemont/common.py +++ b/eemont/common.py @@ -3,7 +3,6 @@ import os import re import warnings -from abc import ABC, abstractmethod import ee import ee_extra @@ -13,7 +12,6 @@ import pkg_resources import requests from box import Box -from ee_extra.STAC.utils import _get_platform_STAC from geopy.geocoders import get_geocoder_for_service from .extending import extend @@ -341,937 +339,3 @@ def _convert_pluscodes_to_lnglats(arr, geocoder, **kwargs): for i, element in enumerate(arr): converted[i] = _convert_pluscodes_to_lnglats(element, geocoder, **kwargs) return converted - - -# Histogram Matching -# -------------------------- - - -def _matchHistogram(source_img, target_img, bands, geometry, maxBuckets): - """Adjust the image's histogram to match a target image. - - Parameters - ---------- - self : ee.Image [this] - Image to adjust. - target : ee.Image - Image to match. - bands : dict - A dictionary of band names to match, with source bands as keys and target bands as values. - geometry : ee.Geometry, default=None - The region to match histograms in that overlaps both images. If none is provided, the geometry of the source image will be used. - maxBuckets : int, default=256 - The maximum number of buckets to use when building histograms. Will be rounded to the nearest power of 2. - - Returns - ------- - ee.Image - The adjusted image containing the matched source bands. - - """ - - def histogram_lookup(source_hist, target_hist): - """Build a list of target values with corresponding counts to source values from a source and target histogram. - - Parameters - ---------- - source_hist : ee.Array - A histogram for a source image returned by ee.Reducer.autoHistogram - target_hist : ee.Array - A histogram for a target image returned by ee.Reducer.autoHistogram - - Returns - ------- - tuple - Source histogram values and target histogram values with corresponding counts. - """ - source_vals = source_hist.slice(1, 0, 1).project([0]) - source_counts = source_hist.slice(1, 1, 2).project([0]) - source_counts = source_counts.divide(source_counts.get([-1])) - - target_vals = target_hist.slice(1, 0, 1).project([0]) - target_counts = target_hist.slice(1, 1, 2).project([0]) - target_counts = target_counts.divide(target_counts.get([-1])) - - def lookup_value(n): - """Find the first target value with at least n counts.""" - index = target_counts.gte(n).argmax() - return target_vals.get(index) - - target_lookup_vals = source_counts.toList().map(lookup_value) - - return (source_vals.toList(), target_lookup_vals) - - if not geometry: - geometry = ee.Element.geometry(source_img) - - bands = ee.Dictionary(bands) - source_bands = bands.keys() - target_bands = bands.values() - - source_img = source_img.select(source_bands) - target_img = target_img.select(target_bands) - - source_histogram = source_img.reduceRegion( - reducer=ee.Reducer.autoHistogram(maxBuckets=maxBuckets, cumulative=True), - geometry=geometry, - scale=30, - maxPixels=1e13, - bestEffort=True, - ) - - target_histogram = target_img.updateMask(source_img.mask()).reduceRegion( - reducer=ee.Reducer.autoHistogram(maxBuckets=maxBuckets, cumulative=True), - geometry=geometry, - scale=30, - maxPixels=1e13, - bestEffort=True, - ) - - def match_bands(source_band, target_band): - """Match the histogram of one source band to a target band. - - Parameters - ---------- - source_band : ee.String - The name of a band in the source image to adjust. - target_band : ee.String - The name of a corresponding band in the target image to match to. - - Returns - ------- - ee.Image - The source band histogram-matched to the target band. - """ - x, y = histogram_lookup( - source_histogram.getArray(source_band), - target_histogram.getArray(target_band), - ) - matched = source_img.select([source_band]).interpolate(x, y) - return matched - - matched = ( - ee.ImageCollection(bands.map(match_bands).values()) - .toBands() - .rename(source_bands) - ) - - matched = ee.Image(matched.copyProperties(source_img, source_img.propertyNames())) - matched = matched.set("eemont:HISTOGRAM_TARGET", target_img) - - return matched - - -def _filter_image_bands(img, keep_bands): - """Remove Image bands that aren't in list of bands to keep. Essentially a version of ee.Image.select() that doesn't - fail if bands are missing. - - Parameters - ---------- - img : ee.Image - An Image to select bands from. - keep_bands : list - A list of band names to keep in the Image. All other bands will be removed. Any specified bands that do not - exist will be ignored. - - Returns - ------- - ee.Image - The image with specified bands selected. - """ - bands = img.bandNames().filter(ee.Filter.inList("item", keep_bands)) - return img.select(bands) - - -def _panSharpen(source, method, qa, **kwargs): - """Apply panchromatic sharpening to an Image or Image Collection. Optionally, run quality assessments between the - original and sharpened Image(s) to measure spectral distortion and set results as properties of the sharpened - Image(s). - - Parameters - ---------- - source : ee.Image | ee.ImageCollection - Image or Image Collection to sharpen. - method : str, default="SFIM" - The sharpening algorithm to apply. Current options are "SFIM" (Smoothing Filter-based Intensity Modulation), - "HPFA" (High Pass Filter Addition), "PCS" (Principal Component Substitution), and "SM" (simple mean). Different - sharpening methods will produce different quality sharpening results in different scenarios. - qa : str | list, default=None - One or more optional quality assessment names to apply after sharpening, e.g. "MSE", "RASE", "UIQI", etc. - **kwargs : - Keyword arguments passed to ee.Image.reduceRegion() such as "geometry", "maxPixels", "bestEffort", etc. These - arguments are only used for PCS sharpening and quality assessments. - - Returns - ------- - ee.Image | ee.ImageCollection - The Image or ImageCollection with all sharpenable bands sharpened to the panchromatic resolution and quality - assessments run and set as properties. - """ - - def get_platform(source): - """Get the correct platform function for sharpening of supported platforms. - - Parameters - ---------- - source : ee.Image | ee.ImageCollection - An Image or ImageCollection identify a platform function for. - - Returns - ------- - function - A function that accepts Images and ImageCollections from the source platform. - """ - platform_methods = { - "LANDSAT/LC08/C01/T1_TOA": L8, - "LANDSAT/LC08/C01/T1_RT_TOA": L8, - "LANDSAT/LC08/C01/T2_TOA": L8, - "LANDSAT/LC08/C01/T1_RT": L8, - "LANDSAT/LO08/C01/T1": L8, - "LANDSAT/LC08/C01/T1": L8, - "LANDSAT/LO08/C01/T2": L8, - "LANDSAT/LC08/C01/T2": L8, - "LANDSAT/LE07/C01/T1_TOA": L7, - "LANDSAT/LE07/C01/T1_RT_TOA": L7, - "LANDSAT/LE07/C01/T2_TOA": L7, - "LANDSAT/LE07/C01/T1_RT": L7, - "LANDSAT/LE07/C01/T1": L7, - "LANDSAT/LE07/C01/T2": L7, - } - - platformDict = _get_platform_STAC(source) - platforms = list(platform_methods.keys()) - platform = platformDict["platform"] - - if platform not in platforms: - raise Exception( - "Sharpening is not supported for the {} platform. Use one of the following platforms: {}".format( - platform, platforms - ) - ) - - return platform_methods[platform] - - def L7(source): - """Apply panchromatic sharpening to an Image from a Landsat 7 platform by passing the sharpenable bands and - panchromatic band to a sharpening function. - - Parameters - ---------- - source : ee.Image - Image to sharpen from Landsat 7. - - Returns - ------- - ee.Image - The Image with all sharpenable bands sharpened to the panchromatic resolution. - """ - sharpenable_bands = ee.List(["B1", "B2", "B3", "B4", "B5", "B7"]) - sharpenable_img = _filter_image_bands(source, sharpenable_bands) - pan_img = source.select("B8") - - return apply_sharpening(sharpenable_img, pan_img) - - def L8(source): - """Apply panchromatic sharpening to an Image from a Landsat 8 platform by passing the sharpenable bands and - panchromatic band to a sharpening function. - - Parameters - ---------- - source : ee.Image - Image to sharpen from Landsat 8. - - Returns - ------- - ee.Image - The Image with all sharpenable bands sharpened to the panchromatic resolution. - """ - sharpenable_bands = ee.List(["B2", "B3", "B4", "B5", "B6", "B7"]) - sharpenable_img = _filter_image_bands(source, sharpenable_bands) - pan_img = source.select("B8") - - return apply_sharpening(sharpenable_img, pan_img) - - def apply_sharpening(source, pan): - """Identify and apply the correct sharpening algorithm to an Image. - - Parameters - ---------- - source : ee.Image - Image to sharpen with only sharpenable bands selected. - pan : ee.Image | ee.ImageCollection - Image with only the panchromatic band selected. - - Returns - ------- - ee.Image - The Image with all sharpenable bands sharpened to the panchromatic resolution. - """ - sharpener = get_sharpener_by_name(method) - sharpened = sharpener(source, pan) - - sharpened = ee.Image(sharpened.copyProperties(source, pan.propertyNames())) - sharpened = sharpened.updateMask(source.mask()) - - if qa: - sharpened = run_and_set_qa(source, sharpened) - - return sharpened - - def get_all_sharpeners(): - """Get the name and class of all supported Sharpener subclasses. - - Returns - ------- - requests.structures.CaseInsensitiveDict - A dictionary of Sharpener subclasses with class names as keys and classes as values. - """ - return requests.structures.CaseInsensitiveDict( - {cls.__name__: cls for cls in Sharpener.__subclasses__()} - ) - - def get_sharpener_by_name(name): - """Return a Sharpener subclass that matches a name. - - Parameters - ---------- - name : str - The name of a Sharpener, e.g. SFIM. - - Returns - ------- - Sharpener - The Sharpener subclass. - """ - options = get_all_sharpeners() - - try: - sharpener = options[name] - except KeyError: - raise AttributeError( - '"{}" is not a supported sharpening method. Supported methods: {}'.format( - name, list(options.keys()) - ) - ) - return sharpener - - class Sharpener(ABC): - """The abstract class that is implemented by all sharpening algorithms.""" - - def __new__(cls, img, pan): - """Apply pansharpening and return the sharpened image when the class is instantiated.""" - return cls.sharpen(img, pan) - - @abstractmethod - def sharpen(img, pan): - """Abstract method implemented by each Sharpener that applies sharpening.""" - pass - - class SFIM(Sharpener): - """The Smoothing Filter-based Intensity Modulation (SFIM) sharpener.""" - - @staticmethod - def sharpen(img, pan): - """Apply Smoothing Filter-based Intensity Modulation (SFIM) sharpening. - - Parameters - ---------- - img : ee.Image - Image to sharpen with only sharpenable bands selected. - pan : ee.Image - Image with only the panchromatic band selected. - - Returns - ------- - ee.Image - The Image with all sharpenable bands sharpened to the panchromatic resolution. - - Reference - --------- - Liu, J. G. (2000). Smoothing Filter-based Intensity Modulation: A spectral preserve image fusion technique for improving spatial details. International Journal of Remote Sensing, 21(18), 3461–3472. https://doi.org/10.1080/014311600750037499 - """ - img_scale = img.projection().nominalScale() - pan_scale = pan.projection().nominalScale() - kernel_width = img_scale.divide(pan_scale) - kernel = ee.Kernel.square(radius=kernel_width.divide(2)) - pan_smooth = pan.reduceNeighborhood( - reducer=ee.Reducer.mean(), kernel=kernel - ) - - img = img.resample("bicubic") - sharp = img.multiply(pan).divide(pan_smooth) - sharp = sharp.reproject(pan.projection()) - return sharp - - class HPFA(Sharpener): - """The High Pass Filter Addition (HPFA) sharpener.""" - - @staticmethod - def sharpen(img, pan): - """Apply High-Pass Filter Addition sharpening. - - Parameters - ---------- - img : ee.Image - Image to sharpen with only sharpenable bands selected. - pan : ee.Image - Image with only the panchromatic band selected. - - Returns - ------- - ee.Image - The Image with all sharpenable bands sharpened to the panchromatic resolution. - - Reference - --------- - Gangkofner, U. G., Pradhan, P. S., & Holcomb, D. W. (2008). Optimizing the High-Pass Filter Addition Technique for Image Fusion. Photogrammetric Engineering & Remote Sensing, 74(9), 1107–1118. https://doi.org/10.14358/pers.74.9.1107 - """ - img_scale = img.projection().nominalScale() - pan_scale = pan.projection().nominalScale() - kernel_width = img_scale.divide(pan_scale).multiply(2).add(1) - - img = img.resample("bicubic") - - center_val = kernel_width.pow(2).subtract(1) - center = kernel_width.divide(2).int() - kernel_row = ee.List.repeat(-1, kernel_width) - kernel = ee.List.repeat(kernel_row, kernel_width) - kernel = kernel.set( - center, ee.List(kernel.get(center)).set(center, center_val) - ) - kernel = ee.Kernel.fixed(weights=kernel, normalize=True) - - pan_hpf = pan.convolve(kernel) - sharp = img.add(pan_hpf) - sharp = sharp.reproject(pan.projection()) - - return sharp - - class PCS(Sharpener): - """The Principal Component Substitution (PCS) sharpener.""" - - @staticmethod - def sharpen(img, pan): - """Apply Principal Component Substitution (PCS) sharpening. - - Parameters - ---------- - img : ee.Image - Image to sharpen with only sharpenable bands selected. - pan : ee.Image - Image with only the panchromatic band selected. - - Returns - ------- - ee.Image - The Image with all sharpenable bands sharpened to the panchromatic resolution. - """ - img = img.resample("bicubic").reproject(pan.projection()) - band_names = img.bandNames() - - band_means = img.reduceRegion(ee.Reducer.mean(), **kwargs) - img_means = band_means.toImage(band_names) - img_centered = img.subtract(img_means) - - img_arr = img_centered.toArray() - covar = img_arr.reduceRegion(ee.Reducer.centeredCovariance(), **kwargs) - covar_arr = ee.Array(covar.get("array")) - eigens = covar_arr.eigen() - eigenvectors = eigens.slice(1, 1) - img_arr_2d = img_arr.toArray(1) - - principal_components = ( - ee.Image(eigenvectors) - .matrixMultiply(img_arr_2d) - .arrayProject([0]) - .arrayFlatten([band_names]) - ) - - pc1_name = principal_components.bandNames().get(0) - - # A dictionary can't use an ee.ComputedObject as a key, so set temporary band names - pc1 = principal_components.select([pc1_name]).rename(["PC1"]) - pan = pan.rename(["pan"]) - - pan_matched = pan.matchHistogram( - pc1, {"pan": "PC1"}, kwargs.get("geometry") - ).rename([pc1_name]) - - principal_components = principal_components.addBands( - pan_matched, overwrite=True - ) - - sharp_centered = ( - ee.Image(eigenvectors) - .matrixSolve(principal_components.toArray().toArray(1)) - .arrayProject([0]) - .arrayFlatten([band_names]) - ) - sharp = sharp_centered.add(img_means) - - return sharp - - class SM(Sharpener): - """The Simple Mean (SM) sharpener.""" - - @staticmethod - def sharpen(img, pan): - """Apply Simple Mean (SM) sharpening. - - Parameters - ---------- - img : ee.Image - Image to sharpen with only sharpenable bands selected. - pan : ee.Image - Image with only the panchromatic band selected. - - Returns - ------- - ee.Image - The Image with all sharpenable bands sharpened to the panchromatic resolution. - """ - img = img.resample("bicubic") - sharp = img.add(pan).multiply(0.5) - sharp = sharp.reproject(pan.projection()) - return sharp - - def run_and_set_qa(original, modified): - """Get any valid requested quality assessment functions and run each of them to assess the quality of the - sharpened Image. Set the results of each quality assessment as a new property (e.g. "eemont:RMSE"). - - Parameters - ---------- - original : ee.Image - The original, pre-sharpened image. - modified : ee.Image - The sharpened image. Quality assessments will be run to quantify distortion between this image and the - original. - - Results - ------- - ee.Image - The modified image with a new property set for each quality assessment. - """ - selected_metrics = get_qa_metrics_by_names(qa) - - original = original.select(modified.bandNames()) - - # Scale should match before calculating QA metrics - original = original.reproject(modified.projection()) - - for metric in selected_metrics: - values = metric(original, modified) - modified = modified.set("eemont:{}".format(metric.__name__), values) - - return modified - - def get_all_qa_metrics(): - """Get the name and class of all supported Metric subclasses. - - Returns - ------- - requests.structures.CaseInsensitiveDict - A dictionary of Metric subclasses with class names as keys and classes as values. - """ - return requests.structures.CaseInsensitiveDict( - {cls.__name__: cls for cls in Metric.__subclasses__()} - ) - - def get_qa_metrics_by_names(names): - """Take a list of names and return a list of matching Metric subclasses. - - Parameters - ---------- - names : list | tuple | str - A list or tuple of strings or a single string with the names of QA metrics. - - Returns - ------- - list - A list of Metric subclasses matching the input names. - """ - names = [names] if not isinstance(names, (list, tuple)) else names - - options = get_all_qa_metrics() - selected = [] - - for name in names: - try: - selected.append(options[name]) - except KeyError: - raise AttributeError( - '"{}" is not a supported quality assessment metric. Supported metrics: {}'.format( - name, list(options.keys()) - ) - ) - - return selected - - class Metric(ABC): - """The abstract class that is implemented by all quality assessment metrics.""" - - def __new__(cls, original, modified): - """Calculate and return the QA metric value when the class is instantiated.""" - return cls.calculate(original, modified) - - @abstractmethod - def calculate(original, modified): - """Abstract method implemented by each Metric where the metric values are calculated between the input images""" - return - - class MSE(Metric): - """The Mean Squared Error (MSE) metric.""" - - @staticmethod - def calculate(original, modified): - """Calculate band-wise Mean Squared Error (MSE) between an original and modified image of the same spatial - resolution. A value of 0 represents no error. - - Parameters - ---------- - original : ee.Image - The original image to use as a reference. - modified : ee.Image - The modified image to compare to the original. - - Returns - ------- - ee.Dictionary - A dictionary with band names as keys and MSE values as values. - """ - mse = ( - original.subtract(modified) - .pow(2) - .reduceRegion(reducer=ee.Reducer.mean(), **kwargs) - ) - - return mse - - class RMSE(Metric): - """The Root-Mean Squared Error (RMSE) metric.""" - - @staticmethod - def calculate(original, modified): - """Calculate band-wise Root-Mean Squared Error (RMSE) between an original and modified image of the same spatial - resolution. A value of 0 represents no error. - - Parameters - ---------- - original : ee.Image - The original image to use as a reference. - modified : ee.Image - The modified image to compare to the original. - - Returns - ------- - ee.Dictionary - A dictionary with band names as keys and RMSE values as values. - """ - mse = MSE()(original, modified) - sqrt_vals = ee.Array(mse.values()).sqrt().toList() - rmse = ee.Dictionary.fromLists(mse.keys(), sqrt_vals) - - return rmse - - class RASE(Metric): - """The Relative Average Spectral Error (RASE) metric.""" - - @staticmethod - def calculate(original, modified): - """Calculate image-wise Relative Average Spectral Error (RASE) between an original and modified image of the - same spatial resolution. A value of 0 represents no error. - - Parameters - ---------- - original : ee.Image - The original image to use as a reference. - modified : ee.Image - The modified image to compare to the original. - - Returns - ------- - ee.Number - The RASE value of the modified image. - - Reference - --------- - Vaiopoulos, A. D. (2011). Developing Matlab scripts for image analysis and quality assessment. Earth Resources and Environmental Remote Sensing/GIS Applications II. https://doi.org/10.1117/12.897806 - """ - mse = ee.Number(MSE(original, modified).values().reduce(ee.Reducer.mean())) - xbar = ( - original.reduceRegion(ee.Reducer.mean(), **kwargs) - .values() - .reduce(ee.Reducer.mean()) - ) - rase = mse.sqrt().multiply(ee.Number(100).divide(xbar)) - return rase - - class ERGAS(Metric): - """The Dimensionless Global Relative Error of Synthesis (ERGAS) metric.""" - - @staticmethod - def calculate(original, modified): - """Calculate image-wise Dimensionless Global Relative Error of Synthesis (ERGAS) between an original and - modified image of the same spatial resolution. A value of 0 represents no error. - - Parameters - ---------- - original : ee.Image - The original image to use as a reference. - modified : ee.Image - The modified image to compare to the original. - - Returns - ------- - ee.Number - The ERGAS value of the modified image. - - Reference - --------- - Vaiopoulos, A. D. (2011). Developing Matlab scripts for image analysis and quality assessment. Earth Resources and Environmental Remote Sensing/GIS Applications II. https://doi.org/10.1117/12.897806 - """ - h = modified.projection().nominalScale() - l = original.projection().nominalScale() - - msek = ee.Array(MSE(original, modified).values()) - xbark = ee.Array( - original.reduceRegion(ee.Reducer.mean(), **kwargs).values() - ) - - band_error = ee.Number( - msek.divide(xbark).toList().reduce(ee.Reducer.mean()) - ).sqrt() - ergas = band_error.multiply(h.divide(l).multiply(100)) - - return ergas - - class DIV(Metric): - """The Difference in Variance (DIV) metric.""" - - @staticmethod - def calculate(original, modified): - """Calculate band-wise Difference in Variance (DIV) between an original and modified image of the same spatial - resolution. A value of 0 represents no change in variance. - - Parameters - ---------- - original : ee.Image - The original image to use as a reference. - modified : ee.Image - The modified image to compare to the original. - - Returns - ------- - ee.Dictionary - A dictionary with band names as keys and DIV values as values. - - Reference - --------- - Vaiopoulos, A. D. (2011). Developing Matlab scripts for image analysis and quality assessment. Earth Resources - and Environmental Remote Sensing/GIS Applications II. https://doi.org/10.1117/12.897806 - """ - var_orig = ee.Array( - original.reduceRegion(ee.Reducer.variance(), **kwargs).values() - ) - var_mod = ee.Array( - modified.reduceRegion(ee.Reducer.variance(), **kwargs).values() - ) - - div = var_mod.divide(var_orig).multiply(-1).add(1) - return ee.Dictionary.fromLists(original.bandNames(), div.toList()) - - class bias(Metric): - """The bias metric.""" - - @staticmethod - def calculate(original, modified): - """Calculate band-wise bias between an original and modified image of the same spatial resolution. A value of 0 - represents no bias. - - Parameters - ---------- - original : ee.Image - The original image to use as a reference. - modified : ee.Image - The modified image to compare to the original. - - Returns - ------- - ee.Dictionary - A dictionary with band names as keys and bias values as values. - - Reference - --------- - Vaiopoulos, A. D. (2011). Developing Matlab scripts for image analysis and quality assessment. Earth Resources and Environmental Remote Sensing/GIS Applications II. https://doi.org/10.1117/12.897806 - """ - xbar = ee.Array(original.reduceRegion(ee.Reducer.mean(), **kwargs).values()) - ybar = ee.Array(modified.reduceRegion(ee.Reducer.mean(), **kwargs).values()) - - bias = ybar.divide(xbar).multiply(-1).add(1) - return ee.Dictionary.fromLists(original.bandNames(), bias.toList()) - - class CC(Metric): - """The Correlation Coefficient (CC) metric.""" - - @staticmethod - def calculate(original, modified): - """Calculate band-wise correlation coefficient (CC) between an original and modified image of the same spatial - resolution. A value of 1 represents perfect correlation. - - Parameters - ---------- - original : ee.Image - The original image to use as a reference. - modified : ee.Image - The modified image to compare to the original. - - Returns - ------- - ee.Dictionary - A dictionary with band names as keys and CC values as values. - - Reference - --------- - Gonzalez, R. C., & Woods, R. E. (2018). Digital Image Processing. Pearson. - """ - xbar = ee.Image.constant( - original.reduceRegion(ee.Reducer.mean(), **kwargs).values() - ) - ybar = ee.Image.constant( - modified.reduceRegion(ee.Reducer.mean(), **kwargs).values() - ) - - x_center = original.subtract(xbar) - y_center = modified.subtract(ybar) - - numerator = ee.Array( - x_center.multiply(y_center) - .reduceRegion(ee.Reducer.sum(), **kwargs) - .values() - ) - - x_denom = ee.Array( - x_center.pow(2).reduceRegion(ee.Reducer.sum(), **kwargs).values() - ) - y_denom = ee.Array( - y_center.pow(2).reduceRegion(ee.Reducer.sum(), **kwargs).values() - ) - - denom = x_denom.multiply(y_denom).sqrt() - - cc = numerator.divide(denom) - - return ee.Dictionary.fromLists(original.bandNames(), cc.toList()) - - class CML(Metric): - """The Change in Mean Luminance (CML) metric.""" - - @staticmethod - def calculate(original, modified): - """Calculate band-wise change in mean luminance (CML) between an original and modified image of the same spatial - resolution. A value of 1 represents no change in luminance. - - Parameters - ---------- - original : ee.Image - The original image to use as a reference. - modified : ee.Image - The modified image to compare to the original. - - Returns - ------- - ee.Dictionary - A dictionary with band names as keys and CML values as values. - - Reference - --------- - Wang, Z., & Bovik, A. C. (2002). A universal image quality index. IEEE Signal Processing Letters, 9(3), 81–84. https://doi.org/10.1109/97.995823 - """ - xbar = ee.Array(original.reduceRegion(ee.Reducer.mean(), **kwargs).values()) - ybar = ee.Array(modified.reduceRegion(ee.Reducer.mean(), **kwargs).values()) - - l = xbar.multiply(ybar).multiply(2).divide(xbar.pow(2).add(ybar.pow(2))) - - return ee.Dictionary.fromLists(original.bandNames(), l.toList()) - - class CMC(Metric): - """The Change in Mean Contrast (CMC) metric.""" - - @staticmethod - def calculate(original, modified): - """Calculate band-wise change in mean contrast (CMC) between an original and modified image of the same spatial - resolution. A value of 1 presents no change in contrast. - - Parameters - ---------- - original : ee.Image - The original image to use as a reference. - modified : ee.Image - The modified image to compare to the original. - - Returns - ------- - ee.Dictionary - A dictionary with band names as keys and CMC values as values. - - Reference - --------- - Wang, Z., & Bovik, A. C. (2002). A universal image quality index. IEEE Signal Processing Letters, 9(3), 81–84. https://doi.org/10.1109/97.995823 - """ - xvar = ee.Array( - original.reduceRegion(ee.Reducer.variance(), **kwargs).values() - ) - yvar = ee.Array( - modified.reduceRegion(ee.Reducer.variance(), **kwargs).values() - ) - xsd = ee.Array( - original.reduceRegion(ee.Reducer.stdDev(), **kwargs).values() - ) - ysd = ee.Array( - modified.reduceRegion(ee.Reducer.stdDev(), **kwargs).values() - ) - - c = xsd.multiply(ysd).multiply(2).divide(xvar.add(yvar)) - - return ee.Dictionary.fromLists(original.bandNames(), c.toList()) - - class UIQI(Metric): - """The Universal Image Quality Index (UIQI) metric.""" - - @staticmethod - def calculate(original, modified): - """Calculate band-wise Universal Image Quality Index (UIQI) between an original and modified image of the same - spatial resolution. A value of 1 represents perfect quality. - - Parameters - ---------- - original : ee.Image - The original image to use as a reference. - modified : ee.Image - The modified image to compare to the original. - - Returns - ------- - ee.Dictionary - A dictionary with band names as keys and UQI values as values. - - Reference - --------- - Wang, Z., & Bovik, A. C. (2002). A universal image quality index. IEEE Signal Processing Letters, 9(3), 81–84. https://doi.org/10.1109/97.995823 - """ - cc = ee.Array(CC(original, modified).values()) - l = ee.Array(CMC(original, modified).values()) - c = ee.Array(CML(original, modified).values()) - - uiq = cc.multiply(l).multiply(c) - - return ee.Dictionary.fromLists(original.bandNames(), uiq.toList()) - - platform = get_platform(source) - - if isinstance(source, ee.image.Image): - sharpened = platform(source) - elif isinstance(source, ee.imagecollection.ImageCollection): - sharpened = source.map(platform) - - return sharpened diff --git a/eemont/image.py b/eemont/image.py index 3bf67c7d..71b2fe35 100755 --- a/eemont/image.py +++ b/eemont/image.py @@ -3,9 +3,9 @@ import ee import ee_extra import ee_extra.Spectral.core +import ee_extra.Algorithms.core import requests -from .common import _matchHistogram, _panSharpen from .extending import extend @@ -1341,8 +1341,8 @@ def panSharpen(self, method="SFIM", qa=None, **kwargs): (Principal Component Substitution), and "SM" (simple mean). Different sharpening methods will produce different quality sharpening results in different scenarios. qa : str | list, default=None - One or more optional quality assessment names to apply after sharpening, - e.g. "MSE", "RASE", "UIQI", etc. + One or more optional quality assessment names to apply after sharpening. Results + will be stored as image properties with the pattern `eemont:metric`, e.g. `eemont:RMSE`. **kwargs : Keyword arguments passed to ee.Image.reduceRegion() such as "geometry", "maxPixels", "bestEffort", etc. These arguments are only used for PCS sharpening @@ -1362,7 +1362,7 @@ def panSharpen(self, method="SFIM", qa=None, **kwargs): >>> source = ee.Image("LANDSAT/LC08/C01/T1_TOA/LC08_047027_20160819") >>> sharp = source.panSharpen(method="HPFA", qa=["MSE", "RMSE"], maxPixels=1e13) """ - return _panSharpen(self, method, qa, **kwargs) + return ee_extra.Algorithms.core.panSharpen(img=self, method=method, qa=qa, prefix="eemont", **kwargs) @extend(ee.image.Image) @@ -1404,7 +1404,13 @@ def matchHistogram(self, target, bands, geometry=None, maxBuckets=256): >>> } >>> matched = source.matchHistogram(target, bands) """ - return _matchHistogram(self, target, bands, geometry, maxBuckets) + return ee_extra.Spectral.core.matchHistogram( + source=self, + target=target, + bands=bands, + geometry=geometry, + maxBuckets=maxBuckets + ) @extend(ee.image.Image) diff --git a/eemont/imagecollection.py b/eemont/imagecollection.py index 5393c0c7..c0768136 100755 --- a/eemont/imagecollection.py +++ b/eemont/imagecollection.py @@ -3,10 +3,10 @@ import ee import ee_extra import ee_extra.Spectral.core +import ee_extra.Algorithms.core import numpy as np import requests -from .common import _panSharpen from .extending import extend @@ -1043,8 +1043,8 @@ def panSharpen(self, method="SFIM", qa=None, **kwargs): sharpening methods will produce different quality sharpening results in different scenarios. qa : str | list, default=None - One or more optional quality assessment names to apply after sharpening, e.g. - "MSE", "RASE", "UIQI", etc. + One or more optional quality assessment names to apply after sharpening. Results + will be stored as image properties with the pattern `eemont:metric`, e.g. `eemont:RMSE`. **kwargs : Keyword arguments passed to ee.Image.reduceRegion() such as "geometry", "maxPixels", "bestEffort", etc. These arguments are only used for PCS sharpening @@ -1064,7 +1064,7 @@ def panSharpen(self, method="SFIM", qa=None, **kwargs): >>> source = ee.ImageCollection("LANDSAT/LC08/C01/T1_TOA") >>> sharp = source.panSharpen(method="HPFA", qa=["MSE", "RMSE"], maxPixels=1e13) """ - return _panSharpen(self, method, qa, **kwargs) + return ee_extra.Algorithms.core.panSharpen(img=self, method=method, qa=qa, prefix="eemont", **kwargs) @extend(ee.imagecollection.ImageCollection)