十一届软件杯遥感解译赛道——预赛第四名方案分享
本Notebook为基于软件杯官方Baseline更改的项目,结合个人理解,进行了一些优化和更改,分数会比Baseline要高一点,并且将可以继续优化的方向都写在项目里了。

总的来说,个人认为这个比赛关键在与如何解决过拟合和用于训练的数据集和测评的数据集相差过大的问题。而更改网络结构这些事情,并不是我们需要关心的。

本人之前并没有接触过遥感相关的任务,也不是相关专业,有很多地方也不太懂,所以也是通过这个比赛开始学习相关的内容,也欢迎大家和我进行交流讨论。

这里也感谢林大佬公布官方的Baseline和在群里的答疑。

看了一下大致预赛过榜0.85+到0.86应该是没有问题的,所以这个Baseline大致的分数也是如此, 另外说明,下面提及有关分数的地方,会根实际结果有一定的出入

任务说明
变化检测部分要求参赛者利用提供的训练数据,实现对多时相图像中的建筑变化检测。具体而言,多时相遥感图像建筑物变化检测任务是给定两张不同时间拍摄的相同位置(地理配准)的遥感图像,要求定位出其中建筑变化的区域。

在这里插入图片描述
法是不是上限比这个方法高,个人没有验证)

而这里其实也不一定需要划分到像256到512的整数倍,而是可以像256,320,416,512,608这样的尺寸,然后对这些多尺寸训练的结果进行融合,也能提升不少的分数。有可能很多同学将图片划分成512,发现预测出来的F1没有256高,就把这个结果扔掉了,其实是不对的,因为多尺度训练的目的,就是在不同尺寸下,网络学习到到或观察到的东西是不一样的。下面列个我自己测试过结果的一部分。

划分尺寸 网络结构 F1分数
256 BIT+HRnet18 0.880
512 BIT+HRnet18 0.872
1024 BIT+HRnet18 0.870
256+512+1024 \ 0.884
In [3]
import os
import cv2 as cv

将图片划分为256x16

def crop_img(image, crop_size):
crop_images = []
split_row = image.shape[0] // crop_size
split_col = image.shape[1] // crop_size
for i in range(0, split_row):
for j in range(0, split_col):
crop = image[i*crop_size:(i+1)crop_size, jcrop_size:(j+1)*crop_size]
crop_images.append(crop)
return crop_images

需要切分的尺寸

crop_size = 256
train_list = os.listdir(‘./datasets/train/A’)
train_root = ‘./datasets/train/’
out_root = ‘./datasets/split_train’
for file in train_list:
point = file.rfind(‘.’)
img_name = file[:point]
A_img_path = os.path.join(train_root, ‘A’, file)
B_img_path = os.path.join(train_root, ‘B’, file)
label_img_path = os.path.join(train_root, ‘label’, file)
A_img = cv.imread(A_img_path)
B_img = cv.imread(B_img_path)
label_img = cv.imread(label_img_path, cv.IMREAD_GRAYSCALE)
A_crop_imgs = crop_img(A_img, crop_size)
B_crop_imgs = crop_img(B_img, crop_size)
label_crop_imgs = crop_img(label_img, crop_size)
number = A_img.shape[0] // crop_size
for i in range(number*number):
A_write_path = os.path.join(out_root, ‘A’,img_name+‘‘+str(i)+’.png’ )
B_write_path = os.path.join(out_root, ‘B’, img_name + '
’ + str(i)+‘.png’)
label_write_path = os.path.join(out_root, ‘label’, img_name + ‘_’ + str(i)+‘.png’)
cv.imwrite(A_write_path, A_crop_imgs[i])
cv.imwrite(B_write_path, B_crop_imgs[i])
cv.imwrite(label_write_path, label_crop_imgs[i])
数据集划分并生成Dataset所需文件
前面根据划分,会得到很多256x256的片段,但其中有很多都是空白的,这里划分数据集则是根据其label是否是空白来进行划分

这里划分方法是将600张有建筑物的label和400张没有建筑物的label划分为验证集

因为数据集较少,虽然说是拿的前600个有label和前400个没有label的进行划分,但这个结果每次其实是不一样的,而且对分数影响也不少,大致有正负0.03到0.06的差别,所以怎么减少这个影响也是值得考虑的,最简单的方法就是做交叉验证,每次拿不一样位置的数据集划分到val里,然后最后把多个结果做集成

In [6]

生成含有建筑物的mask list

