相关链接地址:
(一)1 赛题理解
- 赛题名称:零基础入门语义分割-地表建筑物识别
- 赛题目标:通过本次赛题可以引导大家熟练掌握语义分割任务的定义,具体的解题流程和相应的模型,并掌握语义分割任务的发展。
- 赛题任务:赛题以计算机视觉为背景,要求选手使用给定的航拍图像训练模型并完成地表建筑物识别任务。
1.1 学习目标
- 理解赛题背景和赛题数据
- 完成赛题报名和数据下载,理解赛题的解题思路
1.2 赛题数据
遥感技术已成为获取地表覆盖信息最为行之有效的手段,遥感技术已经成功应用于地表覆盖检测、植被面积检测和建筑物检测任务。本赛题使用航拍数据,需要参赛选手完成地表建筑物识别,将地表航拍图像素划分为有建筑物和无建筑物两类。
如下图,左边为原始航拍图,右边为对应的建筑物标注。
赛题数据来源(Inria Aerial Image Labeling),并进行拆分处理。数据集报名后可见并可下载。赛题数据为航拍图,需要参赛选手识别图片中的地表建筑具体像素位置。
1.3 数据标签
赛题为语义分割任务,因此具体的标签为图像像素类别。在赛题数据中像素属于2类(无建筑物和有建筑物),因此标签为有建筑物的像素。赛题原始图片为jpg格式,标签为RLE编码的字符串。
RLE全称(run-length encoding),翻译为游程编码或行程长度编码,对连续的黑、白像素数以不同的码字进行编码。RLE是一种简单的非破坏性资料压缩法,经常用在在语义分割比赛中对标签进行编码。
RLE与图片之间的转换如下:
import numpy as np import pandas as pd import cv2 # 将图片编码为rle格式 def rle_encode(im): ''' im: numpy array, 1 - mask, 0 - background Returns run length as string formated ''' pixels = im.flatten(order = 'F') pixels = np.concatenate([[0], pixels, [0]]) runs = np.where(pixels[1:] != pixels[:-1])[0] + 1 runs[1::2] -= runs[::2] return ' '.join(str(x) for x in runs) # 将rle格式进行解码为图片 def rle_decode(mask_rle, shape=(512, 512)): ''' mask_rle: run-length as string formated (start length) shape: (height,width) of array to return Returns numpy array, 1 - mask, 0 - background ''' s = mask_rle.split() starts, lengths = [np.asarray(x, dtype=int) for x in (s[0:][::2], s[1:][::2])] starts -= 1 ends = starts + lengths img = np.zeros(shape[0]*shape[1], dtype=np.uint8) for lo, hi in zip(starts, ends): img[lo:hi] = 1 return img.reshape(shape, order='F')
1.4 评价指标
赛题使用Dice coefficient来衡量选手结果与真实标签的差异性,Dice coefficient可以按像素差异性来比较结果的差异性。Dice coefficient的具体计算方式如下:
2 ∗ ∣ X ∩ Y ∣ ∣ X ∣ + ∣ Y ∣ \frac{2 * |X \cap Y|}{|X| + |Y|}∣X∣+∣Y∣2∗∣X∩Y∣
其中X XX是预测结果,Y YY为真实标签的结果。当X XX与Y YY完全相同时Dice coefficient为1,排行榜使用所有测试集图片的平均Dice coefficient来衡量,分数值越大越好。
1.5 读取数据
FileName | Size | 含义 |
test_a.zip | 314.49MB | 测试集A榜图片 |
test_a_samplesubmit.csv | 46.39KB | 测试集A榜提交样例 |
train.zip | 3.68GB | 训练集图片 |
train_mask.csv.zip | 97.52MB | 训练集图片标注 |
具体数据读取案例:
import pandas as pd import cv2 train_mask = pd.read_csv('train_mask.csv', sep='\t', names=['name', 'mask']) # 读取第一张图,并将对于的rle解码为mask矩阵 img = cv2.imread('train/'+ train_mask['name'].iloc[0]) mask = rle_decode(train_mask['mask'].iloc[0]) print(rle_encode(mask) == train_mask['mask'].iloc[0]) # 结果为True
1.6 解题思路
由于本次赛题是一个典型的语义分割任务,因此可以直接使用语义分割的模型来完成:
- 步骤1:使用FCN模型模型跑通具体模型训练过程,并对结果进行预测提交;
- 步骤2:在现有基础上加入数据扩增方法,并划分验证集以监督模型精度;
- 步骤3:使用更加强大模型结构(如Unet和PSPNet)或尺寸更大的输入完成训练;
- 步骤4:训练多个模型完成模型集成操作;
1.7 本章小结
本章主要对赛题背景和主要任务进行讲解,并多对赛题数据和标注读取方式进行介绍,最后列举了赛题解题思路。
1.8 课后作业
- 理解RLE编码过程,并完成赛题数据读取并可视化;
- 统计所有图片整图中没有任何建筑物像素占所有训练集图片的比例;
- 统计所有图片中建筑物像素占所有像素的比例;
- 统计所有图片中建筑物区域平均区域大小;
(二)baseline代码分析
Ⅰ.将图片编码为rle格式
import numpy as np import pandas as pd import cv2 # 将图片编码为rle格式 def rle_encode(im): ''' im: numpy array, 1 - mask, 0 - background Returns run length as string formated ''' pixels = im.flatten(order = 'F') pixels = np.concatenate([[0], pixels, [0]]) runs = np.where(pixels[1:] != pixels[:-1])[0] + 1 runs[1::2] -= runs[::2] return ' '.join(str(x) for x in runs)
1.输入的im
为二值图像,pixels = im.flatten(order = 'F')
是对二维数组展平成一维,order=‘F'
代表按列展平。
得到的结果如下(例子):
2.pixels = np.concatenate([[0], pixels, [0]])
两端补0,是为了后续的错位比较。
3.runs = np.where(pixels[1:] != pixels[:-1])[0] + 1
看到这一步,也许就知道为什么要两端补0了,不然没法比较。
于是得到 0→1变化或者1→0变化的地方的索引了。最后一步转换为从1开始的索引。
4.runs[1::2] -= runs[::2]
当然(索引+个数)只对1进行操作,对0来说没必要了。
5.return ' '.join(str(x) for x in runs)
最后返回的是一个长度为12的字符串,每两个数之间用空格隔开(第一个数字前也要加空格)
Ⅱ.将rle格式进行解码为图片
# 将rle格式进行解码为图片 def rle_decode(mask_rle, shape=(512, 512)): ''' mask_rle: run-length as string formated (start length) shape: (height,width) of array to return Returns numpy array, 1 - mask, 0 - background ''' s = mask_rle.split() starts, lengths = [np.asarray(x, dtype=int) for x in (s[0:][::2], s[1:][::2])] starts -= 1 ends = starts + lengths img = np.zeros(shape[0]*shape[1], dtype=np.uint8) for lo, hi in zip(starts, ends): img[lo:hi] = 1 return img.reshape(shape, order='F')
RLE编码的时候返回的时候每两个数字有空格为间隔,利用s = mask_rle.split()
将空格去掉。
s[0:][::2]
表示(从1开始的)索引,s[1:][::2]
表示个数。于是starts
存的是索引,lengths
存的是个数,两者为一一对应关系。
starts -= 1
转化为(从0开始的)索引。
后续就是创建一副全0的一维序列,填充1,再按列排序,转为二维的二值图,就解码成图片了。
如果输入的mask_rle是空的,那么返回的就是全为0的mask,可以观察数据发现,部分图片的地表建筑不存在,他们的rle标签也就是空的。
Ⅲ.定义数据集
class TianChiDataset(D.Dataset): def __init__(self, paths, rles, transform, test_mode=False): self.paths = paths self.rles = rles self.transform = transform self.test_mode = test_mode self.len = len(paths) self.as_tensor = T.Compose([ T.ToPILImage(), T.Resize(IMAGE_SIZE), T.ToTensor(), T.Normalize([0.625, 0.448, 0.688], [0.131, 0.177, 0.101]), ]) # get data operation def __getitem__(self, index): #img = cv2.imread(self.paths[index]) img = np.array(Image.open(self.paths[index])) if not self.test_mode: mask = rle_decode(self.rles[index]) augments = self.transform(image=img, mask=mask) return self.as_tensor(augments['image']), augments['mask'][None]#(3,256,256),(1,256,256) else: return self.as_tensor(img), '' def __len__(self): """ Total number of samples in the dataset """ return self.len
定义数据集,主要作了数据的预处理
其中,我将opencv的读取图片换成了PIL读取,因为路径中包含中文
augments['mask'][None]
中的[None]
,将(256,256)的mask形状转为(1,256,256),起到升维作用
Ⅳ.可视化一下效果
这一步主要是为了验证上述的代码
用了rle_encode(rle_decode(RLE标签))==RLE标签
来验证之前写的RLE编码和解码正确性。
train_mask = pd.read_csv('数据集/train_mask.csv', sep='\t', names=['name', 'mask']) train_mask['name'] = train_mask['name'].apply(lambda x: '数据集/train/' + x) img = cv2.imread(train_mask['name'].iloc[0]) mask = rle_decode(train_mask['mask'].iloc[0]) print(rle_encode(mask) == train_mask['mask'].iloc[0])
train_mask['name'].apply(lambda x: '数据集/train/' + x)
这一步就是在图片前补全下路径
0 KWP8J3TRSV.jpg 1 DKI3X4VFD3.jpg 2 AYPOE51XNI.jpg 3 1D9V7N0DGF.jpg 4 AWXXR4VYRI.jpg
0 数据集/train/KWP8J3TRSV.jpg 1 数据集/train/DKI3X4VFD3.jpg 2 数据集/train/AYPOE51XNI.jpg 3 数据集/train/1D9V7N0DGF.jpg 4 数据集/train/AWXXR4VYRI.jpg
实例化数据集
dataset = TianChiDataset( train_mask['name'].values, train_mask['mask'].fillna('').values, trfm, False )
fillna('')
起到补全缺失值为''
的作用
可视化
image, mask = dataset[0] plt.figure(figsize=(16,8)) plt.subplot(121) plt.imshow(mask[0], cmap='gray') plt.subplot(122) plt.imshow(image[0]) plt.show()# 补上
看一下第二张图片
image, mask = dataset[1]
没有建筑物,mask全黑。
Ⅴ.加载数据集
#定义数据集 train_mask = pd.read_csv('数据集/train_mask.csv', sep='\t', names=['name', 'mask']) train_mask['name'] = train_mask['name'].apply(lambda x: '数据集/train/' + x) dataset = TianChiDataset( train_mask['name'].values, train_mask['mask'].fillna('').values, trfm, False ) #划分数据集(按index手动去划分) valid_idx, train_idx = [], [] for i in range(len(dataset)): if i % 7 == 0: valid_idx.append(i) else: # elif i % 7 == 1: train_idx.append(i) train_ds = D.Subset(dataset, train_idx) valid_ds = D.Subset(dataset, valid_idx) # print(len(dataset))#30000 # print(len(train_ds))#4286 # print(len(valid_ds))#4286 # define training and validation data loaders loader = D.DataLoader( train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=0) vloader = D.DataLoader( valid_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=0)
D.subset
是按照索引序列来划分数据集的, 于是按照每7个数据里面,1个当作验证集,6个当作训练集。最后放入数据加载器中。
Ⅵ.定义模型、优化器、损失函数
# 定义模型 model = get_model() model.to(DEVICE) #model.load_state_dict(torch.load("model_best.pth")) #定义优化器 optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-3) #定义损失函数 bce_fn = nn.BCEWithLogitsLoss() dice_fn = SoftDiceLoss() def loss_fn(y_pred, y_true): bce = bce_fn(y_pred, y_true) dice = dice_fn(y_pred.sigmoid(), y_true) return 0.8 * bce + 0.2 * dice
Ⅶ.进行训练
header = r''' Train | Valid Epoch | Loss | Loss | Time, m ''' # Epoch metrics time raw_line = '{:6d}' + '\u2502{:7.3f}' * 2 + '\u2502{:6.2f}' print(header) EPOCHES = 10 best_loss = 10 for epoch in range(1, EPOCHES + 1): losses = [] start_time = time.time() model.train() for image, target in tqdm(loader):#取消了tqdm image, target = image.to(DEVICE), target.float().to(DEVICE) optimizer.zero_grad() output = model(image)['out'] loss = loss_fn(output, target) loss.backward() optimizer.step() losses.append(loss.item()) # print(loss.item()) vloss = validation(model, vloader, loss_fn) print(raw_line.format(epoch, np.array(losses).mean(), vloss, (time.time() - start_time) / 60 ** 1)) losses = [] if vloss < best_loss: best_loss = vloss torch.save(model.state_dict(), 'model_best.pth') print("save successful!")
Ⅷ.使用模型对测试集进行预测
trfm = T.Compose([ T.ToPILImage(), T.Resize(IMAGE_SIZE), T.ToTensor(), T.Normalize([0.625, 0.448, 0.688], [0.131, 0.177, 0.101]), ]) subm = [] model.load_state_dict(torch.load("./model_best.pth")) model.eval() test_mask = pd.read_csv('数据集/test_a_samplesubmit.csv', sep='\t', names=['name', 'mask']) test_mask['name'] = test_mask['name'].apply(lambda x: '数据集/test_a/' + x) for idx, name in enumerate(tqdm(test_mask['name'].iloc[:])): image = np.array(Image.open(name))#改成PIL image = trfm(image) with torch.no_grad(): image = image.to(DEVICE)[None] score = model(image)['out'][0][0] score_sigmoid = score.sigmoid().cpu().numpy() score_sigmoid = (score_sigmoid > 0.5).astype(np.uint8) score_sigmoid = cv2.resize(score_sigmoid, (512, 512)) # break subm.append([name.split('/')[-1], rle_encode(score_sigmoid)]) subm = pd.DataFrame(subm) subm.to_csv('./tmp.csv', index=None, header=None, sep='\t')
Ⅸ.可视化模型预测结果
from file1 import rle_decode from PIL import Image import pandas as pd import numpy as np subm = pd.read_csv("./tmp.csv",sep="\t",names=["name","mask"]) def show_predict_pic(num=0): plt.figure(figsize=(16,8)) plt.subplot(121) plt.imshow(rle_decode(subm.fillna('').iloc[num,1]), cmap='gray') plt.subplot(122) plt.imshow(np.array(Image.open('数据集/test_a/' + subm.iloc[num,0]))) plt.show() if __name__ == '__main__': show_predict_pic(num=10)
查看第10张图片的预测结果
(三)作业解答
1. 理解RLE编码过程,并完成赛题数据读取并可视化:
已在baseline中说明。
2. 统计所有图片整图中没有任何建筑物像素占所有训练集图片的比例:
import pandas as pd train_mask = pd.read_csv("数据集/train_mask.csv",sep="\t",names=["name","mask"]) train_mask["mask"]=train_mask["mask"].fillna("") l = len(train_mask) sum=0 for i in range(l): if train_mask["mask"].iloc[i]=="": sum+=1 print(sum/l)
得到输出的结果(没有任何建筑物像素占所有训练集图片的比例)
0.17346666666666666
30000张图中有5204张图是没有任何建筑的
3. 统计所有图片中建筑物像素占所有像素的比例
import pandas as pd import numpy as np from PIL import Image from tqdm import tqdm train_mask = pd.read_csv("数据集/train_mask.csv",sep="\t",names=["name","mask"]) train_mask["mask"]=train_mask["mask"].fillna("") l = len(train_mask) ratio_ls = [] for i in tqdm(range(l)): if train_mask["mask"].iloc[i]!="": ls = list(map(int,train_mask["mask"].iloc[i].split(" "))) number = sum(ls[1::2]) pic_path = "数据集/"+"train/"+train_mask["name"].iloc[i] img = np.array(Image.open(pic_path)) ratio = number/(img.shape[0]*img.shape[1]) else: ratio = 0 ratio_ls.append(ratio) pd.Series(ratio_ls).to_csv("ratio_ls")
将每张图片的建筑物像素占所有像素的比例存入到列表中,并保存下来。
ratio = pd.read_csv("ratio_ls") print("所有图片中建筑像素平均占比:",np.mean(ratio.iloc[:,1])) ratio_ = ratio.iloc[:,1][(ratio.iloc[:,1])!=0] print("有建筑图片中建筑像素平均占比:",np.mean(ratio_)) ratio = np.array(ratio)[:,1] print("建筑像素占比最大值",np.max(ratio)) print("有建筑图片中,建筑像素占比最小值",np.min(ratio_))
得到
所有图片中建筑像素平均占比: 0.15708140207926433 有建筑图片中建筑像素平均占比: 0.19004847807621914 建筑像素占比最大值 0.9992218017578124 有建筑图片中,建筑像素占比最小值 3.814697265625e-06
可以发现,有建筑图片中,建筑像素平均占比为0.2。
但是发现一张,像素占比比较大的图片,于是将它可视化一下。
标签认为它全图都是建筑,虽然很可能是屋顶上建了一些建筑,于是整个图都是建筑。
但是大多数图片并不是这样的,就算是人眼也很难识别出,这是在一个大建筑的屋顶上。
如果能把这样的异常数据给舍弃掉,然后拿剩下的数据去训练,可能会好很多。
4. 统计所有图片中建筑物区域平均区域大小;
区域大小,也就是白色像素点数量和。
import pandas as pd import numpy as np from PIL import Image from tqdm import tqdm train_mask = pd.read_csv("数据集/train_mask.csv",sep="\t",names=["name","mask"]) train_mask["mask"]=train_mask["mask"].fillna("") l = len(train_mask) sum_ls = [] for i in tqdm(range(l)): if train_mask["mask"].iloc[i]!="": ls = list(map(int,train_mask["mask"].iloc[i].split(" "))) number = sum(ls[1::2]) # pic_path = "数据集/"+"train/"+train_mask["name"].iloc[i] # img = np.array(Image.open(pic_path)) # ratio = number/(img.shape[0]*img.shape[1]) else: number = 0 sum_ls.append(number) pd.Series(sum_ls).to_csv("point_sum_ls")
ls = pd.read_csv("point_sum_ls") print(np.mean(ls.iloc[:,1])) ls_ = ls[(ls.iloc[:,1])!=0] print(np.mean(ls_.iloc[:,1]))
得到结果
所有图中建筑物区域平均区域大小 41177.94706666667 含有建筑物的图中建筑物区域平均区域大小 49820.06823681239