Source code for xrspatial.multispectral

from __future__ import annotations

import warnings
from math import sqrt

import numba as nb
import numpy as np
import xarray as xr
from numba import cuda
from xarray import DataArray

from xrspatial.utils import (ArrayTypeFunctionMapping, _validate_raster, cuda_args, ngjit,
                             not_implemented_func, validate_arrays)
from xrspatial.dataset_support import supports_dataset_bands

# 3rd-party
try:
    import cupy
except ImportError:
    class cupy(object):
        ndarray = False

try:
    import dask.array as da
except ImportError:
    da = None


# Memory guard for the eager true_color() backends.
# _true_color_numpy / _true_color_cupy materialize an (H, W, 4) uint8 cube
# plus three (H, W) float32 normalize buffers and one (H, W) alpha buffer
# from np.where -- roughly 17 bytes per input pixel at peak.  Round up to
# a comfortable budget that also covers the temporaries inside
# _normalize_data_cpu (val - min_val, exp(...), etc.).  Dask paths build
# the cube lazily via da.stack and don't need the guard.
_TRUE_COLOR_BYTES_PER_PIXEL = 24


def _available_memory_bytes():
    """Best-effort estimate of available host memory in bytes."""
    try:
        with open('/proc/meminfo', 'r') as f:
            for line in f:
                if line.startswith('MemAvailable:'):
                    return int(line.split()[1]) * 1024  # kB -> bytes
    except (OSError, ValueError, IndexError):
        pass
    try:
        import psutil
        return psutil.virtual_memory().available
    except (ImportError, AttributeError):
        pass
    return 2 * 1024 ** 3  # 2 GB fallback


def _available_gpu_memory_bytes():
    """Best-effort estimate of free GPU memory in bytes.

    Returns 0 when the query fails -- callers treat that as a sentinel
    meaning "no GPU info, skip the guard".
    """
    try:
        import cupy as _cp
        free, _total = _cp.cuda.runtime.memGetInfo()
        return int(free)
    except Exception:
        return 0


def _check_true_color_memory(rows, cols):
    """Raise MemoryError if the numpy true_color buffers exceed 50% of RAM."""
    required = int(rows) * int(cols) * _TRUE_COLOR_BYTES_PER_PIXEL
    available = _available_memory_bytes()
    if required > 0.5 * available:
        raise MemoryError(
            f"true_color() on a {rows}x{cols} raster needs "
            f"~{required / 1e9:.1f} GB of working memory, but only "
            f"~{available / 1e9:.1f} GB is available. "
            f"Use a smaller raster or a dask-backed DataArray."
        )


def _check_true_color_gpu_memory(rows, cols):
    """Raise MemoryError if the cupy true_color buffers exceed 50% of free VRAM.

    Skipped silently when the free-memory query fails -- the cupy
    allocator will surface its own error in that case.
    """
    available = _available_gpu_memory_bytes()
    if available <= 0:
        return
    required = int(rows) * int(cols) * _TRUE_COLOR_BYTES_PER_PIXEL
    if required > 0.5 * available:
        raise MemoryError(
            f"true_color() on a {rows}x{cols} raster needs "
            f"~{required / 1e9:.1f} GB of GPU working memory, but only "
            f"~{available / 1e9:.1f} GB is free on the active device. "
            f"Use a smaller raster or a dask+cupy DataArray."
        )


@ngjit
def _arvi_cpu(nir_data, red_data, blue_data):
    out = np.full(nir_data.shape, np.nan, dtype=np.float32)
    rows, cols = nir_data.shape
    for y in range(0, rows):
        for x in range(0, cols):
            nir = nir_data[y, x]
            red = red_data[y, x]
            blue = blue_data[y, x]
            numerator = (nir - (2.0 * red) + blue)
            denominator = (nir + (2.0 * red) + blue)
            if denominator != 0.0:
                out[y, x] = numerator / denominator

    return out


@cuda.jit
def _arvi_gpu(nir_data, red_data, blue_data, out):
    y, x = cuda.grid(2)
    if y < out.shape[0] and x < out.shape[1]:
        nir = nir_data[y, x]
        red = red_data[y, x]
        blue = blue_data[y, x]
        numerator = (nir - (2.0 * red) + blue)
        denominator = (nir + (2.0 * red) + blue)
        if denominator != 0.0:
            out[y, x] = numerator / denominator


def _arvi_dask(nir_data, red_data, blue_data):
    out = da.map_blocks(_arvi_cpu, nir_data, red_data, blue_data,
                        meta=np.array(()))
    return out


def _arvi_cupy(nir_data, red_data, blue_data):
    griddim, blockdim = cuda_args(nir_data.shape)
    out = cupy.empty(nir_data.shape, dtype='f4')
    out[:] = cupy.nan
    _arvi_gpu[griddim, blockdim](nir_data, red_data, blue_data, out)
    return out


def _arvi_dask_cupy(nir_data, red_data, blue_data):
    out = da.map_blocks(_arvi_cupy, nir_data, red_data, blue_data,
                        dtype=cupy.float32, meta=cupy.array(()))
    return out


