Jelajahi Sumber

add change detection

FlyingQianMM 5 tahun lalu
induk
melakukan
4347c41c47

+ 43 - 0
docs/apis/datasets.md

@@ -140,3 +140,46 @@ paddlex.datasets.EasyDataSeg(data_dir, file_list, label_list, transforms=None, n
 > > * **buffer_size** (int): 数据集中样本在预处理过程中队列的缓存长度,以样本数为单位。默认为100。  
 > > * **parallel_method** (str): 数据集中样本在预处理过程中并行处理的方式,支持'thread'线程和'process'进程两种方式。默认为'process'(Windows和Mac下会强制使用thread,该参数无效)。  
 > > * **shuffle** (bool): 是否需要对数据集中样本打乱顺序。默认为False。
+
+
+## paddlex.datasets.SegDataset
+> **用于语义分割模型**  
+```
+paddlex.datasets.SegDataset(data_dir, file_list, label_list, transforms=None, num_workers='auto', buffer_size=100, parallel_method='process', shuffle=False)
+```
+
+> 读取语义分割任务数据集,并对样本进行相应的处理。语义分割任务数据集格式的介绍可查看文档:[数据集格式说明](../data/format/segmentation.md)  
+
+> 示例:[代码文件](https://github.com/PaddlePaddle/PaddleX/blob/develop/tutorials/train/semantic_segmentation/unet.py)
+
+> **参数**
+
+> > * **data_dir** (str): 数据集所在的目录路径。  
+> > * **file_list** (str): 描述数据集图片文件和对应标注文件的文件路径(文本内每行路径为相对`data_dir`的相对路径)。
+> > * **label_list** (str): 描述数据集包含的类别信息文件路径。  
+> > * **transforms** (paddlex.seg.transforms): 数据集中每个样本的预处理/增强算子,详见[paddlex.seg.transforms](./transforms/seg_transforms.md)。  
+> > * **num_workers** (int|str):数据集中样本在预处理过程中的线程或进程数。默认为'auto'。当设为'auto'时,根据系统的实际CPU核数设置`num_workers`: 如果CPU核数的一半大于8,则`num_workers`为8,否则为CPU核数的一半。
+> > * **buffer_size** (int): 数据集中样本在预处理过程中队列的缓存长度,以样本数为单位。默认为100。  
+> > * **parallel_method** (str): 数据集中样本在预处理过程中并行处理的方式,支持'thread'线程和'process'进程两种方式。默认为'process'(Windows和Mac下会强制使用thread,该参数无效)。  
+> > * **shuffle** (bool): 是否需要对数据集中样本打乱顺序。默认为False。
+
+## paddlex.datasets.ChangeDetDataset
+> **用于完成变化检测的语义分割模型**  
+```
+paddlex.datasets.ChangeDetDataset(data_dir, file_list, label_list, transforms=None, num_workers='auto', buffer_size=100, parallel_method='process', shuffle=False)
+```
+
+> 读取用于完成变化检测的语义分割数据集,并对样本进行相应的处理。地块检测数据集格式的介绍可查看文档:[数据集格式说明](../data/format/change_det.md)  
+
+> 示例:[代码文件](https://github.com/PaddlePaddle/PaddleX/blob/develop/examples/change_detection/train.py)
+
+> **参数**
+
+> > * **data_dir** (str): 数据集所在的目录路径。  
+> > * **file_list** (str): 描述数据集图片1文件、图片2文件和对应标注文件的文件路径(文本内每行路径为相对`data_dir`的相对路径)。
+> > * **label_list** (str): 描述数据集包含的类别信息文件路径。  
+> > * **transforms** (paddlex.seg.transforms): 数据集中每个样本的预处理/增强算子,详见[paddlex.seg.transforms](./transforms/seg_transforms.md)。  
+> > * **num_workers** (int|str):数据集中样本在预处理过程中的线程或进程数。默认为'auto'。当设为'auto'时,根据系统的实际CPU核数设置`num_workers`: 如果CPU核数的一半大于8,则`num_workers`为8,否则为CPU核数的一半。
+> > * **buffer_size** (int): 数据集中样本在预处理过程中队列的缓存长度,以样本数为单位。默认为100。  
+> > * **parallel_method** (str): 数据集中样本在预处理过程中并行处理的方式,支持'thread'线程和'process'进程两种方式。默认为'process'(Windows和Mac下会强制使用thread,该参数无效)。  
+> > * **shuffle** (bool): 是否需要对数据集中样本打乱顺序。默认为False。

+ 1 - 0
docs/data/format/index.rst

@@ -10,3 +10,4 @@
    detection.md
    instance_segmentation.md
    segmentation.md
+   change_detection.md

+ 100 - 0
docs/examples/change_detection.md

@@ -0,0 +1,100 @@
+# 地块变化检测
+
+本案例基于PaddleX实现地块变化检测,将同一地块的前期与后期两张图片进行拼接,而后输入给语义分割网络进行变化区域的预测。在训练阶段,使用随机缩放尺寸、旋转、裁剪、颜色空间扰动、水平翻转、竖直翻转多种数据增强策略。在验证和预测阶段,使用滑动窗口预测方式,以避免在直接对大尺寸图片进行预测时显存不足的发生。
+
+#### 前置依赖
+
+* Paddle paddle >= 1.8.4
+* Python >= 3.5
+* PaddleX >= 1.3.0
+
+安装的相关问题参考[PaddleX安装](../install.md)
+
+下载PaddleX源码:
+
+```
+git clone https://github.com/PaddlePaddle/PaddleX
+```
+
+该案例所有脚本均位于`PaddleX/examples/change_detection/`,进入该目录:
+
+```
+cd PaddleX/examples/change_detection/
+```
+
+## 数据准备
+
+本案例使用[Daifeng Peng等人](https://ieeexplore.ieee.org/document/9161009)开放的[Google Dataset](https://github.com/daifeng2016/Change-Detection-Dataset-for-High-Resolution-Satellite-Imagery), 该数据集涵盖了广州部分区域于2006年至2019年期间的房屋建筑物的变化情况,用于分析城市化进程。一共有20对高清图片,图片有红、绿、蓝三个波段,空间分辨率为0.55m,图片大小有1006x1168至4936x5224不等。
+
+由于Google Dataset仅标注了房屋建筑物是否发生变化,因此本案例是二分类变化检测任务,可根据实际需求修改类别数量即可拓展为多分类变化检测。
+
+本案例将15张图片划分入训练集,5张图片划分入验证集。由于图片尺寸过大,直接训练会发生显存不足的问题,因此以滑动窗口为(1024,1024)、步长为(512, 512)对训练图片进行切分,切分后的训练集一共有743张图片。以滑动窗口为(769, 769)、步长为(769,769)对验证图片进行切分,得到108张子图片,用于训练过程中的验证。
+
+运行以下脚本,下载原始数据集,并完成数据集的切分:
+
+```
+python prepare_data.py
+```
+
+切分后的数据示意如下:
+
+![](../../examples/change_detection/images/change_det_data.jpg)
+
+
+**注意:**
+
+* tiff格式的图片PaddleX统一使用gdal库读取,gdal安装可参考[文档](https://paddlex.readthedocs.io/zh_CN/develop/examples/multi-channel_remote_sensing/README.html#id2)。若数据是tiff格式的三通道RGB图像,如果不想安装gdal,需自行转成jpeg、bmp、png格式图片。
+
+* label文件需为单通道的png格式图片,且标注从0开始计数,标注255表示该类别不参与计算。例如本案例中,0表示`unchanged`类,1表示`changed`类。
+
+## 模型训练
+
+由于数据量较小,分割模型选择较好兼顾浅层细节信息和深层语义信息的UNet模型。运行以下脚本,进行模型训练:
+
+```
+python train.py
+```
+
+本案例使用0,1,2,3号GPU卡完成训练,可根据实际显存大小更改训练脚本中的GPU卡数量和`train_batch_size`的设置值,按`train_batch_size`的调整比例相应地调整学习率`learning_rate`,例如`train_batch_size`由16减少至8时,`learning_rate`则由0.1减少至0.05。此外,不同数据集上能获得最优精度所对应`learning_rate`可能有所不同,可以尝试调整。
+
+也可以跳过模型训练步骤,直接下载预训练模型进行后续的模型评估和预测:
+
+```
+wget https://bj.bcebos.com/paddlex/examples/change_detection/models/google_change_det_model.tar.gz
+tar -xvf google_change_det_model.tar.gz
+```
+
+## 模型评估
+
+在训练过程中,每隔10个迭代轮数会评估一次模型在验证集的精度。由于已事先将原始大尺寸图片切分成小块,相当于使用无重叠的滑动窗口预测方式,最优模型精度:
+
+| mean_iou | category__iou | overal_accuracy | category_accuracy | category_F1-score | kappa |
+| -- | -- | -- | -- | --| -- |
+| 84.24% | 97.54%、70.94%| 97.68% | 98.50%、85.99% | 98.75%、83% | 81.76% |
+
+category分别对应`unchanged`和`changed`两类。
+
+运行以下脚本,将采用有重叠的滑动窗口预测方式,重新评估原始大尺寸图片的模型精度,此时模型精度为:
+
+| mean_iou | category__iou | overal_accuracy | category_accuracy | category_F1-score | kappa |
+| -- | -- | -- | -- | --| -- |
+| 85.33% | 97.79%、72.87% | 97.97% | 98.66%、87.06% | 98.99%、84.30% | 83.19% |
+
+
+```
+python eval.py
+```
+
+滑动窗口预测接口说明详见[API说明](https://paddlex.readthedocs.io/zh_CN/develop/apis/models/semantic_segmentation.html#overlap-tile-predict),已有的使用场景可参考[RGB遥感分割案例](https://paddlex.readthedocs.io/zh_CN/develop/examples/remote_sensing.html#id4)。可根据实际显存大小修改评估脚本中`tile_size`,`pad_size`和`batch_size`。
+
+## 模型预测
+
+执行以下脚本,使用有重叠的滑动预测窗口对验证集进行预测。可根据实际显存大小修改评估脚本中`tile_size`,`pad_size`和`batch_size`。
+
+```
+python predict.py
+```
+
+预测可视化结果如下图所示:
+
+![](../../examples/change_detection/images/change_det_prediction.jpg)

+ 1 - 0
docs/examples/index.rst

@@ -14,3 +14,4 @@ PaddleX精选飞桨视觉开发套件在产业实践中的成熟模型结构,
    human_segmentation.md
    multi-channel_remote_sensing/README.md
    remote_sensing.md
+   change_detection.md

+ 107 - 0
examples/change_detection/README.md

@@ -0,0 +1,107 @@
+# 地块变化检测
+
+本案例基于PaddleX实现地块变化检测,将同一地块的前期与后期两张图片进行拼接,而后输入给语义分割网络进行变化区域的预测。在训练阶段,使用随机缩放尺寸、旋转、裁剪、颜色空间扰动、水平翻转、竖直翻转多种数据增强策略。在验证和预测阶段,使用滑动窗口预测方式,以避免在直接对大尺寸图片进行预测时显存不足的发生。
+
+## 目录
+* [数据准备](#1)
+* [模型训练](#2)
+* [模型评估](#3)
+* [模型预测](#4)
+
+
+#### 前置依赖
+
+* Paddle paddle >= 1.8.4
+* Python >= 3.5
+* PaddleX >= 1.3.0
+
+安装的相关问题参考[PaddleX安装](../install.md)
+
+下载PaddleX源码:
+
+```
+git clone https://github.com/PaddlePaddle/PaddleX
+```
+
+该案例所有脚本均位于`PaddleX/examples/change_detection/`,进入该目录:
+
+```
+cd PaddleX/examples/change_detection/
+```
+
+## <h2 id="1">数据准备</h2>
+
+本案例使用[Daifeng Peng等人](https://ieeexplore.ieee.org/document/9161009)开放的[Google Dataset](https://github.com/daifeng2016/Change-Detection-Dataset-for-High-Resolution-Satellite-Imagery), 该数据集涵盖了广州部分区域于2006年至2019年期间的房屋建筑物的变化情况,用于分析城市化进程。一共有20对高清图片,图片有红、绿、蓝三个波段,空间分辨率为0.55m,图片大小有1006x1168至4936x5224不等。
+
+由于Google Dataset仅标注了房屋建筑物是否发生变化,因此本案例是二分类变化检测任务,可根据实际需求修改类别数量即可拓展为多分类变化检测。
+
+本案例将15张图片划分入训练集,5张图片划分入验证集。由于图片尺寸过大,直接训练会发生显存不足的问题,因此以滑动窗口为(1024,1024)、步长为(512, 512)对训练图片进行切分,切分后的训练集一共有743张图片。以滑动窗口为(769, 769)、步长为(769,769)对验证图片进行切分,得到108张子图片,用于训练过程中的验证。
+
+运行以下脚本,下载原始数据集,并完成数据集的切分:
+
+```
+python prepare_data.py
+```
+
+切分后的数据示意如下:
+
+<img src="./images/change_det_data.jpg" alt="变化检测数据" align=center />
+
+
+**注意:**
+
+* tiff格式的图片PaddleX统一使用gdal库读取,gdal安装可参考[文档](https://paddlex.readthedocs.io/zh_CN/develop/examples/multi-channel_remote_sensing/README.html#id2)。若数据是tiff格式的三通道RGB图像,如果不想安装gdal,需自行转成jpeg、bmp、png格式图片。
+
+* label文件需为单通道的png格式图片,且标注从0开始计数,标注255表示该类别不参与计算。例如本案例中,0表示`unchanged`类,1表示`changed`类。
+
+## <h2 id="2">模型训练</h2>
+
+由于数据量较小,分割模型选择较好兼顾浅层细节信息和深层语义信息的UNet模型。运行以下脚本,进行模型训练:
+
+```
+python train.py
+```
+
+本案例使用0,1,2,3号GPU卡完成训练,可根据实际显存大小更改训练脚本中的GPU卡数量和`train_batch_size`的设置值,按`train_batch_size`的调整比例相应地调整学习率`learning_rate`,例如`train_batch_size`由16减少至8时,`learning_rate`则由0.1减少至0.05。此外,不同数据集上能获得最优精度所对应`learning_rate`可能有所不同,可以尝试调整。
+
+也可以跳过模型训练步骤,直接下载预训练模型进行后续的模型评估和预测:
+
+```
+wget https://bj.bcebos.com/paddlex/examples/change_detection/models/google_change_det_model.tar.gz
+tar -xvf google_change_det_model.tar.gz
+```
+
+## <h2 id="3">模型评估</h2>
+
+在训练过程中,每隔10个迭代轮数会评估一次模型在验证集的精度。由于已事先将原始大尺寸图片切分成小块,相当于使用无重叠的滑动窗口预测方式,最优模型精度:
+
+| mean_iou | category__iou | overal_accuracy | category_accuracy | category_F1-score | kappa |
+| -- | -- | -- | -- | --| -- |
+| 84.24% | 97.54%、70.94%| 97.68% | 98.50%、85.99% | 98.75%、83% | 81.76% |
+
+category分别对应`unchanged`和`changed`两类。
+
+运行以下脚本,将采用有重叠的滑动窗口预测方式,重新评估原始大尺寸图片的模型精度,此时模型精度为:
+
+| mean_iou | category__iou | overal_accuracy | category_accuracy | category_F1-score | kappa |
+| -- | -- | -- | -- | --| -- |
+| 85.33% | 97.79%、72.87% | 97.97% | 98.66%、87.06% | 98.99%、84.30% | 83.19% |
+
+
+```
+python eval.py
+```
+
+滑动窗口预测接口说明详见[API说明](https://paddlex.readthedocs.io/zh_CN/develop/apis/models/semantic_segmentation.html#overlap-tile-predict),已有的使用场景可参考[RGB遥感分割案例](https://paddlex.readthedocs.io/zh_CN/develop/examples/remote_sensing.html#id4)。可根据实际显存大小修改评估脚本中`tile_size`,`pad_size`和`batch_size`。
+
+## <h2 id="4">模型预测</h2>
+
+执行以下脚本,使用有重叠的滑动预测窗口对验证集进行预测。可根据实际显存大小修改评估脚本中`tile_size`,`pad_size`和`batch_size`。
+
+```
+python predict.py
+```
+
+预测可视化结果如下图所示:
+
+<img src="./images/change_det_prediction.jpg" alt="变化检测预测图" align=center />

+ 72 - 0
examples/change_detection/eval.py

@@ -0,0 +1,72 @@
+# 环境变量配置,用于控制是否使用GPU
+# 说明文档:https://paddlex.readthedocs.io/zh_CN/develop/appendix/parameters.html#gpu
+import os
+import os.path as osp
+os.environ['CUDA_VISIBLE_DEVICES'] = '0'
+
+import numpy as np
+import cv2
+from PIL import Image
+from collections import OrderedDict
+
+import paddlex as pdx
+import paddlex.utils.logging as logging
+from paddlex.seg import transforms
+from paddlex.cv.models.utils.seg_eval import ConfusionMatrix
+
+model_dir = 'output/unet/best_model'
+data_dir = 'google_change_det_dataset'
+file_list = 'google_change_det_dataset/val_list.txt'
+
+
+def update_confusion_matrix(confusion_matrix, predction, label):
+    pred = predction["label_map"]
+    pred = pred[np.newaxis, :, :, np.newaxis]
+    pred = pred.astype(np.int64)
+    label = label[np.newaxis, np.newaxis, :, :]
+    mask = label != model.ignore_index
+    confusion_matrix.calculate(pred=pred, label=label, ignore=mask)
+
+
+model = pdx.load_model(model_dir)
+
+conf_mat = ConfusionMatrix(model.num_classes, streaming=True)
+
+with open(file_list, 'r') as f:
+    for line in f:
+        items = line.strip().split()
+        full_path_im1 = osp.join(data_dir, items[0])
+        full_path_im2 = osp.join(data_dir, items[1])
+        full_path_label = osp.join(data_dir, items[2])
+
+        # 原图是tiff格式的图片,PaddleX统一使用gdal库读取
+        # 因训练数据已经转换成bmp格式,故此处使用opencv读取三通道的tiff图片
+        #image1 = transforms.Compose.read_img(full_path_im1)
+        #image2 = transforms.Compose.read_img(full_path_im2)
+        image1 = cv2.imread(full_path_im1)
+        image2 = cv2.imread(full_path_im2)
+        image = np.concatenate((image1, image2), axis=-1)
+
+        # API说明:https://paddlex.readthedocs.io/zh_CN/develop/apis/models/semantic_segmentation.html#overlap-tile-predict
+        overlap_tile_predict = model.overlap_tile_predict(
+            img_file=image,
+            tile_size=(769, 769),
+            pad_size=[512, 512],
+            batch_size=4)
+
+        # 将三通道的label图像转换成单通道的png格式图片
+        # 且将标注0和255转换成0和1
+        label = cv2.imread(full_path_label)
+        label = label[:, :, 0]
+        label = label != 0
+        label = label.astype(np.uint8)
+        update_confusion_matrix(conf_mat, overlap_tile_predict, label)
+
+category_iou, miou = conf_mat.mean_iou()
+category_acc, oacc = conf_mat.accuracy()
+category_f1score = conf_mat.f1_score()
+
+logging.info(
+    "miou={:.6f} category_iou={} oacc={:.6f} category_acc={} kappa={:.6f} category_F1-score={}".
+    format(miou, category_iou, oacc, category_acc,
+           conf_mat.kappa(), conf_mat.f1_score()))

TEMPAT SAMPAH
examples/change_detection/images/change_det_data.jpg


TEMPAT SAMPAH
examples/change_detection/images/change_det_prediction.jpg


+ 43 - 0
examples/change_detection/predict.py

@@ -0,0 +1,43 @@
+# 环境变量配置,用于控制是否使用GPU
+# 说明文档:https://paddlex.readthedocs.io/zh_CN/develop/appendix/parameters.html#gpu
+import os
+os.environ['CUDA_VISIBLE_DEVICES'] = '0'
+import cv2
+import numpy as np
+
+import paddlex as pdx
+
+model_dir = 'output/unet_3/best_model'
+data_dir = 'google_change_det_dataset'
+file_list = 'google_change_det_dataset/val_list.txt'
+save_dir = 'output/unet/pred'
+
+if not os.path.exists(save_dir):
+    os.makedirs(save_dir)
+color = [0, 0, 0, 255, 255, 255]
+
+model = pdx.load_model(model_dir)
+
+with open(file_list, 'r') as f:
+    for line in f:
+        items = line.strip().split()
+        img_file_1 = os.path.join(data_dir, items[0])
+        img_file_2 = os.path.join(data_dir, items[1])
+
+        # 原图是tiff格式的图片,PaddleX统一使用gdal库读取
+        # 因训练数据已经转换成bmp格式,故此处使用opencv读取三通道的tiff图片
+        #image1 = transforms.Compose.read_img(img_file_1)
+        #image2 = transforms.Compose.read_img(img_file_2)
+        image1 = cv2.imread(img_file_1)
+        image2 = cv2.imread(img_file_2)
+        image = np.concatenate((image1, image2), axis=-1)
+
+        # API说明:https://paddlex.readthedocs.io/zh_CN/develop/apis/models/semantic_segmentation.html#overlap-tile-predict
+        pred = model.overlap_tile_predict(
+            img_file=image,
+            tile_size=(769, 769),
+            pad_size=[512, 512],
+            batch_size=4)
+
+        pdx.seg.visualize(
+            img_file_1, pred, weight=0., save_dir=save_dir, color=color)

+ 103 - 79
examples/change_detection/prepara_data.py

@@ -3,104 +3,128 @@ import os.path as osp
 import numpy as np
 import cv2
 import shutil
-from PIL import Image
+import random
+# 为保证每次运行该脚本时划分的样本一致,故固定随机种子
+random.seed(0)
+
 import paddlex as pdx
 
 # 定义训练集切分时的滑动窗口大小和步长,格式为(W, H)
-train_tile_size = (512, 512)
-train_stride = (256, 256)
+train_tile_size = (1024, 1024)
+train_stride = (512, 512)
 # 定义验证集切分时的滑动窗口大小和步长,格式(W, H)
-val_tile_size = (256, 256)
-val_stride = (256, 256)
-
-## 下载并解压2015 CCF大数据比赛提供的高清遥感影像
-#SZTAKI_AirChange_Benchmark = 'https://bj.bcebos.com/paddlex/examples/remote_sensing/datasets/ccf_remote_dataset.tar.gz'
-#pdx.utils.download_and_decompress(SZTAKI_AirChange_Benchmark, path='./')
+val_tile_size = (769, 769)
+val_stride = (769, 769)
+# 训练集和验证集比例
+train_ratio = 0.75
+val_ratio = 0.25
+# 切分后的数据集保存路径
+tiled_dataset = './tiled_dataset'
+# 切分后的图像文件保存路径
+tiled_image_dir = osp.join(tiled_dataset, 'JPEGImages')
+# 切分后的标注文件保存路径
+tiled_anno_dir = osp.join(tiled_dataset, 'Annotations')
 
-if not osp.exists('./dataset/JPEGImages'):
-    os.makedirs('./dataset/JPEGImages')
-if not osp.exists('./dataset/Annotations'):
-    os.makedirs('./dataset/Annotations')
+# 下载和解压Google Dataset数据集
+change_det_dataset = 'https://bj.bcebos.com/paddlex/examples/change_detection/google_change_det_dataset.tar.gz'
+pdx.utils.download_and_decompress(change_det_dataset, path='./')
+change_det_dataset = './google_change_det_dataset'
+image1_dir = osp.join(change_det_dataset, 'T1')
+image2_dir = osp.join(change_det_dataset, 'T2')
+label_dir = osp.join(change_det_dataset, 'labels_change')
 
-# 将前4张图片划分入训练集,并切分成小块之后加入到训练集中
-# 并生成train_list.txt
-train_list = {'Szada': [2, 3, 4, 5, 6, 7], 'Tiszadob': [1, 2, 4, 5]}
-val_list = {'Szada': [1], 'Tiszadob': [3]}
-all_list = [train_list, val_list]
+if not osp.exists(tiled_image_dir):
+    os.makedirs(tiled_image_dir)
+if not osp.exists(tiled_anno_dir):
+    os.makedirs(tiled_anno_dir)
 
-for i, data_list in enumerate(all_list):
-    id = 0
-    if i == 0:
-        for key, value in data_list.items():
-            for v in value:
-                shutil.copyfile(
-                    "SZTAKI_AirChange_Benchmark/{}/{}/im1.bmp".format(key, v),
-                    "./dataset/JPEGImages/{}_{}_im1.bmp".format(key, v))
-                shutil.copyfile(
-                    "SZTAKI_AirChange_Benchmark/{}/{}/im2.bmp".format(key, v),
-                    "./dataset/JPEGImages/{}_{}_im2.bmp".format(key, v))
-                label = cv2.imread(
-                    "SZTAKI_AirChange_Benchmark/{}/{}/gt.bmp".format(key, v))
-                label = label[:, :, 0]
-                label = label != 0
-                label = label.astype(np.uint8)
-                cv2.imwrite("./dataset/Annotations/{}_{}_gt.png".format(
-                    key, v), label)
+# 划分数据集
+im1_file_list = os.listdir(image1_dir)
+im2_file_list = os.listdir(image2_dir)
+label_file_list = os.listdir(label_dir)
+im1_file_list = sorted(
+    im1_file_list, key=lambda k: int(k.split('test')[-1].split('_')[0]))
+im2_file_list = sorted(
+    im2_file_list, key=lambda k: int(k.split('test')[-1].split('_')[0]))
+label_file_list = sorted(
+    label_file_list, key=lambda k: int(k.split('test')[-1].split('_')[0]))
 
-                id += 1
-                mode = 'w' if id == 1 else 'a'
-                with open('./dataset/train_list.txt', mode) as f:
-                    f.write(
-                        "JPEGImages/{}_{}_im1.bmp JPEGImages/{}_{}_im2.bmp Annotations/{}_{}_gt.png\n".
-                        format(key, v, key, v, key, v))
+file_list = list()
+for im1_file, im2_file, label_file in zip(im1_file_list, im2_file_list,
+                                          label_file_list):
+    im1_file = osp.join(image1_dir, im1_file)
+    im2_file = osp.join(image2_dir, im2_file)
+    label_file = osp.join(label_dir, label_file)
+    file_list.append((im1_file, im2_file, label_file))
+random.shuffle(file_list)
+train_num = int(len(file_list) * train_ratio)
 
-    if i == 0:
+# 将大图切分成小图
+for i, item in enumerate(file_list):
+    if i < train_num:
         stride = train_stride
         tile_size = train_tile_size
     else:
         stride = val_stride
         tile_size = val_tile_size
-    for key, value in data_list.items():
-        for v in value:
-            im1 = cv2.imread("SZTAKI_AirChange_Benchmark/{}/{}/im1.bmp".format(
-                key, v))
-            im2 = cv2.imread("SZTAKI_AirChange_Benchmark/{}/{}/im2.bmp".format(
-                key, v))
-            label = cv2.imread(
-                "SZTAKI_AirChange_Benchmark/{}/{}/gt.bmp".format(key, v))
-            label = label[:, :, 0]
-            label = label != 0
-            label = label.astype(np.uint8)
-            H, W, C = im1.shape
-            tile_id = 1
-            for h in range(0, H, stride[1]):
-                for w in range(0, W, stride[0]):
-                    left = w
-                    upper = h
-                    right = min(w + tile_size[0], W)
-                    lower = min(h + tile_size[1], H)
-                    tile_im1 = im1[upper:lower, left:right, :]
-                    tile_im2 = im2[upper:lower, left:right, :]
-                    cv2.imwrite("./dataset/JPEGImages/{}_{}_{}_im1.bmp".format(
-                        key, v, tile_id), tile_im1)
-                    cv2.imwrite("./dataset/JPEGImages/{}_{}_{}_im2.bmp".format(
-                        key, v, tile_id), tile_im2)
-                    cut_label = label[upper:lower, left:right]
-                    cv2.imwrite("./dataset/Annotations/{}_{}_{}_gt.png".format(
-                        key, v, tile_id), cut_label)
-                    with open('./dataset/{}_list.txt'.format(
-                            'train' if i == 0 else 'val'), 'a') as f:
-                        f.write(
-                            "JPEGImages/{}_{}_{}_im1.bmp JPEGImages/{}_{}_{}_im2.bmp Annotations/{}_{}_{}_gt.png\n".
-                            format(key, v, tile_id, key, v, tile_id, key, v,
-                                   tile_id))
-                    tile_id += 1
+    set_name = 'train' if i < train_num else 'val'
+
+    # 生成原图的file_list
+    im1_file, im2_file, label_file = item[:]
+    mode = 'w' if i in [0, train_num] else 'a'
+    with open(
+            osp.join(change_det_dataset, '{}_list.txt'.format(set_name)),
+            mode) as f:
+        f.write("T1/{} T2/{} labels_change/{}\n".format(
+            osp.split(im1_file)[-1],
+            osp.split(im2_file)[-1], osp.split(label_file)[-1]))
+
+    im1 = cv2.imread(im1_file)
+    im2 = cv2.imread(im2_file)
+    # 将三通道的label图像转换成单通道的png格式图片
+    # 且将标注0和255转换成0和1
+    label = cv2.imread(label_file, cv2.IMREAD_GRAYSCALE)
+    label = label != 0
+    label = label.astype(np.uint8)
+
+    H, W, C = im1.shape
+    tile_id = 1
+    im1_name = osp.split(im1_file)[-1].split('.')[0]
+    im2_name = osp.split(im2_file)[-1].split('.')[0]
+    label_name = osp.split(label_file)[-1].split('.')[0]
+    for h in range(0, H, stride[1]):
+        for w in range(0, W, stride[0]):
+            left = w
+            upper = h
+            right = min(w + tile_size[0], W)
+            lower = min(h + tile_size[1], H)
+            tile_im1 = im1[upper:lower, left:right, :]
+            tile_im2 = im2[upper:lower, left:right, :]
+            cv2.imwrite(
+                osp.join(tiled_image_dir,
+                         "{}_{}.bmp".format(im1_name, tile_id)), tile_im1)
+            cv2.imwrite(
+                osp.join(tiled_image_dir,
+                         "{}_{}.bmp".format(im2_name, tile_id)), tile_im2)
+            cut_label = label[upper:lower, left:right]
+            cv2.imwrite(
+                osp.join(tiled_anno_dir,
+                         "{}_{}.png".format(label_name, tile_id)), cut_label)
+            mode = 'w' if i in [0, train_num] and tile_id == 1 else 'a'
+            with open(
+                    osp.join(tiled_dataset, '{}_list.txt'.format(set_name)),
+                    mode) as f:
+                f.write(
+                    "JPEGImages/{}_{}.bmp JPEGImages/{}_{}.bmp Annotations/{}_{}.png\n".
+                    format(im1_name, tile_id, im2_name, tile_id, label_name,
+                           tile_id))
+            tile_id += 1
 
 # 生成labels.txt
 label_list = ['unchanged', 'changed']
 for i, label in enumerate(label_list):
     mode = 'w' if i == 0 else 'a'
-    with open('./dataset/labels.txt', 'a') as f:
+    with open(osp.join(tiled_dataset, 'labels.txt'), 'a') as f:
         name = "{}\n".format(label) if i < len(
             label_list) - 1 else "{}".format(label)
         f.write(name)

+ 63 - 0
examples/change_detection/train.py

@@ -0,0 +1,63 @@
+# 环境变量配置,用于控制是否使用GPU
+# 说明文档:https://paddlex.readthedocs.io/zh_CN/develop/appendix/parameters.html#gpu
+import os
+os.environ['CUDA_VISIBLE_DEVICES'] = '0,1,2,3'
+
+import paddlex as pdx
+from paddlex.seg import transforms
+
+# 定义训练和验证时的transforms
+# API说明 https://paddlex.readthedocs.io/zh_CN/develop/apis/transforms/seg_transforms.html
+train_transforms = transforms.Compose([
+    transforms.ResizeStepScaling(
+        min_scale_factor=0.5, max_scale_factor=2.,
+        scale_step_size=0.25), transforms.RandomRotate(
+            rotate_range=180,
+            im_padding_value=[127.5] * 6), transforms.RandomPaddingCrop(
+                crop_size=769, im_padding_value=[127.5] * 6),
+    transforms.RandomDistort(), transforms.RandomHorizontalFlip(),
+    transforms.RandomVerticalFlip(), transforms.Normalize(
+        mean=[0.5] * 6, std=[0.5] * 6, min_val=[0] * 6, max_val=[255] * 6)
+])
+
+eval_transforms = transforms.Compose([
+    transforms.Padding(
+        target_size=769, im_padding_value=[127.5] * 6), transforms.Normalize(
+            mean=[0.5] * 6, std=[0.5] * 6, min_val=[0] * 6, max_val=[255] * 6)
+])
+
+# 定义训练和验证所用的数据集
+# API说明:https://paddlex.readthedocs.io/zh_CN/develop/apis/datasets.html#paddlex-datasets-changedetdataset
+train_dataset = pdx.datasets.ChangeDetDataset(
+    data_dir='tiled_dataset',
+    file_list='tiled_dataset/train_list.txt',
+    label_list='tiled_dataset/labels.txt',
+    transforms=train_transforms,
+    num_workers=8,
+    shuffle=True)
+eval_dataset = pdx.datasets.ChangeDetDataset(
+    data_dir='tiled_dataset',
+    file_list='tiled_dataset/val_list.txt',
+    label_list='tiled_dataset/labels.txt',
+    num_workers=8,
+    transforms=eval_transforms)
+
+# 初始化模型,并进行训练
+# 可使用VisualDL查看训练指标,参考https://paddlex.readthedocs.io/zh_CN/develop/train/visualdl.html
+num_classes = len(train_dataset.labels)
+
+# API说明:https://paddlex.readthedocs.io/zh_CN/develop/apis/models/semantic_segmentation.html#paddlex-seg-unet
+model = pdx.seg.UNet(num_classes=num_classes, input_channel=6)
+
+# API说明:https://paddlex.readthedocs.io/zh_CN/develop/apis/models/semantic_segmentation.html#train
+# 各参数介绍与调整说明:https://paddlex.readthedocs.io/zh_CN/develop/appendix/parameters.html
+model.train(
+    num_epochs=400,
+    train_dataset=train_dataset,
+    train_batch_size=16,
+    eval_dataset=eval_dataset,
+    learning_rate=0.1,
+    save_interval_epochs=10,
+    pretrain_weights='CITYSCAPES',
+    save_dir='output/unet',
+    use_vdl=True)

+ 2 - 2
paddlex/cv/datasets/change_det_dataet.py

@@ -25,11 +25,11 @@ from .dataset import get_encoding
 
 
 class ChangeDetDataset(Dataset):
-    """读取语义分割任务数据集,并对样本进行相应的处理。
+    """读取用于完成地块检测的语义分割任务数据集,并对样本进行相应的处理。
 
     Args:
         data_dir (str): 数据集所在的目录路径。
-        file_list (str): 描述数据集图片文件和对应标注文件的文件路径(文本内每行路径为相对data_dir的相对路)。
+        file_list (str): 描述数据集图片1文件、图片2文件和对应标注文件的文件路径(文本内每行路径为相对data_dir的相对路)。
         label_list (str): 描述数据集包含的类别信息文件路径。默认值为None。
         transforms (list): 数据集中每个样本的预处理/增强算子。
         num_workers (int): 数据集中样本在预处理过程中的线程或进程数。默认为'auto'。

+ 5 - 3
paddlex/cv/models/utils/visualize.py

@@ -62,7 +62,8 @@ def visualize_segmentation(image,
     label_map = result['label_map']
     color_map = get_color_map_list(256)
     if color is not None:
-        color_map[0:len(color) // 3][:] = color
+        for i in range(len(color) // 3):
+            color_map[i] = color[i * 3:(i + 1) * 3]
     color_map = np.array(color_map).astype("uint8")
 
     # Use OpenCV LUT for color mapping
@@ -93,8 +94,9 @@ def visualize_segmentation(image,
     if abs(weight) < 1e-5:
         vis_result = pseudo_img
     else:
-        print(im.dtype, pseudo_img.dtype)
-        vis_result = cv2.addWeighted(im, weight, pseudo_img, 1 - weight, 0)
+        vis_result = cv2.addWeighted(im, weight,
+                                     pseudo_img.astype(im.dtype), 1 - weight,
+                                     0)
 
     if save_dir is not None:
         if not os.path.exists(save_dir):