616 lines
24 KiB
Python
Executable File
616 lines
24 KiB
Python
Executable File
"""
|
|
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
|