import cv2 as cv
split_root = ‘datasets/split_train’
label_list = os.listdir(os.path.join(split_root, ‘label’))

def generate_mask_txt(mask_path, mask=True):
with open(mask_path, ‘w’) as f:
for label_name in label_list:
A_image_name = os.path.join(split_root, ‘A’, label_name)
B_image_name = os.path.join(split_root, ‘B’, label_name)
label_image_name = os.path.join(split_root, ‘label’, label_name)
label_image = cv.imread(label_image_name)
# 只找出有建筑物的label
if mask == True:
if label_image is not None and label_image.any() == True:
f.write(A_image_name + ’ ’ + B_image_name + ’ '+ label_image_name + ‘\n’)
else:
if label_image is not None and (not label_image.any()) == True:
f.write(A_image_name + ’ ’ + B_image_name + ’ '+ label_image_name + ‘\n’)
f.close()

生成相应的mask文件

mask_path = ‘datasets/mask.txt’
no_mask_path = ‘datasets/no_mask.txt’
generate_mask_txt(mask_path, mask=True)
generate_mask_txt(no_mask_path, mask=False)
In [7]

生成mask需要使用的list列表

def generate_mask_list(mask_path):
mask_list = []
with open(mask_path) as f:
lines = f.readlines()
for line in lines:
line = line.split(’ ‘)
item = {‘image’:’‘, ‘image2’:’‘, ‘mask’:’'}
item[‘image’] = line[0]
item[‘image2’] = line[1]
item[‘mask’] = line[2][:-1]
mask_list.append(item)
f.close()
return mask_list

生成mask list的txt地址

mask_path = ‘datasets/mask.txt’
no_mask_path = ‘datasets/no_mask.txt’
mask_list = generate_mask_list(mask_path)
no_mask_list = generate_mask_list(no_mask_path)
In [8]
import shutil

移动图片函数

def remove_image(path_list, move_num):
for i in range(move_num):
A_image_path = path_list[i][‘image’]
B_image_path = path_list[i][‘image2’]
label_image_path = path_list[i][‘mask’]
point = label_image_path.rfind(‘/’)
image_name = label_image_path[point+1:]

    # 移动地址
    A_move_path = os.path.join(val_root, 'A', image_name)
    B_move_path = os.path.join(val_root, 'B', image_name)
    label_move_path = os.path.join(val_root, 'label', image_name)

    # 移动图片到val下
    shutil.move(A_image_path, A_move_path)
    shutil.move(B_image_path, B_move_path)
    shutil.move(label_image_path, label_move_path)

val_mask_num = 600
val_no_mask_num = 400
val_root = ‘./datasets/val’

移动图片

remove_image(mask_list, val_mask_num)
remove_image(no_mask_list, val_no_mask_num)
In [9]

生成数据列表

import os
import cv2 as cv
def creat_data_list(dataset_path, mode=‘train’):
with open(os.path.join(dataset_path, (mode + ‘_list.txt’)), ‘w’) as f:
A_path = os.path.join(os.path.join(dataset_path, mode), ‘A’)
out_path = os.path.join(mode, ‘A’)
A_imgs_name = os.listdir(A_path) # 获取文件夹下的所有文件名
A_imgs_name.sort()
for A_img_name in A_imgs_name:
A_img = os.path.join(A_path, A_img_name)
B_img = os.path.join(A_path.replace(‘A’, ‘B’), A_img_name)
# 输出名字
A_out_image = os.path.join(out_path, A_img_name)
B_out_image = os.path.join(out_path.replace(‘A’, ‘B’), A_img_name)
A_image = cv.imread(A_img)
B_image = cv.imread(B_img)
if A_image is not None and B_image is not None:
label_img = os.path.join(A_path.replace(‘A’, ‘label’), A_img_name)
label_out = os.path.join(out_path.replace(‘A’, ‘label’), A_img_name)
f.write(A_out_image + ’ ’ + B_out_image + ’ ’ + label_out + ‘\n’) # 写入list.txt
else:
print(“remove :”)
print(A_img)
print(B_img)
print(mode + ‘_data_list generated’)

dataset_path = ‘datasets’ # data的文件夹

分别创建三个list.txt

creat_data_list(dataset_path, mode=‘split_train’)
creat_data_list(dataset_path, mode=‘val’)
split_train_data_list generated
val_data_list generated
In [10]

构建比赛测试集的list

import os
test_A_data_path = ‘./datasets/test/A’
test_root_path = ‘./datasets’
file_list = os.listdir(test_A_data_path)
f = open(‘datasets/test/test.txt’, ‘w’)
for file in file_list:
A_image_path = os.path.join(‘A’, file)
B_image_path = os.path.join(‘B’, file)
label_image_path = os.path.join(‘label’, file)
f.write(A_image_path + ’ ‘+ B_image_path + ’ ’ + label_image_path+’\n’)
f.close()
构建Dataset
这里能改的地方就是数据增强了。整个数据集较少,合理的使用数据增强,才能更好的利用数据,但并不是说越多越好。

本项目使用的数据集增强有:

随机交换

随机模糊

随机水平翻转

随机垂直翻转

其实还使用了一种数据集增强,就是随机把训练集中带label的图片的一部分裁出来,然后resize到128,再贴到一张图片上,因为RS上没有,这里也不加上了,有兴趣可以手撕一下,也不难写,大概能提0.03到0.06个点吧。大致的效果图如下,其实主要就是为了减少一些全黑的label出现,因为按照我的方法进行切片后,数据集中有会大量的label为空的图片,而过多对于这些图片进行训练,是没有意义的。但这里resize之后,图片会有偏移,具体怎么解决这个问题,也可以思考。

另外本人也花了很长的时间在CopyPaste这个数据增强上,试过不同的方式,但最终效果都不好,个人推测是,我试过的CopyPaste,都是将一整个图片的label Copy到另一张图上,如果能分离开某一张图上的某个建筑物label,再去做paste,效果应该会好很多,但后面也没时间去细调了,所谓的把label中的建筑物分离,其实就是做个轮廓检测的样子,这样的效果,个人感觉如果再对这些label做色调变换或者模糊处理,就和GAN的效果差不多了。

下面同样列出本人对其中一些数据增强方法做过的消融实验结果。

一些说明:

结果在数据集裁成256的前提下实验,另外实验结果为没有使用后处理,仅对图片进行滑窗处理

里面提到一些PaddleRS中没有的数据增强,简单解释一下。

随机偏移:对两时图片中的有关建筑物的部分,进行横向或者纵向偏移。

有关随机色调变化,榜一大佬觉得是有助于训练的,但我的实验中,在某些情况下是有助于的,但最终的结果还是负提升,所以不仿自己再多做几次实验

数据增强方法 使用网络 F1分数
随机水平翻转+随机垂直翻转 BIT+Resnet18 0.848
随机水平翻转+随机垂直翻转+随机模糊 BIT+Resnet18 0.856
随机水平翻转+随机垂直翻转+随机模糊+随机交换 BIT+Resnet18 0.859
随机水平翻转+随机垂直翻转+随机模糊+随机裁剪拼接 BIT+Resnet18 0.862
随机水平翻转+随机垂直翻转+随机模糊+随机交换+随机裁剪拼接+随机色调变化(参数为默认值) BIT+Resnet18 0.856
随机水平翻转+随机垂直翻转+随机模糊+随机交换+随机裁剪拼接+随机偏移 BIT+Resnet18 0.856
随机水平翻转+随机垂直翻转+随机模糊+随机交换+随机裁剪拼接+CopyPaste BIT+Resnet18 0.854
随机水平翻转+随机垂直翻转+随机模糊+随机色调变化 BIT+Resnet18 0.859
In [ ]
%cd PaddleRS/
import paddlers as pdrs
from paddlers import transforms as T
%cd …

CPU读取的NUM_Workers

NUM_WORKERS = 4

构建需要使用的数据变换(数据增强、预处理)

使用Compose组合多种变换方式。Compose中包含的变换将按顺序串行执行

train_transforms = T.Compose([
# 随机交换
T.RandomSwap(),
# 滤波
T.RandomBlur(),
# 水平翻转
T.RandomHorizontalFlip(),
# 垂直翻转
T.RandomVerticalFlip(),
# 数据归一化
T.Normalize()
])
eval_transforms = T.Compose([
# 验证阶段与训练阶段的数据归一化方式必须相同
T.Normalize()
])
data_dir = ‘./datasets’
train_file_list = ‘./datasets/split_train_list.txt’
val_file_list = ‘./datasets/val_list.txt’

实例化数据集

train_dataset = pdrs.datasets.CDDataset(
data_dir=data_dir,
file_list=train_file_list,
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=val_file_list,
label_list=None,
transforms=eval_transforms,
num_workers=0,
shuffle=False,
binarize_labels=True
)
构建模型并进行训练
这里和Baseline一样,用的也是BIT,因为其他模型比较大,训练其他比较慢,个人也没卡,没时间试。

backbone使用的是Resnet18,这里backbone也不是越强越好,因为这里的backbone都是分类任务的,其性能并不完全等价与这个任务。

因为数据集的不平衡问题,使用了混合Loss训练,使用了CE和Dice loss

训练时的学习策略为Momentum并使用线性的学习率衰减

有关Loss的改进,可以结合我后面的后处理思考。后面的后处理里,会将所有孔洞去除,所以说,其实我们的模型可以更加关注边界,而不是建筑物的内部,所以说如何用Loss来反应上面那段话,也是可以提分的。

相关的论文大家可以参考一下这一篇:RDP-Net在这里插入图片描述
!](https://img-blog.csdnimg.cn/2564931e4c30401da7c76866fac3b24a.png)
有关Loss,本人也是做过消融实验的,具体结果如下。(使用的数据增强仅为随机水平和垂直翻转)

Loss Loss比例 使用网络 F1分数
BCE 1.0 BIT+Resnet18 0.845
BCE+Dice 0.8:0.2 BIT+Resnet18 0.848
BCE+Dice 0.5:0.5 BIT+Resnet18 0.842
OhemBCE+Dice 0.8:0.2 BIT+Resnet18 0.840
有关BIT backbone的选择,个人觉得HRnet是一个优选,有关HRnet的介绍或者说为什么在这个任务上优于其他网络,可以参考下面的博客: HRnet介绍

具体怎么改HRnet,后面如果有空的话,会提PR给RS在这里插入图片描述
另外再提一嘴,这里用HRnet做Backbone也不需要所有层都用,具体的使用方法可以参考原来BIT是怎么使用Resnet的。

PS:下面所用的包括学习率,和数据增强里的一些参数,个人也是没有时间改的,不代表最优,但个人感觉是比较稳定的。

In [ ]
use_mixed_loss = [(‘CrossEntropyLoss’, 0.8), (‘DiceLoss’, 0.2)]

调用PaddleRS API一键构建模型

model = pdrs.tasks.BIT(
# 模型输出类别数
num_classes=2,
# 是否使用混合损失函数,默认使用交叉熵损失函数训练
use_mixed_loss=use_mixed_loss,
# 模型输入通道数
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
)
In [3]
import paddle

训练的epoch数

NUM_EPOCHS = 100

训练学习率

LR = 0.02

每隔多少轮需要保存

SAVE_INTERVAL_EPOCHS = 2

Batchsize 的大小

BATCH_SIZE = 32

保存的路径

EXP_DIR = ‘/home/aistudio/exp/’

线性递减的学习率

lr_scheduler = paddle.optimizer.lr.LambdaDecay(
learning_rate = LR,
lr_lambda = lambda epoch: 1.0 - (epoch) / float(NUM_EPOCHS + 1)
)

构造Momentum优化器

optimizer = paddle.optimizer.Momentum(
learning_rate=lr_scheduler,
weight_decay = 5e-4,
# 在PaddleRS中,可通过ChangeDetector对象的net属性获取paddle.nn.Layer类型组网
parameters=model.net.parameters()
)
In [ ]

调用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=100,
# 保存目录
save_dir=EXP_DIR,
# 是否使用early stopping策略,当精度不再改善时提前终止训练
early_stop=False,
# 是否启用VisualDL日志功能
use_vdl=True,
# 指定从某个检查点继续训练
resume_checkpoint=None
)
模型推理
在推理前,记得重启一下内核,防止显存炸了

这里整个模型推理部分,采用的和Baseline一样的滑窗推理,没有进行改动。

后处理其实也是提分的关键,但这里只是用了比较简单的两个后处理,一是去除过小的点,二是去除图片中的孔洞

去除过小的点主要考虑的是,伪变换一般都较小,属于零碎的点,所以根据此去除掉过小的变化点,可以有效防止伪变化

去除孔洞则是考虑到都是建筑物,内部不应该是有孔洞的

In [4]

定义推理阶段使用的数据集

import paddle

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)

考虑到原始影像尺寸较大,以下类和函数与影像裁块-拼接有关。

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

定义一些辅助函数

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’)
In [5]

