瀏覽代碼

[Feature] Init add change detection (without test task)

geoyee 3 年之前
父節點
當前提交
0ddfbdcc1b

+ 4 - 3
.gitignore

@@ -102,11 +102,12 @@ celerybeat.pid
 *.sage.py
 
 # Environments
-.env
+# don't filter paddleseg's env
+# .env
 .venv
-env/
+# env/
 venv/
-ENV/
+# ENV/
 env.bak/
 venv.bak/
 

+ 1 - 0
paddlers/datasets/__init__.py

@@ -1,3 +1,4 @@
 from .voc import VOCDetection
 from .seg_dataset import SegDataset
+from .cd_dataset import CDDataset
 from .raster import Raster

+ 97 - 0
paddlers/datasets/cd_dataset.py

@@ -0,0 +1,97 @@
+# Copyright (c) 2021 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 os.path as osp
+import copy
+
+from paddle.io import Dataset
+from paddlers.utils import logging, get_num_workers, get_encoding, path_normalization, is_pic
+
+
+class CDDataset(Dataset):
+    """读取变化检测任务数据集,并对样本进行相应的处理(来自SegDataset,图像标签需要两个)。
+
+    Args:
+        data_dir (str): 数据集所在的目录路径。
+        file_list (str): 描述数据集图片文件和对应标注文件的文件路径(文本内每行路径为相对data_dir的相对路)。
+        label_list (str): 描述数据集包含的类别信息文件路径。默认值为None。
+        transforms (paddlers.transforms): 数据集中每个样本的预处理/增强算子。
+        num_workers (int|str): 数据集中样本在预处理过程中的线程或进程数。默认为'auto'。
+        shuffle (bool): 是否需要对数据集中样本打乱顺序。默认为False。
+    """
+
+    def __init__(self,
+                 data_dir,
+                 file_list,
+                 label_list=None,
+                 transforms=None,
+                 num_workers='auto',
+                 shuffle=False):
+        super(CDDataset, self).__init__()
+        self.transforms = copy.deepcopy(transforms)
+        # TODO batch padding
+        self.batch_transforms = None
+        self.num_workers = get_num_workers(num_workers)
+        self.shuffle = shuffle
+        self.file_list = list()
+        self.labels = list()
+
+        # TODO:非None时,让用户跳转数据集分析生成label_list
+        # 不要在此处分析label file
+        if label_list is not None:
+            with open(label_list, encoding=get_encoding(label_list)) as f:
+                for line in f:
+                    item = line.strip()
+                    self.labels.append(item)
+        with open(file_list, encoding=get_encoding(file_list)) as f:
+            for line in f:
+                items = line.strip().split()
+                if len(items) > 3:
+                    raise Exception(
+                        "A space is defined as the delimiter to separate the image and label path, " \
+                        "so the space cannot be in the image or label path, but the line[{}] of " \
+                        " file_list[{}] has a space in the image or label path.".format(line, file_list))
+                items[0] = path_normalization(items[0])
+                items[1] = path_normalization(items[1])
+                items[2] = path_normalization(items[2])
+                if not is_pic(items[0]) or not is_pic(items[1]) or not is_pic(items[2]):
+                    continue
+                full_path_im_t1 = osp.join(data_dir, items[0])
+                full_path_im_t2 = osp.join(data_dir, items[1])
+                full_path_label = osp.join(data_dir, items[2])
+                if not osp.exists(full_path_im_t1):
+                    raise IOError('Image file {} does not exist!'.format(
+                        full_path_im_t1))
+                if not osp.exists(full_path_im_t2):
+                    raise IOError('Image file {} does not exist!'.format(
+                        full_path_im_t2))
+                if not osp.exists(full_path_label):
+                    raise IOError('Label file {} does not exist!'.format(
+                        full_path_label))
+                self.file_list.append({
+                    'image_t1': full_path_im_t1,
+                    'image_t2': full_path_im_t2,
+                    'mask': full_path_label
+                })
+        self.num_samples = len(self.file_list)
+        logging.info("{} samples in file {}".format(
+            len(self.file_list), file_list))
+
+    def __getitem__(self, idx):
+        sample = copy.deepcopy(self.file_list[idx])
+        outputs = self.transforms(sample)
+        return outputs
+
+    def __len__(self):
+        return len(self.file_list)

+ 7 - 7
paddlers/datasets/raster.py

@@ -42,7 +42,7 @@ class Raster:
             self.path = path
             self.__src_data = np.load(path) if path.split(".")[-1] == "npy" \
                                             else gdal.Open(path)
-            self.__getInfo()
+            self._getInfo()
             self.to_uint8 = to_uint8
             self.setBands(band_list)
         else:
