Explorar o código

[Feat] Add Remote Sensing Indices (#35)

Lin Manhui %!s(int64=2) %!d(string=hai) anos
pai
achega
92933a8d4a

+ 2 - 1
README.md

@@ -35,7 +35,7 @@ PaddleRS具有以下五大特色:
 
 * <img src="./docs/images/f2.png" width="20"/> **针对遥感影像大幅面性质的优化**:支持大幅面影像滑窗推理,使用内存延迟载入技术提升性能;支持对大幅面影像地理坐标信息的读写。
 
-* <img src="./docs/images/f2.png" width="20"/> **顾及遥感特性与地学知识的数据预处理**:针对遥感数据特点,提供对包含任意数量波段的数据以及多时相数据的预处理功能,支持影像配准、辐射校正、波段选择等遥感数据预处理方法。
+* <img src="./docs/images/f2.png" width="20"/> **顾及遥感特性与地学知识的数据预处理**:针对遥感数据特点,提供对包含任意数量波段的数据以及多时相数据的预处理功能,支持影像配准、辐射校正、波段选择等遥感数据预处理方法,支持50余种遥感指数的提取与知识融入
 
 * <img src="./docs/images/f3.png" width="20"/> **工业级训练与部署性能**:支持多进程异步I/O、多卡并行训练等加速策略,结合飞桨核心框架的显存优化功能,可大幅度减少模型的训练开销,帮助开发者以更低成本、更高效地完成遥感的开发和训练。
 
@@ -202,6 +202,7 @@ PaddleRS目录树中关键部分如下:
 * 组件介绍
   * [数据集预处理脚本](./docs/intro/data_prep.md)
   * [模型库](./docs/intro/model_zoo.md)
+  * [遥感指数](./docs/intro/indices.md)
   * [数据变换算子](./docs/intro/transforms.md)
 * 模型训练
   * [模型训练API说明](./docs/apis/train.md)

+ 74 - 0
docs/intro/indices.md

@@ -0,0 +1,74 @@
+# 遥感指数
+
+通过`paddlers.transforms.AppendIndex`算子可以计算遥感指数并追加到输入影像的最后一个波段。在构建`AppendIndex`对象时,需要传入遥感指数名称以及一个包含波段-索引对应关系的字典(字典中的键为波段名称,索引号从1开始计数)。
+
+## PaddleRS已支持的遥感指数列表
+
+|遥感指数名称|全称|用途|参考文献|
+|-----------|----|---|--------|
+| `'ARI'` | Anthocyanin Reflectance Index | 植被 | https://doi.org/10.1562/0031-8655(2001)074%3C0038:OPANEO%3E2.0.CO;2 |
+| `'ARI2'` | Anthocyanin Reflectance Index 2 | 植被 | https://doi.org/10.1562/0031-8655(2001)074%3C0038:OPANEO%3E2.0.CO;2 |
+| `'ARVI'` | Atmospherically Resistant Vegetation Index | 植被 | https://doi.org/10.1109/36.134076 |
+| `'AWEInsh'` | Automated Water Extraction Index | 水体 | https://doi.org/10.1016/j.rse.2013.08.029 |
+| `'AWEIsh'` | Automated Water Extraction Index with Shadows Elimination | 水体 | https://doi.org/10.1016/j.rse.2013.08.029 |
+| `'BAI'` | Burned Area Index | 火烧迹地 | https://digital.csic.es/bitstream/10261/6426/1/Martin_Isabel_Serie_Geografica.pdf |
+| `'BI'` | Bare Soil Index | 城镇 | http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.465.8749&rep=rep1&type=pdf |
+| `'BLFEI'` | Built-Up Land Features Extraction Index | 城镇 | https://doi.org/10.1080/10106049.2018.1497094 |
+| `'BNDVI'` | Blue Normalized Difference Vegetation Index | 植被 | https://doi.org/10.1016/S1672-6308(07)60027-4 |
+| `'BWDRVI'` | Blue Wide Dynamic Range Vegetation Index | 植被 | https://doi.org/10.2135/cropsci2007.01.0031 |
+| `'BaI'` | Bareness Index | 城镇 | https://doi.org/10.1109/IGARSS.2005.1525743 |
+| `'CIG'` | Chlorophyll Index Green | 植被 | https://doi.org/10.1078/0176-1617-00887 |
+| `'CSI'` | Char Soil Index | 火烧迹地 | https://doi.org/10.1016/j.rse.2005.04.014 |
+| `'CSIT'` | Char Soil Index Thermal | 火烧迹地 | https://doi.org/10.1080/01431160600954704 |
+| `'DBI'` | Dry Built-Up Index | 城镇 | https://doi.org/10.3390/land7030081 |
+| `'DBSI'` | Dry Bareness Index | 城镇 | https://doi.org/10.3390/land7030081 |
+| `'DVI'` | Difference Vegetation Index | 植被 | https://doi.org/10.1016/0034-4257(94)00114-3 |
+| `'EBBI'` | Enhanced Built-Up and Bareness Index | 城镇 | https://doi.org/10.3390/rs4102957 |
+| `'EVI'` | Enhanced Vegetation Index | 植被 | https://doi.org/10.1016/S0034-4257(96)00112-5 |
+| `'EVI2'` | Two-Band Enhanced Vegetation Index | 植被 | https://doi.org/10.1016/j.rse.2008.06.006 |
+| `'FCVI'` | Fluorescence Correction Vegetation Index | 植被 | https://doi.org/10.1016/j.rse.2020.111676 |
+| `'GARI'` | Green Atmospherically Resistant Vegetation Index | 植被 | https://doi.org/10.1016/S0034-4257(96)00072-7 |
+| `'GBNDVI'` | Green-Blue Normalized Difference Vegetation Index | 植被 | https://doi.org/10.1016/S1672-6308(07)60027-4 |
+| `'GLI'` | Green Leaf Index | 植被 | http://dx.doi.org/10.1080/10106040108542184 |
+| `'GRVI'` | Green Ratio Vegetation Index | 植被 | https://doi.org/10.2134/agronj2004.0314 |
+| `'IPVI'` | Infrared Percentage Vegetation Index | 植被 | https://doi.org/10.1016/0034-4257(90)90085-Z |
+| `'LSWI'` | Land Surface Water Index | 水体 | https://doi.org/10.1016/j.rse.2003.11.008 |
+| `'MBI'` | Modified Bare Soil Index | 城镇 | https://doi.org/10.3390/land10030231 |
+| `'MGRVI'` | Modified Green Red Vegetation Index | 植被 | https://doi.org/10.1016/j.jag.2015.02.012 |
+| `'MNDVI'` | Modified Normalized Difference Vegetation Index | 植被 | https://doi.org/10.1080/014311697216810 |
+| `'MNDWI'` | Modified Normalized Difference Water Index | 水体 | https://doi.org/10.1080/01431160600589179 |
+| `'MNLI'` | Modified Non-Linear Vegetation Index | 植被 | https://doi.org/10.1109/TGRS.2003.812910 |
+| `'MSI'` | Moisture Stress Index | 植被 | https://doi.org/10.1016/0034-4257(89)90046-1 |
+| `'NBLI'` | Normalized Difference Bare Land Index | 城镇 | https://doi.org/10.3390/rs9030249 |
+| `'NDSI'` | Normalized Difference Snow Index | 雪 | https://doi.org/10.1109/IGARSS.1994.399618 |
+| `'NDVI'` | Normalized Difference Vegetation Index | 植被 | https://ntrs.nasa.gov/citations/19740022614 |
+| `'NDWI'` | Normalized Difference Water Index | 水体 | https://doi.org/10.1080/01431169608948714 |
+| `'NDYI'` | Normalized Difference Yellowness Index | 植被 | https://doi.org/10.1016/j.rse.2016.06.016 |
+| `'NIRv'` | Near-Infrared Reflectance of Vegetation | 植被 | https://doi.org/10.1126/sciadv.1602244 |
+| `'PSRI'` | Plant Senescing Reflectance Index | 植被 | https://doi.org/10.1034/j.1399-3054.1999.106119.x |
+| `'RI'` | Redness Index | 植被 | https://www.documentation.ird.fr/hor/fdi:34390 |
+| `'SAVI'` | Soil-Adjusted Vegetation Index | 植被 | https://doi.org/10.1016/0034-4257(88)90106-X |
+| `'SWI'` | Snow Water Index | 雪 | https://doi.org/10.3390/rs11232774 |
+| `'TDVI'` | Transformed Difference Vegetation Index | 植被 | https://doi.org/10.1109/IGARSS.2002.1026867 |
+| `'UI'` | Urban Index | 城镇 | https://www.isprs.org/proceedings/XXXI/congress/part7/321_XXXI-part7.pdf |
+| `'VIG'` | Vegetation Index Green | 植被 | https://doi.org/10.1016/S0034-4257(01)00289-9 |
+| `'WI1'` | Water Index 1 | 水体 | https://doi.org/10.3390/rs11182186 |
+| `'WI2'` | Water Index 2 | 水体 | https://doi.org/10.3390/rs11182186 |
+| `'WRI'` | Water Ratio Index | 水体 | https://doi.org/10.1109/GEOINFORMATICS.2010.5567762 |
+
+## 波段名称与描述
+
+|    波段名称    |     描述    |
+|---------------|-------------|
+|     `'b'`     | Blue        |
+|     `'g'`     | Green       |
+|     `'r'`     | Red         |
+|    `'re1'`    | Red Edge 1  |
+|    `'re2'`    | Red Edge 2  |
+|    `'re3'`    | Red Edge 3  |
+|     `'n'`     | NIR         |
+|    `'n2'`     | NIR 2       |
+|    `'s1'`     | SWIR 1      |
+|    `'s2'`     | SWIR 2      |
+|    `'t1'`     | Thermal 1   |
+|    `'t2'`     | Thermal 2   |

+ 1 - 0
docs/intro/transforms.md

@@ -6,6 +6,7 @@ PaddleRS对不同遥感任务需要的数据预处理/数据增强(合称为
 
 | 数据变换算子名 | 用途                                                     | 任务     | ... |
 | -------------------- | ------------------------------------------------- | -------- | ---- |
+| AppendIndex          | 计算遥感指数并添加到输入影像中。 | 所有任务  | ... |  
 | CenterCrop           | 对输入影像进行中心裁剪。 | 所有任务 | ... |
 | Dehaze               | 对输入图像进行去雾。 | 所有任务 | ... |
 | MatchRadiance        | 对两个时相的输入影像进行相对辐射校正。 | 变化检测 | ... |

+ 398 - 0
paddlers/transforms/indices.py

@@ -0,0 +1,398 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Refer to https://github.com/awesome-spectral-indices/awesome-spectral-indices .
+# See LICENSE (https://github.com/awesome-spectral-indices/awesome-spectral-indices/blob/main/LICENSE).
+
+import abc
+
+__all__ = [
+    'ARI', 'ARI2', 'ARVI', 'AWEInsh', 'AWEIsh', 'BAI', 'BI', 'BLFEI', 'BNDVI',
+    'BWDRVI', 'BaI', 'CIG', 'CSI', 'CSIT', 'DBI', 'DBSI', 'DVI', 'EBBI', 'EMBI',
+    'EVI', 'EVI2', 'FCVI', 'GARI', 'GBNDVI', 'GLI', 'GNDVI', 'GRVI', 'IPVI',
+    'LSWI', 'MBI', 'MGRVI', 'MNDVI', 'MNDWI', 'MNLI', 'MSI', 'NBLI', 'NDSI',
+    'NDVI', 'NDWI', 'NDYI', 'NIRv', 'PSRI', 'RI', 'SAVI', 'SWI', 'TDVI', 'UI',
+    'VIG', 'WI1', 'WI2', 'WRI'
+]
+
+EPS = 1e-32
+
+# | Band name | Description |
+# |-----------|-------------|
+# |     b     | Blue        |
+# |     g     | Green       |
+# |     r     | Red         |
+# |    re1    | Red Edge 1  |
+# |    re2    | Red Edge 2  |
+# |    re3    | Red Edge 3  |
+# |     n     | NIR         |
+# |    n2     | NIR 2       |
+# |    s1     | SWIR 1      |
+# |    s2     | SWIR 2      |
+# |    t1     | Thermal 1   |
+# |    t2     | Thermal 2   |
+
+
+class RSIndex(metaclass=abc.ABCMeta):
+    def __init__(self, band_indices):
+        super(RSIndex, self).__init__()
+        self.band_indices = band_indices
+
+    @abc.abstractmethod
+    def _compute(self, *args, **kwargs):
+        pass
+
+    def __call__(self, image):
+        bands = self.select_bands(image)
+        return self._compute(**bands)
+
+    def select_bands(self, image, to_float32=True):
+        bands = {}
+        for name, idx in self.band_indices.items():
+            if idx == 0:
+                raise ValueError("Band index starts from 1.")
+            bands[name] = image[..., idx - 1]
+            if to_float32:
+                bands[name] = bands[name].astype('float32')
+        return bands
+
+
+def compute_normalized_difference_index(band1, band2):
+    return (band1 - band2) / (band1 + band2 + EPS)
+
+
+class ARI(RSIndex):
+    def _compute(self, g, re1):
+        index = 1 / (g + EPS)
+        index -= 1 / (re1 + EPS)
+        return index
+
+
+class ARI2(RSIndex):
+    def _compute(self, g, re1, n):
+        index = 1 / (g + EPS)
+        index -= 1 / (re1 + EPS)
+        index = index * n
+        return index
+
+
+class ARVI(RSIndex):
+    def __init__(self, band_indices, c0):
+        super(ARVI, self).__init__(band_indices)
+        self.c0 = c0
+
+    def _compute(self, b, r, n):
+        return compute_normalized_difference_index(n, r - self.c0 * (r - b))
+
+
+class AWEInsh(RSIndex):
+    def _compute(self, g, n, s1, s2):
+        index = 4.0 * (g - s1)
+        index -= 0.25 * n
+        index += 2.75 * s2
+        return index
+
+
+class AWEIsh(RSIndex):
+    def _compute(self, b, g, n, s1, s2):
+        index = 2.5 * g
+        index += b
+        index -= 1.5 * (n + s1)
+        index -= 0.25 * s2
+        return index
+
+
+class BAI(RSIndex):
+    def _compute(self, r, n):
+        index = (0.1 - r)**2.0
+        index += (0.06 - n)**2.0
+        return 1.0 / (index + EPS)
+
+
+class BI(RSIndex):
+    def _compute(self, b, r, n, s1):
+        return compute_normalized_difference_index(s1 + r, n + b)
+
+
+class BLFEI(RSIndex):
+    def _compute(self, g, r, s1, s2):
+        return compute_normalized_difference_index((g + r + s2) / 3.0, s1)
+
+
+class BNDVI(RSIndex):
+    def _compute(self, b, n):
+        return compute_normalized_difference_index(n, b)
+
+
+class BWDRVI(RSIndex):
+    def __init__(self, band_indices, c0):
+        super(BWDRVI, self).__init__(band_indices)
+        self.c0 = c0
+
+    def _compute(self, b, n):
+        return compute_normalized_difference_index(self.c0 * n, b)
+
+
+class BaI(RSIndex):
+    def _compute(self, r, n, s1):
+        index = r + s1
+        index -= n
+        return index
+
+
+class CIG(RSIndex):
+    def _compute(self, g, n):
+        index = n / (g + EPS)
+        index -= 1.0
+        return index
+
+
+class CSI(RSIndex):
+    def _compute(self, n, s2):
+        return n / (s2 + EPS)
+
+
+class CSIT(RSIndex):
+    def _compute(self, n, s2, t1):
+        return n / ((s2 * t1) / 10000.0 + EPS)
+
+
+class DBI(RSIndex):
+    def _compute(self, b, r, n, t1):
+        index = (b - t1) / (b + t1 + EPS)
+        index -= (n - r) / (n + r + EPS)
+        return index
+
+
+class DBSI(RSIndex):
+    def _compute(self, g, r, n, s1):
+        index = (s1 - g) / (s1 + g + EPS)
+        index -= (n - r) / (n + r + EPS)
+        return index
+
+
+class DVI(RSIndex):
+    def _compute(self, r, n):
+        return n - r
+
+
+class EBBI(RSIndex):
+    def _compute(self, n, s1, t1):
+        num = s1 - n
+        denom = (10.0 * ((s1 + t1)**0.5))
+        return num / (denom + EPS)
+
+
+class EMBI(RSIndex):
+    def _compute(self, g, n, s1, s2):
+        item1 = compute_normalized_difference_index(s1, s2 + n)
+        item1 += 0.5
+        item2 = compute_normalized_difference_index(g, s1)
+        return (item1 - item2 - 0.5) / (item1 + item2 + 1.5 + EPS)
+
+
+class EVI(RSIndex):
+    def __init__(self, band_indices, c0=2.5, c1=6, c2=7.5, c3=1):
+        super(EVI, self).__init__(band_indices)
+        self.c0 = c0
+        self.c1 = c1
+        self.c2 = c2
+        self.c3 = c3
+
+    def _compute(self, b, r, n):
+        num = self.c0 * (n - r)
+        denom = n + self.c1 * r - self.c2 * b + self.c3
+        return num / (denom + EPS)
+
+
+class EVI2(RSIndex):
+    def __init__(self, band_indices, c0, c1):
+        super(EVI2, self).__init__(band_indices)
+        self.c0 = c0
+        self.c1 = c1
+
+    def _compute(self, n, r):
+        num = self.c0 * (n - r)
+        denom = n + 2.4 * r + self.c1
+        return num / (denom + EPS)
+
+
+class FCVI(RSIndex):
+    def _compute(self, b, g, r, n):
+        return n - ((r + g + b) / 3.0)
+
+
+class GARI(RSIndex):
+    def _compute(self, b, g, r, n):
+        num = n - (g - (b - r))
+        denom = n - (g + (b - r))
+        return num / (denom + EPS)
+
+
+class GBNDVI(RSIndex):
+    def _compute(self, b, g, n):
+        return compute_normalized_difference_index(n, g + b)
+
+
+class GLI(RSIndex):
+    def _compute(self, b, g, r):
+        return compute_normalized_difference_index(2.0 * g, r + b)
+
+
+class GNDVI(RSIndex):
+    def _compute(self, g, n):
+        return compute_normalized_difference_index(n, g)
+
+
+class GRVI(RSIndex):
+    def _compute(self, g, n):
+        return n / (g + EPS)
+
+
+class IPVI(RSIndex):
+    def _compute(self, r, n):
+        return n / (n + r + EPS)
+
+
+class LSWI(RSIndex):
+    def _compute(self, n, s1):
+        return compute_normalized_difference_index(n, s1)
+
+
+class MBI(RSIndex):
+    def _compute(self, n, s1, s2):
+        index = compute_normalized_difference_index(s1, s2 + n)
+        index += 0.5
+        return index
+
+
+class MGRVI(RSIndex):
+    def _compute(self, g, r):
+        return compute_normalized_difference_index(g**2.0, r**2.0)
+
+
+class MNDVI(RSIndex):
+    def _compute(self, n, s2):
+        return compute_normalized_difference_index(n, s2)
+
+
+class MNDWI(RSIndex):
+    def _compute(self, g, s1):
+        return compute_normalized_difference_index(g, s1)
+
+
+class MNLI(RSIndex):
+    def __init__(self, band_indices, c0):
+        super(MNLI, self).__init__(band_indices)
+        self.c0 = c0
+
+    def _compute(self, r, n):
+        num = (1 + self.c0) * ((n**2) - r)
+        denom = ((n**2) + r + self.c0)
+        return num / (denom + EPS)
+
+
+class MSI(RSIndex):
+    def _compute(self, n, s1):
+        return s1 / (n + EPS)
+
+
+class NBLI(RSIndex):
+    def _compute(self, r, t1):
+        return compute_normalized_difference_index(r, t1)
+
+
+class NDSI(RSIndex):
+    def _compute(self, g, s1):
+        return compute_normalized_difference_index(g, s1)
+
+
+class NDVI(RSIndex):
+    def _compute(self, r, n):
+        return compute_normalized_difference_index(n, r)
+
+
+class NDWI(RSIndex):
+    def _compute(self, g, n):
+        return compute_normalized_difference_index(g, n)
+
+
+class NDYI(RSIndex):
+    def _compute(self, b, g):
+        return compute_normalized_difference_index(g, b)
+
+
+class NIRv(RSIndex):
+    def _compute(self, r, n):
+        return compute_normalized_difference_index(n, r) * n
+
+
+class PSRI(RSIndex):
+    def _compute(self, b, r, re2):
+        return (r - b) / (re2 + EPS)
+
+
+class RI(RSIndex):
+    def _compute(self, g, r):
+        return compute_normalized_difference_index(r, g)
+
+
+class SAVI(RSIndex):
+    def __init__(self, band_indices, c0):
+        super(SAVI, self).__init__(band_indices)
+        self.c0 = c0
+
+    def _compute(self, r, n):
+        num = (1.0 + self.c0) * (n - r)
+        denom = n + r + self.c0
+        return num / (denom + EPS)
+
+
+class SWI(RSIndex):
+    def _compute(self, g, n, s1):
+        num = g * (n - s1)
+        denom = (g + n) * (n + s1)
+        return num / (denom + EPS)
+
+
+class TDVI(RSIndex):
+    def _compute(self, r, n):
+        num = 1.5 * (n - r)
+        denom = (n**2.0 + r + 0.5)**0.5
+        return num / (denom + EPS)
+
+
+class UI(RSIndex):
+    def _compute(self, n, s2):
+        return compute_normalized_difference_index(s2, n)
+
+
+class VIG(RSIndex):
+    def _compute(self, g, r):
+        return compute_normalized_difference_index(g, r)
+
+
+class WI1(RSIndex):
+    def _compute(self, g, s2):
+        return compute_normalized_difference_index(g, s2)
+
+
+class WI2(RSIndex):
+    def _compute(self, b, s2):
+        return compute_normalized_difference_index(b, s2)
+
+
+class WRI(RSIndex):
+    def _compute(self, g, r, n, s1):
+        return (g + r) / (n + s1 + EPS)

+ 67 - 16
paddlers/transforms/operators.py

@@ -18,10 +18,7 @@ import random
 from numbers import Number
 from functools import partial
 from operator import methodcaller
-try:
-    from collections.abc import Sequence
-except Exception:
-    from collections import Sequence
+from collections.abc import Sequence
 
 import numpy as np
 import cv2
@@ -30,6 +27,7 @@ from PIL import Image
 from joblib import load
 
 import paddlers
+import paddlers.transforms.indices as indices
 from .functions import (
     normalize, horizontal_flip, permute, vertical_flip, center_crop, is_poly,
     horizontal_flip_poly, horizontal_flip_rle, vertical_flip_poly,
@@ -39,14 +37,37 @@ from .functions import (
     match_by_regression, match_histograms)
 
 __all__ = [
-    "Compose", "DecodeImg", "Resize", "RandomResize", "ResizeByShort",
-    "RandomResizeByShort", "ResizeByLong", "RandomHorizontalFlip",
-    "RandomVerticalFlip", "Normalize", "CenterCrop", "RandomCrop",
-    "RandomScaleAspect", "RandomExpand", "Pad", "MixupImage", "RandomDistort",
-    "RandomBlur", "RandomSwap", "Dehaze", "ReduceDim", "SelectBand",
-    "RandomFlipOrRotate", "ReloadMask", "MatchRadiance", "ArrangeSegmenter",
-    "ArrangeChangeDetector", "ArrangeClassifier", "ArrangeDetector",
-    "ArrangeRestorer"
+    "Compose",
+    "DecodeImg",
+    "Resize",
+    "RandomResize",
+    "ResizeByShort",
+    "RandomResizeByShort",
+    "ResizeByLong",
+    "RandomHorizontalFlip",
+    "RandomVerticalFlip",
+    "Normalize",
+    "CenterCrop",
+    "RandomCrop",
+    "RandomScaleAspect",
+    "RandomExpand",
+    "Pad",
+    "MixupImage",
+    "RandomDistort",
+    "RandomBlur",
+    "RandomSwap",
+    "Dehaze",
+    "ReduceDim",
+    "SelectBand",
+    "RandomFlipOrRotate",
+    "ReloadMask",
+    "AppendIndex",
+    "MatchRadiance",
+    "ArrangeRestorer",
+    "ArrangeSegmenter",
+    "ArrangeChangeDetector",
+    "ArrangeClassifier",
+    "ArrangeDetector",
 ]
 
 interp_dict = {
@@ -132,16 +153,16 @@ class Transform(object):
         pass
 
     def apply_im(self, image):
-        pass
+        return image
 
     def apply_mask(self, mask):
-        pass
+        return mask
 
     def apply_bbox(self, bbox):
-        pass
+        return bbox
 
     def apply_segm(self, segms):
-        pass
+        return segms
 
     def apply(self, sample):
         if 'image' in sample:
@@ -1930,6 +1951,36 @@ class ReloadMask(Transform):
         return sample
 
 
+class AppendIndex(Transform):
+    """
+    Append remote sensing index to input image(s).
+
+    Args:
+        index_type (str): Type of remote sensinng index. See supported 
+            index types in 
+            https://github.com/PaddlePaddle/PaddleRS/tree/develop/paddlers/transforms/indices.py .
+        band_indices (dict): Mapping of band names to band indices 
+            (starting from 1). See band names in 
+            https://github.com/PaddlePaddle/PaddleRS/tree/develop/paddlers/transforms/indices.py . 
+    """
+
+    def __init__(self, index_type, band_indices, **kwargs):
+        super(AppendIndex, self).__init__()
+        cls = getattr(indices, index_type)
+        self._compute_index = cls(band_indices, **kwargs)
+
+    def apply_im(self, image):
+        index = self._compute_index(image)
+        index = index[..., None].astype('float32')
+        return np.concatenate([image, index], axis=-1)
+
+    def apply(self, sample):
+        sample['image'] = self.apply_im(sample['image'])
+        if 'image2' in sample:
+            sample['image2'] = self.apply_im(sample['image2'])
+        return sample
+
+
 class MatchRadiance(Transform):
     """
     Perform relative radiometric correction between bi-temporal images.

+ 25 - 11
tests/deploy/test_predictor.py

@@ -17,6 +17,7 @@ import tempfile
 import unittest.mock as mock
 
 import paddle
+import numpy as np
 
 import paddlers as pdrs
 from paddlers.transforms import decode_image
@@ -66,7 +67,9 @@ class TestPredictor(CommonTest):
                     predictor = pdrs.deploy.Predictor(
                         static_model_dir,
                         use_gpu=paddle.device.get_device().startswith('gpu'))
-                    self.check_predictor(predictor, trainer)
+                    trainer.net.eval()
+                    with paddle.no_grad():
+                        self.check_predictor(predictor, trainer)
 
             return _test_predictor_impl
 
@@ -80,11 +83,12 @@ class TestPredictor(CommonTest):
     def check_predictor(self, predictor, trainer):
         raise NotImplementedError
 
-    def check_dict_equal(
-            self,
-            dict_,
-            expected_dict,
-            ignore_keys=('label_map', 'mask', 'category', 'category_id')):
+    def check_dict_equal(self,
+                         dict_,
+                         expected_dict,
+                         ignore_keys=('label_map', 'mask', 'class_ids_map',
+                                      'label_names_map', 'category',
+                                      'category_id')):
         # By default do not compare label_maps, masks, or categories,
         # because numeric errors could result in large difference in labels.
         if isinstance(dict_, list):
@@ -100,9 +104,11 @@ class TestPredictor(CommonTest):
             for key in dict_.keys():
                 if key in ignore_keys:
                     continue
-                # Use higher tolerance
-                self.check_output_equal(
-                    dict_[key], expected_dict[key], rtol=1.e-4, atol=1.e-6)
+                diff = np.abs(
+                    np.asarray(dict_[key]) - np.asarray(expected_dict[
+                        key])).ravel()
+                cnt = (diff > (1.e-4 * diff + 1.e-4)).sum()
+                self.assertLess(cnt / diff.size, 0.03)
 
 
 @TestPredictor.add_tests
@@ -111,9 +117,9 @@ class TestCDPredictor(TestPredictor):
     TRAINER_NAME_TO_EXPORT_OPTS = {
         '_default': "--fixed_input_shape [-1,3,256,256]"
     }
-    # HACK: Skip CDNet.
+    # HACK: Skip DSIFN.
     # These models are heavily affected by numeric errors.
-    WHITE_LIST = ['CDNet']
+    WHITE_LIST = ['DSIFN']
 
     def check_predictor(self, predictor, trainer):
         t1_path = "data/ssmt/optical_t1.bmp"
@@ -305,6 +311,14 @@ class TestDetPredictor(TestPredictor):
 @TestPredictor.add_tests
 class TestResPredictor(TestPredictor):
     MODULE = pdrs.tasks.restorer
+    TRAINER_NAME_TO_EXPORT_OPTS = {
+        '_default': "--fixed_input_shape [-1,3,256,256]"
+    }
+
+    def __init__(self, methodName='runTest'):
+        super(TestResPredictor, self).__init__(methodName=methodName)
+        # Do not test with CPUs as it will take long long time.
+        self.places.pop(self.places.index('cpu'))
 
     def check_predictor(self, predictor, trainer):
         # For restoration tasks, do NOT ensure the consistence of numeric values, 

+ 2 - 1
tests/run_ci_dev.sh

@@ -7,7 +7,6 @@ ln -s /usr/local/bin/pip3.7 /usr/local/python2.7.15/bin/pip
 export PYTHONPATH=`pwd`
 
 python -m pip install --upgrade pip --ignore-installed
-# python -m pip install --upgrade numpy --ignore-installed
 python -m pip uninstall paddlepaddle-gpu -y
 if [[ ${branch} == 'develop' ]];then
 echo "checkout develop !"
@@ -33,6 +32,8 @@ python setup.py bdist_wheel
 pip install ./dist/auto_log*.whl
 cd ..
 
+pip install spyndex protobuf==3.19.0 colorama
+
 unset http_proxy https_proxy
 
 set -e

+ 1 - 0
tests/transforms/__init__.py

@@ -14,3 +14,4 @@
 
 from .test_functions import *
 from .test_operators import *
+from .test_indices import *

+ 173 - 0
tests/transforms/test_indices.py

@@ -0,0 +1,173 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import inspect
+import numpy as np
+
+import paddlers.transforms as T
+from testing_utils import CpuCommonTest
+
+__all__ = ['TestIndex']
+
+NAME_MAPPING = {
+    'b': 'B',
+    'g': 'G',
+    'r': 'R',
+    're1': 'RE1',
+    're2': 'RE2',
+    're3': 'RE3',
+    'n': 'N',
+    'n2': 'N2',
+    's1': 'S1',
+    's2': 'S2',
+    't1': 'T1',
+    't2': 'T2'
+}
+
+
+def add_index_tests(cls):
+    """
+    Automatically patch testing functions for remote sensing indices.
+    """
+
+    def _make_test_func(index_name, index_class):
+        def __test_func(self):
+            bands = {}
+            cnt = 0
+            for key in inspect.signature(index_class._compute).parameters:
+                if key == 'self':
+                    continue
+                elif key.startswith('c'):
+                    # key 'c*' stands for a constant
+                    raise RuntimeError(
+                        f"Cannot automatically process key '{key}'!")
+                else:
+                    cnt += 1
+                    bands[key] = cnt
+            dummy = constr_dummy_image(cnt)
+            index1 = index_class(bands)(dummy)
+            params = constr_spyndex_params(dummy, bands)
+            index2 = compute_spyndex_index(index_name, params)
+            self.check_output(index1, index2)
+
+        return __test_func
+
+    for index_name in T.indices.__all__:
+        index_class = getattr(T.indices, index_name)
+        attr_name = 'test_' + index_name
+        if hasattr(cls, attr_name):
+            continue
+        setattr(cls, attr_name, _make_test_func(index_name, index_class))
+    return cls
+
+
+def constr_spyndex_params(image, bands, consts=None):
+    params = {}
+    for k, v in bands.items():
+        k = NAME_MAPPING[k]
+        v = image[..., v - 1]
+        params[k] = v
+    if consts is not None:
+        params.update(consts)
+    return params
+
+
+def compute_spyndex_index(name, params):
+    import spyndex
+    index = spyndex.computeIndex(index=[name], params=params)
+    return index
+
+
+def constr_dummy_image(c):
+    return np.random.uniform(0, 65536, size=(256, 256, c))
+
+
+@add_index_tests
+class TestIndex(CpuCommonTest):
+    def check_output(self, result, expected_result):
+        mask = np.isfinite(expected_result)
+        diff = np.abs(result[mask] - expected_result[mask])
+        cnt = (diff > (1.e-2 * diff + 0.1)).sum()
+        self.assertLess(cnt / diff.size, 0.005)
+
+    def test_ARVI(self):
+        dummy = constr_dummy_image(3)
+        bands = {'b': 1, 'r': 2, 'n': 3}
+        gamma = 0.1
+        arvi = T.indices.ARVI(bands, gamma)
+        index1 = arvi(dummy)
+        index2 = compute_spyndex_index(
+            'ARVI', constr_spyndex_params(dummy, bands, {'gamma': gamma}))
+        self.check_output(index1, index2)
+
+    def test_BWDRVI(self):
+        dummy = constr_dummy_image(2)
+        bands = {'b': 1, 'n': 2}
+        alpha = 0.1
+        bwdrvi = T.indices.BWDRVI(bands, alpha)
+        index1 = bwdrvi(dummy)
+        index2 = compute_spyndex_index(
+            'BWDRVI', constr_spyndex_params(dummy, bands, {'alpha': alpha}))
+        self.check_output(index1, index2)
+
+    def test_EVI(self):
+        dummy = constr_dummy_image(3)
+        bands = {'b': 1, 'r': 2, 'n': 3}
+        g = 2.5
+        C1 = 6.0
+        C2 = 7.5
+        L = 1.0
+        evi = T.indices.EVI(bands, g, C1, C2, L)
+        index1 = evi(dummy)
+        index2 = compute_spyndex_index(
+            'EVI',
+            constr_spyndex_params(dummy, bands,
+                                  {'g': g,
+                                   'C1': C1,
+                                   'C2': C2,
+                                   'L': L}))
+        self.check_output(index1, index2)
+
+    def test_EVI2(self):
+        dummy = constr_dummy_image(2)
+        bands = {'r': 1, 'n': 2}
+        g = 2.5
+        L = 1.0
+        evi2 = T.indices.EVI2(bands, g, L)
+        index1 = evi2(dummy)
+        index2 = compute_spyndex_index('EVI2',
+                                       constr_spyndex_params(dummy, bands,
+                                                             {'g': g,
+                                                              'L': L}))
+        self.check_output(index1, index2)
+
+    def test_MNLI(self):
+        dummy = constr_dummy_image(2)
+        bands = {'r': 1, 'n': 2}
+        L = 1.0
+        mnli = T.indices.MNLI(bands, L)
+        index1 = mnli(dummy)
+        index2 = compute_spyndex_index(
+            'MNLI', constr_spyndex_params(dummy, bands, {'L': L}))
+        self.check_output(index1, index2)
+
+    def test_SAVI(self):
+        dummy = constr_dummy_image(2)
+        bands = {'r': 1, 'n': 2}
+        L = 1.0
+        savi = T.indices.SAVI(bands, L)
+        index1 = savi(dummy)
+        index2 = compute_spyndex_index(
+            'SAVI', constr_spyndex_params(dummy, bands, {'L': L}))
+        self.check_output(index1, index2)

+ 35 - 2
tests/transforms/test_operators.py

@@ -26,7 +26,7 @@ __all__ = ['TestTransform', 'TestCompose', 'TestArrange']
 WHITE_LIST = ['ReloadMask']
 
 
-def _add_op_tests(cls):
+def add_op_tests(cls):
     """
     Automatically patch testing functions for transform operators.
     """
@@ -149,7 +149,7 @@ OP2FILTER = {
 }
 
 
-@_add_op_tests
+@add_op_tests
 class TestTransform(CpuCommonTest):
     def setUp(self):
         self.inputs = [
@@ -350,6 +350,39 @@ class TestTransform(CpuCommonTest):
             _filter=_filter_no_det)
         test_func(self)
 
+    def test_AppendIndex(self):
+        def _out_hook_ndvi(sample):
+            self.check_output_equal(sample['image'].shape[2], 11)
+            self.assertLessEqual(sample['image'][..., -1].max() - 1e-8, 1)
+            self.assertGreaterEqual(sample['image'][..., -1].min() + 1e-8, -1)
+
+            if 'image2' in sample:
+                self.check_output_equal(sample['image2'].shape[2], 11)
+                self.assertLessEqual(sample['image2'][..., -1].max() - 1e-8, 1)
+                self.assertGreaterEqual(sample['image2'][..., -1].min() + 1e-8,
+                                        -1)
+
+            return sample
+
+        test_ndvi = make_test_func(
+            T.AppendIndex,
+            'NDVI', {'r': 1,
+                     'n': 2},
+            _out_hook=_out_hook_ndvi,
+            _filter=_filter_only_multispectral)
+        test_ndvi(self)
+        test_evi = make_test_func(
+            T.AppendIndex,
+            'EVI', {'b': 1,
+                    'r': 2,
+                    'n': 3},
+            c0=1.0,
+            c1=0.5,
+            c2=1.0,
+            c3=1.5,
+            _filter=_filter_only_multispectral)
+        test_evi(self)
+
     def test_MatchRadiance(self):
         test_hist = make_test_func(
             T.MatchRadiance, 'hist', _filter=_filter_only_mt)