重新载入一遍模型

%cd PaddleRS
import paddlers as pdrs
%cd …

调用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
)
/home/aistudio/PaddleRS
/home/aistudio
In [ ]
import os.path as osp
import os
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
from copy import deepcopy

实验路径。实验目录下保存输出的模型权重和结果

EXP_DIR = ‘/home/aistudio/exp/’

裁块大小

CROP_SIZE = 256

模型推理阶段使用的滑窗步长

STRIDE = 64

影像原始大小

ORIGINAL_SIZE = (1024, 1024)

若输出目录不存在,则新建之(递归创建目录)

out_dir = osp.join(EXP_DIR, ‘out’)
if not osp.exists(out_dir):
os.makedirs(out_dir)

选择需要的权重

BEST_CKP_PATH = ‘./exp/best_model/model.pdparams’
#测试集的路径
DATA_DIR = ‘./datasets/test’

为模型加载历史最佳权重

state_dict = paddle.load(BEST_CKP_PATH)

同样通过net属性访问组网对象

model.net.set_state_dict(state_dict)

实例化测试集

test_dataset = InferDataset(
DATA_DIR,
# 注意,测试阶段使用的归一化方式需与训练时相同
# T.FloatTrans()
T.Compose([
T.Normalize()
])
)

创建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
)
In [ ]
from functools import partial
import cv2 as cv
import numpy as np
from skimage import morphology

