天池竞赛-地表建筑物识别

Posted 我本逍遥Kert

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了天池竞赛-地表建筑物识别相关的知识,希望对你有一定的参考价值。

目录

1 案例介绍

2 数据预处理

2.1 rle编码转换

2.2 数据扩增

2.3 异常数据的处理

3 自定义数据库类

4 模型训练

5 语义分割的准确率评价方法

3.1 像素准确率(PA)

3.2 类别像素准确率(CPA)

3.3 类别平均像素准确率(MPA)

3.4 交并比(IoU)

3.5  平均交并比(MIoU)


1 案例介绍

遥感技术已成为获取地表覆盖信息最为行之有效的手段,遥感技术已经成功应用于地表覆盖检测、植被面积检测和建筑物检测任务。本赛题使用航拍数据,需要参赛选手完成地表建筑物识别,将地表航拍图像素划分为有建筑物和无建筑物两类。

如下图,左边为原始航拍图,右边为对应的建筑物标注。

本案例训练集为航拍的地标建筑物,训练集图像为30000张图片。其中训练集的标签为rle序列的csv文档。测试集为2500个图像。

2 数据预处理

2.1 rle编码转换

RLE编码是微软开发为AVI格式开发的一种编码。假设一个图像的像素色彩值是这样排列的:红红红红红红红红红红红红蓝蓝蓝蓝蓝蓝绿绿绿绿,经过RLE压缩后就成为了:红12蓝6绿4。这样既保证了压缩的可行性,而且不会有损失。而且可以看到,当颜色数越少时,压缩效率会更高。

在本案例中,我们首先要对rle编码进行读取,将其转换为jpg格式的图片。

官方给出的解码文件可以将rle编码序列转化为一个numpy矩阵。转码函数如下:

我们首先对csv文件进行读取,保存到一个二维数组中。

train_mask = pd.read_csv('../dataset/train_mask.csv/train_mask.csv', sep='\\t', names=['name', 'mask'])
# 读取第一张图,并将对于的rle解码为mask矩阵
img = cv2.imread('../dataset/train/' + train_mask['name'].iloc[0])  # name列的第0行
mask = rle_decode(train_mask['mask'].iloc[0])
print(train_mask.head())

train_mask['name'].lioc[0]:lioc用于提取行数据,整体含义为name列第0行数据

names字段作用:命名csv文件列名

train_mask.head()输出检验列名,我们可以看到csv文件如下:

 

我们通过观察发现,转码后的变量是一个矩阵,我们将矩阵转化为一个二值图,再将其做为标签存放。需要注意的是,矩阵中的值都是0或1,而二值图的8位编码范围为0-255,这样我们在观察标签的时候会看到几乎全黑的情况。所以我们在得到输出后的矩阵,一定要将其乘上255。

要注意的是二值图和灰度图的区别。二值图是一种单通道图像,其矩阵形式只可表现为两个数值;灰度图是一种RGB三通道图像,每个通道的数值相等,它相比于二值图更多的保留了原始图像的信息。

for i in range(30000):
    try:
        train_mask = rle_decode(train_rle['mask'].iloc[i])
        print(type(train_mask))  # 矩阵形式
        train_mask = train_mask * 255
        train_mask = train_mask.astype(np.uint8)
        cv2.imwrite('D:\\\\00Com_TianChi\\\\dataset\\\\train\\\\build_label\\\\' + train_rle['name'].iloc[i], train_mask)
    except:
        pass
        train_mask = np.zeros((512, 512)).astype('uint8')
        train_mask = train_mask * 255
        cv2.imwrite('D:\\\\00Com_TianChi\\\\dataset\\\\train\\\\build_label\\\\' + train_rle['name'].iloc[i], train_mask)

其中将矩阵转为numpy格式并存储成图片的转换函数为astype()。

使用方法为 train_mask = train_mask.astype(np.uint8)

在训练集中有很多异常数据,对于异常数据,我们使用try-except语法来进行处理。

try:正常情况       

except:数据异常情况

2.2 数据扩增

数据扩增是一种有效的正则化方法,可以防止模型过拟合,在深度学习模型的训练过程中应用广泛。数据扩增的目的是增加数据集中样本的数据量,同时也可以有效增加样本的语义空间。

在语义分割领域,我们通常将训练集的图像与标签进行同步的图像变换,这样可以对模型进行有效的训练。

本案例利用albumentations库进行数据扩增。albumentations是基于OpenCV的快速训练数据增强库,拥有非常简单且强大的可以用于多种任务(分割、检测)的接口,易于定制且添加其他框架非常方便。

