Source code for ee_extra.QA.metrics

from abc import ABC, abstractmethod
from typing import Any, Dict, List, Type, Union

import ee

from ee_extra.utils import _get_case_insensitive_close_matches


[docs]def listMetrics() -> Dict[str, Type["Metric"]]: """Get the name and class of all QA metrics. Returns: A dictionary of Metric subclasses with class names as keys and classes as values. """ metrics: List[Type[Metric]] = Metric.__subclasses__() return {metric.__name__: metric for metric in metrics}
[docs]def getMetrics(names: Union[str, List[str]]) -> List[Type["Metric"]]: """Take one or more metric names and return a list of matching QA metrics. Args: names : A list or tuple of strings or a single string with the names of QA metrics. Returns: A list of Metric subclasses matching the input names. Raises: AttributeError : If at least one of the metrics is invalid. """ names = [names] if not isinstance(names, (list, tuple)) else names options = listMetrics() keys = list(options.keys()) selected = [] for name in names: try: selected.append(options[name]) except KeyError: close_matches = _get_case_insensitive_close_matches(name, keys, n=3) hint = " Close matches: {}.".format(close_matches) if close_matches else "" raise AttributeError( '"{}" is not a supported quality assessment metric. Choose from {}.{}'.format( name, keys, hint ) ) return selected
class Metric(ABC): """The abstract class that is implemented by all quality assessment metrics.""" def __new__( cls, original: ee.Image, modified: ee.Image, reproject: bool = True, **kwargs: Any ) -> ee.Dictionary: """Calculate and return the QA metric value when the class is instantiated.""" if reproject: original = original.resample("bilinear").reproject(modified.projection()) return cls._calculate(original, modified, **kwargs) @staticmethod @abstractmethod def _calculate( original: ee.Image, modified: ee.Image, **kwargs: Any ) -> Union[ee.Number, ee.Dictionary]: """Abstract method implemented by each Metric where the metric values are calculated between the input images. This should always be accessed indirectly by instantiating the Metric class rather than called. """ pass
[docs]class MSE(Metric): """Calculate band-wise Mean Squared Error (MSE) between an original and modified image with the same bands. A value of 0 represents no error. Args: original : The original image to use as a reference. modified : The modified image to compare to the original. reproject : If true, the original image will be reprojected to the modified image scale before calculation. kwargs : Additional keyword arguments passed to `ee.Image.reduceRegion`. Returns: A dictionary with band names as keys and MSE values as values. Examples: >>> from ee_extra.QA import metrics >>> bands = ["B4", "B3", "B2"] >>> img1 = ee.Image("COPERNICUS/S2_SR/20210703T170849_20210703T171938_T14SPG").select(bands) >>> img2 = ee.Image("COPERNICUS/S2_SR/20210708T170851_20210708T171925_T14SPG").select(bands) >>> metrics.MSE(img1, img2, bestEffort=True).getInfo() {'B2': 1329906.30450367, 'B3': 1175020.2097754816, 'B4': 1199736.6394475223} """ @staticmethod def _calculate( original: ee.Image, modified: ee.Image, **kwargs: Any ) -> ee.Dictionary: mse = ( original.subtract(modified) .pow(2) .reduceRegion(reducer=ee.Reducer.mean(), **kwargs) ) return mse
[docs]class RMSE(Metric): """Calculate band-wise Root-Mean Squared Error (RMSE) between an original and modified image with the same bands. A value of 0 represents no error. Args: original : The original image to use as a reference. modified : The modified image to compare to the original. reproject : If true, the original image will be reprojected to the modified image scale before calculation. kwargs : Additional keyword arguments passed to `ee.Image.reduceRegion`. Returns: A dictionary with band names as keys and RMSE values as values. Examples: >>> from ee_extra.QA import metrics >>> bands = ["B4", "B3", "B2"] >>> img1 = ee.Image("COPERNICUS/S2_SR/20210703T170849_20210703T171938_T14SPG").select(bands) >>> img2 = ee.Image("COPERNICUS/S2_SR/20210708T170851_20210708T171925_T14SPG").select(bands) >>> metrics.RMSE(img1, img2, bestEffort=True).getInfo() {'B2': 1153.215636602136, 'B3': 1083.9834914681503, 'B4': 1095.3249013181078} """ @staticmethod def _calculate( original: ee.Image, modified: ee.Image, **kwargs: Any ) -> ee.Dictionary: mse: ee.Dictionary = MSE(original, modified, **kwargs) sqrt_vals = ee.Array(mse.values()).sqrt().toList() rmse = ee.Dictionary.fromLists(mse.keys(), sqrt_vals) return rmse
[docs]class RASE(Metric): """Calculate image-wise Relative Average Spectral Error (RASE) between an original and modified image with the same bands. A value of 0 represents no error. Args: original : The original image to use as a reference. modified : The modified image to compare to the original. reproject : If true, the original image will be reprojected to the modified image scale before calculation. kwargs : Additional keyword arguments passed to `ee.Image.reduceRegion`. Returns: A dictionary with band names as keys and RASE values as values. References: 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 Examples: >>> from ee_extra.QA import metrics >>> bands = ["B4", "B3", "B2"] >>> img1 = ee.Image("COPERNICUS/S2_SR/20210703T170849_20210703T171938_T14SPG").select(bands) >>> img2 = ee.Image("COPERNICUS/S2_SR/20210708T170851_20210708T171925_T14SPG").select(bands) >>> metrics.RASE(img1, img2, bestEffort=True).getInfo() 125.72348999711838 """ @staticmethod def _calculate(original: ee.Image, modified: ee.Image, **kwargs: Any) -> ee.Number: mse: ee.Dictionary = MSE(original, modified, **kwargs) msek = ee.Number(mse.values().reduce(ee.Reducer.mean())) xbar = ( original.reduceRegion(ee.Reducer.mean(), **kwargs) .values() .reduce(ee.Reducer.mean()) ) rase = msek.sqrt().multiply(ee.Number(100).divide(xbar)) return rase
[docs]class ERGAS(Metric): """Calculate image-wise Dimensionless Global Relative Error of Synthesis (ERGAS) between an original and modified image with the same bands. A value of 0 represents no error. ERGAS results are weighted by the change in spatial resolution, identified from the `nominalScale` of the images. Args: original : The original image to use as a reference. modified : The modified image to compare to the original. reproject : If true, the original image will be reprojected to the modified image scale before calculation. kwargs : Additional keyword arguments passed to `ee.Image.reduceRegion`. Returns: A dictionary with band names as keys and ERGAS values as values. References: 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 Examples: >>> from ee_extra.QA import metrics >>> bands = ["B4", "B3", "B2"] >>> img1 = ee.Image("COPERNICUS/S2_SR/20210703T170849_20210703T171938_T14SPG").select(bands) >>> img2 = ee.Image("COPERNICUS/S2_SR/20210708T170851_20210708T171925_T14SPG").select(bands) >>> metrics.ERGAS(img1, img2, h=10, l=10, bestEffort=True).getInfo() 3774.9270912567363 """ def __new__( cls, original: ee.Image, modified: ee.Image, reproject: bool = True, **kwargs: Any ) -> ee.Number: """Calculate and return the QA metric value when the class is instantiated. Unlike other metrics, ERGAS must know the scale of the images before reprojection. """ l = original.projection().nominalScale() h = modified.projection().nominalScale() if reproject: original = original.resample("bilinear").reproject(modified.projection()) return cls._calculate(original, modified, h=h, l=l, **kwargs) @staticmethod def _calculate( # type: ignore original: ee.Image, modified: ee.Image, h: ee.Number, l: ee.Number, **kwargs: Any ) -> ee.Number: mse: ee.Dictionary = MSE(original, modified, **kwargs) msek = ee.Array(mse.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
[docs]class DIV(Metric): """Calculate band-wise Difference in Variance (DIV) between an original and modified image with the same bands. A value of 0 represents no change in variance. Args: original : The original image to use as a reference. modified : The modified image to compare to the original. reproject : If true, the original image will be reprojected to the modified image scale before calculation. kwargs : Additional keyword arguments passed to `ee.Image.reduceRegion`. Returns: A dictionary with band names as keys and DIV values as values. References: 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 Examples: >>> from ee_extra.QA import metrics >>> bands = ["B4", "B3", "B2"] >>> img1 = ee.Image("COPERNICUS/S2_SR/20210703T170849_20210703T171938_T14SPG").select(bands) >>> img2 = ee.Image("COPERNICUS/S2_SR/20210708T170851_20210708T171925_T14SPG").select(bands) >>> metrics.DIV(img1, img2, bestEffort=True).getInfo() {'B2': -0.11554855234271111, 'B3': -0.053204512324202424, 'B4': -0.07635340111753797} """ @staticmethod def _calculate( original: ee.Image, modified: ee.Image, **kwargs: Any ) -> ee.Dictionary: 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())
[docs]class bias(Metric): """Calculate band-wise bias between an original and modified image with the same bands. A value of 0 represents no bias. Args: original : The original image to use as a reference. modified : The modified image to compare to the original. reproject : If true, the original image will be reprojected to the modified image scale before calculation. kwargs : Additional keyword arguments passed to `ee.Image.reduceRegion`. Returns: A dictionary with band names as keys and bias values as values. References: 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 Examples: >>> from ee_extra.QA import metrics >>> bands = ["B4", "B3", "B2"] >>> img1 = ee.Image("COPERNICUS/S2_SR/20210703T170849_20210703T171938_T14SPG").select(bands) >>> img2 = ee.Image("COPERNICUS/S2_SR/20210708T170851_20210708T171925_T14SPG").select(bands) >>> metrics.bias(img1, img2, bestEffort=True).getInfo() {'B2': -0.09946485586107534, 'B3': -0.06336055792360518, 'B4': -0.008140914735944804} """ @staticmethod def _calculate( original: ee.Image, modified: ee.Image, **kwargs: Any ) -> ee.Dictionary: 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())
[docs]class CC(Metric): """Calculate band-wise correlation coefficient (CC) between an original and modified image with the same bands. A value of 1 represents perfect correlation. Args: original : The original image to use as a reference. modified : The modified image to compare to the original. reproject : If true, the original image will be reprojected to the modified image scale before calculation. kwargs : Additional keyword arguments passed to `ee.Image.reduceRegion`. Returns: A dictionary with band names as keys and CC values as values. References: Gonzalez, R. C., & Woods, R. E. (2018). Digital Image Processing. Pearson. Examples: >>> from ee_extra.QA import metrics >>> bands = ["B4", "B3", "B2"] >>> img1 = ee.Image("COPERNICUS/S2_SR/20210703T170849_20210703T171938_T14SPG").select(bands) >>> img2 = ee.Image("COPERNICUS/S2_SR/20210708T170851_20210708T171925_T14SPG").select(bands) >>> metrics.CC(img1, img2, bestEffort=True).getInfo() {'B2': 0.21228665943220423, 'B3': 0.02972520903338099, 'B4': 0.06995703183852472} """ @staticmethod def _calculate( original: ee.Image, modified: ee.Image, **kwargs: Any ) -> ee.Dictionary: xbar = ee.Image.constant( original.reduceRegion(ee.Reducer.mean(), **kwargs).values() ) ybar = ee.Image.constant( modified.reduceRegion(ee.Reducer.mean(), **kwargs).values() ) a = original.subtract(xbar) b = modified.subtract(ybar) x1 = ee.Array(a.multiply(b).reduceRegion(ee.Reducer.sum(), **kwargs).values()) x2 = ee.Array(a.pow(2).reduceRegion(ee.Reducer.sum(), **kwargs).values()) x3 = ee.Array(b.pow(2).reduceRegion(ee.Reducer.sum(), **kwargs).values()) cc = x1.divide(x2.multiply(x3).sqrt()) return ee.Dictionary.fromLists(original.bandNames(), cc.toList())
[docs]class CML(Metric): """Calculate band-wise change in mean luminance (CML) between an original and modified image with the same bands. A value of 1 represents no change in luminance. Args: original : The original image to use as a reference. modified : The modified image to compare to the original. reproject : If true, the original image will be reprojected to the modified image scale before calculation. kwargs : Additional keyword arguments passed to `ee.Image.reduceRegion`. Returns: A dictionary with band names as keys and CML values as values. References: 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 Examples: >>> from ee_extra.QA import metrics >>> bands = ["B4", "B3", "B2"] >>> img1 = ee.Image("COPERNICUS/S2_SR/20210703T170849_20210703T171938_T14SPG").select(bands) >>> img2 = ee.Image("COPERNICUS/S2_SR/20210708T170851_20210708T171925_T14SPG").select(bands) >>> metrics.CML(img1, img2, bestEffort=True).getInfo() {'B2': 0.99552102740279, 'B3': 0.9981158806578726, 'B4': 0.9999671314230874} """ @staticmethod def _calculate( original: ee.Image, modified: ee.Image, **kwargs: Any ) -> ee.Dictionary: 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())
[docs]class CMC(Metric): """Calculate band-wise change in mean contrast (CMC) between an original and modified image with the same bands. A value of 1 represents no change in contrast. Args: original : The original image to use as a reference. modified : The modified image to compare to the original. reproject : If true, the original image will be reprojected to the modified image scale before calculation. kwargs : Additional keyword arguments passed to `ee.Image.reduceRegion`. Returns: A dictionary with band names as keys and CMC values as values. References: 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 Examples: >>> from ee_extra.QA import metrics >>> bands = ["B4", "B3", "B2"] >>> img1 = ee.Image("COPERNICUS/S2_SR/20210703T170849_20210703T171938_T14SPG").select(bands) >>> img2 = ee.Image("COPERNICUS/S2_SR/20210708T170851_20210708T171925_T14SPG").select(bands) >>> metrics.CMC(img1, img2, bestEffort=True).getInfo() {'B2': 0.9985072836552178, 'B3': 0.9996642040598637, 'B4': 0.9993236505770505} """ @staticmethod def _calculate( original: ee.Image, modified: ee.Image, **kwargs: Any ) -> ee.Dictionary: 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())
[docs]class UIQI(Metric): """Calculate band-wise Universal Image Quality Index (UIQI) between an original and modified image with the same bands. A value of 1 represents perfect quality. Args: original : The original image to use as a reference. modified : The modified image to compare to the original. reproject : If true, the original image will be reprojected to the modified image scale before calculation. kwargs : Additional keyword arguments passed to `ee.Image.reduceRegion`. Returns: A dictionary with band names as keys and UIQI values as values. References: 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 Examples: >>> from ee_extra.QA import metrics >>> bands = ["B4", "B3", "B2"] >>> img1 = ee.Image("COPERNICUS/S2_SR/20210703T170849_20210703T171938_T14SPG").select(bands) >>> img2 = ee.Image("COPERNICUS/S2_SR/20210708T170851_20210708T171925_T14SPG").select(bands) >>> metrics.UIQI(img1, img2, bestEffort=True).getInfo() {'B2': 0.06990741860751772, 'B3': 0.029659240394113433, 'B4': 0.2110203688492463} """ @staticmethod def _calculate( original: ee.Image, modified: ee.Image, **kwargs: Any ) -> ee.Dictionary: cc: ee.Dictionary = CC(original, modified, **kwargs) cmc: ee.Dictionary = CMC(original, modified, **kwargs) cml: ee.Dictionary = CML(original, modified, **kwargs) cc = ee.Array(cc.values()) cmc = ee.Array(cmc.values()) cml = ee.Array(cml.values()) uiqi = cc.multiply(cml).multiply(cmc) return ee.Dictionary.fromLists(original.bandNames(), uiqi.toList())