每次推理载入的Batch_size

INFER_BATCH_SIZE = 32

Minsize 定义最小区域阈值

MIN_SIZE = 400

MinArea 定义最小内面积

MIN_AREA = 50000

推理过程主循环

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):
shape = paddle.shape(t1)
pred = paddle.zeros(shape=(shape[0],2,*shape[2:]))
for i in range(0, shape[0], INFER_BATCH_SIZE):
pred[i:i+INFER_BATCH_SIZE] = model.net(t1[i:i+INFER_BATCH_SIZE], t2[i:i+INFER_BATCH_SIZE])[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.4)
ret, thresh = cv.threshold(out, 127, 255, cv.THRESH_BINARY)
thresh = thresh>1
stage = morphology.remove_small_objects(thresh, min_size=MIN_SIZE, connectivity=2)
stage2 = morphology.remove_small_holes(stage, area_threshold=MIN_AREA, connectivity=1)
stage2 = np.array(stage2, dtype=‘uint8’)
stage2[stage2True]=255
stage2[stage2
False]=0
imsave(osp.join(out_dir, name), stage2, check_contrast=False)

info(“模型推理完成”)
生成比赛文件,并保存权重文件
保存最好的权重文件,还是比较必要的,要不然,下次有可能就没有这么好的结果了。

