Эх сурвалжийг харах

Merge pull request #338 from FlyingQianMM/develop_test

add change detection example
Jason 5 жил өмнө
parent
commit
4a6c125339

+ 21 - 0
docs/apis/datasets.md

@@ -140,3 +140,24 @@ 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.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。

+ 9 - 9
docs/apis/transforms/seg_transforms.md

@@ -87,10 +87,10 @@ paddlex.seg.transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5], min_
 3.对图像进行减均值除以标准差操作。
 
 ### 参数
-* **mean** (list): 图像数据集的均值。默认值[0.5, 0.5, 0.5]。
-* **std** (list): 图像数据集的标准差。默认值[0.5, 0.5, 0.5]。
-* **min_val** (list): 图像数据集的最小值。默认值[0, 0, 0]。
-* **max_val** (list): 图像数据集的最大值。默认值[255.0, 255.0, 255.0]。
+* **mean** (list): 图像数据集的均值。默认值[0.5, 0.5, 0.5]。长度应与图像通道数量相同。
+* **std** (list): 图像数据集的标准差。默认值[0.5, 0.5, 0.5]。长度应与图像通道数量相同。
+* **min_val** (list): 图像数据集的最小值。默认值[0, 0, 0]。长度应与图像通道数量相同。
+* **max_val** (list): 图像数据集的最大值。默认值[255.0, 255.0, 255.0]。长度应与图像通道数量相同。
 
 ## Padding
 ```python
@@ -99,7 +99,7 @@ paddlex.seg.transforms.Padding(target_size, im_padding_value=[127.5, 127.5, 127.
 对图像或标注图像进行padding,padding方向为右和下。根据提供的值对图像或标注图像进行padding操作。
 ### 参数
 * **target_size** (int|list|tuple): padding后图像的大小。
-* **im_padding_value** (list): 图像padding的值。默认为[127.5, 127.5, 127.5]。
+* **im_padding_value** (list): 图像padding的值。默认为[127.5, 127.5, 127.5]。长度应与图像通道数量相同。
 * **label_padding_value** (int): 标注图像padding的值。默认值为255(仅在训练时需要设定该参数)。
 
 
@@ -110,7 +110,7 @@ paddlex.seg.transforms.RandomPaddingCrop(crop_size=512, im_padding_value=[127.5,
 对图像和标注图进行随机裁剪,当所需要的裁剪尺寸大于原图时,则进行padding操作,模型训练时的数据增强操作。
 ### 参数
 * **crop_size**(int|list|tuple): 裁剪图像大小。默认为512。
-* **im_padding_value** (list): 图像padding的值。默认为[127.5, 127.5, 127.5]。
+* **im_padding_value** (list): 图像padding的值。默认为[127.5, 127.5, 127.5]。长度应与图像通道数量相同。
 * **label_padding_value** (int): 标注图像padding的值。默认值为255。
 
 
@@ -127,13 +127,13 @@ paddlex.seg.transforms.RandomBlur(prob=0.1)
 ```python
 paddlex.seg.transforms.RandomRotate(rotate_range=15, im_padding_value=[127.5, 127.5, 127.5], label_padding_value=255)
 ```
-对图像进行随机旋转,模型训练时的数据增强操作。
+对图像进行随机旋转,模型训练时的数据增强操作。目前支持多通道的RGB图像,例如支持多张RGB图像沿通道轴做concatenate后的图像数据,不支持通道数量不是3的倍数的图像数据。
 
 在旋转区间[-rotate_range, rotate_range]内,对图像进行随机旋转,当存在标注图像时,同步进行,
 并对旋转后的图像和标注图像进行相应的padding。
 ### 参数
 * **rotate_range** (float): 最大旋转角度。默认为15度。
-* **im_padding_value** (list): 图像padding的值。默认为[127.5, 127.5, 127.5]。
+* **im_padding_value** (list): 图像padding的值。默认为[127.5, 127.5, 127.5]。长度应与图像通道数量相同。
 * **label_padding_value** (int): 标注图像padding的值。默认为255。
 
 
@@ -153,7 +153,7 @@ paddlex.seg.transforms.RandomScaleAspect(min_scale=0.5, aspect_ratio=0.33)
 ```python
 paddlex.seg.transforms.RandomDistort(brightness_range=0.5, brightness_prob=0.5, contrast_range=0.5, contrast_prob=0.5, saturation_range=0.5, saturation_prob=0.5, hue_range=18, hue_prob=0.5)
 ```
-以一定的概率对图像进行随机像素内容变换,模型训练时的数据增强操作。
+以一定的概率对图像进行随机像素内容变换,模型训练时的数据增强操作。目前支持多通道的RGB图像,例如支持多张RGB图像沿通道轴做concatenate后的图像数据,不支持通道数量不是3的倍数的图像数据。
 
 1.对变换的操作顺序进行随机化操作。
 2.按照1中的顺序以一定的概率对图像在范围[-range, range]内进行随机像素内容变换。  

+ 53 - 0
docs/data/format/change_detection.md

@@ -0,0 +1,53 @@
+# 地块检测ChangeDet
+
+## 数据集文件夹结构
+
+在PaddleX中,**标注文件为png文件**。建议用户将数据集按照如下方式进行组织,同一地块不同时期的地貌原图均放在同一目录,如`JPEGImages`,标注的同名png文件均放在同一目录,如`Annotations`,示例如下
+```
+MyDataset/ # 语义分割数据集根目录
+|--JPEGImages/ # 原图文件所在目录,包含同一物体前期和后期的图片
+|  |--1_1.jpg
+|  |--1_2.jpg
+|  |--2_1.jpg
+|  |--2_2.jpg
+|  |--...
+|  |--...
+|
+|--Annotations/ # 标注文件所在目录
+|  |--1.png
+|  |--2.png
+|  |--...
+|  |--...
+```
+同一地块不同时期的地貌原图,如1_1.jpg和1_2.jpg,可以是RGB彩色图像、灰度图、或tiff格式的多通道图像。语义分割的标注图像,如1.png,为单通道图像,像素标注类别需要从0开始递增(一般0表示background背景), 例如0, 1, 2, 3表示4种类别,标注类别最多255个类别(其中像素值255不参与训练和评估)。
+
+## 划分训练集验证集
+
+**为了用于训练,我们需要在`MyDataset`目录下准备`train_list.txt`, `val_list.txt`和`labels.txt`三个文件**,分别用于表示训练集列表,验证集列表和类别标签列表。
+
+**labels.txt**  
+
+labels.txt用于列出所有类别,类别对应行号表示模型训练过程中类别的id(行号从0开始计数),例如labels.txt为以下内容
+```
+unchanged
+changed
+```
+表示该检测数据集中共有2个分割类别,分别为`unchanged`和`changed`,在模型训练中`unchanged`对应的类别id为0, `changed`对应1,以此类推,如不知具体类别标签,可直接在labels.txt逐行写0,1,2...序列即可。
+
+**train_list.txt**  
+
+train_list.txt列出用于训练时的图片集合,与其对应的标注文件,示例如下
+```
+JPEGImages/1_1.jpg JPEGImages/1_2.jpg Annotations/1.png
+JPEGImages/2_1.jpg JPEGImages/2_2.jpg Annotations/2.png
+... ...
+```
+其中第一列和第二列为原图相对`MyDataset`的相对路径,对应同一地块不同时期的地貌图像,第三列为标注文件相对`MyDataset`的相对路径
+
+**val_list.txt**  
+
+val_list列出用于验证时的图片集成,与其对应的标注文件,格式与val_list.txt一致
+
+## PaddleX数据集加载  
+
+[示例代码](https://github.com/PaddlePaddle/PaddleX/blob/develop/examples/change_detection/train.py)

+ 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 | overall_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 | overall_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 | overall_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 | overall_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()))

BIN
examples/change_detection/images/change_det_data.jpg


BIN
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)

+ 130 - 0
examples/change_detection/prepara_data.py

@@ -0,0 +1,130 @@
+import os
+import os.path as osp
+import numpy as np
+import cv2
+import shutil
+import random
+# 为保证每次运行该脚本时划分的样本一致,故固定随机种子
+random.seed(0)
+
+import paddlex as pdx
+
+# 定义训练集切分时的滑动窗口大小和步长,格式为(W, H)
+train_tile_size = (1024, 1024)
+train_stride = (512, 512)
+# 定义验证集切分时的滑动窗口大小和步长,格式(W, H)
+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')
+
+# 下载和解压Google Dataset数据集
+change_det_dataset = 'https://bj.bcebos.com/paddlex/examples/change_detection/dataset/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')
+
+if not osp.exists(tiled_image_dir):
+    os.makedirs(tiled_image_dir)
+if not osp.exists(tiled_anno_dir):
+    os.makedirs(tiled_anno_dir)
+
+# 划分数据集
+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]))
+
+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)
+
+# 将大图切分成小图
+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
+    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(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)

+ 1 - 0
paddlex/cv/datasets/__init__.py

@@ -21,3 +21,4 @@ from .easydata_det import EasyDataDet
 from .easydata_seg import EasyDataSeg
 from .dataset import generate_minibatch
 from .analysis import Seg
+from .change_det_dataset import ChangeDetDataset

+ 108 - 0
paddlex/cv/datasets/change_det_dataset.py

@@ -0,0 +1,108 @@
+# 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 __future__ import absolute_import
+import os.path as osp
+import random
+import copy
+import numpy as np
+import paddlex.utils.logging as logging
+from paddlex.cv.transforms import seg_transforms
+from paddlex.utils import path_normalization
+from .dataset import Dataset
+from .dataset import get_encoding
+
+
+class ChangeDetDataset(Dataset):
+    """读取用于完成地块检测的语义分割任务数据集,并对样本进行相应的处理。
+
+    Args:
+        data_dir (str): 数据集所在的目录路径。
+        file_list (str): 描述数据集图片1文件、图片2文件和对应标注文件的文件路径(文本内每行路径为相对data_dir的相对路)。
+        label_list (str): 描述数据集包含的类别信息文件路径。默认值为None。
+        transforms (list): 数据集中每个样本的预处理/增强算子。
+        num_workers (int): 数据集中样本在预处理过程中的线程或进程数。默认为'auto'。
+        buffer_size (int): 数据集中样本在预处理过程中队列的缓存长度,以样本数为单位。默认为100。
+        parallel_method (str): 数据集中样本在预处理过程中并行处理的方式,支持'thread'
+            线程和'process'进程两种方式。默认为'process'(Windows和Mac下会强制使用thread,该参数无效)。
+        shuffle (bool): 是否需要对数据集中样本打乱顺序。默认为False。
+    """
+
+    def __init__(self,
+                 data_dir,
+                 file_list,
+                 label_list=None,
+                 transforms=None,
+                 num_workers='auto',
+                 buffer_size=100,
+                 parallel_method='process',
+                 shuffle=False):
+        super(ChangeDetDataset, self).__init__(
+            transforms=transforms,
+            num_workers=num_workers,
+            buffer_size=buffer_size,
+            parallel_method=parallel_method,
+            shuffle=shuffle)
+        self.file_list = list()
+        self.labels = list()
+        self._epoch = 0
+
+        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 separator, but it exists in image or label name {}."
+                        .format(line))
+                items[0] = path_normalization(items[0])
+                items[1] = path_normalization(items[1])
+                items[2] = path_normalization(items[2])
+                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])
+                if not osp.exists(full_path_im1):
+                    raise IOError('The image file {} is not exist!'.format(
+                        full_path_im1))
+                if not osp.exists(full_path_im2):
+                    raise IOError('The image file {} is not exist!'.format(
+                        full_path_im2))
+                if not osp.exists(full_path_label):
+                    raise IOError('The image file {} is not exist!'.format(
+                        full_path_label))
+                self.file_list.append(
+                    [full_path_im1, full_path_im2, full_path_label])
+        self.num_samples = len(self.file_list)
+        logging.info("{} samples in file {}".format(
+            len(self.file_list), file_list))
+
+    def iterator(self):
+        self._epoch += 1
+        self._pos = 0
+        files = copy.deepcopy(self.file_list)
+        if self.shuffle:
+            random.shuffle(files)
+        files = files[:self.num_samples]
+        self.num_samples = len(files)
+        for f in files:
+            label_path = f[2]
+            image1 = seg_transforms.Compose.read_img(f[0])
+            image2 = seg_transforms.Compose.read_img(f[1])
+            image = np.concatenate((image1, image2), axis=-1)
+            sample = [image, None, label_path]
+            yield sample

+ 9 - 3
paddlex/cv/models/deeplabv3p.py

@@ -436,11 +436,17 @@ class DeepLabv3p(BaseAPI):
                 epoch_id, step + 1, total_steps, iou))
 
         category_iou, miou = conf_mat.mean_iou()
-        category_acc, macc = conf_mat.accuracy()
+        category_acc, oacc = conf_mat.accuracy()
+        category_f1score = conf_mat.f1_score()
 
         metrics = OrderedDict(
-            zip(['miou', 'category_iou', 'macc', 'category_acc', 'kappa'],
-                [miou, category_iou, macc, category_acc, conf_mat.kappa()]))
+            zip([
+                'miou', 'category_iou', 'oacc', 'category_acc', 'kappa',
+                'category_F1-score'
+            ], [
+                miou, category_iou, oacc, category_acc, conf_mat.kappa(),
+                category_f1score
+            ]))
         if return_details:
             eval_details = {
                 'confusion_matrix': conf_mat.confusion_matrix.tolist()

+ 33 - 0
paddlex/cv/models/utils/seg_eval.py

@@ -142,3 +142,36 @@ class ConfusionMatrix(object):
 
         kappa = (po - pe) / (1 - pe)
         return kappa
+
+    def f1_score(self):
+        f1score_list = []
+        # TODO: use numpy sum axis api to simpliy
+        vji = np.zeros(self.num_classes, dtype=int)
+        vij = np.zeros(self.num_classes, dtype=int)
+        for j in range(self.num_classes):
+            v_j = 0
+            for i in range(self.num_classes):
+                v_j += self.confusion_matrix[j][i]
+            vji[j] = v_j
+
+        for i in range(self.num_classes):
+            v_i = 0
+            for j in range(self.num_classes):
+                v_i += self.confusion_matrix[j][i]
+            vij[i] = v_i
+
+        for c in range(self.num_classes):
+            if vji[c] == 0:
+                precision = 0
+            else:
+                precision = self.confusion_matrix[c][c] / vji[c]
+            if vij[c] == 0:
+                recall = 0
+            else:
+                recall = self.confusion_matrix[c][c] / vij[c]
+            if vji[c] == 0 and vij[c] == 0:
+                f1score = 0
+            else:
+                f1score = 2 * precision * recall / (recall + precision)
+            f1score_list.append(f1score)
+        return np.array(f1score_list)

+ 3 - 2
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
@@ -94,7 +95,7 @@ def visualize_segmentation(image,
         vis_result = pseudo_img
     else:
         vis_result = cv2.addWeighted(im, weight,
-                                     pseudo_img.astype('float32'), 1 - weight,
+                                     pseudo_img.astype(im.dtype), 1 - weight,
                                      0)
 
     if save_dir is not None:

+ 25 - 14
paddlex/cv/transforms/seg_transforms.py

@@ -936,7 +936,7 @@ class RandomRotate(SegTransform):
                 存储与图像相关信息的字典和标注图像np.ndarray数据。
         """
         if self.rotate_range > 0:
-            (h, w) = im.shape[:2]
+            h, w, c = im.shape
             do_rotation = np.random.uniform(-self.rotate_range,
                                             self.rotate_range)
             pc = (w // 2, h // 2)
@@ -951,13 +951,18 @@ class RandomRotate(SegTransform):
             r[0, 2] += (nw / 2) - cx
             r[1, 2] += (nh / 2) - cy
             dsize = (nw, nh)
-            im = cv2.warpAffine(
-                im,
-                r,
-                dsize=dsize,
-                flags=cv2.INTER_LINEAR,
-                borderMode=cv2.BORDER_CONSTANT,
-                borderValue=self.im_padding_value)
+            rot_ims = list()
+            for i in range(0, c, 3):
+                ori_im = im[:, :, i:i + 3]
+                rot_im = cv2.warpAffine(
+                    ori_im,
+                    r,
+                    dsize=dsize,
+                    flags=cv2.INTER_LINEAR,
+                    borderMode=cv2.BORDER_CONSTANT,
+                    borderValue=self.im_padding_value[i:i + 3])
+                rot_ims.append(rot_im)
+            im = np.concatenate(rot_ims, axis=-1)
             label = cv2.warpAffine(
                 label,
                 r,
@@ -1119,12 +1124,18 @@ class RandomDistort(SegTransform):
             'saturation': self.saturation_prob,
             'hue': self.hue_prob
         }
-        for id in range(4):
-            params = params_dict[ops[id].__name__]
-            prob = prob_dict[ops[id].__name__]
-            params['im'] = im
-            if np.random.uniform(0, 1) < prob:
-                im = ops[id](**params)
+        dis_ims = list()
+        h, w, c = im.shape
+        for i in range(0, c, 3):
+            ori_im = im[:, :, i:i + 3]
+            for id in range(4):
+                params = params_dict[ops[id].__name__]
+                prob = prob_dict[ops[id].__name__]
+                params['im'] = ori_im
+                if np.random.uniform(0, 1) < prob:
+                    ori_im = ops[id](**params)
+            dis_ims.append(ori_im)
+        im = np.concatenate(dis_ims, axis=-1)
         im = im.astype('float32')
         if label is None:
             return (im, im_info)

+ 18 - 5
paddlex/cv/transforms/visualize.py

@@ -236,16 +236,22 @@ def seg_compose(im,
                     len(im.shape)))
     else:
         try:
-            im = cv2.imread(im).astype('float32')
+            im = cv2.imread(im)
         except:
             raise ValueError('Can\'t read The image file {}!'.format(im))
     im = cv2.cvtColor(im, cv2.COLOR_BGR2RGB)
+    im = im.astype('float32')
+    h, w, c = im.shape
     if label is not None:
         if not isinstance(label, np.ndarray):
             label = np.asarray(Image.open(label))
     if vdl_writer is not None:
-        vdl_writer.add_image(
-            tag='0. OriginalImage' + '/' + str(step), img=im, step=0)
+        for i in range(0, c, 3):
+            if c > 3:
+                tag = '0. OriginalImage/{}_{}'.format(str(step), str(i // 3))
+            else:
+                tag = '0. OriginalImage/{}'.format(str(step))
+            vdl_writer.add_image(tag=tag, img=im[:, :, i:i + 3], step=0)
     op_id = 1
     for op in transforms:
         if isinstance(op, SegTransform):
@@ -264,8 +270,15 @@ def seg_compose(im,
             else:
                 outputs = (im, im_info)
         if vdl_writer is not None:
-            tag = str(op_id) + '. ' + op.__class__.__name__ + '/' + str(step)
-            vdl_writer.add_image(tag=tag, img=im, step=0)
+            for i in range(0, c, 3):
+                if c > 3:
+                    tag = str(
+                        op_id) + '. ' + op.__class__.__name__ + '/' + str(
+                            step) + '_' + str(i // 3)
+                else:
+                    tag = str(
+                        op_id) + '. ' + op.__class__.__name__ + '/' + str(step)
+                vdl_writer.add_image(tag=tag, img=im[:, :, i:i + 3], step=0)
         op_id += 1