commit
5869d6cc76
3 changed files with 818 additions and 3 deletions
@ -0,0 +1,592 @@ |
|||||||
|
# [第十一届 “中国软件杯”百度遥感赛项:变化检测功能](https://aistudio.baidu.com/aistudio/projectdetail/3684588) |
||||||
|
|
||||||
|
## 比赛介绍 |
||||||
|
|
||||||
|
“中国软件杯”大学生软件设计大赛是一项面向中国在校学生的公益性赛事,是2021年全国普通高校大学生竞赛榜单内竞赛。大赛由国家工业和信息化部、教育部、江苏省人民政府共同主办,致力于正确引导我国在校学生积极参加软件科研活动,切实增强自我创新能力和实际动手能力,为我国软件和信息技术服务业培养出更多高端、优秀的人才。2022年,百度飞桨承办了A组和B组两个赛道,本赛题为A组。 |
||||||
|
|
||||||
|
[比赛官网链接](https://aistudio.baidu.com/aistudio/competition/detail/151/0/introduction) |
||||||
|
|
||||||
|
### 赛题背景 |
||||||
|
|
||||||
|
掌握国土资源利用和土地覆盖类型,是地理国情普查与监测的重要内容。高效获取准确、客观的土地利用情况,监测国土变化情况,可以为国家和地方提供地理国情信息决策支撑。随着遥感、传感器技术的发展,特别是多时相高分辨率遥感图像数据的普及,使我们可以足不出户,就能掌握全球任一地表的细微变化。 |
||||||
|
|
||||||
|
目前,我国遥感领域已步入了高分辨率影像的快车道,对遥感数据的分析应用服务的需求也与日俱增。传统方式对高分辨率卫星遥感图像的对特征刻画能力差且依赖人工经验工作量巨大。随着人工智能技术的兴起,特别是基于深度学习的图像识别方法获得了极大的发展,相关技术也推动了遥感领域的变革。相对于传统基于人海战术的目视解译方法,基于深度学习的遥感图像识别技术可以自动分析图像中的地物类型,在准确率和效率方面展现出极大的潜力。 |
||||||
|
|
||||||
|
此次赛题由百度飞桨和[北航LEVIR团队](http://levir.buaa.edu.cn/) 共同设置,要求选手使用百度AI Studio平台进行训练,基于国产化人工智能框架——百度飞桨PaddlePaddle框架进行开发,设计并开发一个可以通过深度学习技术实现对遥感图像自动解译的WEB系统。 |
||||||
|
|
||||||
|
### 任务说明 |
||||||
|
|
||||||
|
变化检测部分要求参赛者利用提供的训练数据,实现对多时相图像中的建筑变化检测。具体而言,多时相遥感图像建筑物变化检测任务是给定两张不同时间拍摄的相同位置(地理配准)的遥感图像,要求定位出其中建筑变化的区域。 |
||||||
|
|
||||||
|
参考链接:[什么是遥感影像变化检测?](https://baike.baidu.com/item/%E5%8F%98%E5%8C%96%E6%A3%80%E6%B5%8B/8636264) |
||||||
|
|
||||||
|
### 数据集介绍 |
||||||
|
|
||||||
|
参见[数据集链接](https://aistudio.baidu.com/aistudio/datasetdetail/134796)和[赛题说明](https://aistudio.baidu.com/aistudio/competition/detail/151/0/task-definition)。 |
||||||
|
|
||||||
|
## 数据预处理 |
||||||
|
|
||||||
|
```python |
||||||
|
# 划分训练集/验证集,并生成文件名列表 |
||||||
|
|
||||||
|
import random |
||||||
|
import os.path as osp |
||||||
|
from glob import glob |
||||||
|
|
||||||
|
|
||||||
|
# 随机数生成器种子 |
||||||
|
RNG_SEED = 114514 |
||||||
|
# 调节此参数控制训练集数据的占比 |
||||||
|
TRAIN_RATIO = 0.95 |
||||||
|
# 数据集路径 |
||||||
|
DATA_DIR = '/home/aistudio/data/data134796/dataset/' |
||||||
|
|
||||||
|
|
||||||
|
def write_rel_paths(phase, names, out_dir, prefix=''): |
||||||
|
"""将文件相对路径存储在txt格式文件中""" |
||||||
|
with open(osp.join(out_dir, phase+'.txt'), 'w') as f: |
||||||
|
for name in names: |
||||||
|
f.write( |
||||||
|
' '.join([ |
||||||
|
osp.join(prefix, 'A', name), |
||||||
|
osp.join(prefix, 'B', name), |
||||||
|
osp.join(prefix, 'label', name) |
||||||
|
]) |
||||||
|
) |
||||||
|
f.write('\n') |
||||||
|
|
||||||
|
|
||||||
|
random.seed(RNG_SEED) |
||||||
|
|
||||||
|
# 随机划分训练集/验证集 |
||||||
|
names = list(map(osp.basename, glob(osp.join(DATA_DIR, 'train', 'label', '*.png')))) |
||||||
|
# 对文件名进行排序,以确保多次运行结果一致 |
||||||
|
names.sort() |
||||||
|
random.shuffle(names) |
||||||
|
len_train = int(len(names)*TRAIN_RATIO) # 向下取整 |
||||||
|
write_rel_paths('train', names[:len_train], DATA_DIR, prefix='train') |
||||||
|
write_rel_paths('val', names[len_train:], DATA_DIR, prefix='train') |
||||||
|
write_rel_paths( |
||||||
|
'test', |
||||||
|
map(osp.basename, glob(osp.join(DATA_DIR, 'test', 'A', '*.png'))), |
||||||
|
DATA_DIR, |
||||||
|
prefix='test' |
||||||
|
) |
||||||
|
|
||||||
|
print("数据集划分已完成。") |
||||||
|
|
||||||
|
``` |
||||||
|
|
||||||
|
## 模型训练与推理 |
||||||
|
|
||||||
|
本项目使用[PaddleRS](https://github.com/PaddleCV-SIG/PaddleRS)套件搭建模型训练与推理框架。PaddleRS是基于飞桨开发的遥感处理平台,支持遥感图像分类、目标检测、图像分割、以及变化检测等常用遥感任务,能够帮助开发者更便捷地完成从训练到部署全流程遥感深度学习应用。在变化检测方面,PaddleRS目前支持9个state-of-the-art(SOTA)模型,且复杂的训练和推理过程被封装到数个API中,能够提供开箱即用的用户体验。 |
||||||
|
|
||||||
|
```python |
||||||
|
# 安装第三方库 |
||||||
|
!pip install scikit-image > /dev/null |
||||||
|
!pip install matplotlib==3.4 > /dev/null |
||||||
|
|
||||||
|
# 安装PaddleRS(AI Studio上缓存的版本) |
||||||
|
!unzip -o -d /home/aistudio/ /home/aistudio/data/data135375/PaddleRS-develop.zip > /dev/null |
||||||
|
!mv /home/aistudio/PaddleRS-develop /home/aistudio/PaddleRS |
||||||
|
!pip install -e /home/aistudio/PaddleRS > /dev/null |
||||||
|
# 因为`sys.path`可能没有及时更新,这里选择手动更新 |
||||||
|
import sys |
||||||
|
sys.path.append('/home/aistudio/PaddleRS') |
||||||
|
``` |
||||||
|
|
||||||
|
```python |
||||||
|
# 导入一些需要用到的库 |
||||||
|
|
||||||
|
import random |
||||||
|
import os |
||||||
|
import os.path as osp |
||||||
|
from copy import deepcopy |
||||||
|
from functools import partial |
||||||
|
|
||||||
|
import numpy as np |
||||||
|
import paddle |
||||||
|
import paddlers as pdrs |
||||||
|
from paddlers import transforms as T |
||||||
|
from PIL import Image |
||||||
|
from skimage.io import imread, imsave |
||||||
|
from tqdm import tqdm |
||||||
|
from matplotlib import pyplot as plt |
||||||
|
``` |
||||||
|
|
||||||
|
```python |
||||||
|
# 定义全局变量 |
||||||
|
# 可在此处调整实验所用超参数 |
||||||
|
|
||||||
|
# 随机种子 |
||||||
|
SEED = 1919810 |
||||||
|
|
||||||
|
# 数据集路径 |
||||||
|
DATA_DIR = '/home/aistudio/data/data134796/dataset/' |
||||||
|
# 实验路径。实验目录下保存输出的模型权重和结果 |
||||||
|
EXP_DIR = '/home/aistudio/exp/' |
||||||
|
# 保存最佳模型的路径 |
||||||
|
BEST_CKP_PATH = osp.join(EXP_DIR, 'best_model', 'model.pdparams') |
||||||
|
|
||||||
|
# 训练的epoch数 |
||||||
|
NUM_EPOCHS = 100 |
||||||
|
# 每多少个epoch保存一次模型权重参数 |
||||||
|
SAVE_INTERVAL_EPOCHS = 10 |
||||||
|
# 初始学习率 |
||||||
|
LR = 0.001 |
||||||
|
# 学习率衰减步长(注意,单位为迭代次数而非epoch数),即每多少次迭代将学习率衰减一半 |
||||||
|
DECAY_STEP = 1000 |
||||||
|
# batch size |
||||||
|
BATCH_SIZE = 16 |
||||||
|
# 加载数据所使用的进程数 |
||||||
|
NUM_WORKERS = 4 |
||||||
|
# 裁块大小 |
||||||
|
CROP_SIZE = 256 |
||||||
|
# 模型推理阶段使用的滑窗步长 |
||||||
|
STRIDE = 64 |
||||||
|
# 影像原始大小 |
||||||
|
ORIGINAL_SIZE = (1024, 1024) |
||||||
|
``` |
||||||
|
|
||||||
|
```python |
||||||
|
# 固定随机种子,尽可能使实验结果可复现 |
||||||
|
|
||||||
|
random.seed(SEED) |
||||||
|
np.random.seed(SEED) |
||||||
|
paddle.seed(SEED) |
||||||
|
``` |
||||||
|
|
||||||
|
```python |
||||||
|
# 定义一些辅助函数 |
||||||
|
|
||||||
|
def info(msg, **kwargs): |
||||||
|
print(msg, **kwargs) |
||||||
|
|
||||||
|
|
||||||
|
def warn(msg, **kwargs): |
||||||
|
print('\033[0;31m'+msg, **kwargs) |
||||||
|
|
||||||
|
|
||||||
|
def quantize(arr): |
||||||
|
return (arr*255).astype('uint8') |
||||||
|
``` |
||||||
|
|
||||||
|
### 模型构建 |
||||||
|
|
||||||
|
作为演示,本项目选用LEVIR小组2021年的作品——基于Transformer的变化检测模型BIT-CD[1]。原论文请参考[此链接](https://ieeexplore.ieee.org/document/9491802),原作者官方实现请参考[此链接](https://github.com/justchenhao/BIT_CD)。 |
||||||
|
|
||||||
|
> [1] Hao Chen, Zipeng Qi, and Zhenwei Shi. **Remote Sensing Image Change Detection with Transformers.** *IEEE Transactions on Geoscience and Remote Sensing.* |
||||||
|
|
||||||
|
```python |
||||||
|
# 调用PaddleRS API一键构建模型 |
||||||
|
model = pdrs.tasks.BIT( |
||||||
|
# 模型输出类别数 |
||||||
|
num_classes=2, |
||||||
|
# 是否使用混合损失函数,默认使用交叉熵损失函数训练 |
||||||
|
use_mixed_loss=False, |
||||||
|
# 模型输入通道数 |
||||||
|
in_channels=3, |
||||||
|
# 模型使用的骨干网络,支持'resnet18'或'resnet34' |
||||||
|
backbone='resnet18', |
||||||
|
# 骨干网络中的resnet stage数量 |
||||||
|
n_stages=4, |
||||||
|
# 是否使用tokenizer获取语义token |
||||||
|
use_tokenizer=True, |
||||||
|
# token的长度 |
||||||
|
token_len=4, |
||||||
|
# 若不使用tokenizer,则使用池化方式获取token。此参数设置池化模式,有'max'和'avg'两种选项,分别对应最大池化与平均池化 |
||||||
|
pool_mode='max', |
||||||
|
# 池化操作输出特征图的宽和高(池化方式得到的token的长度为pool_size的平方) |
||||||
|
pool_size=2, |
||||||
|
# 是否在Transformer编码器中加入位置编码(positional embedding) |
||||||
|
enc_with_pos=True, |
||||||
|
# Transformer编码器使用的注意力模块(attention block)个数 |
||||||
|
enc_depth=1, |
||||||
|
# Transformer编码器中每个注意力头的嵌入维度(embedding dimension) |
||||||
|
enc_head_dim=64, |
||||||
|
# Transformer解码器使用的注意力模块个数 |
||||||
|
dec_depth=8, |
||||||
|
# Transformer解码器中每个注意力头的嵌入维度 |
||||||
|
dec_head_dim=8 |
||||||
|
) |
||||||
|
``` |
||||||
|
|
||||||
|
### 数据集构建 |
||||||
|
|
||||||
|
```python |
||||||
|
# 构建需要使用的数据变换(数据增强、预处理) |
||||||
|
# 使用Compose组合多种变换方式。Compose中包含的变换将按顺序串行执行 |
||||||
|
train_transforms = T.Compose([ |
||||||
|
# 随机裁剪 |
||||||
|
T.RandomCrop( |
||||||
|
# 裁剪区域将被缩放到此大小 |
||||||
|
crop_size=CROP_SIZE, |
||||||
|
# 将裁剪区域的横纵比固定为1 |
||||||
|
aspect_ratio=[1.0, 1.0], |
||||||
|
# 裁剪区域相对原始影像长宽比例在一定范围内变动,最小不低于原始长宽的1/5 |
||||||
|
scaling=[0.2, 1.0] |
||||||
|
), |
||||||
|
# 以50%的概率实施随机水平翻转 |
||||||
|
T.RandomHorizontalFlip(prob=0.5), |
||||||
|
# 以50%的概率实施随机垂直翻转 |
||||||
|
T.RandomVerticalFlip(prob=0.5), |
||||||
|
# 数据归一化到[-1,1] |
||||||
|
T.Normalize( |
||||||
|
mean=[0.5, 0.5, 0.5], |
||||||
|
std=[0.5, 0.5, 0.5] |
||||||
|
) |
||||||
|
]) |
||||||
|
eval_transforms = T.Compose([ |
||||||
|
# 在验证阶段,输入原始尺寸影像,对输入影像仅进行归一化处理 |
||||||
|
# 验证阶段与训练阶段的数据归一化方式必须相同 |
||||||
|
T.Normalize( |
||||||
|
mean=[0.5, 0.5, 0.5], |
||||||
|
std=[0.5, 0.5, 0.5] |
||||||
|
) |
||||||
|
]) |
||||||
|
|
||||||
|
# 实例化数据集 |
||||||
|
train_dataset = pdrs.datasets.CDDataset( |
||||||
|
data_dir=DATA_DIR, |
||||||
|
file_list=osp.join(DATA_DIR, 'train.txt'), |
||||||
|
label_list=None, |
||||||
|
transforms=train_transforms, |
||||||
|
num_workers=NUM_WORKERS, |
||||||
|
shuffle=True, |
||||||
|
binarize_labels=True |
||||||
|
) |
||||||
|
eval_dataset = pdrs.datasets.CDDataset( |
||||||
|
data_dir=DATA_DIR, |
||||||
|
file_list=osp.join(DATA_DIR, 'val.txt'), |
||||||
|
label_list=None, |
||||||
|
transforms=eval_transforms, |
||||||
|
num_workers=0, |
||||||
|
shuffle=False, |
||||||
|
binarize_labels=True |
||||||
|
) |
||||||
|
``` |
||||||
|
|
||||||
|
### 模型训练 |
||||||
|
|
||||||
|
使用AI Studio高级版硬件配置(16G V100)和默认的超参数,训练总时长约为50分钟。 |
||||||
|
|
||||||
|
如果在训练中启用了VisualDL日志记录的功能(默认开启),则可以在“数据模型可视化”页签中查看可视化结果,请将logdir设置为`EXP_DIR`目录下的vdl_log子目录。在notebook中使用VisualDL的相关教程可参考[此处](https://ai.baidu.com/ai-doc/AISTUDIO/Dk3e2vxg9#visualdl%E5%B7%A5%E5%85%B7)。 |
||||||
|
|
||||||
|
需要注意的是,PaddleRS默认以mIoU评价验证集上的最优模型,而赛事官方则选用F1分数作为评价指标。 |
||||||
|
|
||||||
|
此外,PaddleRS在验证集上汇报针对每一类的指标,因此对于二类变化检测来说,category_acc、category_F1-score等指标均存在两个数据项,以列表形式体现。由于变化检测任务主要关注变化类,因此观察和比较每种指标的第二个数据项(即列表的第二个元素)是更有意义的。 |
||||||
|
|
||||||
|
```python |
||||||
|
# 若实验目录不存在,则新建之(递归创建目录) |
||||||
|
if not osp.exists(EXP_DIR): |
||||||
|
os.makedirs(EXP_DIR) |
||||||
|
``` |
||||||
|
|
||||||
|
```python |
||||||
|
# 构建学习率调度器和优化器 |
||||||
|
|
||||||
|
# 制定定步长学习率衰减策略 |
||||||
|
lr_scheduler = paddle.optimizer.lr.StepDecay( |
||||||
|
LR, |
||||||
|
step_size=DECAY_STEP, |
||||||
|
# 学习率衰减系数,这里指定每次减半 |
||||||
|
gamma=0.5 |
||||||
|
) |
||||||
|
# 构造Adam优化器 |
||||||
|
optimizer = paddle.optimizer.Adam( |
||||||
|
learning_rate=lr_scheduler, |
||||||
|
# 在PaddleRS中,可通过ChangeDetector对象的net属性获取paddle.nn.Layer类型组网 |
||||||
|
parameters=model.net.parameters() |
||||||
|
) |
||||||
|
``` |
||||||
|
|
||||||
|
```python |
||||||
|
# 调用PaddleRS API实现一键训练 |
||||||
|
model.train( |
||||||
|
num_epochs=NUM_EPOCHS, |
||||||
|
train_dataset=train_dataset, |
||||||
|
train_batch_size=BATCH_SIZE, |
||||||
|
eval_dataset=eval_dataset, |
||||||
|
optimizer=optimizer, |
||||||
|
save_interval_epochs=SAVE_INTERVAL_EPOCHS, |
||||||
|
# 每多少次迭代记录一次日志 |
||||||
|
log_interval_steps=10, |
||||||
|
save_dir=EXP_DIR, |
||||||
|
# 是否使用early stopping策略,当精度不再改善时提前终止训练 |
||||||
|
early_stop=False, |
||||||
|
# 是否启用VisualDL日志功能 |
||||||
|
use_vdl=True, |
||||||
|
# 指定从某个检查点继续训练 |
||||||
|
resume_checkpoint=None |
||||||
|
) |
||||||
|
``` |
||||||
|
|
||||||
|
### 模型推理 |
||||||
|
|
||||||
|
使用AI Studio高级版硬件配置(16G V100)和默认的超参数,推理总时长约为3分钟。 |
||||||
|
|
||||||
|
推理脚本使用固定阈值法从变化概率图获取二值变化图(binary change map),默认阈值为0.5,可根据模型实际表现调整阈值。当然,也可以换用[Otsu法](https://baike.baidu.com/item/otsu/16252828?fr=aladdin)、[k-means聚类法](https://baike.baidu.com/item/K%E5%9D%87%E5%80%BC%E8%81%9A%E7%B1%BB%E7%AE%97%E6%B3%95/15779627)等更先进的阈值分割算法。 |
||||||
|
|
||||||
|
模型前向推理结果存储在`EXP_DIR`目录下的out子目录中,可将该子目录内的文件打包、并将压缩文件重命名后提交到比赛系统。在提交结果前,请仔细阅读[提交规范](https://aistudio.baidu.com/aistudio/competition/detail/151/0/submit-result)。 |
||||||
|
|
||||||
|
```python |
||||||
|
# 定义推理阶段使用的数据集 |
||||||
|
|
||||||
|
class InferDataset(paddle.io.Dataset): |
||||||
|
""" |
||||||
|
变化检测推理数据集。 |
||||||
|
|
||||||
|
Args: |
||||||
|
data_dir (str): 数据集所在的目录路径。 |
||||||
|
transforms (paddlers.transforms.Compose): 需要执行的数据变换操作。 |
||||||
|
""" |
||||||
|
|
||||||
|
def __init__( |
||||||
|
self, |
||||||
|
data_dir, |
||||||
|
transforms |
||||||
|
): |
||||||
|
super().__init__() |
||||||
|
|
||||||
|
self.data_dir = data_dir |
||||||
|
self.transforms = deepcopy(transforms) |
||||||
|
|
||||||
|
pdrs.transforms.arrange_transforms( |
||||||
|
model_type='changedetector', |
||||||
|
transforms=self.transforms, |
||||||
|
mode='test' |
||||||
|
) |
||||||
|
|
||||||
|
with open(osp.join(data_dir, 'test.txt'), 'r') as f: |
||||||
|
lines = f.read() |
||||||
|
lines = lines.strip().split('\n') |
||||||
|
|
||||||
|
samples = [] |
||||||
|
names = [] |
||||||
|
for line in lines: |
||||||
|
items = line.strip().split(' ') |
||||||
|
items = list(map(pdrs.utils.path_normalization, items)) |
||||||
|
item_dict = { |
||||||
|
'image_t1': osp.join(data_dir, items[0]), |
||||||
|
'image_t2': osp.join(data_dir, items[1]) |
||||||
|
} |
||||||
|
samples.append(item_dict) |
||||||
|
names.append(osp.basename(items[0])) |
||||||
|
|
||||||
|
self.samples = samples |
||||||
|
self.names = names |
||||||
|
|
||||||
|
def __getitem__(self, idx): |
||||||
|
sample = deepcopy(self.samples[idx]) |
||||||
|
output = self.transforms(sample) |
||||||
|
return paddle.to_tensor(output[0]), \ |
||||||
|
paddle.to_tensor(output[1]) |
||||||
|
|
||||||
|
def __len__(self): |
||||||
|
return len(self.samples) |
||||||
|
``` |
||||||
|
|
||||||
|
```python |
||||||
|
# 考虑到原始影像尺寸较大,以下类和函数与影像裁块-拼接有关。 |
||||||
|
|
||||||
|
class WindowGenerator: |
||||||
|
def __init__(self, h, w, ch, cw, si=1, sj=1): |
||||||
|
self.h = h |
||||||
|
self.w = w |
||||||
|
self.ch = ch |
||||||
|
self.cw = cw |
||||||
|
if self.h < self.ch or self.w < self.cw: |
||||||
|
raise NotImplementedError |
||||||
|
self.si = si |
||||||
|
self.sj = sj |
||||||
|
self._i, self._j = 0, 0 |
||||||
|
|
||||||
|
def __next__(self): |
||||||
|
# 列优先移动(C-order) |
||||||
|
if self._i > self.h: |
||||||
|
raise StopIteration |
||||||
|
|
||||||
|
bottom = min(self._i+self.ch, self.h) |
||||||
|
right = min(self._j+self.cw, self.w) |
||||||
|
top = max(0, bottom-self.ch) |
||||||
|
left = max(0, right-self.cw) |
||||||
|
|
||||||
|
if self._j >= self.w-self.cw: |
||||||
|
if self._i >= self.h-self.ch: |
||||||
|
# 设置一个非法值,使得迭代可以early stop |
||||||
|
self._i = self.h+1 |
||||||
|
self._goto_next_row() |
||||||
|
else: |
||||||
|
self._j += self.sj |
||||||
|
if self._j > self.w: |
||||||
|
self._goto_next_row() |
||||||
|
|
||||||
|
return slice(top, bottom, 1), slice(left, right, 1) |
||||||
|
|
||||||
|
def __iter__(self): |
||||||
|
return self |
||||||
|
|
||||||
|
def _goto_next_row(self): |
||||||
|
self._i += self.si |
||||||
|
self._j = 0 |
||||||
|
|
||||||
|
|
||||||
|
def crop_patches(dataloader, ori_size, window_size, stride): |
||||||
|
""" |
||||||
|
将`dataloader`中的数据裁块。 |
||||||
|
|
||||||
|
Args: |
||||||
|
dataloader (paddle.io.DataLoader): 可迭代对象,能够产生原始样本(每个样本中包含任意数量影像)。 |
||||||
|
ori_size (tuple): 原始影像的长和宽,表示为二元组形式(h,w)。 |
||||||
|
window_size (int): 裁块大小。 |
||||||
|
stride (int): 裁块使用的滑窗每次在水平或垂直方向上移动的像素数。 |
||||||
|
|
||||||
|
Returns: |
||||||
|
一个生成器,能够产生iter(`dataloader`)中每一项的裁块结果。一幅图像产生的块在batch维度拼接。例如,当`ori_size`为1024,而 |
||||||
|
`window_size`和`stride`均为512时,`crop_patches`返回的每一项的batch_size都将是iter(`dataloader`)中对应项的4倍。 |
||||||
|
""" |
||||||
|
|
||||||
|
for ims in dataloader: |
||||||
|
ims = list(ims) |
||||||
|
h, w = ori_size |
||||||
|
win_gen = WindowGenerator(h, w, window_size, window_size, stride, stride) |
||||||
|
all_patches = [] |
||||||
|
for rows, cols in win_gen: |
||||||
|
# NOTE: 此处不能使用生成器,否则因为lazy evaluation的缘故会导致结果不是预期的 |
||||||
|
patches = [im[...,rows,cols] for im in ims] |
||||||
|
all_patches.append(patches) |
||||||
|
yield tuple(map(partial(paddle.concat, axis=0), zip(*all_patches))) |
||||||
|
|
||||||
|
|
||||||
|
def recons_prob_map(patches, ori_size, window_size, stride): |
||||||
|
"""从裁块结果重建原始尺寸影像,与`crop_patches`相对应""" |
||||||
|
# NOTE: 目前只能处理batch size为1的情况 |
||||||
|
h, w = ori_size |
||||||
|
win_gen = WindowGenerator(h, w, window_size, window_size, stride, stride) |
||||||
|
prob_map = np.zeros((h,w), dtype=np.float) |
||||||
|
cnt = np.zeros((h,w), dtype=np.float) |
||||||
|
# XXX: 需要保证win_gen与patches具有相同长度。此处未做检查 |
||||||
|
for (rows, cols), patch in zip(win_gen, patches): |
||||||
|
prob_map[rows, cols] += patch |
||||||
|
cnt[rows, cols] += 1 |
||||||
|
prob_map /= cnt |
||||||
|
return prob_map |
||||||
|
``` |
||||||
|
|
||||||
|
```python |
||||||
|
# 若输出目录不存在,则新建之(递归创建目录) |
||||||
|
out_dir = osp.join(EXP_DIR, 'out') |
||||||
|
if not osp.exists(out_dir): |
||||||
|
os.makedirs(out_dir) |
||||||
|
|
||||||
|
# 为模型加载历史最佳权重 |
||||||
|
state_dict = paddle.load(BEST_CKP_PATH) |
||||||
|
# 同样通过net属性访问组网对象 |
||||||
|
model.net.set_state_dict(state_dict) |
||||||
|
|
||||||
|
# 实例化测试集 |
||||||
|
test_dataset = InferDataset( |
||||||
|
DATA_DIR, |
||||||
|
# 注意,测试阶段使用的归一化方式需与训练时相同 |
||||||
|
T.Compose([ |
||||||
|
T.Normalize( |
||||||
|
mean=[0.5, 0.5, 0.5], |
||||||
|
std=[0.5, 0.5, 0.5] |
||||||
|
) |
||||||
|
]) |
||||||
|
) |
||||||
|
|
||||||
|
# 创建DataLoader |
||||||
|
test_dataloader = paddle.io.DataLoader( |
||||||
|
test_dataset, |
||||||
|
batch_size=1, |
||||||
|
shuffle=False, |
||||||
|
num_workers=0, |
||||||
|
drop_last=False, |
||||||
|
return_list=True |
||||||
|
) |
||||||
|
test_dataloader = crop_patches( |
||||||
|
test_dataloader, |
||||||
|
ORIGINAL_SIZE, |
||||||
|
CROP_SIZE, |
||||||
|
STRIDE |
||||||
|
) |
||||||
|
``` |
||||||
|
|
||||||
|
```python |
||||||
|
# 推理过程主循环 |
||||||
|
info("模型推理开始") |
||||||
|
|
||||||
|
model.net.eval() |
||||||
|
len_test = len(test_dataset.names) |
||||||
|
with paddle.no_grad(): |
||||||
|
for name, (t1, t2) in tqdm(zip(test_dataset.names, test_dataloader), total=len_test): |
||||||
|
pred = model.net(t1, t2)[0] |
||||||
|
# 取softmax结果的第1(从0开始计数)个通道的输出作为变化概率 |
||||||
|
prob = paddle.nn.functional.softmax(pred, axis=1)[:,1] |
||||||
|
# 由patch重建完整概率图 |
||||||
|
prob = recons_prob_map(prob.numpy(), ORIGINAL_SIZE, CROP_SIZE, STRIDE) |
||||||
|
# 默认将阈值设置为0.5,即,将变化概率大于0.5的像素点分为变化类 |
||||||
|
out = quantize(prob>0.5) |
||||||
|
|
||||||
|
imsave(osp.join(out_dir, name), out, check_contrast=False) |
||||||
|
|
||||||
|
info("模型推理完成") |
||||||
|
|
||||||
|
``` |
||||||
|
|
||||||
|
```python |
||||||
|
# 推理结果展示 |
||||||
|
# 重复运行本单元可以查看不同结果 |
||||||
|
|
||||||
|
def show_images_in_row(im_paths, fig, title=''): |
||||||
|
n = len(im_paths) |
||||||
|
fig.suptitle(title) |
||||||
|
axs = fig.subplots(nrows=1, ncols=n) |
||||||
|
for idx, (path, ax) in enumerate(zip(im_paths, axs)): |
||||||
|
# 去掉刻度线和边框 |
||||||
|
ax.spines['top'].set_visible(False) |
||||||
|
ax.spines['right'].set_visible(False) |
||||||
|
ax.spines['bottom'].set_visible(False) |
||||||
|
ax.spines['left'].set_visible(False) |
||||||
|
ax.get_xaxis().set_ticks([]) |
||||||
|
ax.get_yaxis().set_ticks([]) |
||||||
|
|
||||||
|
im = imread(path) |
||||||
|
ax.imshow(im) |
||||||
|
|
||||||
|
|
||||||
|
# 需要展示的样本个数 |
||||||
|
num_imgs_to_show = 4 |
||||||
|
# 随机抽取样本 |
||||||
|
chosen_indices = random.choices(range(len_test), k=num_imgs_to_show) |
||||||
|
|
||||||
|
# 参考 https://stackoverflow.com/a/68209152 |
||||||
|
fig = plt.figure(constrained_layout=True) |
||||||
|
fig.suptitle("Inference Results") |
||||||
|
|
||||||
|
subfigs = fig.subfigures(nrows=3, ncols=1) |
||||||
|
|
||||||
|
# 读入第一时相影像 |
||||||
|
im_paths = [osp.join(DATA_DIR, test_dataset.samples[idx]['image_t1']) for idx in chosen_indices] |
||||||
|
show_images_in_row(im_paths, subfigs[0], title='Image 1') |
||||||
|
|
||||||
|
# 读入第二时相影像 |
||||||
|
im_paths = [osp.join(DATA_DIR, test_dataset.samples[idx]['image_t2']) for idx in chosen_indices] |
||||||
|
show_images_in_row(im_paths, subfigs[1], title='Image 2') |
||||||
|
|
||||||
|
# 读入变化图 |
||||||
|
im_paths = [osp.join(out_dir, test_dataset.names[idx]) for idx in chosen_indices] |
||||||
|
show_images_in_row(im_paths, subfigs[2], title='Change Map') |
||||||
|
|
||||||
|
# 渲染结果 |
||||||
|
fig.canvas.draw() |
||||||
|
Image.frombytes('RGB', fig.canvas.get_width_height(), fig.canvas.tostring_rgb()) |
||||||
|
``` |
||||||
|
|
||||||
|
![output_23_0](https://user-images.githubusercontent.com/71769312/161358173-552a7cca-b5b5-4e5e-8d10-426f40df530b.png) |
||||||
|
|
||||||
|
## 参考资料 |
||||||
|
|
||||||
|
- [遥感数据介绍](https://github.com/PaddleCV-SIG/PaddleRS/blob/develop/docs/data/rs_data_cn.md) |
||||||
|
- [PaddleRS文档](https://github.com/PaddleCV-SIG/PaddleRS/blob/develop/tutorials/train/README.md) |
@ -0,0 +1,224 @@ |
|||||||
|
# [PaddleRS:使用超分模块提高真实的低分辨率无人机影像的分割精度](https://aistudio.baidu.com/aistudio/projectdetail/3696814) |
||||||
|
|
||||||
|
## 一、项目背景 |
||||||
|
|
||||||
|
- 前段时间写了个项目:[PaddleSeg:使用Transfomer模型对航空遥感图像分割](https://aistudio.baidu.com/aistudio/projectdetail/3565870),项目利用PaddleSeg模块训练Transfomer类的语义分割模型,在UDD6数据集中**mIOU达到74.50%** ,原论文使用DeepLabV3+的mIOU为73.18%, **高1.32%** ,训练效果图如下,其中:车辆:红色;道路:浅蓝色;植被:深蓝色;建筑立面:亮绿色;建筑屋顶:紫色;其他:焦绿色 |
||||||
|
|
||||||
|
```python |
||||||
|
%cd /home/aistudio/ |
||||||
|
import matplotlib.pyplot as plt |
||||||
|
from PIL import Image |
||||||
|
|
||||||
|
output = Image.open(r"work/example/Seg/UDD6_result/added_prediction/000161.JPG") |
||||||
|
|
||||||
|
plt.figure(figsize=(18, 12)) # 设置窗口大小 |
||||||
|
plt.imshow(output), plt.axis('off') |
||||||
|
``` |
||||||
|
|
||||||
|
![output_1_2](https://user-images.githubusercontent.com/71769312/161358238-5dc85c26-de33-4552-83ea-ad9936a5c85a.png) |
||||||
|
|
||||||
|
- 训练的结果很不错,所使用的UDD6数据是从北京、葫芦岛、沧州、郑州四个城市,使用大疆精灵四无人机在60m-100m高度之间采集。但是,**在实际的生产过程中,城市、飞行的高度、图像的质量会发生变化** |
||||||
|
- 采集飞行高度升高可以在相同时间内获取更大面积的数据,但分辨率会降低,对低质量的数据,**直接使用先前训练的数据预测效果不理想**,**再标注数据、训练模型将是一个不小的工作量**,解决的方法除了提升模型的泛化能力,也可以考虑使用图像超分对低质量的无人机图像重建,然后再进行预测 |
||||||
|
- 本项目使用PaddleRS提供的无人机遥感图像超分模块,对**真实的低质量无人机影像**数据进行**超分**,然后再使用前段时间用UDD6训练的Segformer模型预测,与直接使用低分辨率模型对比。由于没有对低质量数据进行标注无法计算指标。但人眼判别,超分之后的预测结果更好,**左边是人工标注的label,中间是低分辨率的预测结果,右边是超分辨率重建后的结果** |
||||||
|
|
||||||
|
```python |
||||||
|
img = Image.open(r"work/example/Seg/gt_result/data_05_2_14.png") |
||||||
|
lq = Image.open(r"work/example/Seg/lq_result/added_prediction/data_05_2_14.png") |
||||||
|
sr = Image.open(r"work/example/Seg/sr_result/added_prediction/data_05_2_14.png") |
||||||
|
|
||||||
|
plt.figure(figsize=(18, 12)) |
||||||
|
plt.subplot(1,3,1), plt.title('GT') |
||||||
|
plt.imshow(img), plt.axis('off') |
||||||
|
plt.subplot(1,3,2), plt.title('predict_LR') |
||||||
|
plt.imshow(lq), plt.axis('off') |
||||||
|
plt.subplot(1,3,3), plt.title('predict_SR') |
||||||
|
plt.imshow(sr), plt.axis('off') |
||||||
|
plt.show() |
||||||
|
``` |
||||||
|
|
||||||
|
![output_3_0](https://user-images.githubusercontent.com/71769312/161358300-b85cdda4-7d1f-40e7-a39b-74b2cd5347b6.png) |
||||||
|
|
||||||
|
## 二、数据介绍与展示 |
||||||
|
- 使用的数据是使用大疆精灵四无人机在**上海,飞行高度为300m**采集的,采集的时候天气也一般,可以看后续的示例发现质量不高。由于只是展示超分重建后进行预测的效果,所以只是简单标注了其中5张照片,毕竟**标注数据真的是一件很费力的事!** 要是能用公开数据集训练的模型来预测自己的数据,这多是一件美事! |
||||||
|
- 部分标注数据展示如下 |
||||||
|
|
||||||
|
```python |
||||||
|
add_lb = Image.open(r"work/example/Seg/gt_result/data_05_2_19.png") |
||||||
|
lb = Image.open(r"work/example/Seg/gt_label/data_05_2_19.png") |
||||||
|
img = Image.open(r"work/ValData/DJI300/data_05_2_19.png") |
||||||
|
|
||||||
|
plt.figure(figsize=(18, 12)) |
||||||
|
plt.subplot(1,3,1), plt.title('image') |
||||||
|
plt.imshow(img), plt.axis('off') |
||||||
|
plt.subplot(1,3,2), plt.title('label') |
||||||
|
plt.imshow(lb), plt.axis('off') |
||||||
|
plt.subplot(1,3,3), plt.title('add_label') |
||||||
|
plt.imshow(add_lb), plt.axis('off') |
||||||
|
plt.show() |
||||||
|
``` |
||||||
|
|
||||||
|
![output_5_0](https://user-images.githubusercontent.com/71769312/161358312-3c16cbb0-1162-4fbe-b3d6-9403502aefef.png) |
||||||
|
|
||||||
|
## 三、无人机遥感图像超分 |
||||||
|
- 因为PaddleRS提供了预训练的超分模型,所以这步主要分为以下两个步骤: |
||||||
|
- 准备PaddleRS并设置好环境 |
||||||
|
- 调用PaddleRS中的超分预测接口,对低分辨率无人机影像进行**超分重建** |
||||||
|
|
||||||
|
```python |
||||||
|
# 从github上克隆仓库 |
||||||
|
!git clone https://github.com/PaddleCV-SIG/PaddleRS.git |
||||||
|
``` |
||||||
|
|
||||||
|
```python |
||||||
|
# 安装依赖,大概一分多钟 |
||||||
|
%cd PaddleRS/ |
||||||
|
!pip install -r requirements.txt |
||||||
|
``` |
||||||
|
|
||||||
|
```python |
||||||
|
# 进行图像超分处理,使用的模型为DRN |
||||||
|
import os |
||||||
|
import paddle |
||||||
|
import numpy as np |
||||||
|
from PIL import Image |
||||||
|
from paddlers.models.ppgan.apps.drn_predictor import DRNPredictor |
||||||
|
|
||||||
|
# 输出预测结果的文件夹 |
||||||
|
output = r'../work/example' |
||||||
|
# 待输入的低分辨率影像位置 |
||||||
|
input_dir = r"../work/ValData/DJI300" |
||||||
|
|
||||||
|
paddle.device.set_device("gpu:0") # 若是cpu环境,则替换为 paddle.device.set_device("cpu") |
||||||
|
predictor = DRNPredictor(output) # 实例化 |
||||||
|
|
||||||
|
filenames = [f for f in os.listdir(input_dir) if f.endswith('.png')] |
||||||
|
for filename in filenames: |
||||||
|
imgPath = os.path.join(input_dir, filename) |
||||||
|
predictor.run(imgPath) # 预测 |
||||||
|
``` |
||||||
|
|
||||||
|
- 超分重建结果前后对比展示 |
||||||
|
|
||||||
|
```python |
||||||
|
# 可视化 |
||||||
|
import os |
||||||
|
import matplotlib.pyplot as plt |
||||||
|
%matplotlib inline |
||||||
|
|
||||||
|
lq_dir = r"../work/ValData/DJI300" # 低分辨率影像文件夹 |
||||||
|
sr_dir = r"../work/example/DRN" # 超分辨率影像所在文件夹 |
||||||
|
img_list = [f for f in os.listdir(lq_dir) if f.endswith('.png')] |
||||||
|
show_num = 3 # 展示多少对影像 |
||||||
|
for i in range(show_num): |
||||||
|
lq_box = (100, 100, 175, 175) |
||||||
|
sr_box = (400, 400, 700, 700) |
||||||
|
filename = img_list[i] |
||||||
|
image = Image.open(os.path.join(lq_dir, filename)).crop(lq_box) # 读取低分辨率影像 |
||||||
|
sr_img = Image.open(os.path.join(sr_dir, filename)).crop(sr_box) # 读取超分辨率影像 |
||||||
|
|
||||||
|
plt.figure(figsize=(12, 8)) |
||||||
|
plt.subplot(1,2,1), plt.title('Input') |
||||||
|
plt.imshow(image), plt.axis('off') |
||||||
|
plt.subplot(1,2,2), plt.title('Output') |
||||||
|
plt.imshow(sr_img), plt.axis('off') |
||||||
|
plt.show() |
||||||
|
``` |
||||||
|
|
||||||
|
![output_11_0](https://user-images.githubusercontent.com/71769312/161358324-c45d750d-b47e-4201-b70c-3c374498fd86.png) |
||||||
|
|
||||||
|
![output_11_1](https://user-images.githubusercontent.com/71769312/161358335-0b85035e-0a9d-4b5a-8d0c-14ecaeffd947.png) |
||||||
|
|
||||||
|
![output_11_2](https://user-images.githubusercontent.com/71769312/161358342-d2875098-cb9b-4bc2-99b0-bcab4c1bc5e1.png) |
||||||
|
|
||||||
|
## 四、超分前后图像分割效果对比 |
||||||
|
|
||||||
|
- 使用的模型为Segformer_b3,用UDD6数据集训练了40000次 |
||||||
|
- 已经将性能最好的模型以及.yml文件放在work文件夹下 |
||||||
|
- 运行以下命令可对指定的文件夹下的图像进行预测 |
||||||
|
- 首先用该模型对低质量的无人机数据进行预测,然后再使用超分重建后的图像预测,最后对比一下预测的效果 |
||||||
|
|
||||||
|
```python |
||||||
|
%cd .. |
||||||
|
# clone PaddleSeg的项目 |
||||||
|
!git clone https://gitee.com/paddlepaddle/PaddleSeg |
||||||
|
``` |
||||||
|
|
||||||
|
```python |
||||||
|
# 安装依赖 |
||||||
|
%cd /home/aistudio/PaddleSeg |
||||||
|
!pip install -r requirements.txt |
||||||
|
``` |
||||||
|
|
||||||
|
```python |
||||||
|
# 对低分辨率的无人机影像进行预测 |
||||||
|
!python predict.py \ |
||||||
|
--config ../work/segformer_b3_UDD.yml \ |
||||||
|
--model_path ../work/best_model/model.pdparams \ |
||||||
|
--image_path ../work/ValData/DJI300 \ |
||||||
|
--save_dir ../work/example/Seg/lq_result |
||||||
|
``` |
||||||
|
|
||||||
|
```python |
||||||
|
# 对使用DRN超分重建后的影像进行预测 |
||||||
|
!python predict.py \ |
||||||
|
--config ../work/segformer_b3_UDD.yml \ |
||||||
|
--model_path ../work/best_model/model.pdparams \ |
||||||
|
--image_path ../work/example/DRN \ |
||||||
|
--save_dir ../work/example/Seg/sr_result |
||||||
|
``` |
||||||
|
|
||||||
|
**展示预测结果** |
||||||
|
- 其中,颜色如下: |
||||||
|
|
||||||
|
| 种类 | 颜色 | |
||||||
|
|----------|---------| |
||||||
|
| **其他** | 焦绿色 | |
||||||
|
| 建筑外立面 | 亮绿色 | |
||||||
|
| **道路** | 淡蓝色 | |
||||||
|
| 植被 | 深蓝色 | |
||||||
|
| **车辆** | 红色 | |
||||||
|
| 屋顶 | 紫色 | |
||||||
|
|
||||||
|
- 由于只标注了五张图片,所以只展示五张图片的结果,剩下的预测结果均在 `work/example/Seg/`文件夹下,其中左边是真值,中间是低分辨率影像预测结果,右边是超分重建后预测结果 |
||||||
|
|
||||||
|
```python |
||||||
|
# 展示部分预测的结果 |
||||||
|
%cd /home/aistudio/ |
||||||
|
import matplotlib.pyplot as plt |
||||||
|
from PIL import Image |
||||||
|
import os |
||||||
|
|
||||||
|
img_dir = r"work/example/Seg/gt_result" # 低分辨率影像文件夹 |
||||||
|
lq_dir = r"work/example/Seg/lq_result/added_prediction" |
||||||
|
sr_dir = r"work/example/Seg/sr_result/added_prediction" # 超分辨率预测的结果影像所在文件夹 |
||||||
|
img_list = [f for f in os.listdir(img_dir) if f.endswith('.png') ] |
||||||
|
for filename in img_list: |
||||||
|
img = Image.open(os.path.join(img_dir, filename)) |
||||||
|
lq_pred = Image.open(os.path.join(lq_dir, filename)) |
||||||
|
sr_pred = Image.open(os.path.join(sr_dir, filename)) |
||||||
|
|
||||||
|
plt.figure(figsize=(12, 8)) |
||||||
|
plt.subplot(1,3,1), plt.title('GT') |
||||||
|
plt.imshow(img), plt.axis('off') |
||||||
|
plt.subplot(1,3,2), plt.title('LR_pred') |
||||||
|
plt.imshow(lq_pred), plt.axis('off') |
||||||
|
plt.subplot(1,3,3), plt.title('SR_pred') |
||||||
|
plt.imshow(sr_pred), plt.axis('off') |
||||||
|
plt.show() |
||||||
|
|
||||||
|
``` |
||||||
|
|
||||||
|
![output_18_1](https://user-images.githubusercontent.com/71769312/161358523-42063419-b490-4fca-b0d4-cb2b05f7f74a.png) |
||||||
|
|
||||||
|
![output_18_2](https://user-images.githubusercontent.com/71769312/161358556-e2f66be4-4758-4c7a-9b3b-636aa2b53215.png) |
||||||
|
|
||||||
|
![output_18_3](https://user-images.githubusercontent.com/71769312/161358599-e74696f3-b374-4d5c-a9f4-7ffaef8938a0.png) |
||||||
|
|
||||||
|
![output_18_4](https://user-images.githubusercontent.com/71769312/161358621-c3c0d225-b67f-4bff-91ba-4be714162584.png) |
||||||
|
|
||||||
|
![output_18_5](https://user-images.githubusercontent.com/71769312/161358643-9aba7db1-6c68-48f2-be53-8eec30f27d60.png) |
||||||
|
|
||||||
|
## 五、总结 |
||||||
|
- 本项目调用PaddleRS提供的超分重建接口,选用DRN模型对真实采集的低分辨率影像进行重建,再对重建后的图像进行分割,从结果上看,**超分重建后的图片的分割结果更好** |
||||||
|
- **不足之处**:虽然相对于低分辨率影像,超分重建后的预测精度从目视的角度有所提高,但是并没有达到UDD6测试集中的效果,所以**模型的泛化能力也需要提高才行,光靠超分重建依然不够** |
||||||
|
- **后续工作**:将会把超分重建这一步整合到PaddleRS中的transform模块,在high-level任务预测之前可以进行调用改善图像质量,请大家多多关注[PaddleRS](https://github.com/PaddleCV-SIG/PaddleRS) |
Loading…
Reference in new issue