From e637b92c0c81df077868a347d5c062aa9007a3e4 Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Wed, 17 Aug 2022 21:34:26 +0800 Subject: [PATCH] Update rs_research example --- docs/data/coco_tools.md | 52 ++++---- examples/README.md | 4 +- examples/rs_research/.gitignore | 2 + examples/rs_research/README.md | 86 +++++++++++- .../configs/levircd/changeformer.yaml | 0 .../rs_research/configs/levircd/snunet.yaml | 0 .../configs/svcd/changeformer.yaml | 0 examples/rs_research/configs/svcd/snunet.yaml | 0 tools/prepare_dataset/common.py | 122 ++++++++++++++++++ tools/prepare_dataset/prepare_levircd.py | 42 ++++++ tools/prepare_dataset/prepare_svcd.py | 31 +++++ 11 files changed, 307 insertions(+), 32 deletions(-) create mode 100644 examples/rs_research/.gitignore delete mode 100644 examples/rs_research/configs/levircd/changeformer.yaml delete mode 100644 examples/rs_research/configs/levircd/snunet.yaml delete mode 100644 examples/rs_research/configs/svcd/changeformer.yaml delete mode 100644 examples/rs_research/configs/svcd/snunet.yaml create mode 100644 tools/prepare_dataset/common.py diff --git a/docs/data/coco_tools.md b/docs/data/coco_tools.md index 3e8f81e..6e2af18 100644 --- a/docs/data/coco_tools.md +++ b/docs/data/coco_tools.md @@ -17,7 +17,7 @@ coco_tools是PaddleRS提供的用于处理COCO格式标注文件的工具集, ## 3 使用示例 -## 3.1 示例数据集 +### 3.1 示例数据集 本文档以COCO 2017数据集作为示例数据进行演示。您可以在以下链接下载该数据集: @@ -47,11 +47,11 @@ coco_tools是PaddleRS提供的用于处理COCO格式标注文件的工具集, | |--... ``` -## 3.2 打印json信息 +### 3.2 打印json信息 使用`json_InfoShow.py`可以打印json文件中的各个键值对的key, 并输出value中排列靠前的元素,从而帮助您快速了解标注信息。对于COCO格式标注数据而言,您应该特别留意`'image'`和`'annotation'`字段的内容。 -### 3.2.1 命令演示 +#### 3.2.1 命令演示 执行如下命令,打印`instances_val2017.json`中的信息: @@ -61,7 +61,7 @@ python ./coco_tools/json_InfoShow.py \ --show_num 5 ``` -### 3.2.2 参数说明 +#### 3.2.2 参数说明 | 参数名 | 含义 | 默认值 | @@ -70,7 +70,7 @@ python ./coco_tools/json_InfoShow.py \ | `--show_num` | (可选)输出value中排列靠前的元素的个数 | `5` | | `--Args_show` | (可选)是否打印输入参数信息 | `True` | -### 3.2.3 结果展示 +#### 3.2.3 结果展示 执行上述命令后,输出结果如下: @@ -151,7 +151,7 @@ contributor : COCO Consortium ``` -### 3.2.4 结果说明 +#### 3.2.4 结果说明 `instances_val2017.json`的key有5个,分别为: @@ -166,11 +166,11 @@ contributor : COCO Consortium - `annotations`键对应的值为列表,共有36781个元素,输出展示了前5个; - `categories`键对应的值为列表,共有80个元素,输出展示了前5个。 -## 3.3 统计图像信息 +### 3.3 统计图像信息 使用`json_ImgSta.py`可以从`instances_val2017.json`中快速提取图像信息,生成csv表格,并生成统计图。 -### 3.3.1 命令演示 +#### 3.3.1 命令演示 执行如下命令,打印`instances_val2017.json`信息: @@ -182,7 +182,7 @@ python ./coco_tools/json_ImgSta.py \ --png_shapeRate_path=./img_sta/images_shapeRate.png ``` -### 3.3.2 参数说明 +#### 3.3.2 参数说明 | 参数名 | 含义 | 默认值 | | ---------------------- | --------------------------------------------------------------------- | -------- | @@ -193,7 +193,7 @@ python ./coco_tools/json_ImgSta.py \ | `--image_keyname` | (可选)json文件中,图像所对应的key |`'images'`| | `--Args_show` | (可选)是否打印输入参数信息 |`True` | -### 3.3.3 结果展示 +#### 3.3.3 结果展示 执行上述命令后,输出结果如下: @@ -232,11 +232,11 @@ csv save to ./img_sta/images.csv 所有图像shape比例(宽/高)的一维分布: ![image.png](./assets/1650011634205-image.png) -## 3.4 统计目标检测标注框信息 +### 3.4 统计目标检测标注框信息 使用`json_AnnoSta.py`,可以从`instances_val2017.json`中快速提取标注信息,生成csv表格,并生成统计图。 -### 3.4.1 命令演示 +#### 3.4.1 命令演示 执行如下命令,打印`instances_val2017.json`信息: @@ -253,7 +253,7 @@ python ./coco_tools/json_AnnoSta.py \ --get_relative=True ``` -### 3.4.2 参数说明 +#### 3.4.2 参数说明 | 参数名 | 含义 | 默认值 | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------- | @@ -270,7 +270,7 @@ python ./coco_tools/json_AnnoSta.py \ | `--anno_keyname` | (可选)json文件中,标注所对应的key | `'annotations'`| | `--Args_show` | (可选)是否打印输入参数信息 | `True` | -### 3.4.3 结果展示 +#### 3.4.3 结果展示 执行上述命令后,输出结果如下: @@ -344,11 +344,11 @@ csv save to ./anno_sta/annos.csv ![image.png](./assets/1650026559309-image.png) -## 3.5 统计图像信息生成json +### 3.5 统计图像信息生成json 使用`json_Test2Json.py`,可以根据`test2017`中的文件信息与训练集json文件快速提取图像信息,生成测试集json文件。 -### 3.5.1 命令演示 +#### 3.5.1 命令演示 执行如下命令,统计并生成`test2017`信息: @@ -359,7 +359,7 @@ python ./coco_tools/json_Img2Json.py \ --json_test_path=./test.json ``` -### 3.5.2 参数说明 +#### 3.5.2 参数说明 | 参数名 | 含义 | 默认值 | @@ -371,7 +371,7 @@ python ./coco_tools/json_Img2Json.py \ | `--cat_keyname` | (可选)json文件中,类别对应的key | `'categories'`| | `--Args_show` | (可选)是否打印输入参数信息 | `True` | -### 3.5.3 结果展示 +#### 3.5.3 结果展示 执行上述命令后,输出结果如下: @@ -431,11 +431,11 @@ json keys: dict_keys(['images', 'categories']) ... ``` -## 3.6 json文件拆分 +### 3.6 json文件拆分 使用`json_Split.py`,可以将`instances_val2017.json`文件拆分为2个子集。 -### 3.6.1 命令演示 +#### 3.6.1 命令演示 执行如下命令,拆分`instances_val2017.json`文件: @@ -446,7 +446,7 @@ python ./coco_tools/json_Split.py \ --json_val_path=./instances_val2017_val.json ``` -### 3.6.2 参数说明 +#### 3.6.2 参数说明 | 参数名 | 含义 | 默认值 | @@ -461,7 +461,7 @@ python ./coco_tools/json_Split.py \ | `--cat_keyname` | (可选)json文件中,类别对应的key | `'categories'`| | `--Args_show` | (可选)是否打印输入参数信息 | `'True'` | -### 3.6.3 结果展示 +#### 3.6.3 结果展示 执行上述命令后,输出结果如下: @@ -485,11 +485,11 @@ image total 5000, train 4500, val 500 anno total 36781, train 33119, val 3662 ``` -## 3.7 json文件合并 +### 3.7 json文件合并 使用`json_Merge.py`,可以合并2个json文件。 -### 3.7.1 命令演示 +#### 3.7.1 命令演示 执行如下命令,合并`instances_train2017.json`与`instances_val2017.json`: @@ -500,7 +500,7 @@ python ./coco_tools/json_Merge.py \ --save_path=./instances_trainval2017.json ``` -### 3.7.2 参数说明 +#### 3.7.2 参数说明 | 参数名 | 含义 | 默认值 | @@ -511,7 +511,7 @@ python ./coco_tools/json_Merge.py \ | `--merge_keys` | (可选)合并过程中需要合并的key | `['images', 'annotations']` | | `--Args_show` | (可选)是否打印输入参数信息 | `True` | -### 3.7.3 结果展示 +#### 3.7.3 结果展示 执行上述命令后,输出结果如下: diff --git a/examples/README.md b/examples/README.md index e6f9acb..41e7598 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,12 +2,12 @@ PaddleRS提供从科学研究到产业应用的丰富示例,希望帮助遥感领域科研从业者快速完成算法的研发、验证和调优,以及帮助投身于产业实践的开发者便捷地实现从数据预处理到模型部署的全流程遥感深度学习应用。 -## 官方案例 +## 1 官方案例 - [PaddleRS竞赛实战:第十一届中国软件杯遥感赛项](./rs_competition/) - [PaddleRS科研实战:设计深度学习变化检测模型](./rs_research/) -## 社区贡献案例 +## 2 社区贡献案例 [AI Studio](https://aistudio.baidu.com/aistudio/index)是基于百度深度学习平台飞桨的人工智能学习与实训社区,提供在线编程环境、免费GPU算力、海量开源算法和开放数据,帮助开发者快速创建和部署模型。您可以在AI Studio上探索PaddleRS的更多玩法: diff --git a/examples/rs_research/.gitignore b/examples/rs_research/.gitignore new file mode 100644 index 0000000..30ed70a --- /dev/null +++ b/examples/rs_research/.gitignore @@ -0,0 +1,2 @@ +/data/ +/exp/ \ No newline at end of file diff --git a/examples/rs_research/README.md b/examples/rs_research/README.md index 2c7cee4..4501578 100644 --- a/examples/rs_research/README.md +++ b/examples/rs_research/README.md @@ -1,9 +1,87 @@ # PaddleRS科研实战:设计深度学习变化检测模型 -## 环境配置 +本案例演示如何使用PaddleRS设计变化检测模型,并开展消融实验和对比实验。 -## 数据准备 +## 1 环境配置 -## 模型设计与验证 +根据[教程](https://github.com/PaddlePaddle/PaddleRS/tree/develop/tutorials/train#环境准备)安装PaddleRS及相关依赖。在本项目中,GDAL库并不是必需的。 -## 获取对比算法指标 +配置好环境后,在PaddleRS仓库根目录中执行如下指令切换到本案例所在目录: + +```shell +cd examples/rs_research +``` + +## 2 数据准备 + +本案例在[LEVIR-CD数据集](https://www.mdpi.com/2072-4292/12/10/1662)[1]和[synthetic images and real season-varying remote sensing images(SVCD)数据集](https://www.int-arch-photogramm-remote-sens-spatial-inf-sci.net/XLII-2/565/2018/isprs-archives-XLII-2-565-2018.pdf)[2]上开展实验。请在[LEVIR-CD数据集下载链接](https://justchenhao.github.io/LEVIR/)和[SVCD数据集下载链接](https://drive.google.com/file/d/1GX656JqqOyBi_Ef0w65kDGVto-nHrNs9/edit)分别下载这两个数据集,解压至本地目录,并执行如下指令: + +```shell +mkdir data/ +python ../../tools/prepare_dataset/prepare_levircd.py \ + --in_dataset_dir {LEVIR-CD数据集存放目录路径} \ + --out_dataset_dir "data/levircd" \ + --crop_size 256 \ + --crop_stride 256 +python ../../tools/prepare_dataset/prepare_svcd.py \ + --in_dataset_dir {SVCD数据集存放目录路径} \ + --out_dataset_dir "data/svcd" +``` + +以上指令利用PaddleRS提供的数据集准备工具完成数据集切分、file list创建等操作。具体而言,对于LEVIR-CD数据集,使用官方的训练/验证/测试集划分,并将原始的`1024x1024`大小的影像切分为无重叠的`256x256`的小块(参考[3]中的做法);对于SVCD数据集,使用官方的训练/验证/测试集划分,不做其它额外处理。 + +## 3 模型设计与验证 + +### 3.1 问题分析与思路拟定 + +科学研究是为了解决实际问题的,本案例也不例外。本案例的研究动机如下:随着深度学习技术应用的不断深入,变化检测领域涌现了许多。与之相对应的是,模型的参数量也越来越大。 + +[近年来变化检测模型]() + +诚然,。 + +1. 存储开销。 +2. 过拟合。 + +为了解决上述问题,本案例拟提出一种基于网络迭代优化思想的深度学习变化检测算法。本案例的基本思路是,构造一个轻量级的变化检测模型,并以其作为基础迭代单元。每次迭代开始时,由上一次迭代输出的概率图以及原始的输入影像对构造新的输入,实现coarse-to-fine优化。考虑到增加迭代单元的数量将使模型参数量成倍增加,在迭代过程中始终复用同一迭代单元的参数,充分挖掘变化检测网络的拟合能力,迫使其学习到更加有效的特征。这一做法类似[循环神经网络](https://baike.baidu.com/item/%E5%BE%AA%E7%8E%AF%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C/23199490)。根据此思路可以绘制框图如下: + +[思路展示]() + +### 3.2 确定baseline + +科研工作往往需要“站在巨人的肩膀上”,在前人工作的基础上做“增量创新”。因此,对模型设计类工作而言,选用一个合适的baseline网络至关重要。考虑到本案例的出发点是解决,并且使用了。 + +### 3.3 定义新模型 + +[算法整体框图]() + +### 3.4 进行参数分析与消融实验 + +#### 3.4.1 实验设置 + +#### 3.4.2 实验结果 + +### 3.5 开展特征可视化实验 + +## 4 对比实验 + +### 4.1 确定对比算法 + +### 4.2 准备对比算法配置文件 + +### 4.3 实验结果 + +#### 4.3.1 LEVIR-CD数据集上的对比结果 + +#### 4.3.2 SVCD数据集上的对比结果 + +精度、FLOPs、运行时间 + +## 5 总结与展望 + +## 参考文献 + +> [1] Chen, Hao, and Zhenwei Shi. "A spatial-temporal attention-based method and a new dataset for remote sensing image change detection." *Remote Sensing* 12.10 (2020): 1662. +[2] Lebedev, M. A., et al. "CHANGE DETECTION IN REMOTE SENSING IMAGES USING CONDITIONAL ADVERSARIAL NETWORKS." *International Archives of the Photogrammetry, Remote Sensing & Spatial Information Sciences* 42.2 (2018). +[3] Chen, Hao, Zipeng Qi, and Zhenwei Shi. "Remote sensing image change detection with transformers." *IEEE Transactions on Geoscience and Remote Sensing* 60 (2021): 1-14. +[4] Daudt, Rodrigo Caye, Bertr Le Saux, and Alexandre Boulch. "Fully convolutional siamese networks for change detection." *2018 25th IEEE International Conference on Image Processing (ICIP)*. IEEE, 2018. diff --git a/examples/rs_research/configs/levircd/changeformer.yaml b/examples/rs_research/configs/levircd/changeformer.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/examples/rs_research/configs/levircd/snunet.yaml b/examples/rs_research/configs/levircd/snunet.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/examples/rs_research/configs/svcd/changeformer.yaml b/examples/rs_research/configs/svcd/changeformer.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/examples/rs_research/configs/svcd/snunet.yaml b/examples/rs_research/configs/svcd/snunet.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/tools/prepare_dataset/common.py b/tools/prepare_dataset/common.py new file mode 100644 index 0000000..b9e1d82 --- /dev/null +++ b/tools/prepare_dataset/common.py @@ -0,0 +1,122 @@ +import argparse +import os +import os.path as osp +from glob import glob +from itertools import count +from functools import partial +from concurrent.futures import ThreadPoolExecutor + +from skimage.io import imread, imsave +from tqdm import tqdm + + +def get_default_parser(): + parser = argparse.ArgumentParser() + parser.add_argument( + '--in_dataset_dir', + type=str, + required=True, + help="Input dataset directory.") + parser.add_argument( + '--out_dataset_dir', type=str, help="Output dataset directory.") + return parser + + +def add_crop_options(parser): + parser.add_argument( + '--crop_size', type=int, help="Size of cropped patches.") + parser.add_argument( + '--crop_stride', + type=int, + help="Stride of sliding windows when cropping patches. `crop_size` will be used only if `crop_size` is not None.", + ) + return parser + + +def crop_and_save(path, out_subdir, crop_size, stride): + name, ext = osp.splitext(osp.basename(path)) + out_subsubdir = osp.join(out_subdir, name) + if not osp.exists(out_subsubdir): + os.makedirs(out_subsubdir) + img = imread(path) + w, h = img.shape[:2] + counter = count() + for i in range(0, h - crop_size + 1, stride): + for j in range(0, w - crop_size + 1, stride): + imsave( + osp.join(out_subsubdir, '{}_{}{}'.format(name, + next(counter), ext)), + img[i:i + crop_size, j:j + crop_size], + check_contrast=False) + + +def crop_patches(crop_size, + stride, + data_dir, + out_dir, + subsets=('train', 'val', 'test'), + subdirs=('A', 'B', 'label'), + glob_pattern='*', + max_workers=0): + if max_workers < 0: + raise ValueError("`max_workers` must be a non-negative integer!") + + if max_workers == 0: + for subset in subsets: + for subdir in subdirs: + paths = glob( + osp.join(data_dir, subset, subdir, glob_pattern), + recursive=True) + out_subdir = osp.join(out_dir, subset, subdir) + for p in tqdm(paths): + crop_and_save( + p, + out_subdir=out_subdir, + crop_size=crop_size, + stride=stride) + else: + # Concurrently crop image patches + with ThreadPoolExecutor(max_workers=max_workers) as executor: + for subset in subsets: + for subdir in subdirs: + paths = glob( + osp.join(data_dir, subset, subdir, glob_pattern), + recursive=True) + out_subdir = osp.join(out_dir, subset, subdir) + for _ in tqdm( + executor.map(partial( + crop_and_save, + out_subdir=out_subdir, + crop_size=crop_size, + stride=stride), + paths), + total=len(paths)): + pass + + +def get_path_tuples(*dirs, glob_pattern='*', data_dir=None): + all_paths = [] + for dir_ in dirs: + paths = glob(osp.join(dir_, glob_pattern), recursive=True) + paths = sorted(paths) + if data_dir is not None: + paths = [osp.relpath(p, data_dir) for p in paths] + all_paths.append(paths) + all_paths = list(zip(*all_paths)) + return all_paths + + +def create_file_list(file_list, path_tuples, sep=' '): + with open(file_list, 'w') as f: + for tup in path_tuples: + line = sep.join(tup) + f.write(line + '\n') + + +def link_dataset(src, dst): + if osp.exists(dst) and not osp.isdir(dst): + raise ValueError(f"{dst} exists and is not a directory.") + elif not osp.exists(dst): + os.makedirs(dst) + name = osp.basename(osp.normpath(src)) + os.symlink(src, osp.join(dst, name), target_is_directory=True) diff --git a/tools/prepare_dataset/prepare_levircd.py b/tools/prepare_dataset/prepare_levircd.py index e69de29..a09b1d9 100644 --- a/tools/prepare_dataset/prepare_levircd.py +++ b/tools/prepare_dataset/prepare_levircd.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python + +import os.path as osp + +from common import (get_default_parser, add_crop_options, crop_patches, + get_path_tuples, create_file_list, link_dataset) + +SUBSETS = ('train', 'val', 'test') +SUBDIRS = ('A', 'B', 'label') +FILE_LIST_PATTERN = "{subset}.txt" +URL = "" + +if __name__ == '__main__': + parser = get_default_parser() + parser = add_crop_options(parser) + args = parser.parse_args() + + out_dir = osp.join(args.out_dataset_dir, + osp.basename(osp.normpath(args.in_dataset_dir))) + + if args.crop_size is not None: + crop_patches( + args.crop_size, + args.crop_stride, + data_dir=args.in_dataset_dir, + out_dir=out_dir, + subsets=SUBSETS, + subdirs=SUBDIRS, + glob_pattern='*.png', + max_workers=0) + else: + link_dataset(args.in_dataset_dir, args.out_dataset_dir) + + for subset in SUBSETS: + path_tuples = get_path_tuples( + *(osp.join(out_dir, subset, subdir) for subdir in SUBDIRS), + glob_pattern='**/*.png', + data_dir=args.out_dataset_dir) + file_list = osp.join( + args.out_dataset_dir, FILE_LIST_PATTERN.format(subset=subset)) + create_file_list(file_list, path_tuples) + print(f"Write file list to {file_list}.") diff --git a/tools/prepare_dataset/prepare_svcd.py b/tools/prepare_dataset/prepare_svcd.py index e69de29..5351a90 100644 --- a/tools/prepare_dataset/prepare_svcd.py +++ b/tools/prepare_dataset/prepare_svcd.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python + +import os.path as osp + +from common import (get_default_parser, get_path_tuples, create_file_list, + link_dataset) + +SUBSETS = ('train', 'val', 'test') +SUBDIRS = ('A', 'B', 'OUT') +FILE_LIST_PATTERN = "{subset}.txt" +URL = "" + +if __name__ == '__main__': + parser = get_default_parser() + args = parser.parse_args() + + out_dir = osp.join(args.out_dataset_dir, + osp.basename(osp.normpath(args.in_dataset_dir))) + + link_dataset(args.in_dataset_dir, args.out_dataset_dir) + + for subset in SUBSETS: + # NOTE: Only use cropped real samples. + path_tuples = get_path_tuples( + *(osp.join(out_dir, 'Real', 'subset', subset, subdir) + for subdir in SUBDIRS), + data_dir=args.out_dataset_dir) + file_list = osp.join( + args.out_dataset_dir, FILE_LIST_PATTERN.format(subset=subset)) + create_file_list(file_list, path_tuples) + print(f"Write file list to {file_list}.")