first commit
This commit is contained in:
@@ -0,0 +1,615 @@
|
||||
"""
|
||||
Functions for computing remote sensing band indices on Digital Earth Africa
|
||||
data.
|
||||
"""
|
||||
|
||||
# Import required packages
|
||||
import warnings
|
||||
import numpy as np
|
||||
|
||||
# Define custom functions
|
||||
def calculate_indices(
|
||||
ds,
|
||||
index=None,
|
||||
collection=None,
|
||||
satellite_mission=None,
|
||||
custom_varname=None,
|
||||
normalise=True,
|
||||
drop=False,
|
||||
deep_copy=True,
|
||||
):
|
||||
"""
|
||||
Takes an xarray dataset containing spectral bands, calculates one of
|
||||
a set of remote sensing indices, and adds the resulting array as a
|
||||
new variable in the original dataset.
|
||||
|
||||
Last modified: July 2022
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ds : xarray Dataset
|
||||
A two-dimensional or multi-dimensional array with containing the
|
||||
spectral bands required to calculate the index. These bands are
|
||||
used as inputs to calculate the selected water index.
|
||||
|
||||
index : str or list of strs
|
||||
A string giving the name of the index to calculate or a list of
|
||||
strings giving the names of the indices to calculate:
|
||||
|
||||
* ``'ASI'`` (Artificial Surface Index, Yongquan Zhao & Zhe Zhu 2022)
|
||||
* ``'AWEI_ns'`` (Automated Water Extraction Index, no shadows, Feyisa 2014)
|
||||
* ``'AWEI_sh'`` (Automated Water Extraction Index, shadows, Feyisa 2014)
|
||||
* ``'BAEI'`` (Built-Up Area Extraction Index, Bouzekri et al. 2015)
|
||||
* ``'BAI'`` (Burn Area Index, Martin 1998)
|
||||
* ``'BSI'`` (Bare Soil Index, Rikimaru et al. 2002)
|
||||
* ``'BUI'`` (Built-Up Index, He et al. 2010)
|
||||
* ``'CMR'`` (Clay Minerals Ratio, Drury 1987)
|
||||
* ``'ENDISI'`` (Enhanced Normalised Difference for Impervious Surfaces Index, Chen et al. 2019)
|
||||
* ``'EVI'`` (Enhanced Vegetation Index, Huete 2002)
|
||||
* ``'FMR'`` (Ferrous Minerals Ratio, Segal 1982)
|
||||
* ``'IOR'`` (Iron Oxide Ratio, Segal 1982)
|
||||
* ``'LAI'`` (Leaf Area Index, Boegh 2002)
|
||||
* ``'MBI'`` (Modified Bare Soil Index, Nguyen et al. 2021)
|
||||
* ``'MNDWI'`` (Modified Normalised Difference Water Index, Xu 1996)
|
||||
* ``'MSAVI'`` (Modified Soil Adjusted Vegetation Index, Qi et al. 1994)
|
||||
* ``'NBI'`` (New Built-Up Index, Jieli et al. 2010)
|
||||
* ``'NBR'`` (Normalised Burn Ratio, Lopez Garcia 1991)
|
||||
* ``'NDBI'`` (Normalised Difference Built-Up Index, Zha 2003)
|
||||
* ``'NDCI'`` (Normalised Difference Chlorophyll Index, Mishra & Mishra, 2012)
|
||||
* ``'NDMI'`` (Normalised Difference Moisture Index, Gao 1996)
|
||||
* ``'NDSI'`` (Normalised Difference Snow Index, Hall 1995)
|
||||
* ``'NDTI'`` (Normalised Difference Turbidity Index, Lacaux et al. 2007)
|
||||
* ``'NDVI'`` (Normalised Difference Vegetation Index, Rouse 1973)
|
||||
* ``'NDWI'`` (Normalised Difference Water Index, McFeeters 1996)
|
||||
* ``'SAVI'`` (Soil Adjusted Vegetation Index, Huete 1988)
|
||||
* ``'TCB'`` (Tasseled Cap Brightness, Crist 1985)
|
||||
* ``'TCG'`` (Tasseled Cap Greeness, Crist 1985)
|
||||
* ``'TCW'`` (Tasseled Cap Wetness, Crist 1985)
|
||||
* ``'WI'`` (Water Index, Fisher 2016)
|
||||
|
||||
collection : str
|
||||
Deprecated in version 0.1.7. Use `satellite_mission` instead.
|
||||
|
||||
Valid options are:
|
||||
* ``'c2'`` (for USGS Landsat Collection 2)
|
||||
If 'c2', then `satellite_mission='ls'`.
|
||||
* ``'s2'`` (for Sentinel-2)
|
||||
If 's2', then `satellite_mission='s2'`.
|
||||
|
||||
satellite_mission : str
|
||||
An string that tells the function which satellite mission's data is
|
||||
being used to calculate the index. This is necessary because
|
||||
different satellite missions use different names for bands covering
|
||||
a similar spectra.
|
||||
|
||||
Valid options are:
|
||||
|
||||
* ``'ls'`` (for USGS Landsat)
|
||||
* ``'s2'`` (for Copernicus Sentinel-2)
|
||||
|
||||
custom_varname : str, optional
|
||||
By default, the original dataset will be returned with
|
||||
a new index variable named after `index` (e.g. 'NDVI'). To
|
||||
specify a custom name instead, you can supply e.g.
|
||||
`custom_varname='custom_name'`. Defaults to None, which uses
|
||||
`index` to name the variable.
|
||||
|
||||
normalise : bool, optional
|
||||
Some coefficient-based indices (e.g. ``'WI'``, ``'BAEI'``,
|
||||
``'AWEI_ns'``, ``'AWEI_sh'``, ``'TCW'``, ``'TCG'``, ``'TCB'``,
|
||||
``'EVI'``, ``'LAI'``, ``'SAVI'``, ``'MSAVI'``)
|
||||
produce different results if surface reflectance values are not
|
||||
scaled between 0.0 and 1.0 prior to calculating the index.
|
||||
Setting `normalise=True` first scales values to a 0.0-1.0 range
|
||||
by dividing by 10000.0. Defaults to True.
|
||||
|
||||
drop : bool, optional
|
||||
Provides the option to drop the original input data, thus saving
|
||||
space. If `drop=True`, returns only the index and its values.
|
||||
|
||||
deep_copy: bool, optional
|
||||
If `deep_copy=False`, calculate_indices will modify the original
|
||||
array, adding bands to the input dataset and not removing them.
|
||||
If the calculate_indices function is run more than once, variables
|
||||
may be dropped incorrectly producing unexpected behaviour. This is
|
||||
a bug and may be fixed in future releases. This is only a problem
|
||||
when `drop=True`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ds : xarray Dataset
|
||||
The original xarray Dataset inputted into the function, with a
|
||||
new varible containing the remote sensing index as a DataArray.
|
||||
If drop = True, the new variable/s as DataArrays in the
|
||||
original Dataset.
|
||||
"""
|
||||
|
||||
# Set ds equal to a copy of itself in order to prevent the function
|
||||
# from editing the input dataset. This is to prevent unexpected
|
||||
# behaviour though it uses twice as much memory.
|
||||
if deep_copy:
|
||||
ds = ds.copy(deep=True)
|
||||
|
||||
# Capture input band names in order to drop these if drop=True
|
||||
if drop:
|
||||
bands_to_drop = list(ds.data_vars)
|
||||
print(f"Dropping bands {bands_to_drop}")
|
||||
|
||||
# Dictionary containing remote sensing index band recipes
|
||||
index_dict = {
|
||||
# Normalised Difference Vegation Index, Rouse 1973
|
||||
"NDVI": lambda ds: (ds.nir - ds.red) / (ds.nir + ds.red),
|
||||
# Enhanced Vegetation Index, Huete 2002
|
||||
"EVI": lambda ds: (
|
||||
2.5 * ((ds.nir - ds.red) / (ds.nir + 6 * ds.red - 7.5 * ds.blue + 1))
|
||||
),
|
||||
# Leaf Area Index, Boegh 2002
|
||||
"LAI": lambda ds: (
|
||||
3.618
|
||||
* ((2.5 * (ds.nir - ds.red)) / (ds.nir + (6 * ds.red) - (7.5 * ds.blue) + 1))
|
||||
- 0.118
|
||||
),
|
||||
# Soil Adjusted Vegetation Index, Huete 1988
|
||||
"SAVI": lambda ds: ((1.5 * (ds.nir - ds.red)) / (ds.nir + ds.red + 0.5)),
|
||||
# Mod. Soil Adjusted Vegetation Index, Qi et al. 1994
|
||||
"MSAVI": lambda ds: (
|
||||
(2 * ds.nir + 1 - ((2 * ds.nir + 1) ** 2 - 8 * (ds.nir - ds.red)) ** 0.5)
|
||||
/ 2
|
||||
),
|
||||
# Normalised Difference Moisture Index, Gao 1996
|
||||
"NDMI": lambda ds: (ds.nir - ds.swir_1) / (ds.nir + ds.swir_1),
|
||||
# Normalised Burn Ratio, Lopez Garcia 1991
|
||||
"NBR": lambda ds: (ds.nir - ds.swir_2) / (ds.nir + ds.swir_2),
|
||||
# Burn Area Index, Martin 1998
|
||||
"BAI": lambda ds: (1.0 / ((0.10 - ds.red) ** 2 + (0.06 - ds.nir) ** 2)),
|
||||
# Normalised Difference Chlorophyll Index,
|
||||
# (Mishra & Mishra, 2012)
|
||||
"NDCI": lambda ds: (ds.red_edge_1 - ds.red) / (ds.red_edge_1 + ds.red),
|
||||
# Normalised Difference Snow Index, Hall 1995
|
||||
"NDSI": lambda ds: (ds.green - ds.swir_1) / (ds.green + ds.swir_1),
|
||||
# Normalised Difference Water Index, McFeeters 1996
|
||||
"NDWI": lambda ds: (ds.green - ds.nir) / (ds.green + ds.nir),
|
||||
# Modified Normalised Difference Water Index, Xu 2006
|
||||
"MNDWI": lambda ds: (ds.green - ds.swir_1) / (ds.green + ds.swir_1),
|
||||
# Normalised Difference Built-Up Index, Zha 2003
|
||||
"NDBI": lambda ds: (ds.swir_1 - ds.nir) / (ds.swir_1 + ds.nir),
|
||||
# Built-Up Index, He et al. 2010
|
||||
"BUI": lambda ds: ((ds.swir_1 - ds.nir) / (ds.swir_1 + ds.nir))
|
||||
- ((ds.nir - ds.red) / (ds.nir + ds.red)),
|
||||
# Built-up Area Extraction Index, Bouzekri et al. 2015
|
||||
"BAEI": lambda ds: (ds.red + 0.3) / (ds.green + ds.swir_1),
|
||||
# New Built-up Index, Jieli et al. 2010
|
||||
"NBI": lambda ds: (ds.swir_1 + ds.red) / ds.nir,
|
||||
# Bare Soil Index, Rikimaru et al. 2002
|
||||
"BSI": lambda ds: ((ds.swir_1 + ds.red) - (ds.nir + ds.blue))
|
||||
/ ((ds.swir_1 + ds.red) + (ds.nir + ds.blue)),
|
||||
# Automated Water Extraction Index (no shadows), Feyisa 2014
|
||||
"AWEI_ns": lambda ds: (
|
||||
4 * (ds.green - ds.swir_1) - (0.25 * ds.nir * +2.75 * ds.swir_2)
|
||||
),
|
||||
# Automated Water Extraction Index (shadows), Feyisa 2014
|
||||
"AWEI_sh": lambda ds: (
|
||||
ds.blue + 2.5 * ds.green - 1.5 * (ds.nir + ds.swir_1) - 0.25 * ds.swir_2
|
||||
),
|
||||
# Water Index, Fisher 2016
|
||||
"WI": lambda ds: (
|
||||
1.7204
|
||||
+ 171 * ds.green
|
||||
+ 3 * ds.red
|
||||
- 70 * ds.nir
|
||||
- 45 * ds.swir_1
|
||||
- 71 * ds.swir_2
|
||||
),
|
||||
# Tasseled Cap Wetness, Crist 1985
|
||||
"TCW": lambda ds: (
|
||||
0.0315 * ds.blue
|
||||
+ 0.2021 * ds.green
|
||||
+ 0.3102 * ds.red
|
||||
+ 0.1594 * ds.nir
|
||||
+ -0.6806 * ds.swir_1
|
||||
+ -0.6109 * ds.swir_2
|
||||
),
|
||||
# Tasseled Cap Greeness, Crist 1985
|
||||
"TCG": lambda ds: (
|
||||
-0.1603 * ds.blue
|
||||
+ -0.2819 * ds.green
|
||||
+ -0.4934 * ds.red
|
||||
+ 0.7940 * ds.nir
|
||||
+ -0.0002 * ds.swir_1
|
||||
+ -0.1446 * ds.swir_2
|
||||
),
|
||||
# Tasseled Cap Brightness, Crist 1985
|
||||
"TCB": lambda ds: (
|
||||
0.2043 * ds.blue
|
||||
+ 0.4158 * ds.green
|
||||
+ 0.5524 * ds.red
|
||||
+ 0.5741 * ds.nir
|
||||
+ 0.3124 * ds.swir_1
|
||||
+ -0.2303 * ds.swir_2
|
||||
),
|
||||
# Clay Minerals Ratio, Drury 1987
|
||||
"CMR": lambda ds: (ds.swir_1 / ds.swir_2),
|
||||
# Ferrous Minerals Ratio, Segal 1982
|
||||
"FMR": lambda ds: (ds.swir_1 / ds.nir),
|
||||
# Iron Oxide Ratio, Segal 1982
|
||||
"IOR": lambda ds: (ds.red / ds.blue),
|
||||
# Normalized Difference Turbidity Index, Lacaux, J.P. et al. 2007
|
||||
"NDTI": lambda ds: (ds.red - ds.green) / (ds.red + ds.green),
|
||||
# Modified Bare Soil Index, Nguyen et al. 2021
|
||||
"MBI": lambda ds: ((ds.swir_1 - ds.swir_2 - ds.nir) / (ds.swir_1 + ds.swir_2 + ds.nir)) + 0.5,
|
||||
}
|
||||
|
||||
# Enhanced Normalised Difference Impervious Surfaces Index, Chen et al. 2019
|
||||
def mndwi(ds):
|
||||
return (ds.green - ds.swir_1) / (ds.green + ds.swir_1)
|
||||
def swir_diff(ds):
|
||||
return ds.swir_1/ds.swir_2
|
||||
def alpha(ds):
|
||||
return (2*(np.mean(ds.blue)))/(np.mean(swir_diff(ds)) + np.mean(mndwi(ds)**2))
|
||||
def ENDISI(ds):
|
||||
m = mndwi(ds)
|
||||
s = swir_diff(ds)
|
||||
a = alpha(ds)
|
||||
return (ds.blue - (a)*(s + m**2))/(ds.blue + (a)*(s + m**2))
|
||||
|
||||
index_dict["ENDISI"] = ENDISI
|
||||
|
||||
## Artificial Surface Index, Yongquan Zhao & Zhe Zhu 2022
|
||||
def af(ds):
|
||||
AF = (ds.nir - ds.blue) / (ds.nir + ds.blue)
|
||||
AF_norm = (AF - AF.min(dim=["y","x"]))/(AF.max(dim=["y","x"]) - AF.min(dim=["y","x"]))
|
||||
return AF_norm
|
||||
def ndvi(ds):
|
||||
return (ds.nir - ds.red) / (ds.nir + ds.red)
|
||||
def msavi(ds):
|
||||
return ((2 * ds.nir + 1 - ((2 * ds.nir + 1) ** 2 - 8 * (ds.nir - ds.red)) ** 0.5) / 2 )
|
||||
def vsf(ds):
|
||||
NDVI = ndvi(ds)
|
||||
MSAVI = msavi(ds)
|
||||
VSF = 1 - NDVI * MSAVI
|
||||
VSF_norm = (VSF - VSF.min(dim=["y","x"]))/(VSF.max(dim=["y","x"]) - VSF.min(dim=["y","x"]))
|
||||
return VSF_norm
|
||||
def mbi(ds):
|
||||
return ((ds.swir_1 - ds.swir_2 - ds.nir) / (ds.swir_1 + ds.swir_2 + ds.nir)) + 0.5
|
||||
def embi(ds):
|
||||
MBI = mbi(ds)
|
||||
MNDWI = mndwi(ds)
|
||||
return (MBI - MNDWI - 0.5) / (MBI + MNDWI + 1.5)
|
||||
def ssf(ds):
|
||||
EMBI = embi(ds)
|
||||
SSF = 1 - EMBI
|
||||
SSF_norm = (SSF - SSF.min(dim=["y","x"]))/(SSF.max(dim=["y","x"]) - SSF.min(dim=["y","x"]))
|
||||
return SSF_norm
|
||||
# Overall modulation using the Modulation Factor (MF).
|
||||
def mf(ds):
|
||||
MF = ((ds.blue + ds.green) - (ds.nir + ds.swir_1)) / ((ds.blue + ds.green) + (ds.nir + ds.swir_1))
|
||||
MF_norm = (MF - MF.min(dim=["y","x"]))/(MF.max(dim=["y","x"]) - MF.min(dim=["y","x"]))
|
||||
return MF_norm
|
||||
def ASI(ds):
|
||||
AF = af(ds)
|
||||
VSF = vsf(ds)
|
||||
SSF = ssf(ds)
|
||||
MF = mf(ds)
|
||||
return AF * VSF * SSF * MF
|
||||
|
||||
index_dict["ASI"] = ASI
|
||||
|
||||
# If index supplied is not a list, convert to list. This allows us to
|
||||
# iterate through either multiple or single indices in the loop below
|
||||
indices = index if isinstance(index, list) else [index]
|
||||
|
||||
# calculate for each index in the list of indices supplied (indexes)
|
||||
for index in indices:
|
||||
|
||||
# Select an index function from the dictionary
|
||||
index_func = index_dict.get(str(index))
|
||||
|
||||
# If no index is provided or if no function is returned due to an
|
||||
# invalid option being provided, raise an exception informing user to
|
||||
# choose from the list of valid options
|
||||
if index is None:
|
||||
|
||||
raise ValueError(
|
||||
f"No remote sensing `index` was provided. Please "
|
||||
"refer to the function \ndocumentation for a full "
|
||||
"list of valid options for `index` (e.g. 'NDVI')"
|
||||
)
|
||||
|
||||
elif (
|
||||
index
|
||||
in [
|
||||
"WI",
|
||||
"BAEI",
|
||||
"AWEI_ns",
|
||||
"AWEI_sh",
|
||||
"EVI",
|
||||
"LAI",
|
||||
"SAVI",
|
||||
"MSAVI",
|
||||
]
|
||||
and not normalise
|
||||
):
|
||||
|
||||
warnings.warn(
|
||||
f"\nA coefficient-based index ('{index}') normally "
|
||||
"applied to surface reflectance values in the \n"
|
||||
"0.0-1.0 range was applied to values in the 0-10000 "
|
||||
"range. This can produce unexpected results; \nif "
|
||||
"required, resolve this by setting `normalise=True`"
|
||||
)
|
||||
|
||||
elif index_func is None:
|
||||
|
||||
raise ValueError(
|
||||
f"The selected index '{index}' is not one of the "
|
||||
"valid remote sensing index options. \nPlease "
|
||||
"refer to the function documentation for a full "
|
||||
"list of valid options for `index`"
|
||||
)
|
||||
|
||||
# Deprecation warning if `collection` is specified instead of `satellite_mission`.
|
||||
if collection is not None:
|
||||
warnings.warn('`collection` was deprecated in version 0.1.7. Use `satelite_mission` instead.',
|
||||
DeprecationWarning,
|
||||
stacklevel=2)
|
||||
# Map the collection values to the valid satellite_mission values.
|
||||
if collection == "c2":
|
||||
satellite_mission = "ls"
|
||||
elif collection == "s2":
|
||||
satellite_mission = "s2"
|
||||
# Raise error if no valid collection name is provided:
|
||||
else:
|
||||
raise ValueError(
|
||||
f"'{collection}' is not a valid option for "
|
||||
"`collection`. Please specify either \n"
|
||||
"'c2' or 's2'.")
|
||||
|
||||
|
||||
# Rename bands to a consistent format if depending on what satellite mission
|
||||
# is specified in `satellite_mission`. This allows the same index calculations
|
||||
# to be applied to all satellite missions. If no satellite mission was provided,
|
||||
# raise an exception.
|
||||
if satellite_mission is None:
|
||||
|
||||
raise ValueError(
|
||||
"No `satellite_mission` was provided. Please specify "
|
||||
"either 'ls' or 's2' to ensure the \nfunction "
|
||||
"calculates indices using the correct spectral "
|
||||
"bands."
|
||||
)
|
||||
|
||||
elif satellite_mission == "ls":
|
||||
sr_max = 1.0
|
||||
# Dictionary mapping full data names to simpler alias names
|
||||
# This only applies to properly-scaled "ls" data i.e. from
|
||||
# the Landsat geomedians. calculate_indices will not show
|
||||
# correct output for raw (unscaled) Landsat data (i.e. default
|
||||
# outputs from dc.load)
|
||||
bandnames_dict = {
|
||||
"SR_B1": "blue",
|
||||
"SR_B2": "green",
|
||||
"SR_B3": "red",
|
||||
"SR_B4": "nir",
|
||||
"SR_B5": "swir_1",
|
||||
"SR_B7": "swir_2",
|
||||
}
|
||||
|
||||
# Rename bands in dataset to use simple names (e.g. 'red')
|
||||
bands_to_rename = {
|
||||
a: b for a, b in bandnames_dict.items() if a in ds.variables
|
||||
}
|
||||
|
||||
elif satellite_mission == "s2":
|
||||
sr_max = 10000
|
||||
# Dictionary mapping full data names to simpler alias names
|
||||
bandnames_dict = {
|
||||
"nir_1": "nir",
|
||||
"B02": "blue",
|
||||
"B03": "green",
|
||||
"B04": "red",
|
||||
"B05": "red_edge_1",
|
||||
"B06": "red_edge_2",
|
||||
"B07": "red_edge_3",
|
||||
"B08": "nir",
|
||||
"B11": "swir_1",
|
||||
"B12": "swir_2",
|
||||
}
|
||||
|
||||
# Rename bands in dataset to use simple names (e.g. 'red')
|
||||
bands_to_rename = {
|
||||
a: b for a, b in bandnames_dict.items() if a in ds.variables
|
||||
}
|
||||
|
||||
# Raise error if no valid satellite_mission name is provided:
|
||||
else:
|
||||
raise ValueError(
|
||||
f"'{satellite_mission}' is not a valid option for "
|
||||
"`satellite_mission`. Please specify either \n"
|
||||
"'ls' or 's2'"
|
||||
)
|
||||
|
||||
# Apply index function
|
||||
try:
|
||||
# If normalised=True, divide data by 10,000 before applying func
|
||||
mult = sr_max if normalise else 1.0
|
||||
index_array = index_func(ds.rename(bands_to_rename) / mult)
|
||||
|
||||
except AttributeError:
|
||||
raise ValueError(
|
||||
f"Please verify that all bands required to "
|
||||
f"compute {index} are present in `ds`."
|
||||
)
|
||||
|
||||
# Add as a new variable in dataset
|
||||
output_band_name = custom_varname if custom_varname else index
|
||||
ds[output_band_name] = index_array
|
||||
|
||||
# Once all indexes are calculated, drop input bands if drop=True
|
||||
if drop:
|
||||
ds = ds.drop(bands_to_drop)
|
||||
|
||||
# Return input dataset with added water index variable
|
||||
return ds
|
||||
|
||||
def dualpol_indices(
|
||||
ds,
|
||||
co_pol='vv',
|
||||
cross_pol='vh',
|
||||
index=None,
|
||||
custom_varname=None,
|
||||
drop=False,
|
||||
deep_copy=True,
|
||||
):
|
||||
"""
|
||||
Takes an xarray dataset containing dual-polarization radar backscatter,
|
||||
calculates one or a set of indices, and adds the resulting array as a
|
||||
new variable in the original dataset.
|
||||
|
||||
Last modified: July 2021
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ds : xarray Dataset
|
||||
A two-dimensional or multi-dimensional array containing the
|
||||
two polarization bands.
|
||||
|
||||
co_pol: str
|
||||
Measurement name for the co-polarization band.
|
||||
Default is 'vv' for Sentinel-1.
|
||||
|
||||
cross_pol: str
|
||||
Measurement name for the cross-polarization band.
|
||||
Default is 'vh' for Sentinel-1.
|
||||
|
||||
index : str or list of strs
|
||||
A string giving the name of the index to calculate or a list of
|
||||
strings giving the names of the indices to calculate:
|
||||
|
||||
* ``'RVI'`` (Radar Vegetation Index for dual-pol, Trudel et al. 2012; Nasirzadehdizaji et al., 2019; Gururaj et al., 2019)
|
||||
* ``'VDDPI'`` (Vertical dual depolarization index, Periasamy 2018)
|
||||
* ``'theta'`` (pseudo scattering-type, Bhogapurapu et al. 2021)
|
||||
* ``'entropy'`` (pseudo scattering entropy, Bhogapurapu et al. 2021)
|
||||
* ``'purity'`` (co-pol purity, Bhogapurapu et al. 2021)
|
||||
* ``'ratio'`` (cross-pol/co-pol ratio)
|
||||
|
||||
custom_varname : str, optional
|
||||
By default, the original dataset will be returned with
|
||||
a new index variable named after `index` (e.g. 'RVI'). To
|
||||
specify a custom name instead, you can supply e.g.
|
||||
`custom_varname='custom_name'`. Defaults to None, which uses
|
||||
`index` to name the variable.
|
||||
|
||||
drop : bool, optional
|
||||
Provides the option to drop the original input data, thus saving
|
||||
space. If `drop=True`, returns only the index and its values.
|
||||
|
||||
deep_copy: bool, optional
|
||||
If `deep_copy=False`, calculate_indices will modify the original
|
||||
array, adding bands to the input dataset and not removing them.
|
||||
If the calculate_indices function is run more than once, variables
|
||||
may be dropped incorrectly producing unexpected behaviour. This is
|
||||
a bug and may be fixed in future releases. This is only a problem
|
||||
when `drop=True`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ds : xarray Dataset
|
||||
The original xarray Dataset inputted into the function, with a
|
||||
new varible containing the remote sensing index as a DataArray.
|
||||
If drop = True, the new variable/s as DataArrays in the
|
||||
original Dataset.
|
||||
"""
|
||||
|
||||
if not co_pol in list(ds.data_vars):
|
||||
raise ValueError(f"{co_pol} measurement is not in the dataset")
|
||||
if not cross_pol in list(ds.data_vars):
|
||||
raise ValueError(f"{cross_pol} measurement is not in the dataset")
|
||||
|
||||
# Set ds equal to a copy of itself in order to prevent the function
|
||||
# from editing the input dataset. This is to prevent unexpected
|
||||
# behaviour though it uses twice as much memory.
|
||||
if deep_copy:
|
||||
ds = ds.copy(deep=True)
|
||||
|
||||
# Capture input band names in order to drop these if drop=True
|
||||
if drop:
|
||||
bands_to_drop = list(ds.data_vars)
|
||||
print(f"Dropping bands {bands_to_drop}")
|
||||
|
||||
def ratio(ds):
|
||||
return ds[cross_pol] / ds[co_pol]
|
||||
|
||||
def purity(ds):
|
||||
return (1 - ratio(ds)) / (1 + ratio(ds))
|
||||
|
||||
def theta(ds):
|
||||
return np.arctan((1 - ratio(ds))**2 / (1 + ratio(ds)**2 - ratio(ds)))
|
||||
|
||||
def P1(ds):
|
||||
return 1 / (1 + ratio(ds))
|
||||
|
||||
def P2(ds):
|
||||
return 1 - P1(ds)
|
||||
|
||||
def entropy(ds):
|
||||
return P1(ds)*np.log2(P1(ds)) + P2(ds)*np.log2(P2(ds))
|
||||
|
||||
# Dictionary containing remote sensing index band recipes
|
||||
index_dict = {
|
||||
# Radar Vegetation Index for dual-pol, Trudel et al. 2012
|
||||
"RVI": lambda ds: 4*ds[cross_pol] / (ds[co_pol] + ds[cross_pol]),
|
||||
# Vertical dual depolarization index, Periasamy 2018
|
||||
"VDDPI": lambda ds: (ds[co_pol] + ds[cross_pol]) / ds[co_pol],
|
||||
# cross-pol/co-pol ratio
|
||||
"ratio": ratio,
|
||||
# co-pol purity, Bhogapurapu et al. 2021
|
||||
"purity": purity,
|
||||
# pseudo scattering-type, Bhogapurapu et al. 2021
|
||||
"theta": theta,
|
||||
# pseudo scattering entropy, Bhogapurapu et al. 2021
|
||||
"entropy": entropy,
|
||||
}
|
||||
|
||||
# If index supplied is not a list, convert to list. This allows us to
|
||||
# iterate through either multiple or single indices in the loop below
|
||||
indices = index if isinstance(index, list) else [index]
|
||||
|
||||
# calculate for each index in the list of indices supplied (indexes)
|
||||
for index in indices:
|
||||
|
||||
# Select an index function from the dictionary
|
||||
index_func = index_dict.get(str(index))
|
||||
|
||||
# If no index is provided or if no function is returned due to an
|
||||
# invalid option being provided, raise an exception informing user to
|
||||
# choose from the list of valid options
|
||||
if index is None:
|
||||
|
||||
raise ValueError(
|
||||
f"No radar `index` was provided. Please "
|
||||
"refer to the function \ndocumentation for a full "
|
||||
"list of valid options for `index` (e.g. 'RVI')"
|
||||
)
|
||||
|
||||
elif index_func is None:
|
||||
|
||||
raise ValueError(
|
||||
f"The selected index '{index}' is not one of the "
|
||||
"valid remote sensing index options. \nPlease "
|
||||
"refer to the function documentation for a full "
|
||||
"list of valid options for `index`"
|
||||
)
|
||||
|
||||
# Apply index function
|
||||
index_array = index_func(ds)
|
||||
|
||||
# Add as a new variable in dataset
|
||||
output_band_name = custom_varname if custom_varname else index
|
||||
ds[output_band_name] = index_array
|
||||
|
||||
# Once all indexes are calculated, drop input bands if drop=True
|
||||
if drop:
|
||||
ds = ds.drop(bands_to_drop)
|
||||
|
||||
# Return input dataset with added water index variable
|
||||
return ds
|
||||
Reference in New Issue
Block a user