@@ -78,16 +78,16 @@ class Raster:
             np.ndarray: data's ndarray.
         """
         if start_loc is None:
-            return self.__getAarray()
+            return self._getAarray()
         else:
-            return self.__getBlock(start_loc, block_size)
+            return self._getBlock(start_loc, block_size)
 
-    def __getInfo(self) -> None:
+    def _getInfo(self) -> None:
         self.bands = self.__src_data.RasterCount
         self.width = self.__src_data.RasterXSize
         self.height = self.__src_data.RasterYSize
 
-    def __getAarray(self, window: Union[None, List[int], Tuple[int]]=None) -> np.ndarray:
+    def _getAarray(self, window: Union[None, List[int], Tuple[int]]=None) -> np.ndarray:
         if window is not None:
             xoff, yoff, xsize, ysize = window
         if self.band_list is None:
@@ -114,7 +114,7 @@ class Raster:
             ima = raster2uint8(ima)
         return ima
 
-    def __getBlock(self,
+    def _getBlock(self,
                    start_loc: Union[List[int], Tuple[int]], 
                    block_size: Union[List[int], Tuple[int]]=[512, 512]) -> np.ndarray:
         if len(start_loc) != 2 or len(block_size) != 2:
@@ -128,7 +128,7 @@ class Raster:
             xsize = self.width - xoff
         if yoff + ysize > self.height:
             ysize = self.height - yoff
-        ima = self.__getAarray([int(xoff), int(yoff), int(xsize), int(ysize)])
+        ima = self._getAarray([int(xoff), int(yoff), int(xsize), int(ysize)])
         h, w = ima.shape[:2] if len(ima.shape) == 3 else ima.shape
         if self.bands != 1:
             tmp = np.zeros((block_size[0], block_size[1], self.bands), dtype=ima.dtype)

+ 1 - 0
paddlers/models/ppcd/__init__.py

@@ -0,0 +1 @@
+from .cdnet import CDNet

+ 75 - 0
paddlers/models/ppcd/cdnet.py

@@ -0,0 +1,75 @@
+# Copyright (c) 2021 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 paddle
+import paddle.nn as nn
+
+
+class CDNet(nn.Layer):
+    def __init__(self, in_channels=6, num_classes=2):
+        super(CDNet, self).__init__()
+        self.conv1 = Conv7x7(in_channels, 64, norm=True, act=True)
+        self.pool1 = nn.MaxPool2D(2, 2, return_mask=True)
+        self.conv2 = Conv7x7(64, 64, norm=True, act=True)
+        self.pool2 = nn.MaxPool2D(2, 2, return_mask=True)
+        self.conv3 = Conv7x7(64, 64, norm=True, act=True)
+        self.pool3 = nn.MaxPool2D(2, 2, return_mask=True)
+        self.conv4 = Conv7x7(64, 64, norm=True, act=True)
+        self.pool4 = nn.MaxPool2D(2, 2, return_mask=True)
+        self.conv5 = Conv7x7(64, 64, norm=True, act=True)
+        self.upool4 = nn.MaxUnPool2D(2, 2)
+        self.conv6 = Conv7x7(64, 64, norm=True, act=True)
+        self.upool3 = nn.MaxUnPool2D(2, 2)
+        self.conv7 = Conv7x7(64, 64, norm=True, act=True)
+        self.upool2 = nn.MaxUnPool2D(2, 2)
+        self.conv8 = Conv7x7(64, 64, norm=True, act=True)
+        self.upool1 = nn.MaxUnPool2D(2, 2)
+        self.conv_out = Conv7x7(64, num_classes, norm=False, act=False)
+    
+    def forward(self, t1, t2):
+        x = paddle.concat([t1, t2], axis=1)
+        x, ind1 = self.pool1(self.conv1(x))
+        x, ind2 = self.pool2(self.conv2(x))
+        x, ind3 = self.pool3(self.conv3(x))
+        x, ind4 = self.pool4(self.conv4(x))
+        x = self.conv5(self.upool4(x, ind4))
+        x = self.conv6(self.upool3(x, ind3))
+        x = self.conv7(self.upool2(x, ind2))
+        x = self.conv8(self.upool1(x, ind1))
+        return [self.conv_out(x)]
+
+
+class Conv7x7(nn.Layer):
+    def __init__(self, in_ch, out_ch, norm=False, act=False):
+        super(Conv7x7, self).__init__()
+        layers = [
+            nn.Pad2D(3),
+            nn.Conv2D(in_ch, out_ch, 7, bias_attr=(False if norm else None))
+        ]
+        if norm:
+            layers.append(nn.BatchNorm2D(out_ch))
+        if act:
+            layers.append(nn.ReLU())
+        self.layers = nn.Sequential(*layers)
+    
+    def forward(self, x):
+        return self.layers(x)
+
+
+if __name__ == "__main__":
+    t1 = paddle.randn((1, 3, 512, 512), dtype="float32")
+    t2 = paddle.randn((1, 3, 512, 512), dtype="float32")
+    model = CDNet(6, 2)
+    pred = model(t1, t2)[0]
+    print(pred.shape)

+ 16 - 0
paddlers/models/ppseg/utils/env/__init__.py

@@ -0,0 +1,16 @@
+# Copyright (c) 2020  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.
+
+from . import seg_env
+from .sys_env import get_sys_env

+ 56 - 0
paddlers/models/ppseg/utils/env/seg_env.py

@@ -0,0 +1,56 @@
+# Copyright (c) 2020  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.
+"""
+This module is used to store environmental parameters in PaddleSeg.
+
+SEG_HOME : Root directory for storing PaddleSeg related data. Default to ~/.paddleseg.
+           Users can change the default value through the SEG_HOME environment variable.
+DATA_HOME : The directory to store the automatically downloaded dataset, e.g ADE20K.
+PRETRAINED_MODEL_HOME : The directory to store the automatically downloaded pretrained model.
+"""
+
+import os
+
+from paddleseg.utils import logger
+
+
+def _get_user_home():
+    return os.path.expanduser('~')
+
+
+def _get_seg_home():
+    if 'SEG_HOME' in os.environ:
+        home_path = os.environ['SEG_HOME']
+        if os.path.exists(home_path):
+            if os.path.isdir(home_path):
+                return home_path
+            else:
+                logger.warning('SEG_HOME {} is a file!'.format(home_path))
+        else:
+            return home_path
+    return os.path.join(_get_user_home(), '.paddleseg')
+
+
+def _get_sub_home(directory):
+    home = os.path.join(_get_seg_home(), directory)
+    if not os.path.exists(home):
+        os.makedirs(home, exist_ok=True)
+    return home
+
+
+USER_HOME = _get_user_home()
+SEG_HOME = _get_seg_home()
+DATA_HOME = _get_sub_home('dataset')
+TMP_HOME = _get_sub_home('tmp')
+PRETRAINED_MODEL_HOME = _get_sub_home('pretrained_model')

+ 124 - 0
paddlers/models/ppseg/utils/env/sys_env.py

@@ -0,0 +1,124 @@
+# Copyright (c) 2020 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 glob
+import os
+import platform
+import subprocess
+import sys
+
+import cv2
+import paddle
+import paddleseg
+
+IS_WINDOWS = sys.platform == 'win32'
+
+
+def _find_cuda_home():
+    '''Finds the CUDA install path. It refers to the implementation of
+    pytorch <https://github.com/pytorch/pytorch/blob/master/torch/utils/cpp_extension.py>.
+    '''
+    # Guess #1
+    cuda_home = os.environ.get('CUDA_HOME') or os.environ.get('CUDA_PATH')
+    if cuda_home is None:
+        # Guess #2
+        try:
+            which = 'where' if IS_WINDOWS else 'which'
+            nvcc = subprocess.check_output([which,
+                                            'nvcc']).decode().rstrip('\r\n')
+            cuda_home = os.path.dirname(os.path.dirname(nvcc))
+        except Exception:
+            # Guess #3
+            if IS_WINDOWS:
+                cuda_homes = glob.glob(
+                    'C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v*.*')
+                if len(cuda_homes) == 0:
+                    cuda_home = ''
+                else:
+                    cuda_home = cuda_homes[0]
+            else:
+                cuda_home = '/usr/local/cuda'
+            if not os.path.exists(cuda_home):
+                cuda_home = None
+    return cuda_home
+
+
+def _get_nvcc_info(cuda_home):
+    if cuda_home is not None and os.path.isdir(cuda_home):
+        try:
+            nvcc = os.path.join(cuda_home, 'bin/nvcc')
+            nvcc = subprocess.check_output(
+                "{} -V".format(nvcc), shell=True).decode()
+            nvcc = nvcc.strip().split('\n')[-1]
+        except subprocess.SubprocessError:
+            nvcc = "Not Available"
+    else:
+        nvcc = "Not Available"
+    return nvcc
+
+
+def _get_gpu_info():
+    try:
+        gpu_info = subprocess.check_output(['nvidia-smi',
+                                            '-L']).decode().strip()
+        gpu_info = gpu_info.split('\n')
+        for i in range(len(gpu_info)):
+            gpu_info[i] = ' '.join(gpu_info[i].split(' ')[:4])
+    except:
+        gpu_info = ' Can not get GPU information. Please make sure CUDA have been installed successfully.'
+    return gpu_info
+
+
+def get_sys_env():
+    """collect environment information"""
+    env_info = {}
+    env_info['platform'] = platform.platform()
+
+    env_info['Python'] = sys.version.replace('\n', '')
+
+    # TODO is_compiled_with_cuda() has not been moved
+    compiled_with_cuda = paddle.is_compiled_with_cuda()
+    env_info['Paddle compiled with cuda'] = compiled_with_cuda
+
+    if compiled_with_cuda:
+        cuda_home = _find_cuda_home()
+        env_info['NVCC'] = _get_nvcc_info(cuda_home)
+        # refer to https://github.com/PaddlePaddle/Paddle/blob/release/2.0-rc/paddle/fluid/platform/device_context.cc#L327
+        v = paddle.get_cudnn_version()
+        v = str(v // 1000) + '.' + str(v % 1000 // 100)
+        env_info['cudnn'] = v
+        if 'gpu' in paddle.get_device():
+            gpu_nums = paddle.distributed.ParallelEnv().nranks
+        else:
+            gpu_nums = 0
+        env_info['GPUs used'] = gpu_nums
+
+        env_info['CUDA_VISIBLE_DEVICES'] = os.environ.get(
+            'CUDA_VISIBLE_DEVICES')
+        if gpu_nums == 0:
+            os.environ['CUDA_VISIBLE_DEVICES'] = ''
+        env_info['GPU'] = _get_gpu_info()
+
+    try:
+        gcc = subprocess.check_output(['gcc', '--version']).decode()
+        gcc = gcc.strip().split('\n')[0]
+        env_info['GCC'] = gcc
+    except:
+        pass
+
+    env_info['PaddleSeg'] = paddleseg.__version__
+    env_info['PaddlePaddle'] = paddle.__version__
+    env_info['OpenCV'] = cv2.__version__
+
+    return env_info

+ 671 - 0
paddlers/tasks/changedetector.py

@@ -0,0 +1,671 @@
+# Copyright (c) 2021 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 math
+import os.path as osp
+import numpy as np
+import cv2
+from collections import OrderedDict
+import paddle
+import paddle.nn.functional as F
+from paddle.static import InputSpec
+import paddlers.models.ppseg as paddleseg
+import paddlers
+from paddlers.transforms import arrange_transforms
+from paddlers.utils import get_single_card_bs, DisablePrint
+import paddlers.utils.logging as logging
+from .base import BaseModel
+from .utils import seg_metrics as metrics
+from paddlers.utils.checkpoint import seg_pretrain_weights_dict
+from paddlers.transforms import Decode, Resize
+from paddlers.models.ppcd import CDNet
+
+__all__ = ["CDNet"]
+
+
+class BaseChangeDetector(BaseModel):
+    def __init__(self,
+                 model_name,
+                 num_classes=2,
+                 use_mixed_loss=False,
+                 **params):
+        self.init_params = locals()
+        if 'with_net' in self.init_params:
+            del self.init_params['with_net']
+        super(BaseChangeDetector, self).__init__('changedetector')
+        if model_name not in __all__:
+            raise Exception("ERROR: There's no model named {}.".format(
+                model_name))
+        self.model_name = model_name
+        self.num_classes = num_classes
+        self.use_mixed_loss = use_mixed_loss
+        self.losses = None
+        self.labels = None
+        if params.get('with_net', True):
+            params.pop('with_net', None)
+            self.net = self.build_net(**params)
+        self.find_unused_parameters = True
+
+    def build_net(self, **params):
+        # TODO: add other model
+        net = CDNet(num_classes=self.num_classes, **params)
+        return net
+
+    def _fix_transforms_shape(self, image_shape):
+        if hasattr(self, 'test_transforms'):
+            if self.test_transforms is not None:
+                has_resize_op = False
+                resize_op_idx = -1
+                normalize_op_idx = len(self.test_transforms.transforms)
+                for idx, op in enumerate(self.test_transforms.transforms):
+                    name = op.__class__.__name__
+                    if name == 'Normalize':
+                        normalize_op_idx = idx
+                    if 'Resize' in name:
+                        has_resize_op = True
+                        resize_op_idx = idx
+
+                if not has_resize_op:
+                    self.test_transforms.transforms.insert(
+                        normalize_op_idx, Resize(target_size=image_shape))
+                else:
+                    self.test_transforms.transforms[resize_op_idx] = Resize(
+                        target_size=image_shape)
+
+    def _get_test_inputs(self, image_shape):
+        if image_shape is not None:
+            if len(image_shape) == 2:
+                image_shape = [1, 3] + image_shape
+            self._fix_transforms_shape(image_shape[-2:])
+        else:
+            image_shape = [None, 3, -1, -1]
+        self.fixed_input_shape = image_shape
+        input_spec = [
+            InputSpec(
+                shape=image_shape, name='image', dtype='float32')
+        ]
+        return input_spec
+
+    def run(self, net, inputs, mode):
+        net_out = net(inputs[0], inputs[1])
+        logit = net_out[0]
+        outputs = OrderedDict()
+        if mode == 'test':
+            origin_shape = inputs[2]
+            if self.status == 'Infer':
+                label_map_list, score_map_list = self._postprocess(
+                    net_out, origin_shape, transforms=inputs[3])
+            else:
+                logit_list = self._postprocess(
+                    logit, origin_shape, transforms=inputs[3])
+                label_map_list = []
+                score_map_list = []
+                for logit in logit_list:
+                    logit = paddle.transpose(logit, perm=[0, 2, 3, 1])  # NHWC
+                    label_map_list.append(
+                        paddle.argmax(
+                            logit, axis=-1, keepdim=False, dtype='int32')
+                        .squeeze().numpy())
+                    score_map_list.append(
+                        F.softmax(
+                            logit, axis=-1).squeeze().numpy().astype(
+                                'float32'))
+            outputs['label_map'] = label_map_list
+            outputs['score_map'] = score_map_list
+
+        if mode == 'eval':
+            if self.status == 'Infer':
+                pred = paddle.unsqueeze(net_out[0], axis=1)  # NCHW
+            else:
+                pred = paddle.argmax(
+                    logit, axis=1, keepdim=True, dtype='int32')
+            label = inputs[2]
+            origin_shape = [label.shape[-2:]]
+            pred = self._postprocess(
+                pred, origin_shape, transforms=inputs[3])[0]  # NCHW
+            intersect_area, pred_area, label_area = paddleseg.utils.metrics.calculate_area(
+                pred, label, self.num_classes)
+            outputs['intersect_area'] = intersect_area
+            outputs['pred_area'] = pred_area
+            outputs['label_area'] = label_area
+            outputs['conf_mat'] = metrics.confusion_matrix(pred, label,
+                                                           self.num_classes)
+        if mode == 'train':
+            loss_list = metrics.loss_computation(
+                logits_list=net_out, labels=inputs[2], losses=self.losses)
+            loss = sum(loss_list)
+            outputs['loss'] = loss
+        return outputs
+
+    def default_loss(self):
+        if isinstance(self.use_mixed_loss, bool):
+            if self.use_mixed_loss:
+                losses = [
+                    paddleseg.models.CrossEntropyLoss(),
+                    paddleseg.models.LovaszSoftmaxLoss()
+                ]
+                coef = [.8, .2]
+                loss_type = [
+                    paddleseg.models.MixedLoss(
+                        losses=losses, coef=coef),
+                ]
+            else:
+                loss_type = [paddleseg.models.CrossEntropyLoss()]
+        else:
+            losses, coef = list(zip(*self.use_mixed_loss))
+            if not set(losses).issubset(
+                ['CrossEntropyLoss', 'DiceLoss', 'LovaszSoftmaxLoss']):
+                raise ValueError(
+                    "Only 'CrossEntropyLoss', 'DiceLoss', 'LovaszSoftmaxLoss' are supported."
+                )
+            losses = [getattr(paddleseg.models, loss)() for loss in losses]
+            loss_type = [
+                paddleseg.models.MixedLoss(
+                    losses=losses, coef=list(coef))
+            ]
+        if self.model_name == 'FastSCNN':
+            loss_type *= 2
+            loss_coef = [1.0, 0.4]
+        elif self.model_name == 'BiSeNetV2':
+            loss_type *= 5
+            loss_coef = [1.0] * 5
+        else:
+            loss_coef = [1.0]
+        losses = {'types': loss_type, 'coef': loss_coef}
+        return losses
+
+    def default_optimizer(self,
+                          parameters,
+                          learning_rate,
+                          num_epochs,
+                          num_steps_each_epoch,
+                          lr_decay_power=0.9):
+        decay_step = num_epochs * num_steps_each_epoch
+        lr_scheduler = paddle.optimizer.lr.PolynomialDecay(
+            learning_rate, decay_step, end_lr=0, power=lr_decay_power)
+        optimizer = paddle.optimizer.Momentum(
+            learning_rate=lr_scheduler,
+            parameters=parameters,
+            momentum=0.9,
+            weight_decay=4e-5)
+        return optimizer
+
+    def train(self,
+              num_epochs,
+              train_dataset,
+              train_batch_size=2,
+              eval_dataset=None,
+              optimizer=None,
+              save_interval_epochs=1,
+              log_interval_steps=2,
+              save_dir='output',
+              pretrain_weights='CITYSCAPES',
+              learning_rate=0.01,
+              lr_decay_power=0.9,
+              early_stop=False,
+              early_stop_patience=5,
+              use_vdl=True,
+              resume_checkpoint=None):
+        """
+        Train the model.
+        Args:
+            num_epochs(int): The number of epochs.
+            train_dataset(paddlers.dataset): Training dataset.
+            train_batch_size(int, optional): Total batch size among all cards used in training. Defaults to 2.
+            eval_dataset(paddlers.dataset, optional):
+                Evaluation dataset. If None, the model will not be evaluated furing training process. Defaults to None.
+            optimizer(paddle.optimizer.Optimizer or None, optional):
+                Optimizer used in training. If None, a default optimizer is used. Defaults to None.
+            save_interval_epochs(int, optional): Epoch interval for saving the model. Defaults to 1.
+            log_interval_steps(int, optional): Step interval for printing training information. Defaults to 10.
+            save_dir(str, optional): Directory to save the model. Defaults to 'output'.
+            pretrain_weights(str or None, optional):
+                None or name/path of pretrained weights. If None, no pretrained weights will be loaded. Defaults to 'CITYSCAPES'.
+            learning_rate(float, optional): Learning rate for training. Defaults to .025.
+            lr_decay_power(float, optional): Learning decay power. Defaults to .9.
+            early_stop(bool, optional): Whether to adopt early stop strategy. Defaults to False.
+            early_stop_patience(int, optional): Early stop patience. Defaults to 5.
+            use_vdl(bool, optional): Whether to use VisualDL to monitor the training process. Defaults to True.
+            resume_checkpoint(str or None, optional): The path of the checkpoint to resume training from.
+                If None, no training checkpoint will be resumed. At most one of `resume_checkpoint` and
+                `pretrain_weights` can be set simultaneously. Defaults to None.
+
+        """
+        if self.status == 'Infer':
+            logging.error(
+                "Exported inference model does not support training.",
+                exit=True)
+        if pretrain_weights is not None and resume_checkpoint is not None:
+            logging.error(
+                "pretrain_weights and resume_checkpoint cannot be set simultaneously.",
+                exit=True)
+        self.labels = train_dataset.labels
+        if self.losses is None:
+            self.losses = self.default_loss()
+
+        if optimizer is None:
+            num_steps_each_epoch = train_dataset.num_samples // train_batch_size
+            self.optimizer = self.default_optimizer(
+                self.net.parameters(), learning_rate, num_epochs,
+                num_steps_each_epoch, lr_decay_power)
+        else:
+            self.optimizer = optimizer
+
+        if pretrain_weights is not None and not osp.exists(pretrain_weights):
+            if pretrain_weights not in seg_pretrain_weights_dict[
+                    self.model_name]:
+                logging.warning(
+                    "Path of pretrain_weights('{}') does not exist!".format(
+                        pretrain_weights))
+                logging.warning("Pretrain_weights is forcibly set to '{}'. "
+                                "If don't want to use pretrain weights, "
+                                "set pretrain_weights to be None.".format(
+                                    seg_pretrain_weights_dict[self.model_name][
+                                        0]))
+                pretrain_weights = seg_pretrain_weights_dict[self.model_name][
+                    0]
+        elif pretrain_weights is not None and osp.exists(pretrain_weights):
+            if osp.splitext(pretrain_weights)[-1] != '.pdparams':
+                logging.error(
+                    "Invalid pretrain weights. Please specify a '.pdparams' file.",
+                    exit=True)
+        pretrained_dir = osp.join(save_dir, 'pretrain')
+        is_backbone_weights = pretrain_weights == 'IMAGENET'
+        self.net_initialize(
+            pretrain_weights=pretrain_weights,
+            save_dir=pretrained_dir,
+            resume_checkpoint=resume_checkpoint,
+            is_backbone_weights=is_backbone_weights)
+
+        self.train_loop(
+            num_epochs=num_epochs,
+            train_dataset=train_dataset,
+            train_batch_size=train_batch_size,
+            eval_dataset=eval_dataset,
+            save_interval_epochs=save_interval_epochs,
+            log_interval_steps=log_interval_steps,
+            save_dir=save_dir,
+            early_stop=early_stop,
+            early_stop_patience=early_stop_patience,
+            use_vdl=use_vdl)
+
+    def quant_aware_train(self,
+                          num_epochs,
+                          train_dataset,
+                          train_batch_size=2,
+                          eval_dataset=None,
+                          optimizer=None,
+                          save_interval_epochs=1,
+                          log_interval_steps=2,
+                          save_dir='output',
+                          learning_rate=0.0001,
+                          lr_decay_power=0.9,
+                          early_stop=False,
+                          early_stop_patience=5,
+                          use_vdl=True,
+                          resume_checkpoint=None,
+                          quant_config=None):
+        """
+        Quantization-aware training.
+        Args:
+            num_epochs(int): The number of epochs.
+            train_dataset(paddlers.dataset): Training dataset.
+            train_batch_size(int, optional): Total batch size among all cards used in training. Defaults to 2.
+            eval_dataset(paddlers.dataset, optional):
+                Evaluation dataset. If None, the model will not be evaluated furing training process. Defaults to None.
+            optimizer(paddle.optimizer.Optimizer or None, optional):
+                Optimizer used in training. If None, a default optimizer is used. Defaults to None.
+            save_interval_epochs(int, optional): Epoch interval for saving the model. Defaults to 1.
+            log_interval_steps(int, optional): Step interval for printing training information. Defaults to 10.
+            save_dir(str, optional): Directory to save the model. Defaults to 'output'.
+            learning_rate(float, optional): Learning rate for training. Defaults to .025.
+            lr_decay_power(float, optional): Learning decay power. Defaults to .9.
+            early_stop(bool, optional): Whether to adopt early stop strategy. Defaults to False.
+            early_stop_patience(int, optional): Early stop patience. Defaults to 5.
+            use_vdl(bool, optional): Whether to use VisualDL to monitor the training process. Defaults to True.
+            quant_config(dict or None, optional): Quantization configuration. If None, a default rule of thumb
+                configuration will be used. Defaults to None.
+            resume_checkpoint(str or None, optional): The path of the checkpoint to resume quantization-aware training
+                from. If None, no training checkpoint will be resumed. Defaults to None.
+
+        """
+        self._prepare_qat(quant_config)
+        self.train(
+            num_epochs=num_epochs,
+            train_dataset=train_dataset,
+            train_batch_size=train_batch_size,
+            eval_dataset=eval_dataset,
+            optimizer=optimizer,
+            save_interval_epochs=save_interval_epochs,
+            log_interval_steps=log_interval_steps,
+            save_dir=save_dir,
+            pretrain_weights=None,
+            learning_rate=learning_rate,
+            lr_decay_power=lr_decay_power,
+            early_stop=early_stop,
+            early_stop_patience=early_stop_patience,
+            use_vdl=use_vdl,
+            resume_checkpoint=resume_checkpoint)
+
+    def evaluate(self, eval_dataset, batch_size=1, return_details=False):
+        """
+        Evaluate the model.
+        Args:
+            eval_dataset(paddlers.dataset): Evaluation dataset.
+            batch_size(int, optional): Total batch size among all cards used for evaluation. Defaults to 1.
+            return_details(bool, optional): Whether to return evaluation details. Defaults to False.
+
+        Returns:
+            collections.OrderedDict with key-value pairs:
+                {"miou": `mean intersection over union`,
+                 "category_iou": `category-wise mean intersection over union`,
+                 "oacc": `overall accuracy`,
+                 "category_acc": `category-wise accuracy`,
+                 "kappa": ` kappa coefficient`,
+                 "category_F1-score": `F1 score`}.
+
+        """
+        arrange_transforms(
+            model_type=self.model_type,
+            transforms=eval_dataset.transforms,
+            mode='eval')
+
+        self.net.eval()
+        nranks = paddle.distributed.get_world_size()
+        local_rank = paddle.distributed.get_rank()
+        if nranks > 1:
+            # Initialize parallel environment if not done.
+            if not paddle.distributed.parallel.parallel_helper._is_parallel_ctx_initialized(
+            ):
+                paddle.distributed.init_parallel_env()
+
+        batch_size_each_card = get_single_card_bs(batch_size)
+        if batch_size_each_card > 1:
+            batch_size_each_card = 1
+            batch_size = batch_size_each_card * paddlers.env_info['num']
+            logging.warning(
+                "Segmenter only supports batch_size=1 for each gpu/cpu card " \
+                "during evaluation, so batch_size " \
+                "is forcibly set to {}.".format(batch_size))
+        self.eval_data_loader = self.build_data_loader(
+            eval_dataset, batch_size=batch_size, mode='eval')
+
+        intersect_area_all = 0
+        pred_area_all = 0
+        label_area_all = 0
+        conf_mat_all = []
+        logging.info(
+            "Start to evaluate(total_samples={}, total_steps={})...".format(
+                eval_dataset.num_samples,
+                math.ceil(eval_dataset.num_samples * 1.0 / batch_size)))
+        with paddle.no_grad():
+            for step, data in enumerate(self.eval_data_loader):
+                data.append(eval_dataset.transforms.transforms)
+                outputs = self.run(self.net, data, 'eval')
+                pred_area = outputs['pred_area']
+                label_area = outputs['label_area']
+                intersect_area = outputs['intersect_area']
+                conf_mat = outputs['conf_mat']
+
+                # Gather from all ranks
+                if nranks > 1:
+                    intersect_area_list = []
+                    pred_area_list = []
+                    label_area_list = []
+                    conf_mat_list = []
+                    paddle.distributed.all_gather(intersect_area_list,
+                                                  intersect_area)
+                    paddle.distributed.all_gather(pred_area_list, pred_area)
+                    paddle.distributed.all_gather(label_area_list, label_area)
+                    paddle.distributed.all_gather(conf_mat_list, conf_mat)
+
+                    # Some image has been evaluated and should be eliminated in last iter
+                    if (step + 1) * nranks > len(eval_dataset):
+                        valid = len(eval_dataset) - step * nranks
+                        intersect_area_list = intersect_area_list[:valid]
+                        pred_area_list = pred_area_list[:valid]
+                        label_area_list = label_area_list[:valid]
+                        conf_mat_list = conf_mat_list[:valid]
+
+                    intersect_area_all += sum(intersect_area_list)
+                    pred_area_all += sum(pred_area_list)
+                    label_area_all += sum(label_area_list)
+                    conf_mat_all.extend(conf_mat_list)
+
+                else:
+                    intersect_area_all = intersect_area_all + intersect_area
+                    pred_area_all = pred_area_all + pred_area
+                    label_area_all = label_area_all + label_area
+                    conf_mat_all.append(conf_mat)
+        class_iou, miou = paddleseg.utils.metrics.mean_iou(
+            intersect_area_all, pred_area_all, label_area_all)
+        # TODO 确认是按oacc还是macc
+        class_acc, oacc = paddleseg.utils.metrics.accuracy(intersect_area_all,
+                                                           pred_area_all)
+        kappa = paddleseg.utils.metrics.kappa(intersect_area_all,
+                                              pred_area_all, label_area_all)
+        category_f1score = metrics.f1_score(intersect_area_all, pred_area_all,
+                                            label_area_all)
+        eval_metrics = OrderedDict(
+            zip([
+                'miou', 'category_iou', 'oacc', 'category_acc', 'kappa',
+                'category_F1-score'
+            ], [miou, class_iou, oacc, class_acc, kappa, category_f1score]))
+
+        if return_details:
+            conf_mat = sum(conf_mat_all)
+            eval_details = {'confusion_matrix': conf_mat.tolist()}
+            return eval_metrics, eval_details
+        return eval_metrics
+
+    def predict(self, img_file, transforms=None):
+        """
+        Do inference.
+        Args:
+            Args:
+            img_file(List[np.ndarray or str], str or np.ndarray):
+                Image path or decoded image data in a BGR format, which also could constitute a list,
+                meaning all images to be predicted as a mini-batch.
+            transforms(paddlers.transforms.Compose or None, optional):
+                Transforms for inputs. If None, the transforms for evaluation process will be used. Defaults to None.
+
+        Returns:
+            If img_file is a string or np.array, the result is a dict with key-value pairs:
+            {"label map": `label map`, "score_map": `score map`}.
+            If img_file is a list, the result is a list composed of dicts with the corresponding fields:
+            label_map(np.ndarray): the predicted label map (HW)
+            score_map(np.ndarray): the prediction score map (HWC)
+
+        """
+        if transforms is None and not hasattr(self, 'test_transforms'):
+            raise Exception("transforms need to be defined, now is None.")
+        if transforms is None:
+            transforms = self.test_transforms
+        if isinstance(img_file, (str, np.ndarray)):
+            images = [img_file]
+        else:
+            images = img_file
+        batch_im, batch_origin_shape = self._preprocess(images, transforms,
+                                                        self.model_type)
+        self.net.eval()
+        data = (batch_im, batch_origin_shape, transforms.transforms)
+        outputs = self.run(self.net, data, 'test')
+        label_map_list = outputs['label_map']
+        score_map_list = outputs['score_map']
+        if isinstance(img_file, list):
+            prediction = [{
+                'label_map': l,
+                'score_map': s
+            } for l, s in zip(label_map_list, score_map_list)]
+        else:
+            prediction = {
+                'label_map': label_map_list[0],
+                'score_map': score_map_list[0]
+            }
+        return prediction
+
+    def _preprocess(self, images, transforms, to_tensor=True):
+        arrange_transforms(
+            model_type=self.model_type, transforms=transforms, mode='test')
+        batch_im = list()
+        batch_ori_shape = list()
+        for im in images:
+            sample = {'image': im}
+            if isinstance(sample['image'], str):
+                sample = Decode(to_rgb=False)(sample)
+            ori_shape = sample['image'].shape[:2]
+            im = transforms(sample)[0]
+            batch_im.append(im)
+            batch_ori_shape.append(ori_shape)
+        if to_tensor:
+            batch_im = paddle.to_tensor(batch_im)
+        else:
+            batch_im = np.asarray(batch_im)
+
+        return batch_im, batch_ori_shape
+
+    @staticmethod
+    def get_transforms_shape_info(batch_ori_shape, transforms):
+        batch_restore_list = list()
+        for ori_shape in batch_ori_shape:
+            restore_list = list()
+            h, w = ori_shape[0], ori_shape[1]
+            for op in transforms:
+                if op.__class__.__name__ == 'Resize':
+                    restore_list.append(('resize', (h, w)))
+                    h, w = op.target_size
+                elif op.__class__.__name__ == 'ResizeByShort':
+                    restore_list.append(('resize', (h, w)))
+                    im_short_size = min(h, w)
+                    im_long_size = max(h, w)
+                    scale = float(op.short_size) / float(im_short_size)
+                    if 0 < op.max_size < np.round(scale * im_long_size):
+                        scale = float(op.max_size) / float(im_long_size)
+                    h = int(round(h * scale))
+                    w = int(round(w * scale))
+                elif op.__class__.__name__ == 'ResizeByLong':
+                    restore_list.append(('resize', (h, w)))
+                    im_long_size = max(h, w)
+                    scale = float(op.long_size) / float(im_long_size)
+                    h = int(round(h * scale))
+                    w = int(round(w * scale))
+                elif op.__class__.__name__ == 'Padding':
+                    if op.target_size:
+                        target_h, target_w = op.target_size
+                    else:
+                        target_h = int(
+                            (np.ceil(h / op.size_divisor) * op.size_divisor))
+                        target_w = int(
+                            (np.ceil(w / op.size_divisor) * op.size_divisor))
+
+                    if op.pad_mode == -1:
+                        offsets = op.offsets
+                    elif op.pad_mode == 0:
+                        offsets = [0, 0]
+                    elif op.pad_mode == 1:
+                        offsets = [(target_h - h) // 2, (target_w - w) // 2]
+                    else:
+                        offsets = [target_h - h, target_w - w]
+                    restore_list.append(('padding', (h, w), offsets))
+                    h, w = target_h, target_w
+
+            batch_restore_list.append(restore_list)
+        return batch_restore_list
+
+    def _postprocess(self, batch_pred, batch_origin_shape, transforms):
+        batch_restore_list = BaseSegmenter.get_transforms_shape_info(
+            batch_origin_shape, transforms)
+        if isinstance(batch_pred, (tuple, list)) and self.status == 'Infer':
+            return self._infer_postprocess(
+                batch_label_map=batch_pred[0],
+                batch_score_map=batch_pred[1],
+                batch_restore_list=batch_restore_list)
+        results = []
+        if batch_pred.dtype == paddle.float32:
+            mode = 'bilinear'
+        else:
+            mode = 'nearest'
+        for pred, restore_list in zip(batch_pred, batch_restore_list):
+            pred = paddle.unsqueeze(pred, axis=0)
+            for item in restore_list[::-1]:
+                h, w = item[1][0], item[1][1]
+                if item[0] == 'resize':
+                    pred = F.interpolate(
+                        pred, (h, w), mode=mode, data_format='NCHW')
+                elif item[0] == 'padding':
+                    x, y = item[2]
+                    pred = pred[:, :, y:y + h, x:x + w]
+                else:
+                    pass
+            results.append(pred)
+        return results
+
+    def _infer_postprocess(self, batch_label_map, batch_score_map,
+                           batch_restore_list):
+        label_maps = []
+        score_maps = []
+        for label_map, score_map, restore_list in zip(
+                batch_label_map, batch_score_map, batch_restore_list):
+            if not isinstance(label_map, np.ndarray):
+                label_map = paddle.unsqueeze(label_map, axis=[0, 3])
+                score_map = paddle.unsqueeze(score_map, axis=0)
+            for item in restore_list[::-1]:
+                h, w = item[1][0], item[1][1]
+                if item[0] == 'resize':
+                    if isinstance(label_map, np.ndarray):
+                        label_map = cv2.resize(
+                            label_map, (w, h), interpolation=cv2.INTER_NEAREST)
+                        score_map = cv2.resize(
+                            score_map, (w, h), interpolation=cv2.INTER_LINEAR)
+                    else:
+                        label_map = F.interpolate(
+                            label_map, (h, w),
+                            mode='nearest',
+                            data_format='NHWC')
+                        score_map = F.interpolate(
+                            score_map, (h, w),
+                            mode='bilinear',
+                            data_format='NHWC')
+                elif item[0] == 'padding':
+                    x, y = item[2]
+                    if isinstance(label_map, np.ndarray):
+                        label_map = label_map[..., y:y + h, x:x + w]
+                        score_map = score_map[..., y:y + h, x:x + w]
+                    else:
+                        label_map = label_map[:, :, y:y + h, x:x + w]
+                        score_map = score_map[:, :, y:y + h, x:x + w]
+                else:
+                    pass
+            label_map = label_map.squeeze()
+            score_map = score_map.squeeze()
+            if not isinstance(label_map, np.ndarray):
+                label_map = label_map.numpy()
+                score_map = score_map.numpy()
+            label_maps.append(label_map.squeeze())
+            score_maps.append(score_map.squeeze())
+        return label_maps, score_maps
+
+
+class CDNet(BaseChangeDetector):
+    def __init__(self,
+                 num_classes=2,
+                 use_mixed_loss=False,
+                 in_channels=6,
+                 **params):
+        params.update({'in_channels': in_channels})
+        super(CDNet, self).__init__(
+            model_name='UNet',
+            num_classes=num_classes,
+            use_mixed_loss=use_mixed_loss,
+            **params)

+ 7 - 0
paddlers/transforms/__init__.py

@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from operator import mod
 from .operators import *
 from .batch_operators import BatchRandomResize, BatchRandomResizeByShort, _BatchPadding
 from paddlers import transforms as T
@@ -25,6 +26,12 @@ def arrange_transforms(model_type, transforms, mode='train'):
         else:
             transforms.apply_im_only = False
         arrange_transform = ArrangeSegmenter(mode)
+    elif model_type == 'changedetctor':
+        if mode == 'eval':
+            transforms.apply_im_only = True
+        else:
+            transforms.apply_im_only = False
+        arrange_transform = ArrangeChangeDetector(mode)
     elif model_type == 'classifier':
         arrange_transform = ArrangeClassifier(mode)
     elif model_type == 'detector':

+ 26 - 0
paddlers/transforms/operators.py

@@ -1370,6 +1370,32 @@ class ArrangeSegmenter(Transform):
             return image,
 
 
+class ArrangeChangeDetector(Transform):
+    def __init__(self, mode):
+        super(ArrangeChangeDetector, self).__init__()
+        if mode not in ['train', 'eval', 'test', 'quant']:
+            raise ValueError(
+                "mode should be defined as one of ['train', 'eval', 'test', 'quant']!"
+            )
+        self.mode = mode
+
+    def apply(self, sample):
+        if 'mask' in sample:
+            mask = sample['mask']
+
+        image_t1 = permute(sample['image_t1'], False)
+        image_t2 = permute(sample['image_t2'], False)
+        if self.mode == 'train':
+            mask = mask.astype('int64')
+            return image_t1, image_t2, mask
+        if self.mode == 'eval':
+            mask = np.asarray(Image.open(mask))
+            mask = mask[np.newaxis, :, :].astype('int64')
+            return image_t1, image_t2, mask
+        if self.mode == 'test':
+            return image_t1, image_t2,
+
+
 class ArrangeClassifier(Transform):
     def __init__(self, mode):
         super(ArrangeClassifier, self).__init__()

+ 1 - 1
requirements.txt

@@ -14,4 +14,4 @@ motmetrics
 matplotlib
 chardet
 openpyxl
-GDAL >= 3.2.2
+GDAL >= 3.1.3