下载在目录下的submission文件,就可以提交啦

In [ ]

压缩提交文件

!zip -j submission.zip /home/aistudio/exp/out/* > /dev/null
In [ ]

保存最好结果

!cp -r exp/best_model save_model_1
模型融合
上面在数据集划分上也提到了集成,其实集成不同模型的结果对于变化检测来说,是比较有用的,可以有效的去除伪变化,和挖掘出一些没被检测到的建筑物。

所以说在我原先的数据集划分的基础上,进行3次左右的交叉验证,然后集成,分数会提不少哦~

整体思路也很简单,就是把不同预测结果的图片在维度上拼接,然后在维度层面求众数就好了

In [ ]
import cv2 as cv
import os
import numpy as np

result1_root = ‘results_1’
result2_root = ‘results_2’
result3_root = ‘results_3’
final_root = ‘final_output’

file_list = os.listdir(result1_root)
for image_name in file_list:
image_1_path = os.path.join(result1_root, image_name)
image_2_path = os.path.join(result2_root, image_name)
image_3_path = os.path.join(result3_root, image_name)
#
image_1 = cv.imread(image_1_path, cv.IMREAD_GRAYSCALE)
image_2 = cv.imread(image_2_path, cv.IMREAD_GRAYSCALE)
image_3 = cv.imread(image_3_path, cv.IMREAD_GRAYSCALE)
if image_1 is not None:
image_1 = image_1[None]
image_2 = image_2[None]
image_3 = image_3[None]
#
concat_image = np.concatenate((image_1, image_2, image3), axis=0)
sum_image = np.sum(concat_image, axis=0)

    sum_image[np.where(sum_image<500)]=0
    sum_image[np.where(sum_image>500)]=255
    
    final_out_path = os.path.join(final_root, image_name)
    cv.imwrite(final_out_path, sum_image)

In [ ]

压缩提交文件

!zip -j final_submission.zip /home/aistudio/final_output/* > /dev/null

Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