[docs] @supports_dataset_bands(nir='nir_agg', red='red_agg', blue='blue_agg') def arvi(nir_agg: xr.DataArray, red_agg: xr.DataArray, blue_agg: xr.DataArray, name='arvi'): """ Computes Atmospherically Resistant Vegetation Index. Allows for molecular and ozone correction with no further need for aerosol correction, except for dust conditions. Parameters ---------- nir_agg : xarray.DataArray 2D array of near-infrared band data. red_agg : xarray.DataArray 2D array of red band data. blue_agg : xarray.DataArray 2D array of blue band data. name : str, default='arvi' Name of output DataArray. Alternatively, a single ``xr.Dataset`` may be passed as the first argument with keyword arguments mapping band names to Dataset variables. For example:: arvi(ds, nir='B8', red='B4', blue='B2') Returns ------- arvi_agg : xarray.DataArray of the same type as inputs. 2D array arvi values. All other input attributes are preserved. References ---------- - MODIS: https://modis.gsfc.nasa.gov/sci_team/pubs/abstract_new.php?id=03667 # noqa Examples -------- In this example, we'll use data available in xrspatial.datasets .. plot:: :include-source: >>> from xrspatial.datasets import get_data >>> data = get_data('sentinel-2') # Open Example Data >>> nir = data['NIR'] >>> red = data['Red'] >>> blue = data['Blue'] >>> from xrspatial.multispectral import arvi >>> # Generate ARVI Aggregate Array >>> arvi_agg = arvi(nir_agg=nir, red_agg=red, blue_agg=blue) >>> nir.plot(cmap='Greys', aspect=2, size=4) >>> red.plot(aspect=2, size=4) >>> blue.plot(aspect=2, size=4) >>> arvi_agg.plot(aspect=2, size=4) .. sourcecode:: python >>> y1, x1, y2, x2 = 100, 100, 103, 104 >>> print(nir[y1:y2, x1:x2].data) [[1519. 1504. 1530. 1589.] [1491. 1473. 1542. 1609.] [1479. 1461. 1592. 1653.]] >>> print(red[y1:y2, x1:x2].data) [[1327. 1329. 1363. 1392.] [1309. 1331. 1423. 1424.] [1293. 1337. 1455. 1414.]] >>> print(blue[y1:y2, x1:x2].data) [[1281. 1270. 1254. 1297.] [1241. 1249. 1280. 1309.] [1239. 1257. 1322. 1329.]] >>> print(arvi_agg[y1:y2, x1:x2].data) [[ 0.02676934 0.02135493 0.01052632 0.01798942] [ 0.02130841 0.01114413 -0.0042343 0.01214013] [ 0.02488688 0.00816024 0.00068681 0.02650602]] """ _validate_raster(nir_agg, func_name='arvi', name='nir_agg') _validate_raster(red_agg, func_name='arvi', name='red_agg') _validate_raster(blue_agg, func_name='arvi', name='blue_agg') validate_arrays(red_agg, nir_agg, blue_agg) mapper = ArrayTypeFunctionMapping(numpy_func=_arvi_cpu, dask_func=_arvi_dask, cupy_func=_arvi_cupy, dask_cupy_func=_arvi_dask_cupy) out = mapper(red_agg)( nir_agg.data.astype('f4'), red_agg.data.astype('f4'), blue_agg.data.astype('f4') ) return DataArray(out, name=name, coords=nir_agg.coords, dims=nir_agg.dims, attrs=nir_agg.attrs)
# EVI ---------- @ngjit def _evi_cpu(nir_data, red_data, blue_data, c1, c2, soil_factor, gain): out = np.full(nir_data.shape, np.nan, dtype=np.float32) rows, cols = nir_data.shape for y in range(0, rows): for x in range(0, cols): nir = nir_data[y, x] red = red_data[y, x] blue = blue_data[y, x] numerator = nir - red denominator = nir + c1 * red - c2 * blue + soil_factor if denominator != 0.0: out[y, x] = gain * (numerator / denominator) return out @cuda.jit def _evi_gpu(nir_data, red_data, blue_data, c1, c2, soil_factor, gain, out): y, x = cuda.grid(2) if y < out.shape[0] and x < out.shape[1]: nir = nir_data[y, x] red = red_data[y, x] blue = blue_data[y, x] numerator = nir - red denominator = nir + c1 * red - c2 * blue + soil_factor if denominator != 0.0: out[y, x] = gain * (numerator / denominator) def _evi_dask(nir_data, red_data, blue_data, c1, c2, soil_factor, gain): out = da.map_blocks(_evi_cpu, nir_data, red_data, blue_data, c1, c2, soil_factor, gain, meta=np.array(())) return out def _evi_cupy(nir_data, red_data, blue_data, c1, c2, soil_factor, gain): griddim, blockdim = cuda_args(nir_data.shape) out = cupy.empty(nir_data.shape, dtype='f4') out[:] = cupy.nan args = (nir_data, red_data, blue_data, c1, c2, soil_factor, gain, out) _evi_gpu[griddim, blockdim](*args) return out def _evi_dask_cupy(nir_data, red_data, blue_data, c1, c2, soil_factor, gain): out = da.map_blocks(_evi_cupy, nir_data, red_data, blue_data, c1, c2, soil_factor, gain, dtype=cupy.float32, meta=cupy.array(())) return out
[docs] @supports_dataset_bands(nir='nir_agg', red='red_agg', blue='blue_agg') def evi(nir_agg: xr.DataArray, red_agg: xr.DataArray, blue_agg: xr.DataArray, c1=6.0, c2=7.5, soil_factor=1.0, gain=2.5, name='evi'): """ Computes Enhanced Vegetation Index. Allows for importved sensitivity in high biomass regions, de-coupling of the canopy background signal and reduction of atmospheric influences. Parameters ---------- nir_agg : xr.DataArray 2D array of near-infrared band data. red_agg : xr.DataArray 2D array of red band data. blue_agg : xr.DataArray 2D array of blue band data. c1 : float, default=6.0 First coefficient of the aerosol resistance term. c2 : float, default=7.5 Second coefficients of the aerosol resistance term. soil_factor : float, default=1.0 Soil adjustment factor between -1.0 and 1.0. gain : float, default=2.5 Amplitude adjustment factor. name : str, default='evi' Name of output DataArray. Alternatively, a single ``xr.Dataset`` may be passed as the first argument with keyword arguments mapping band names to Dataset variables. For example:: evi(ds, nir='B8', red='B4', blue='B2') Returns ------- evi_agg : xarray.DataArray of same type as inputs 2D array of evi values. All other input attributes are preserved. References ---------- - Wikipedia: https://en.wikipedia.org/wiki/Enhanced_vegetation_index Examples -------- .. plot:: :include-source: >>> from xrspatial.datasets import get_data >>> data = get_data('sentinel-2') # Open Example Data >>> nir = data['NIR'] >>> red = data['Red'] >>> blue = data['Blue'] >>> from xrspatial.multispectral import evi >>> # Generate EVI Aggregate Array >>> evi_agg = evi(nir_agg=nir, red_agg=red, blue_agg=blue) >>> nir.plot(aspect=2, size=4) >>> red.plot(aspect=2, size=4) >>> blue.plot(aspect=2, size=4) >>> evi_agg.plot(aspect=2, size=4) .. sourcecode:: python >>> y, x = 100, 100 >>> m, n = 3, 4 >>> print(nir[y1:y2, x1:x2].data) [[1519. 1504. 1530. 1589.] [1491. 1473. 1542. 1609.] [1479. 1461. 1592. 1653.]] >>> print(red[y1:y2, x1:x2].data) [[1327. 1329. 1363. 1392.] [1309. 1331. 1423. 1424.] [1293. 1337. 1455. 1414.]] >>> print(blue[y1:y2, x1:x2].data) [[1281. 1270. 1254. 1297.] [1241. 1249. 1280. 1309.] [1239. 1257. 1322. 1329.]] >>> print(evi_agg[y1:y2, x1:x2].data) [[-3.8247013 -9.51087 1.3733553 2.2960372] [11.818182 3.837838 0.6185031 1.3744428] [-8.53211 5.486726 0.8394608 3.5043988]] """ _validate_raster(nir_agg, func_name='evi', name='nir_agg') _validate_raster(red_agg, func_name='evi', name='red_agg') _validate_raster(blue_agg, func_name='evi', name='blue_agg') if not red_agg.shape == nir_agg.shape == blue_agg.shape: raise ValueError("input layers expected to have equal shapes") if not isinstance(c1, (float, int)): raise ValueError("c1 must be numeric") if not isinstance(c2, (float, int)): raise ValueError("c2 must be numeric") if soil_factor > 1.0 or soil_factor < -1.0: raise ValueError("soil factor must be between [-1.0, 1.0]") if gain < 0: raise ValueError("gain must be greater than 0") validate_arrays(nir_agg, red_agg, blue_agg) mapper = ArrayTypeFunctionMapping(numpy_func=_evi_cpu, dask_func=_evi_dask, cupy_func=_evi_cupy, dask_cupy_func=_evi_dask_cupy) out = mapper(red_agg)( nir_agg.data.astype('f4'), red_agg.data.astype('f4'), blue_agg.data.astype('f4'), c1, c2, soil_factor, gain ) return DataArray(out, name=name, coords=nir_agg.coords, dims=nir_agg.dims, attrs=nir_agg.attrs)
# GCI ---------- @ngjit def _gci_cpu(nir_data, green_data): out = np.full(nir_data.shape, np.nan, dtype=np.float32) rows, cols = nir_data.shape for y in range(0, rows): for x in range(0, cols): nir = nir_data[y, x] green = green_data[y, x] if green != 0: out[y, x] = nir / green - 1 return out @cuda.jit def _gci_gpu(nir_data, green_data, out): y, x = cuda.grid(2) if y < out.shape[0] and x < out.shape[1]: nir = nir_data[y, x] green = green_data[y, x] if green != 0: out[y, x] = nir / green - 1 def _gci_dask(nir_data, green_data): out = da.map_blocks(_gci_cpu, nir_data, green_data, meta=np.array(())) return out def _gci_cupy(nir_data, green_data): griddim, blockdim = cuda_args(nir_data.shape) out = cupy.empty(nir_data.shape, dtype='f4') out[:] = cupy.nan _gci_gpu[griddim, blockdim](nir_data, green_data, out) return out def _gci_dask_cupy(nir_data, green_data): out = da.map_blocks(_gci_cupy, nir_data, green_data, dtype=cupy.float32, meta=cupy.array(())) return out
[docs] @supports_dataset_bands(nir='nir_agg', green='green_agg') def gci(nir_agg: xr.DataArray, green_agg: xr.DataArray, name='gci'): """ Computes Green Chlorophyll Index. Used to estimate the content of leaf chorophyll and predict the physiological state of vegetation and plant health. Parameters ---------- nir_agg : xr.DataArray 2D array of near-infrared band data. green_agg : xr.DataArray 2D array of green band data. name : str, default='gci' Name of output DataArray. Alternatively, a single ``xr.Dataset`` may be passed as the first argument with keyword arguments mapping band names to Dataset variables. For example:: gci(ds, nir='B8', green='B3') Returns ------- gci_agg : xarray.DataArray of the same type as inputs 2D array of gci values. All other input attributes are preserved. References ---------- - Wikipedia: https://en.wikipedia.org/wiki/Enhanced_vegetation_index Examples -------- .. plot:: :include-source: >>> from xrspatial.datasets import get_data >>> data = get_data('sentinel-2') # Open Example Data >>> nir = data['NIR'] >>> green = data['Green'] >>> from xrspatial.multispectral import gci >>> # Generate GCI Aggregate Array >>> gci_agg = gci(nir_agg=nir, green_agg=green) >>> nir.plot(aspect=2, size=4) >>> green.plot(aspect=2, size=4) >>> gci_agg.plot(aspect=2, size=4) .. sourcecode:: python >>> y1, x1, y2, x2 = 100, 100, 103, 104 >>> print(nir[y1:y2, x1:x2].data) [[1519. 1504. 1530. 1589.] [1491. 1473. 1542. 1609.] [1479. 1461. 1592. 1653.]] >>> print(green[y1:y2, x1:x2].data]) [[1120. 1130. 1157. 1191.] [1111. 1137. 1190. 1221.] [1097. 1139. 1228. 1216.]] >>> print(gci_agg[y1:y2, x1:x2].data) [[0.35625 0.33097345 0.3223855 0.33417296] [0.3420342 0.29551452 0.29579833 0.31777233] [0.34822243 0.28270411 0.29641694 0.359375 ]] """ _validate_raster(nir_agg, func_name='gci', name='nir_agg') _validate_raster(green_agg, func_name='gci', name='green_agg') validate_arrays(nir_agg, green_agg) mapper = ArrayTypeFunctionMapping(numpy_func=_gci_cpu, dask_func=_gci_dask, cupy_func=_gci_cupy, dask_cupy_func=_gci_dask_cupy) out = mapper(nir_agg)(nir_agg.data.astype('f4'), green_agg.data.astype('f4')) return DataArray(out, name=name, coords=nir_agg.coords, dims=nir_agg.dims, attrs=nir_agg.attrs)
# NBR ----------
[docs] @supports_dataset_bands(nir='nir_agg', swir2='swir2_agg') def nbr(nir_agg: xr.DataArray, swir2_agg: xr.DataArray, name='nbr'): """ Computes Normalized Burn Ratio. Used to identify burned areas and provide a measure of burn severity. Parameters ---------- nir_agg : xr.DataArray 2D array of near-infrared band. swir_agg : xr.DataArray 2D array of shortwave infrared band. (Landsat 4-7: Band 6) (Landsat 8: Band 7) name : str, default='nbr' Name of output DataArray. Alternatively, a single ``xr.Dataset`` may be passed as the first argument with keyword arguments mapping band names to Dataset variables. For example:: nbr(ds, nir='B8', swir2='B12') Returns ------- nbr_agg : xr.DataArray of the same type as inputs 2D array of nbr values. All other input attributes are preserved. References ---------- - USGS: https://www.usgs.gov/land-resources/nli/landsat/landsat-normalized-burn-ratio # noqa Examples -------- .. plot:: :include-source: >>> from xrspatial.datasets import get_data >>> data = get_data('sentinel-2') # Open Example Data >>> nir = data['NIR'] >>> swir2 = data['SWIR2'] >>> from xrspatial.multispectral import nbr >>> # Generate NBR Aggregate Array >>> nbr_agg = nbr(nir_agg=nir, swir2_agg=swir2) >>> nir.plot(aspect=2, size=4) >>> swir2.plot(aspect=2, size=4) >>> nbr_agg.plot(aspect=2, size=4) .. sourcecode:: python >>> y1, x1, y2, x2 = 100, 100, 103, 104 >>> print(nir[y1:y2, x1:x2].data) [[1519. 1504. 1530. 1589.] [1491. 1473. 1542. 1609.] [1479. 1461. 1592. 1653.]] >>> print(swir2[y1:y2, x1:x2].data) [[1866. 1962. 2086. 2112.] [1811. 1900. 2012. 2041.] [1838. 1956. 2067. 2109.]] >>> print(nbr_agg[y1:y2, x1:x2].data) [[-0.10251108 -0.1321408 -0.15376106 -0.14131317] [-0.09691096 -0.12659353 -0.13224536 -0.11835616] [-0.10823033 -0.14486392 -0.12981689 -0.12121212]] """ _validate_raster(nir_agg, func_name='nbr', name='nir_agg') _validate_raster(swir2_agg, func_name='nbr', name='swir2_agg') validate_arrays(nir_agg, swir2_agg) mapper = ArrayTypeFunctionMapping( numpy_func=_normalized_ratio_cpu, dask_func=_run_normalized_ratio_dask, cupy_func=_run_normalized_ratio_cupy, dask_cupy_func=_run_normalized_ratio_dask_cupy, ) out = mapper(nir_agg)(nir_agg.data.astype('f4'), swir2_agg.data.astype('f4')) return DataArray(out, name=name, coords=nir_agg.coords, dims=nir_agg.dims, attrs=nir_agg.attrs)
[docs] @supports_dataset_bands(swir1='swir1_agg', swir2='swir2_agg') def nbr2(swir1_agg: xr.DataArray, swir2_agg: xr.DataArray, name='nbr2'): """ Computes Normalized Burn Ratio 2 "NBR2 modifies the Normalized Burn Ratio (NBR) to highlight water sensitivity in vegetation and may be useful in post-fire recovery studies." [1]_ Parameters ---------- swir1_agg : xr.DataArray 2D array of near-infrared band data. shortwave infrared band (Sentinel 2: Band 11) (Landsat 4-7: Band 5) (Landsat 8: Band 6) swir2_agg : xr.DataArray 2D array of shortwave infrared band data. (Landsat 4-7: Band 6) (Landsat 8: Band 7) name : str default='nbr2' Name of output DataArray. Alternatively, a single ``xr.Dataset`` may be passed as the first argument with keyword arguments mapping band names to Dataset variables. For example:: nbr2(ds, swir1='B11', swir2='B12') Returns ------- nbr2_agg : xr.DataArray of same type as inputs. 2D array of nbr2 values. All other input attributes are preserved. Notes ----- .. [1] https://www.usgs.gov/land-resources/nli/landsat/landsat-normalized-burn-ratio-2 # noqa Examples -------- .. plot:: :include-source: >>> from xrspatial.datasets import get_data >>> data = get_data('sentinel-2') # Open Example Data >>> swir1 = data['SWIR1'] >>> swir2 = data['SWIR2'] >>> from xrspatial.multispectral import nbr2 >>> # Generate NBR2 Aggregate Array >>> nbr2_agg = nbr2(swir1_agg=swir1, swir2_agg=swir2) >>> swir1.plot(aspect=2, size=4) >>> swir2.plot(aspect=2, size=4) >>> nbr2_agg.plot(aspect=2, size=4) .. sourcecode:: python >>> y1, x1, y2, x2 = 100, 100, 103, 104 >>> print(swir1[y1:y2, x1:x2].data) [[2092. 2242. 2333. 2382.] [2017. 2150. 2303. 2344.] [2124. 2244. 2367. 2452.]] >>> print(swir2[y1:y2, x1:x2].data) [[1866. 1962. 2086. 2112.] [1811. 1900. 2012. 2041.] [1838. 1956. 2067. 2109.]] >>> print(nbr2_agg[y1:y2, x1:x2].data) [[0.05709954 0.06660324 0.055895 0.06008011] [0.053814 0.0617284 0.06743917 0.0690992 ] [0.07218576 0.06857143 0.067659 0.07520281]] """ _validate_raster(swir1_agg, func_name='nbr2', name='swir1_agg') _validate_raster(swir2_agg, func_name='nbr2', name='swir2_agg') validate_arrays(swir1_agg, swir2_agg) mapper = ArrayTypeFunctionMapping( numpy_func=_normalized_ratio_cpu, dask_func=_run_normalized_ratio_dask, cupy_func=_run_normalized_ratio_cupy, dask_cupy_func=_run_normalized_ratio_dask_cupy, ) out = mapper(swir1_agg)(swir1_agg.data.astype('f4'), swir2_agg.data.astype('f4')) return DataArray(out, name=name, coords=swir1_agg.coords, dims=swir1_agg.dims, attrs=swir1_agg.attrs)
# NDVI ----------
[docs] @supports_dataset_bands(nir='nir_agg', red='red_agg') def ndvi(nir_agg: xr.DataArray, red_agg: xr.DataArray, name='ndvi'): """ Computes Normalized Difference Vegetation Index (NDVI). Used to determine if a cell contains live green vegetation. Parameters ---------- nir_agg : xr.DataArray 2D array of near-infrared band data. red_agg : xr.DataArray 2D array red band data. name : str default='ndvi' Name of output DataArray. Alternatively, a single ``xr.Dataset`` may be passed as the first argument with keyword arguments mapping band names to Dataset variables. For example:: ndvi(ds, nir='B8', red='B4') Returns ------- ndvi_agg : xarray.DataArray of same type as inputs 2D array of ndvi values. All other input attributes are preserved. References ---------- - Chris Holden: http://ceholden.github.io/open-geo-tutorial/python/chapter_2_indices.html # noqa Examples -------- .. plot:: :include-source: >>> from xrspatial.datasets import get_data >>> data = get_data('sentinel-2') # Open Example Data >>> nir = data['NIR'] >>> red = data['Red'] >>> from xrspatial.multispectral import ndvi >>> # Generate NDVI Aggregate Array >>> ndvi_agg = ndvi(nir_agg=nir, red_agg=red) >>> nir.plot(aspect=2, size=4) >>> red.plot(aspect=2, size=4) >>> ndvi_agg.plot(aspect=2, size=4) .. sourcecode:: python >>> y1, x1, y2, x2 = 100, 100, 103, 104 >>> print(nir[y1:y2, x1:x2].data) [[1519. 1504. 1530. 1589.] [1491. 1473. 1542. 1609.] [1479. 1461. 1592. 1653.]] >>> print(red[y1:y2, x1:x2].data) [[1327. 1329. 1363. 1392.] [1309. 1331. 1423. 1424.] [1293. 1337. 1455. 1414.]] >>> print(ndvi_agg[y1:y2, x1:x2].data) [[0.06746311 0.06177197 0.05772555 0.0660852 ] [0.065 0.05064194 0.04013491 0.06099571] [0.06709956 0.04431737 0.04496226 0.07792632]] """ _validate_raster(nir_agg, func_name='ndvi', name='nir_agg') _validate_raster(red_agg, func_name='ndvi', name='red_agg') validate_arrays(nir_agg, red_agg) mapper = ArrayTypeFunctionMapping( numpy_func=_normalized_ratio_cpu, dask_func=_run_normalized_ratio_dask, cupy_func=_run_normalized_ratio_cupy, dask_cupy_func=_run_normalized_ratio_dask_cupy, ) out = mapper(nir_agg)(nir_agg.data.astype('f4'), red_agg.data.astype('f4')) return DataArray(out, name=name, coords=nir_agg.coords, dims=nir_agg.dims, attrs=nir_agg.attrs)
# NDMI ----------
[docs] @supports_dataset_bands(nir='nir_agg', swir1='swir1_agg') def ndmi(nir_agg: xr.DataArray, swir1_agg: xr.DataArray, name='ndmi'): """ Computes Normalized Difference Moisture Index. Used to determine vegetation water content. Parameters ---------- nir_agg : xr.DataArray 2D array of near-infrared band data. (Landsat 4-7: Band 4) (Landsat 8: Band 5) swir1_agg : xr.DataArray 2D array of shortwave infrared band. (Landsat 4-7: Band 5) (Landsat 8: Band 6) name: str, default='ndmi' Name of output DataArray. Alternatively, a single ``xr.Dataset`` may be passed as the first argument with keyword arguments mapping band names to Dataset variables. For example:: ndmi(ds, nir='B8', swir1='B11') Returns ------- ndmi_agg : xr.DataArray of same type as inputs 2D array of ndmi values. All other input attributes are preserved. References ---------- - USGS: https://www.usgs.gov/land-resources/nli/landsat/normalized-difference-moisture-index # noqa Examples -------- .. plot:: :include-source: >>> from xrspatial.datasets import get_data >>> data = get_data('sentinel-2') # Open Example Data >>> nir = data['NIR'] >>> swir1 = data['SWIR1'] >>> from xrspatial.multispectral import ndmi >>> # Generate NDMI Aggregate Array >>> ndmi_agg = ndmi(nir_agg=nir, swir1_agg=swir1) >>> nir.plot(aspect=2, size=4) >>> swir1.plot(aspect=2, size=4) >>> ndmi_agg.plot(aspect=2, size=4) .. sourcecode:: python >>> y1, x1, y2, x2 = 100, 100, 103, 104 >>> print(nir[y1:y2, x1:x2].data) [[1519. 1504. 1530. 1589.] [1491. 1473. 1542. 1609.] [1479. 1461. 1592. 1653.]] >>> print(swir1[y1:y2, x1:x2].data) [[2092. 2242. 2333. 2382.] [2017. 2150. 2303. 2344.] [2124. 2244. 2367. 2452.]] >>> print(ndmi_agg[y1:y2, x1:x2].data) [[-0.15868181 -0.19701014 -0.20786953 -0.1996978 ] [-0.149943 -0.18686172 -0.19791937 -0.18593474] [-0.17901748 -0.21133603 -0.19575651 -0.19464068]] """ _validate_raster(nir_agg, func_name='ndmi', name='nir_agg') _validate_raster(swir1_agg, func_name='ndmi', name='swir1_agg') validate_arrays(nir_agg, swir1_agg) mapper = ArrayTypeFunctionMapping( numpy_func=_normalized_ratio_cpu, dask_func=_run_normalized_ratio_dask, cupy_func=_run_normalized_ratio_cupy, dask_cupy_func=_run_normalized_ratio_dask_cupy, ) out = mapper(nir_agg)(nir_agg.data.astype('f4'), swir1_agg.data.astype('f4')) return DataArray(out, name=name, coords=nir_agg.coords, dims=nir_agg.dims, attrs=nir_agg.attrs)
# NDWI ----------
[docs] @supports_dataset_bands(green='green_agg', nir='nir_agg') def ndwi(green_agg: xr.DataArray, nir_agg: xr.DataArray, name='ndwi'): """ Computes Normalized Difference Water Index (NDWI). NDWI highlights open water features while suppressing vegetation and soil signal. Parameters ---------- green_agg : xr.DataArray 2D array of green band data. (Landsat 8: Band 3) (Sentinel-2: Band 3) nir_agg : xr.DataArray 2D array of near-infrared band data. (Landsat 8: Band 5) (Sentinel-2: Band 8) name : str, default='ndwi' Name of output DataArray. Alternatively, a single ``xr.Dataset`` may be passed as the first argument with keyword arguments mapping band names to Dataset variables. For example:: ndwi(ds, green='B3', nir='B8') Returns ------- ndwi_agg : xr.DataArray of same type as inputs 2D array of ndwi values in the range [-1, 1]. All other input attributes are preserved. References ---------- - McFeeters, S.K., 1996. The use of the Normalized Difference Water Index (NDWI) in the delineation of open water features. International Journal of Remote Sensing, 17(7), pp.1425-1432. Examples -------- .. sourcecode:: python >>> import numpy as np >>> import xarray as xr >>> from xrspatial.multispectral import ndwi >>> green = xr.DataArray(np.array([[600., 500.], [400., 300.]])) >>> nir = xr.DataArray(np.array([[300., 400.], [500., 600.]])) >>> ndwi(green, nir).values array([[ 0.33333334, 0.11111111], [-0.11111111, -0.33333334]], dtype=float32) """ _validate_raster(green_agg, func_name='ndwi', name='green_agg') _validate_raster(nir_agg, func_name='ndwi', name='nir_agg') validate_arrays(green_agg, nir_agg) mapper = ArrayTypeFunctionMapping( numpy_func=_normalized_ratio_cpu, dask_func=_run_normalized_ratio_dask, cupy_func=_run_normalized_ratio_cupy, dask_cupy_func=_run_normalized_ratio_dask_cupy, ) out = mapper(green_agg)(green_agg.data.astype('f4'), nir_agg.data.astype('f4')) return DataArray(out, name=name, coords=green_agg.coords, dims=green_agg.dims, attrs=green_agg.attrs)
# MNDWI ----------
[docs] @supports_dataset_bands(green='green_agg', swir='swir_agg') def mndwi(green_agg: xr.DataArray, swir_agg: xr.DataArray, name='mndwi'): """ Computes Modified Normalized Difference Water Index (MNDWI). MNDWI improves on NDWI for urban areas by substituting SWIR for NIR, which reduces false positives from built-up surfaces. Parameters ---------- green_agg : xr.DataArray 2D array of green band data. (Landsat 8: Band 3) (Sentinel-2: Band 3) swir_agg : xr.DataArray 2D array of shortwave infrared band data. (Landsat 8: Band 6) (Sentinel-2: Band 11) name : str, default='mndwi' Name of output DataArray. Alternatively, a single ``xr.Dataset`` may be passed as the first argument with keyword arguments mapping band names to Dataset variables. For example:: mndwi(ds, green='B3', swir='B11') Returns ------- mndwi_agg : xr.DataArray of same type as inputs 2D array of mndwi values in the range [-1, 1]. All other input attributes are preserved. References ---------- - Xu, H., 2006. Modification of normalised difference water index (NDWI) to enhance open water features in remotely sensed imagery. International Journal of Remote Sensing, 27(14), pp.3025-3033. Examples -------- .. sourcecode:: python >>> import numpy as np >>> import xarray as xr >>> from xrspatial.multispectral import mndwi >>> green = xr.DataArray(np.array([[600., 500.], [400., 300.]])) >>> swir = xr.DataArray(np.array([[300., 400.], [500., 600.]])) >>> mndwi(green, swir).values array([[ 0.33333334, 0.11111111], [-0.11111111, -0.33333334]], dtype=float32) """ _validate_raster(green_agg, func_name='mndwi', name='green_agg') _validate_raster(swir_agg, func_name='mndwi', name='swir_agg') validate_arrays(green_agg, swir_agg) mapper = ArrayTypeFunctionMapping( numpy_func=_normalized_ratio_cpu, dask_func=_run_normalized_ratio_dask, cupy_func=_run_normalized_ratio_cupy, dask_cupy_func=_run_normalized_ratio_dask_cupy, ) out = mapper(green_agg)(green_agg.data.astype('f4'), swir_agg.data.astype('f4')) return DataArray(out, name=name, coords=green_agg.coords, dims=green_agg.dims, attrs=green_agg.attrs)
@ngjit def _normalized_ratio_cpu(arr1, arr2): out = np.full(arr1.shape, np.nan, dtype=np.float32) rows, cols = arr1.shape for y in range(0, rows): for x in range(0, cols): val1 = arr1[y, x] val2 = arr2[y, x] numerator = val1 - val2 denominator = val1 + val2 if denominator == 0.0: continue else: out[y, x] = numerator / denominator return out def _run_normalized_ratio_dask(arr1, arr2): out = da.map_blocks(_normalized_ratio_cpu, arr1, arr2, meta=np.array(())) return out @cuda.jit def _normalized_ratio_gpu(arr1, arr2, out): y, x = cuda.grid(2) if y < out.shape[0] and x < out.shape[1]: val1 = arr1[y, x] val2 = arr2[y, x] numerator = val1 - val2 denominator = val1 + val2 if denominator != 0.0: out[y, x] = numerator / denominator def _run_normalized_ratio_cupy(arr1, arr2): griddim, blockdim = cuda_args(arr1.shape) out = cupy.empty(arr1.shape, dtype='f4') out[:] = cupy.nan _normalized_ratio_gpu[griddim, blockdim](arr1, arr2, out) return out def _run_normalized_ratio_dask_cupy(arr1, arr2): out = da.map_blocks(_run_normalized_ratio_cupy, arr1, arr2, dtype=cupy.float32, meta=cupy.array(())) return out @ngjit def _savi_cpu(nir_data, red_data, soil_factor): out = np.full(nir_data.shape, np.nan, dtype=np.float32) rows, cols = nir_data.shape for y in range(0, rows): for x in range(0, cols): nir = nir_data[y, x] red = red_data[y, x] numerator = nir - red soma = nir + red + soil_factor if soma != 0.0: out[y, x] = (numerator / soma) * (1.0 + soil_factor) return out @cuda.jit def _savi_gpu(nir_data, red_data, soil_factor, out): y, x = cuda.grid(2) if y < out.shape[0] and x < out.shape[1]: nir = nir_data[y, x] red = red_data[y, x] numerator = nir - red soma = nir + red + soil_factor if soma != 0.0: out[y, x] = (numerator / soma) * (1.0 + soil_factor) def _savi_dask(nir_data, red_data, soil_factor): out = da.map_blocks(_savi_cpu, nir_data, red_data, soil_factor, meta=np.array(())) return out def _savi_cupy(nir_data, red_data, soil_factor): griddim, blockdim = cuda_args(nir_data.shape) out = cupy.empty(nir_data.shape, dtype='f4') out[:] = cupy.nan _savi_gpu[griddim, blockdim](nir_data, red_data, soil_factor, out) return out def _savi_dask_cupy(nir_data, red_data, soil_factor): out = da.map_blocks(_savi_cupy, nir_data, red_data, soil_factor, dtype=cupy.float32, meta=cupy.array(())) return out # SAVI ----------
[docs] @supports_dataset_bands(nir='nir_agg', red='red_agg') def savi(nir_agg: xr.DataArray, red_agg: xr.DataArray, soil_factor: float = 1.0, name: str = 'savi'): """ Computes Soil Adjusted Vegetation Index (SAVI). Used to determine if a cell contains living vegetation while minimizing soil brightness. Parameters ---------- nir_agg : xr.DataArray 2D array of near-infrared band data. red_agg : xr.DataArray 2D array of red band data. soil_factor : float, default=1.0 soil adjustment factor between -1.0 and 1.0. When set to zero, savi will return the same as ndvi. name : str, default='savi' Name of output DataArray. Alternatively, a single ``xr.Dataset`` may be passed as the first argument with keyword arguments mapping band names to Dataset variables. For example:: savi(ds, nir='B8', red='B4') Returns ------- savi_agg : xr.DataArray of same type as inputs 2D array of savi values. All other input attributes are preserved. References ---------- - ScienceDirect: https://www.sciencedirect.com/science/article/abs/pii/003442578890106X # noqa Examples -------- .. plot:: :include-source: >>> from xrspatial.datasets import get_data >>> data = get_data('sentinel-2') # Open Example Data >>> nir = data['NIR'] >>> red = data['Red'] >>> from xrspatial.multispectral import savi >>> # Generate SAVI Aggregate Array >>> savi_agg = savi(nir_agg=nir, red_agg=red) >>> nir.plot(aspect=2, size=4) >>> red.plot(aspect=2, size=4) >>> savi_agg.plot(aspect=2, size=4) .. sourcecode:: python >>> print(nir[y1:y2, x1:x2].data) [[1519. 1504. 1530. 1589.] [1491. 1473. 1542. 1609.] [1479. 1461. 1592. 1653.]] >>> print(red[y1:y2, x1:x2].data) [[1327. 1329. 1363. 1392.] [1309. 1331. 1423. 1424.] [1293. 1337. 1455. 1414.]] >>> print(savi_agg[y1:y2, x1:x2].data) [[0.0337197 0.03087509 0.0288528 0.03303152] [0.0324884 0.02531194 0.02006069 0.03048781] [0.03353769 0.02215077 0.02247375 0.03895046]] """ _validate_raster(nir_agg, func_name='savi', name='nir_agg') _validate_raster(red_agg, func_name='savi', name='red_agg') validate_arrays(red_agg, nir_agg) if not -1.0 <= soil_factor <= 1.0: raise ValueError("soil factor must be between [-1.0, 1.0]") mapper = ArrayTypeFunctionMapping(numpy_func=_savi_cpu, dask_func=_savi_dask, cupy_func=_savi_cupy, dask_cupy_func=_savi_dask_cupy) out = mapper(red_agg)(nir_agg.data.astype('f4'), red_agg.data.astype('f4'), soil_factor) return DataArray(out, name=name, coords=nir_agg.coords, dims=nir_agg.dims, attrs=nir_agg.attrs)
# SIPI ---------- @ngjit def _sipi_cpu(nir_data, red_data, blue_data): out = np.full(nir_data.shape, np.nan, dtype=np.float32) rows, cols = nir_data.shape for y in range(0, rows): for x in range(0, cols): nir = nir_data[y, x] red = red_data[y, x] blue = blue_data[y, x] numerator = nir - blue denominator = nir - red if denominator != 0.0: out[y, x] = numerator / denominator return out @cuda.jit def _sipi_gpu(nir_data, red_data, blue_data, out): y, x = cuda.grid(2) if y < out.shape[0] and x < out.shape[1]: nir = nir_data[y, x] red = red_data[y, x] blue = blue_data[y, x] numerator = nir - blue denominator = nir - red if denominator != 0.0: out[y, x] = numerator / denominator def _sipi_dask(nir_data, red_data, blue_data): out = da.map_blocks(_sipi_cpu, nir_data, red_data, blue_data, meta=np.array(())) return out def _sipi_cupy(nir_data, red_data, blue_data): griddim, blockdim = cuda_args(nir_data.shape) out = cupy.empty(nir_data.shape, dtype='f4') out[:] = cupy.nan _sipi_gpu[griddim, blockdim](nir_data, red_data, blue_data, out) return out def _sipi_dask_cupy(nir_data, red_data, blue_data): out = da.map_blocks(_sipi_cupy, nir_data, red_data, blue_data, dtype=cupy.float32, meta=cupy.array(())) return out
[docs] @supports_dataset_bands(nir='nir_agg', red='red_agg', blue='blue_agg') def sipi(nir_agg: xr.DataArray, red_agg: xr.DataArray, blue_agg: xr.DataArray, name='sipi'): """ Computes Structure Insensitive Pigment Index which helpful in early disease detection in vegetation. Parameters ---------- nir_agg : xr.DataArray 2D array of near-infrared band data. red_agg : xr.DataArray 2D array of red band data. blue_agg : xr.DataArray 2D array of blue band data. name: str, default='sipi' Name of output DataArray. Alternatively, a single ``xr.Dataset`` may be passed as the first argument with keyword arguments mapping band names to Dataset variables. For example:: sipi(ds, nir='B8', red='B4', blue='B2') Returns ------- sipi_agg : xr.DataArray of same type as inputs 2D array of sipi values. All other input attributes are preserved. References ---------- - Wikipedia: https://en.wikipedia.org/wiki/Enhanced_vegetation_index Examples -------- .. plot:: :include-source: >>> from xrspatial.datasets import get_data >>> data = get_data('sentinel-2') # Open Example Data >>> nir = data['NIR'] >>> red = data['Red'] >>> blue = data['Blue'] >>> from xrspatial.multispectral import sipi >>> # Generate SIPI Aggregate Array >>> sipi_agg = sipi(nir_agg=nir, red_agg=red, blue_agg=blue) >>> nir.plot(cmap='Greys', aspect=2, size=4) >>> red.plot(aspect=2, size=4) >>> blue.plot(aspect=2, size=4) >>> sipi_agg.plot(aspect=2, size=4) .. sourcecode:: python >>> y1, x1, y2, x2 = 100, 100, 103, 104 >>> print(nir[y1:y2, x1:x2].data) [[1519. 1504. 1530. 1589.] [1491. 1473. 1542. 1609.] [1479. 1461. 1592. 1653.]] >>> print(red[y1:y2, x1:x2].data) [[1327. 1329. 1363. 1392.] [1309. 1331. 1423. 1424.] [1293. 1337. 1455. 1414.]] >>> print(blue[y1:y2, x1:x2].data) [[1281. 1270. 1254. 1297.] [1241. 1249. 1280. 1309.] [1239. 1257. 1322. 1329.]] >>> print(sipi_agg[y1:y2, x1:x2].data) [[1.2395834 1.3371428 1.6526946 1.4822335] [1.3736264 1.5774648 2.2016807 1.6216216] [1.2903225 1.6451613 1.9708029 1.3556485]] """ _validate_raster(nir_agg, func_name='sipi', name='nir_agg') _validate_raster(red_agg, func_name='sipi', name='red_agg') _validate_raster(blue_agg, func_name='sipi', name='blue_agg') validate_arrays(red_agg, nir_agg, blue_agg) mapper = ArrayTypeFunctionMapping(numpy_func=_sipi_cpu, dask_func=_sipi_dask, cupy_func=_sipi_cupy, dask_cupy_func=_sipi_dask_cupy) out = mapper(red_agg)( nir_agg.data.astype('f4'), red_agg.data.astype('f4'), blue_agg.data.astype('f4') ) return DataArray(out, name=name, coords=nir_agg.coords, dims=nir_agg.dims, attrs=nir_agg.attrs)
# EBBI ---------- @ngjit def _ebbi_cpu(red_data, swir_data, tir_data): out = np.full(red_data.shape, np.nan, dtype=np.float32) rows, cols = red_data.shape for y in range(0, rows): for x in range(0, cols): red = red_data[y, x] swir = swir_data[y, x] tir = tir_data[y, x] numerator = swir - red denominator = 10 * np.sqrt(swir + tir) if denominator != 0.0: out[y, x] = numerator / denominator return out @cuda.jit def _ebbi_gpu(red_data, swir_data, tir_data, out): y, x = cuda.grid(2) if y < out.shape[0] and x < out.shape[1]: red = red_data[y, x] swir = swir_data[y, x] tir = tir_data[y, x] numerator = swir - red denominator = 10.0 * sqrt(swir + tir) if denominator != 0.0: out[y, x] = numerator / denominator def _ebbi_dask(red_data, swir_data, tir_data): out = da.map_blocks(_ebbi_cpu, red_data, swir_data, tir_data, meta=np.array(())) return out def _ebbi_cupy(red_data, swir_data, tir_data): griddim, blockdim = cuda_args(red_data.shape) out = cupy.empty(red_data.shape, dtype='f4') out[:] = cupy.nan _ebbi_gpu[griddim, blockdim](red_data, swir_data, tir_data, out) return out def _ebbi_dask_cupy(red_data, swir_data, tir_data): out = da.map_blocks(_ebbi_cupy, red_data, swir_data, tir_data, dtype=cupy.float32, meta=cupy.array(())) return out
[docs] @supports_dataset_bands(red='red_agg', swir='swir_agg', tir='tir_agg') def ebbi(red_agg: xr.DataArray, swir_agg: xr.DataArray, tir_agg: xr.DataArray, name='ebbi'): """ Computes Enhanced Built-Up and Bareness Index (EBBI) which allows for easily distinguishing between built-up and bare land areas. Parameters ---------- red_agg : xr.DataArray 2D array of red band data. swir_agg : xr.DataArray 2D array of shortwave infrared band data. tir_agg: xr.DataArray 2D array of thermal infrared band data. name: str, default='ebbi' Name of output DataArray. Alternatively, a single ``xr.Dataset`` may be passed as the first argument with keyword arguments mapping band names to Dataset variables. For example:: ebbi(ds, red='B4', swir='B11', tir='B10') Returns ------- ebbi_agg = xr.DataArray of same type as inputs 2D array of ebbi values. All other input attributes are preserved References ---------- - rdrr: https://rdrr.io/cran/LSRS/man/EBBI.html Examples -------- .. sourcecode:: python >>> # Imports >>> import numpy as np >>> import xarray as xr >>> from xrspatial.multispectral import ebbi >>> # Create Sample Band Data, RED band >>> np.random.seed(1) >>> red_agg = xr.DataArray(np.random.rand(4,4), dims = ["lat", "lon"]) >>> height, width = red_agg.shape >>> _lat = np.linspace(0, height - 1, height) >>> _lon = np.linspace(0, width - 1, width) >>> red_agg["lat"] = _lat >>> red_agg["lon"] = _lon >>> # SWIR band >>> np.random.seed(5) >>> swir_agg = xr.DataArray(np.random.rand(4,4), dims = ["lat", "lon"]) >>> height, width = swir_agg.shape >>> _lat = np.linspace(0, height - 1, height) >>> _lon = np.linspace(0, width - 1, width) >>> swir_agg["lat"] = _lat >>> swir_agg["lon"] = _lon >>> # TIR band >>> np.random.seed(6) >>> tir_agg = xr.DataArray(np.random.rand(4,4), dims = ["lat", "lon"]) >>> height, width = tir_agg.shape >>> _lat = np.linspace(0, height - 1, height) >>> _lon = np.linspace(0, width - 1, width) >>> tir_agg["lat"] = _lat >>> tir_agg["lon"] = _lon >>> print(red_agg, swir_agg, tir_agg) <xarray.DataArray (lat: 4, lon: 4)> array([[4.17022005e-01, 7.20324493e-01, 1.14374817e-04, 3.02332573e-01], # noqa [1.46755891e-01, 9.23385948e-02, 1.86260211e-01, 3.45560727e-01], # noqa [3.96767474e-01, 5.38816734e-01, 4.19194514e-01, 6.85219500e-01], # noqa [2.04452250e-01, 8.78117436e-01, 2.73875932e-02, 6.70467510e-01]]) # noqa Coordinates: * lat (lat) float64 0.0 1.0 2.0 3.0 * lon (lon) float64 0.0 1.0 2.0 3.0 <xarray.DataArray (lat: 4, lon: 4)> array([[0.22199317, 0.87073231, 0.20671916, 0.91861091], [0.48841119, 0.61174386, 0.76590786, 0.51841799], [0.2968005 , 0.18772123, 0.08074127, 0.7384403 ], [0.44130922, 0.15830987, 0.87993703, 0.27408646]]) Coordinates: * lat (lat) float64 0.0 1.0 2.0 3.0 * lon (lon) float64 0.0 1.0 2.0 3.0 <xarray.DataArray (lat: 4, lon: 4)> array([[0.89286015, 0.33197981, 0.82122912, 0.04169663], [0.10765668, 0.59505206, 0.52981736, 0.41880743], [0.33540785, 0.62251943, 0.43814143, 0.73588211], [0.51803641, 0.5788586 , 0.6453551 , 0.99022427]]) Coordinates: * lat (lat) float64 0.0 1.0 2.0 3.0 * lon (lon) float64 0.0 1.0 2.0 3.0 >>> # Create EBBI DataArray >>> ebbi_agg = ebbi(red_agg, swir_agg, tir_agg) >>> print(ebbi_agg) <xarray.DataArray 'ebbi' (lat: 4, lon: 4)> array([[-2.43983486, -2.58194492, 3.97432599, -0.42291921], [-0.11444052, 0.96786363, 0.59269999, 0.42374096], [ 0.61379897, -0.23840436, -0.05598088, 0.95193251], [ 1.32393891, 0.41574839, 0.72484653, -0.80669034]]) Coordinates: * lat (lat) float64 0.0 1.0 2.0 3.0 * lon (lon) float64 0.0 1.0 2.0 3.0 """ _validate_raster(red_agg, func_name='ebbi', name='red_agg') _validate_raster(swir_agg, func_name='ebbi', name='swir_agg') _validate_raster(tir_agg, func_name='ebbi', name='tir_agg') validate_arrays(red_agg, swir_agg, tir_agg) mapper = ArrayTypeFunctionMapping(numpy_func=_ebbi_cpu, dask_func=_ebbi_dask, cupy_func=_ebbi_cupy, dask_cupy_func=_ebbi_dask_cupy) out = mapper(red_agg)( red_agg.data.astype('f4'), swir_agg.data.astype('f4'), tir_agg.data.astype('f4') ) return DataArray(out, name=name, coords=red_agg.coords, dims=red_agg.dims, attrs=red_agg.attrs)
@ngjit def _normalize_data_cpu(data, min_val, max_val, pixel_max, c, th): out = np.full(data.shape, np.nan, dtype=np.float32) range_val = max_val - min_val rows, cols = data.shape # check range_val to avoid dividing by zero if range_val != 0: for y in range(rows): for x in range(cols): val = data[y, x] norm = (val - min_val) / range_val # sigmoid contrast enhancement norm = 1 / (1 + np.exp(c * (th - norm))) out[y, x] = norm * pixel_max return out def _normalize_data_numpy(data, pixel_max, c, th): min_val = np.nanmin(data) max_val = np.nanmax(data) out = _normalize_data_cpu( data, min_val, max_val, pixel_max, c, th ) return out def _normalize_data_dask(data, pixel_max, c, th): min_val = da.nanmin(data) max_val = da.nanmax(data) out = da.map_blocks( _normalize_data_cpu, data, min_val, max_val, pixel_max, c, th, meta=np.array(()) ) return out def _normalize_data_cupy(data, pixel_max, c, th): min_val = cupy.nanmin(data) max_val = cupy.nanmax(data) range_val = max_val - min_val out = cupy.full(data.shape, cupy.nan, dtype=cupy.float32) if range_val != 0: norm = (data - min_val) / range_val norm = 1 / (1 + cupy.exp(c * (th - norm))) out = norm * pixel_max return out def _normalize_data_cupy_block(data, min_val, max_val, pixel_max, c, th): range_val = max_val - min_val out = cupy.full(data.shape, cupy.nan, dtype=cupy.float32) if range_val != 0: norm = (data - min_val) / range_val norm = 1 / (1 + cupy.exp(c * (th - norm))) out = norm * pixel_max return out def _normalize_data_dask_cupy(data, pixel_max, c, th): min_val = da.nanmin(data) max_val = da.nanmax(data) out = da.map_blocks( _normalize_data_cupy_block, data, min_val, max_val, pixel_max, c, th, meta=cupy.array(()) ) return out def _normalize_data(agg, pixel_max, c, th): mapper = ArrayTypeFunctionMapping(numpy_func=_normalize_data_numpy, dask_func=_normalize_data_dask, cupy_func=_normalize_data_cupy, dask_cupy_func=_normalize_data_dask_cupy) out = mapper(agg)(agg.data.astype('f4'), pixel_max, c, th) return out def _true_color_numpy(r, g, b, nodata, c, th): h, w = r.shape _check_true_color_memory(h, w) a = np.where(np.logical_or(np.isnan(r), r <= nodata), 0, 255) out = np.zeros((h, w, 4), dtype=np.uint8) pixel_max = 255 out[:, :, 0] = (_normalize_data(r, pixel_max, c, th)).astype(np.uint8) out[:, :, 1] = (_normalize_data(g, pixel_max, c, th)).astype(np.uint8) out[:, :, 2] = (_normalize_data(b, pixel_max, c, th)).astype(np.uint8) out[:, :, 3] = a.astype(np.uint8) return out def _true_color_dask(r, g, b, nodata, c, th): pixel_max = 255 alpha = da.where( da.logical_or(da.isnan(r), r <= nodata), 0, pixel_max ).astype(np.uint8) red = (_normalize_data(r, pixel_max, c, th)).astype(np.uint8) green = (_normalize_data(g, pixel_max, c, th)).astype(np.uint8) blue = (_normalize_data(b, pixel_max, c, th)).astype(np.uint8) out = da.stack([red, green, blue, alpha], axis=-1) return out def _true_color_cupy(r, g, b, nodata, c, th): h, w = r.shape _check_true_color_gpu_memory(h, w) pixel_max = 255 r_data = r.data a = cupy.where( cupy.logical_or(cupy.isnan(r_data), r_data <= nodata), 0, pixel_max ).astype(cupy.uint8) red = (_normalize_data(r, pixel_max, c, th)).astype(cupy.uint8) green = (_normalize_data(g, pixel_max, c, th)).astype(cupy.uint8) blue = (_normalize_data(b, pixel_max, c, th)).astype(cupy.uint8) out = cupy.stack([red, green, blue, a], axis=-1) return out def _true_color_dask_cupy(r, g, b, nodata, c, th): pixel_max = 255 r_data = r.data alpha = da.where( da.logical_or(da.isnan(r_data), r_data <= nodata), 0, pixel_max ).astype(cupy.uint8) red = (_normalize_data(r, pixel_max, c, th)).astype(cupy.uint8) green = (_normalize_data(g, pixel_max, c, th)).astype(cupy.uint8) blue = (_normalize_data(b, pixel_max, c, th)).astype(cupy.uint8) out = da.stack([red, green, blue, alpha], axis=-1) return out
[docs] def true_color(r, g, b, nodata=1, c=10.0, th=0.125, name='true_color'): """ Create true color composite from a combination of red, green and blue bands satellite images. A sigmoid function will be used to improve the contrast of output image. The function is defined as ``normalized_pixel = 1 / (1 + np.exp(c * (th - normalized_pixel)))`` where ``c`` and ``th`` are contrast and brightness controlling parameters. Parameters ---------- r : xarray.DataArray 2D array of red band data. g : xarray.DataArray 2D array of green band data. b : xarray.DataArray 2D array of blue band data. nodata : int, float numeric value Nodata value of input DataArrays. c : float, default=10 Contrast and brighness controlling parameter for output image. th : float, default=0.125 Contrast and brighness controlling parameter for output image. name : str, default='true_color' Name of output DataArray. Returns ------- true_color_agg : xarray.DataArray of the same type as inputs 3D array true color image with dims of [y, x, band]. All output attributes are copied from red band image. Raises ------ MemoryError On the numpy and cupy backends, raised when the projected peak working memory (about 24 bytes per input pixel) exceeds 50% of available host RAM or free GPU VRAM. Use a smaller raster or a dask-backed DataArray for out-of-core processing. Examples -------- .. plot:: :include-source: >>> from xrspatial.datasets import get_data >>> data = get_data('sentinel-2') # Open Example Data >>> red = data['Red'] >>> green = data['Green'] >>> blue = data['Blue'] >>> from xrspatial.multispectral import true_color >>> # Generate true color image >>> true_color_img = true_color(r=red, g=green, b=blue) >>> true_color_img.plot.imshow() """ _validate_raster(r, func_name='true_color', name='r') _validate_raster(g, func_name='true_color', name='g') _validate_raster(b, func_name='true_color', name='b') validate_arrays(r, g, b) mapper = ArrayTypeFunctionMapping( numpy_func=_true_color_numpy, dask_func=_true_color_dask, cupy_func=_true_color_cupy, dask_cupy_func=_true_color_dask_cupy, ) with warnings.catch_warnings(): warnings.simplefilter('ignore') out = mapper(r)(r, g, b, nodata, c, th) # TODO: output metadata: coords, dims, attrs _dims = ['y', 'x', 'band'] _attrs = r.attrs _coords = {'y': r['y'], 'x': r['x'], 'band': [0, 1, 2, 3]} return DataArray( out, name=name, dims=_dims, coords=_coords, attrs=_attrs, )
# NDSI ----------
[docs] @supports_dataset_bands(green='green_agg', swir1='swir1_agg') def ndsi(green_agg: xr.DataArray, swir1_agg: xr.DataArray, name='ndsi'): """ Computes Normalized Difference Snow Index (NDSI). NDSI separates snow and ice from clouds and other bright surfaces by exploiting the high reflectance of snow in the green band and low reflectance in the shortwave infrared. Parameters ---------- green_agg : xr.DataArray 2D array of green band data. (Landsat 8: Band 3) (Sentinel-2: Band 3) swir1_agg : xr.DataArray 2D array of shortwave infrared band data. (Landsat 8: Band 6) (Sentinel-2: Band 11) name : str, default='ndsi' Name of output DataArray. Alternatively, a single ``xr.Dataset`` may be passed as the first argument with keyword arguments mapping band names to Dataset variables. For example:: ndsi(ds, green='B3', swir1='B11') Returns ------- ndsi_agg : xr.DataArray of same type as inputs 2D array of ndsi values in the range [-1, 1]. All other input attributes are preserved. References ---------- - Hall, D.K., Riggs, G.A. and Salomonson, V.V., 1995. Development of methods for mapping global snow cover using moderate resolution imaging spectroradiometer data. Remote Sensing of Environment, 54(2), pp.127-140. Examples -------- .. sourcecode:: python >>> import numpy as np >>> import xarray as xr >>> from xrspatial.multispectral import ndsi >>> green = xr.DataArray(np.array([[600., 500.], [400., 300.]])) >>> swir1 = xr.DataArray(np.array([[100., 200.], [300., 400.]])) >>> ndsi(green, swir1).values array([[ 0.71428573, 0.42857143], [ 0.14285715, -0.14285715]], dtype=float32) """ _validate_raster(green_agg, func_name='ndsi', name='green_agg') _validate_raster(swir1_agg, func_name='ndsi', name='swir1_agg') validate_arrays(green_agg, swir1_agg) mapper = ArrayTypeFunctionMapping( numpy_func=_normalized_ratio_cpu, dask_func=_run_normalized_ratio_dask, cupy_func=_run_normalized_ratio_cupy, dask_cupy_func=_run_normalized_ratio_dask_cupy, ) out = mapper(green_agg)(green_agg.data.astype('f4'), swir1_agg.data.astype('f4')) return DataArray(out, name=name, coords=green_agg.coords, dims=green_agg.dims, attrs=green_agg.attrs)
# NDBI ----------
[docs] @supports_dataset_bands(swir1='swir1_agg', nir='nir_agg') def ndbi(swir1_agg: xr.DataArray, nir_agg: xr.DataArray, name='ndbi'): """ Computes Normalized Difference Built-up Index (NDBI). NDBI picks out built-up and urban areas by exploiting the higher reflectance of impervious surfaces in SWIR relative to NIR. Parameters ---------- swir1_agg : xr.DataArray 2D array of shortwave infrared band data. (Landsat 8: Band 6) (Sentinel-2: Band 11) nir_agg : xr.DataArray 2D array of near-infrared band data. (Landsat 8: Band 5) (Sentinel-2: Band 8) name : str, default='ndbi' Name of output DataArray. Alternatively, a single ``xr.Dataset`` may be passed as the first argument with keyword arguments mapping band names to Dataset variables. For example:: ndbi(ds, swir1='B11', nir='B8') Returns ------- ndbi_agg : xr.DataArray of same type as inputs 2D array of ndbi values in the range [-1, 1]. All other input attributes are preserved. References ---------- - Zha, Y., Gao, J. and Ni, S., 2003. Use of normalized difference built-up index in automatically mapping urban areas from TM imagery. International Journal of Remote Sensing, 24(3), pp.583-594. Examples -------- .. sourcecode:: python >>> import numpy as np >>> import xarray as xr >>> from xrspatial.multispectral import ndbi >>> swir1 = xr.DataArray(np.array([[600., 500.], [400., 300.]])) >>> nir = xr.DataArray(np.array([[300., 400.], [500., 600.]])) >>> ndbi(swir1, nir).values array([[ 0.33333334, 0.11111111], [-0.11111111, -0.33333334]], dtype=float32) """ _validate_raster(swir1_agg, func_name='ndbi', name='swir1_agg') _validate_raster(nir_agg, func_name='ndbi', name='nir_agg') validate_arrays(swir1_agg, nir_agg) mapper = ArrayTypeFunctionMapping( numpy_func=_normalized_ratio_cpu, dask_func=_run_normalized_ratio_dask, cupy_func=_run_normalized_ratio_cupy, dask_cupy_func=_run_normalized_ratio_dask_cupy, ) out = mapper(swir1_agg)(swir1_agg.data.astype('f4'), nir_agg.data.astype('f4')) return DataArray(out, name=name, coords=swir1_agg.coords, dims=swir1_agg.dims, attrs=swir1_agg.attrs)
# BAI ---------- @ngjit def _bai_cpu(red_data, nir_data): out = np.full(red_data.shape, np.nan, dtype=np.float32) rows, cols = red_data.shape for y in range(0, rows): for x in range(0, cols): red = red_data[y, x] nir = nir_data[y, x] dr = np.float32(0.1) - red dn = np.float32(0.06) - nir denominator = dr * dr + dn * dn if denominator != 0.0: out[y, x] = np.float32(1.0) / denominator return out @cuda.jit def _bai_gpu(red_data, nir_data, out): y, x = cuda.grid(2) if y < out.shape[0] and x < out.shape[1]: red = red_data[y, x] nir = nir_data[y, x] dr = nb.float32(0.1) - red dn = nb.float32(0.06) - nir denominator = dr * dr + dn * dn if denominator != 0.0: out[y, x] = nb.float32(1.0) / denominator def _bai_dask(red_data, nir_data): out = da.map_blocks(_bai_cpu, red_data, nir_data, meta=np.array(())) return out def _bai_cupy(red_data, nir_data): griddim, blockdim = cuda_args(red_data.shape) out = cupy.empty(red_data.shape, dtype='f4') out[:] = cupy.nan _bai_gpu[griddim, blockdim](red_data, nir_data, out) return out def _bai_dask_cupy(red_data, nir_data): out = da.map_blocks(_bai_cupy, red_data, nir_data, dtype=cupy.float32, meta=cupy.array(())) return out
[docs] @supports_dataset_bands(red='red_agg', nir='nir_agg') def bai(red_agg: xr.DataArray, nir_agg: xr.DataArray, name='bai'): """ Computes Burn Area Index (BAI). BAI measures the spectral distance of each pixel to a charcoal reflectance point (red=0.1, NIR=0.06). Higher values indicate recently burned areas. Unlike NBR, BAI works on the raw reflectance values rather than a normalized difference. Input bands should be in reflectance units (0-1 range). If your data is in DN or scaled integers, divide by the appropriate scale factor first. Parameters ---------- red_agg : xr.DataArray 2D array of red band reflectance data (0-1 range). nir_agg : xr.DataArray 2D array of near-infrared band reflectance data (0-1 range). name : str, default='bai' Name of output DataArray. Alternatively, a single ``xr.Dataset`` may be passed as the first argument with keyword arguments mapping band names to Dataset variables. For example:: bai(ds, red='B4', nir='B8') Returns ------- bai_agg : xr.DataArray of same type as inputs 2D array of bai values. Higher values indicate burned areas. All other input attributes are preserved. References ---------- - Chuvieco, E., Martin, M.P. and Palacios, A., 2002. Assessment of different spectral conditions for the detection of burned areas with Landsat TM data. International Journal of Remote Sensing, 23(1), pp.71-85. Examples -------- .. sourcecode:: python >>> import numpy as np >>> import xarray as xr >>> from xrspatial.multispectral import bai >>> red = xr.DataArray(np.array([[0.1, 0.2], [0.3, 0.05]])) >>> nir = xr.DataArray(np.array([[0.06, 0.3], [0.4, 0.02]])) >>> bai(red, nir).values # pixel (0,0) is at charcoal point array([[ inf, 0.01686341, 0.00858369, 1111.1111 ]], dtype=float32) """ _validate_raster(red_agg, func_name='bai', name='red_agg') _validate_raster(nir_agg, func_name='bai', name='nir_agg') validate_arrays(red_agg, nir_agg) mapper = ArrayTypeFunctionMapping(numpy_func=_bai_cpu, dask_func=_bai_dask, cupy_func=_bai_cupy, dask_cupy_func=_bai_dask_cupy) out = mapper(red_agg)(red_agg.data.astype('f4'), nir_agg.data.astype('f4')) return DataArray(out, name=name, coords=red_agg.coords, dims=red_agg.dims, attrs=red_agg.attrs)
# MSAVI2 ---------- @ngjit def _msavi2_cpu(nir_data, red_data): out = np.full(nir_data.shape, np.nan, dtype=np.float32) rows, cols = nir_data.shape for y in range(0, rows): for x in range(0, cols): nir = nir_data[y, x] red = red_data[y, x] term = (np.float32(2.0) * nir + np.float32(1.0)) discriminant = term * term - np.float32(8.0) * (nir - red) if discriminant >= 0.0: out[y, x] = (term - np.sqrt(discriminant)) / np.float32(2.0) return out @cuda.jit def _msavi2_gpu(nir_data, red_data, out): y, x = cuda.grid(2) if y < out.shape[0] and x < out.shape[1]: nir = nir_data[y, x] red = red_data[y, x] term = nb.float32(2.0) * nir + nb.float32(1.0) discriminant = term * term - nb.float32(8.0) * (nir - red) if discriminant >= nb.float32(0.0): out[y, x] = (term - sqrt(discriminant)) / nb.float32(2.0) def _msavi2_dask(nir_data, red_data): out = da.map_blocks(_msavi2_cpu, nir_data, red_data, meta=np.array(())) return out def _msavi2_cupy(nir_data, red_data): griddim, blockdim = cuda_args(nir_data.shape) out = cupy.empty(nir_data.shape, dtype='f4') out[:] = cupy.nan _msavi2_gpu[griddim, blockdim](nir_data, red_data, out) return out def _msavi2_dask_cupy(nir_data, red_data): out = da.map_blocks(_msavi2_cupy, nir_data, red_data, dtype=cupy.float32, meta=cupy.array(())) return out
[docs] @supports_dataset_bands(nir='nir_agg', red='red_agg') def msavi2(nir_agg: xr.DataArray, red_agg: xr.DataArray, name='msavi2'): """ Computes Modified Soil Adjusted Vegetation Index (MSAVI2). MSAVI2 is a self-adjusting vegetation index that does not require an empirical soil-brightness correction factor (L). It produces less noisy results than SAVI in areas with sparse vegetation and exposed soil. Parameters ---------- nir_agg : xr.DataArray 2D array of near-infrared band data. red_agg : xr.DataArray 2D array of red band data. name : str, default='msavi2' Name of output DataArray. Alternatively, a single ``xr.Dataset`` may be passed as the first argument with keyword arguments mapping band names to Dataset variables. For example:: msavi2(ds, nir='B8', red='B4') Returns ------- msavi2_agg : xr.DataArray of same type as inputs 2D array of msavi2 values. All other input attributes are preserved. References ---------- - Qi, J., Chehbouni, A., Huete, A.R., Kerr, Y.H. and Sorooshian, S., 1994. A modified soil adjusted vegetation index. Remote Sensing of Environment, 48(2), pp.119-126. Examples -------- .. sourcecode:: python >>> import numpy as np >>> import xarray as xr >>> from xrspatial.multispectral import msavi2 >>> nir = xr.DataArray(np.array([[0.5, 0.3], [0.1, 0.4]])) >>> red = xr.DataArray(np.array([[0.1, 0.2], [0.05, 0.3]])) >>> msavi2(nir, red).values array([[0.38729835, 0.08284271, 0.0248457 , 0.08284271]], dtype=float32) """ _validate_raster(nir_agg, func_name='msavi2', name='nir_agg') _validate_raster(red_agg, func_name='msavi2', name='red_agg') validate_arrays(nir_agg, red_agg) mapper = ArrayTypeFunctionMapping(numpy_func=_msavi2_cpu, dask_func=_msavi2_dask, cupy_func=_msavi2_cupy, dask_cupy_func=_msavi2_dask_cupy) out = mapper(nir_agg)(nir_agg.data.astype('f4'), red_agg.data.astype('f4')) return DataArray(out, name=name, coords=nir_agg.coords, dims=nir_agg.dims, attrs=nir_agg.attrs)
# OSAVI ---------- @ngjit def _osavi_cpu(nir_data, red_data): out = np.full(nir_data.shape, np.nan, dtype=np.float32) rows, cols = nir_data.shape for y in range(0, rows): for x in range(0, cols): nir = nir_data[y, x] red = red_data[y, x] numerator = nir - red denominator = nir + red + np.float32(0.16) if denominator != 0.0: out[y, x] = numerator / denominator return out @cuda.jit def _osavi_gpu(nir_data, red_data, out): y, x = cuda.grid(2) if y < out.shape[0] and x < out.shape[1]: nir = nir_data[y, x] red = red_data[y, x] numerator = nir - red denominator = nir + red + nb.float32(0.16) if denominator != 0.0: out[y, x] = numerator / denominator def _osavi_dask(nir_data, red_data): out = da.map_blocks(_osavi_cpu, nir_data, red_data, meta=np.array(())) return out def _osavi_cupy(nir_data, red_data): griddim, blockdim = cuda_args(nir_data.shape) out = cupy.empty(nir_data.shape, dtype='f4') out[:] = cupy.nan _osavi_gpu[griddim, blockdim](nir_data, red_data, out) return out def _osavi_dask_cupy(nir_data, red_data): out = da.map_blocks(_osavi_cupy, nir_data, red_data, dtype=cupy.float32, meta=cupy.array(())) return out
[docs] @supports_dataset_bands(nir='nir_agg', red='red_agg') def osavi(nir_agg: xr.DataArray, red_agg: xr.DataArray, name='osavi'): """ Computes Optimized Soil Adjusted Vegetation Index (OSAVI). OSAVI uses a fixed soil-brightness correction factor of L=0.16, chosen to work well across a range of soil conditions without requiring per-scene tuning. It performs best in areas with sparse to moderate vegetation cover. Parameters ---------- nir_agg : xr.DataArray 2D array of near-infrared band data. red_agg : xr.DataArray 2D array of red band data. name : str, default='osavi' Name of output DataArray. Alternatively, a single ``xr.Dataset`` may be passed as the first argument with keyword arguments mapping band names to Dataset variables. For example:: osavi(ds, nir='B8', red='B4') Returns ------- osavi_agg : xr.DataArray of same type as inputs 2D array of osavi values. All other input attributes are preserved. References ---------- - Rondeaux, G., Steven, M. and Baret, F., 1996. Optimization of soil-adjusted vegetation indices. Remote Sensing of Environment, 55(2), pp.95-107. Examples -------- .. sourcecode:: python >>> import numpy as np >>> import xarray as xr >>> from xrspatial.multispectral import osavi >>> nir = xr.DataArray(np.array([[0.5, 0.3], [0.1, 0.4]])) >>> red = xr.DataArray(np.array([[0.1, 0.2], [0.05, 0.3]])) >>> osavi(nir, red).values array([[0.5263158 , 0.15151516, 0.16129032, 0.11627907]], dtype=float32) """ _validate_raster(nir_agg, func_name='osavi', name='nir_agg') _validate_raster(red_agg, func_name='osavi', name='red_agg') validate_arrays(nir_agg, red_agg) mapper = ArrayTypeFunctionMapping(numpy_func=_osavi_cpu, dask_func=_osavi_dask, cupy_func=_osavi_cupy, dask_cupy_func=_osavi_dask_cupy) out = mapper(nir_agg)(nir_agg.data.astype('f4'), red_agg.data.astype('f4')) return DataArray(out, name=name, coords=nir_agg.coords, dims=nir_agg.dims, attrs=nir_agg.attrs)