天池竞赛-地表建筑物识别
Posted 我本逍遥Kert
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了天池竞赛-地表建筑物识别相关的知识,希望对你有一定的参考价值。
目录
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)
模型对每一类交并比,求和再平均的结果。
以上是关于天池竞赛-地表建筑物识别的主要内容,如果未能解决你的问题,请参考以下文章