# ---------------数据扩增部分---------------
    aug_data = 'D:\\\\00Com_TianChi\\\\dataset\\\\train_aug\\\\'
    image_build_aug = "build_image_aug"
    label_build_aug = "build_label_aug"
    # 扩增img和扩增label的路径
    image_build_aug_path = os.path.join(aug_data, image_build_aug)
    label_build_aug_path = os.path.join(aug_data, label_build_aug)

        # 原始图像的名称 build_dataset.image_list[0] build_dataset.label_list[0]

    # 路径测试
    # print(os.path.join(root_dir, image_build,  build_dataset.image_list[0]))

    # print( os.path.join(image_build_aug_path,  'scale' + build_dataset.image_list[0]))

    for i in range(0, 5):
        print(i)
        # 将 原始图像和原始标签路径 放入函数 得到路径
        img_path = os.path.join(root_dir, image_build,  build_dataset.image_list[i])
        label_path = os.path.join(root_dir, label_build, build_dataset.label_list[i])
        # 根据路径加载图片 转为np类
        trans_img = np.asarray(Image.open(img_path))
        trans_label = np.asarray(Image.open(label_path))


        # 水平翻转操作
        augments = aug.HorizontalFlip(p=1)(image=trans_img, mask=trans_label)
        img_aug_hor, mask_aug_hor = augments['image'], augments['mask']
        # 随即裁剪操作
        augments = aug.RandomCrop(p=1, height=256, width=256)(image=trans_img, mask=trans_label)
        img_aug_ran, mask_aug_ran = augments['image'], augments['mask']
        # 旋转操作
        augments = aug.ShiftScaleRotate(p=1)(image=trans_img, mask=trans_label)
        img_aug_rot, mask_aug_rot = augments['image'], augments['mask']
        # 复合操作
        trfm = aug.Compose([
            aug.Resize(256, 256),
            aug.HorizontalFlip(p=0.5),
            aug.VerticalFlip(p=0.5),
            aug.RandomRotate90(),
        ])
        augments = trfm(image=trans_img, mask=trans_label)
        img_aug_mix, mask_aug_mix = augments['image'], augments['mask']


        # 保存路径 变换后的文件名
        # 水平翻转
        save_hor_path_img = os.path.join(image_build_aug_path,  'hor' + build_dataset.image_list[i])
        save_hor_path_label = os.path.join(label_build_aug_path, 'hor' + build_dataset.label_list[i])
        cv2.imwrite(save_hor_path_img, img_aug_hor)
        cv2.imwrite(save_hor_path_label, mask_aug_hor)
        # 随即裁剪
        save_ran_path_img = os.path.join(image_build_aug_path, 'ran' + build_dataset.image_list[i])
        save_ran_path_label = os.path.join(label_build_aug_path, 'ran' + build_dataset.label_list[i])
        cv2.imwrite(save_ran_path_img, img_aug_ran)
        cv2.imwrite(save_ran_path_label, mask_aug_ran)
        # 旋转操作
        save_rot_path_img = os.path.join(image_build_aug_path, 'rot' + build_dataset.image_list[i])
        save_rot_path_label = os.path.join(label_build_aug_path, 'rot' + build_dataset.label_list[i])
        cv2.imwrite(save_rot_path_img, img_aug_rot)
        cv2.imwrite(save_rot_path_label, mask_aug_rot)
        # 复合操作
        save_mix_path_img = os.path.join(image_build_aug_path, 'rot' + build_dataset.image_list[i])
        save_mix_path_label = os.path.join(label_build_aug_path, 'rot' + build_dataset.label_list[i])
        cv2.imwrite(save_mix_path_img, img_aug_mix)
        cv2.imwrite(save_mix_path_label, mask_aug_mix)

2.3 异常数据的处理

在rle转mask编码的处理中,我们将异常rle数据转换成全黑图片处理。可是在后面的训练中发现,损失函数的振荡较大,于是考虑将异常数据全部剔除,再次训练函数观察损失函数的变化。(待更

3 自定义数据库类

在数据预处理后,我们进行数据库类的定义。在每次进行模型训练前,我们要将训练集的数据输入给一个类中,这样能够使我们清晰地有条理地利用好我们的训练集数据。本案例的数据库类定义如下。

class MyData(Dataset):
    def __init__(self, root_dir, image_dir, label_dir, transform):
        self.root_dir = root_dir
        self.image_dir = image_dir
        self.label_dir = label_dir
        self.label_path = os.path.join(self.root_dir, self.label_dir)
        self.image_path = os.path.join(self.root_dir, self.image_dir)
        self.image_list = os.listdir(self.image_path)
        self.label_list = os.listdir(self.label_path)
        self.transform = transform
        # 因为label 和 Image文件名相同,进行一样的排序,可以保证取出的数据和label是一一对应的
        self.image_list.sort()
        self.label_list.sort()

    def __getitem__(self, idx):
        img_name = self.image_list[idx]
        label_name = self.label_list[idx]
        img_item_path = os.path.join(self.root_dir, self.image_dir, img_name)
        label_item_path = os.path.join(self.root_dir, self.label_dir, label_name)
        img = Image.open(img_item_path)
        label = Image.open(label_item_path)
        # label = self.label_dir
        trans_tensor = transforms.ToTensor()
        img = trans_tensor(img)  # 将图片变为tensor格式
        label = trans_tensor(label)
        return img, label
        # with open(label_item_path, 'r') as f:
        #     label = f.readline()
        #
        # # img = np.array(img)
        # img = self.transform(img)
        # sample = {'img': img, 'label': label}
        # return sample

    def __len__(self):
        assert len(self.image_list) == len(self.label_list)
        return len(self.image_list)

函数有四个输入变量:

  • root_dir:为数据集根目录
  • train_dir:为训练集目录
  • text_dir:为测试集目录
  • transform:为对数据集做的transform

我们利用os对路径进行整合,这一部分有很多实用的数据转换代码,在这里小结一下。

存图片

cv2.imwrite('D:\\\\00Com_TianChi\\\\dataset\\\\train\\\\build_label\\\\' + train_rle['name'].iloc[i], train_mask)

加载一张图片:

# 加载后图片格式为PIL.JpegImagePlugin.JpegImageFile
img = Image.open(img_item_path)

将PIL.JpegImagePlugin.JpegImageFile类型转为数组

# 转换后变量的数据类型为np型
img = np.asarray(img)

将数组转为torch.Tensor类:

img = torch.tensor(img)

注意transforms.ToTensor和torch.Tensor的区别:

  • transforms.ToTensor:可以将np或PIL类型的图片转为tensor型,但是转换的同时也会将其归一化,因为transform封装的函数中将tensor型变量中的每个量设置的范围为[0,1]。且图片的tensor数据排列顺序为通道数在前,用一个512×512的RGB图片举例:torch.Size([3, 512, 512])。
  • torch.Tensor:这个函数和transforms.ToTensor的功能类似,但是没有将张量归一化。且图片的tensor数据排列顺序为通道数在后,用一个512×512的RGB图片举例:torch.Size([512, 512, 3])。

下面对数据库类实例化。

# 定义训练集
transform = transforms.Compose([transforms.Resize((512, 512)), transforms.ToTensor()])
root_dir = "D:/00Com_TianChi/dataset/train/"
image_build = "build_image"
label_build = "build_label"
build_dataset = MyData(root_dir, image_build, label_build, transform=transform)
# 定义测试集
test_dir = "D:/00Com_TianChi/dataset/test/"
image_build_test = "img"
label_build_test = "label"
test_dataset = MyData(test_dir, image_build_test, label_build_test, transform=transform)

在对数据库类进行实例化后,我们定义模型的data_loader,批处理量定位8。

train_dataloader = DataLoader(build_dataset, batch_size=8, shuffle=True, num_workers=4)

4 模型训练

待更

5 语义分割的准确率评价方法

在语言分割的评价方法中,我们主要利用混淆矩阵对模型准确率进行评价。在前几期的博客中已经对混淆矩阵进行了介绍,我们再次来回顾一下混淆矩阵的概念,并尝试从语义分割领域对混淆军阵进行新的理解。

我们已经了解到,经模型输出后图像能够根据预测的结果分为不同的mask,每一类mask就是模型输出的某一个类别,或者也可以成为某一个通道。当我们对背景感兴趣时,图(b)中真实值=1的情况则为全部的背景,即图中清晰部分;模型输出中预测值=1的部分为正确的预测,即图中紫色部分;模型输出中预测值=0的部分为错误的预测,即黄色的部分。

当我们对人物感兴趣时也是同理,图(c)中真实值=1时,则为我们感兴趣的部分,即人物;当预测值=1时,则为预测正确的部分,这张图恰巧精确度很高,图中黄紫蓝组成的颜色则为预测值=1时的情况。

那么问题来了——当真实值=0时,该是哪个区域呢?当我们对于人物感兴趣时,真实值=1为人物,那么真实值=0时则为人物以外的区域,则为背景区域。图(d)中当真实值=0,预测值=1时,则为黑色线条圈出来的部分,通俗的讲可以理解为:本该预测成背景,可是预测错了。

3.1 像素准确率(PA)

  • 预测类别正确的像素数占总像素数的比例
  • PA = (TP + TN) / (TP + TN + FP + FN) 

3.2 类别像素准确率(CPA)

在类别 i 的预测值中,真实属于 i 类的像素准确率,换言之:模型对类别 i 的预测值有很多,其中有对有错,预测对的值占预测总值的比例。

P1 = TP / (TP + FP)

3.3 类别平均像素准确率(MPA)

分别计算每个类被正确分类像素数的比例,即:CPA,然后累加求平均

  • 每个类别像素准确率为:Pi(计算:对角线值 / 对应列的像素总数)
  • MPA = sum(Pi) / 类别数

3.4 交并比(IoU)

  • 模型对某一类别预测结果和真实值的交集与并集的比值
  • 混淆矩阵计算:
    • IoU = TP / (TP + FP + FN)

3.5  平均交并比(MIoU)

模型对每一类交并比,求和再平均的结果。

以上是关于天池竞赛-地表建筑物识别的主要内容,如果未能解决你的问题,请参考以下文章

阿里云天池 学习赛汇总(教学赛,零基础入门,长期赛)

图像分割库segmentation_models.pytorch和Albumentations 实现图像分割

HERE-API识别建筑物的地板/地平面

协同过滤算法(天池竞赛试题)

地表车神争霸赛,且看第16届大学生智能汽车竞赛

阿里天池竞赛记录