1、项目背景
俄乌冲突以来,国际粮价进一步抬升并盘旋高位。印度等国实施出口禁令,加剧国际粮荒预期。

在这种大环境下,农业更是重中之重,国之根本,但侵占基本农田的案例时有发生,我国土地面积辽阔,对农业用地非法侵占行为的识别需要耗费大量的时间和人力物力。

利用深度学习的语义分割,帮助政府管理人员识别和统计农业用地地块,大大减少人工工作量。在这里插入图片描述
2、前期准备
2.1、安装PaddleSeg
In [ ]
! pip install paddlex==2.0.0
2.2、 解压数据集
数据集介绍:@InProceedings{DeepGlobe18, author = {Demir, Ilke and Koperski, Krzysztof and Lindenbaum, David and Pang, Guan and Huang, Jing and Basu, Saikat and Hughes, Forest and Tuia, Devis and Raskar, Ramesh}, title = {DeepGlobe 2018: A Challenge to Parse the Earth Through Satellite Images}, booktitle = {The IEEE Conference on Computer Vision and Pattern Recognition (CVPR) Workshops}, month = {June}, year = {2018} }

In [ ]
!unzip data/data145161/DeepGlobeLandCoverClassificationDataset.zip -d ./Source/
3、基本原理
对于当前十分流行的实时分割场景,例如短视频领域、直播领域,都需要实时分割算法,这就需要我们的算法足够轻量。那么一个好的分割模型网络是什么样子的呢? 首先我们来看下常见的分割网络是什么样的结构。 一个分割网络的输出是一个N×H×WN\times H\times WN×H×W的概率图,其中N是一个向量,向量的维数表示为类别的数量,也就是说向量的每个元素对应一个类别,而这个元素的数值是概率值,表示为对应该类别的概率。因为要给出每一个像素类别,所以H×WH\times WH×W大小需要与原图保持一致。分割网络可以看作是一个没有全连接层的全卷积网络,常见的网络结构可以分为Encoder和Decoder两个部分:

Encoder部分特征图的长宽会逐步缩小,也就是需要下采样。
Decoder部分特征图的长宽会逐步增大,也就是需要上采样。
一个好的分割网络需要解决哪些问题?第一个问题就是如何通过较少Encoder层数来获得较大的感受野。首先来看下什么是感受野。输出特征图上每个点的数值,是由输入图片上大小为kh×kwk_h\times k_wk
h

×k
w

的区域的元素与卷积核每个元素相乘再相加得到的,所以输入图像上kh×kwk_h\times k_wk
h

×k
w

区域内每个元素数值的改变,都会影响输出点的像素值。我们将这个区域叫做输出特征图上对应点的感受野。感受野内每个像素数值的变动,都会影响输出点的数值变化。因此感受野也可以表示为特征图上每个点所受图像区域范围影响的大小。感受野的大小是分割网络的特征表示能力的一个重要评价指标,因为我们希望尽可能提高感受野的大小。最直接的方法就是提高Encoder网络的层数,但是这种方法会在扩大感受野的同时,带来一些弊端,例如增加了网络参数规模,导致训练耗时增加。那么有没有更好的方法呢?

通过扩大卷积核可触达的区域来增大感受野,扩大卷积所包含的信息范围。如下图所示,该图为空洞卷积作用的示意图示意图。可以看到未使用空洞卷积的网络,一个输出的深蓝色的点可以覆盖5个输入像素点。在使用空洞卷积之后,一个输出点可以覆盖9个输入像素的点。其具体应用示例可以参见下面DeepLabv3+的网络结构。在这里插入图片描述
分割网络需要面临的第二个问题是下采样过程中特征的长和宽在不断减少,我们虽然得到了高阶特征,但是维度较小,如果直接作为上采样输入,则有可能丢失一部分信息,怎么解决这个问题呢?这时可以使用跳跃连接功能。通过跳跃连接,使上采样过程不仅使用高阶特征,也可以使用同维度的低阶特征,具体使用示例可以参见基本原理中的U-Net模型介绍。

除了上述两个问题之外,分割网络还需要解决多尺度问题,多尺度问题是指同一个图片中目标有大有小,如何保证不同尺度的目标均能被很好的处理呢?这里可以使用ASPP网络结构,通俗的讲,其实现思想是让不同尺度的目标交给不同感受野的卷积层来处理,大感受野处理大目标,小感受野处理小目标。其具体应用示例可以参见下面DeepLabv3+的网络结构。

3.1、DeepLabv3+
DeepLabv3+是DeepLab系列的最后一篇文章,其前作有DeepLabv1、DeepLabv2和DeepLabv3。在最新作中,作者结合编码器-解码器(encoder-decoder)结构和空间金字塔池化模块(Spatial Pyramid Pooling, SPP)的优点提出新的语义分割网络DeepLabv3+,在 PASCAL VOC 2012和Cityscapes数据集上取得新的state-of-art performance.
其整体结构如下所示,Encoder的主体是带有空洞卷积(Atrous Convolution)的骨干网络,骨干网络可采用ResNet等常用的分类网络,作者使用了改进的Xception模型作为骨干网络。紧跟其后的空洞空间金字塔池化模块(Atrous Spatial Pyramid Pooling, ASPP)则引入了多尺度信息。相比前作DeepLabv3,DeepLabv3+加入decoder模块,将浅层特征和深层特征进一步融合,优化分割效果,尤其是目标边缘的效果。此外,作者将深度可分离卷积(Depthwise Separable Convolution)应用到ASPP和Decoder模块,提高了语义分割的健壮性和运行速率。在这里插入图片描述
具体原理细节请参考Encoder-Decoder with Atrous Separable Convolution for Semantic Image Segmentation。

4、预处理
4.1. 导入相关库
In [ ]
import paddlex as pdx
from numba import jit
import multiprocessing
from multiprocessing import Manager
import os
from PIL import Image
import numpy as np
import cv2
import pandas as pd
import numpy as numpy
from tqdm import trange
import matplotlib.pyplot as plt
4.2 样本均衡分析
In [11]
classpd = pd.read_csv(‘./Source/class_dict.csv’)
classpd
name r g b
0 urban_land 0 255 255
1 agriculture_land 255 255 0
2 rangeland 255 0 255
3 forest_land 0 255 0
4 water 0 0 255
5 barren_land 255 255 255
6 unknown 0 0 0
In [12]
d = dict() # 存储各标签数量
def countlabel(img_path):
label= Image.open(img_path)
label = np.asarray(label)
img= np.zeros((label.shape[0],label.shape[1]),np.uint8)
label.shape
for i in range(len(classpd)):
row = classpd.loc[i,:]
r1,g1,b1 = row.r,row.g,row.b
red,green,blue = label[:,:,0], label[:,:,1], label[:,:,2]
mask = (redr1) & (greeng1) & (blue==b1)
if(row[‘name’] not in d.keys()):
d[row[‘name’]]=0
d[row[‘name’]]+=np.sum(mask)
In [13]
data_all = pd.read_csv(‘Source/metadata.csv’)
data_all.head()
image_id split sat_image_path mask_path
0 100694 train train/100694_sat.jpg train/100694_mask.png
1 102122 train train/102122_sat.jpg train/102122_mask.png
2 10233 train train/10233_sat.jpg train/10233_mask.png
3 103665 train train/103665_sat.jpg train/103665_mask.png
4 103730 train train/103730_sat.jpg train/103730_mask.png
In [14]
mask_ = pd.notna(data_all.mask_path)
data_notna = data_all[mask_]
ddata_mask_path = data_notna.mask_path
In [ ]

for i in trange(len(ddata_mask_path),desc='Progress ',leave=False):
countlabel(“Source/”+ddata_mask_path[i])
In [7]
plt.figure(figsize=(10, 10), dpi=100)
plt.pie(d.values(),labels=d.keys(), autopct=“%1.2f%%”, colors=[‘c’, ‘m’, ‘y’],
textprops={‘fontsize’: 24}, labeldistance=1.05)
plt.legend(fontsize=16)
plt.title(‘Label Count’)

plt.show() # 有时候运行不显示,不知道为什么,故采用下面方式

plt.savefig(‘./LabelCount.png’)
Image.open(“./LabelCount.png”).resize((500,500), Image.ANTIALIAS)在这里插入图片描述
4.3、生成模型需要的标签
因标注图像的标签从0,1依次取值,不可间隔。若有需要忽略的像素,则按255进行标注。

我们这里通过Numpy的逻辑判断,提取对应不同通道值的类别,再生成由从0开始,这样的标签图片,满足模型训练要求。

In [ ]
def imageconvlabel(img_path):
label= Image.open(img_path)
label = np.asarray(label)
img= np.zeros((label.shape[0],label.shape[1]),np.uint8)
for i in range(len(classpd)):
row = classpd.loc[i,:]
r1,g1,b1 = row.r,row.g,row.b
red,green,blue = label[:,:,0], label[:,:,1], label[:,:,2]
mask = (redr1) & (greeng1) & (blue==b1)
img[mask]=i
cv2.imwrite(img_path+‘1.png’,img)
In [ ]
for i in trange(len(ddata_mask_path),desc='Progress ',leave=False):
imageconvlabel(“Source/”+ddata_mask_path[i])
4.4、标签RGB、单通道对比
4.4.1、源RGB三通道标签
In [17]
Image.open(“Source/”+ ddata_mask_path[0]).resize((500,500), Image.ANTIALIAS)在这里插入图片描述
4.4.2、处理后单通道标签
In [18]
import PIL.ImageOps as ImageOps

ImageOps.autocontrast(Image.open(“Source/”+ ddata_mask_path[0]+‘1.png’)).resize((500,500),在这里插入图片描述
4.5、生成模型需要的数据集文件
In [ ]
traincount = int(len(data_notna)*0.7)
traindata = data_notna.loc[:traincount,‘sat_image_path’:]
valdata = data_notna.loc[traincount+1:,‘sat_image_path’:]
In [ ]
with open(‘Source/train.txt’,‘w’) as ftrain:
for item in traindata.values:
image_path = item[0]
grt_path = item[1]+‘1.png’
ftrain.write(‘{} {}\n’.format(image_path,grt_path))
In [ ]
with open(‘Source/val.txt’,‘w’) as fval:
for item in valdata.values:
image_path = item[0]
grt_path = item[1]+‘1.png’
fval.write(‘{} {}\n’.format(image_path,grt_path))
In [ ]
with open(‘Source/label_names.txt’,‘w’) as fval:
for item in d.keys():
fval.write(‘{}\n’.format(item))
5. 模型训练
5.1 定义图像预处理流程transforms
定义数据处理流程,其中训练和测试需分别定义,训练过程包括了部分测试过程中不需要的数据增强操作,如在本项目中,训练过程使用了RandomHorizontalFlip和RandomPaddingCrop两种数据增强方式,更多图像预处理流程transforms的使用可参见paddlex.transforms。

In [ ]
from paddlex import transforms as T
import paddlex as pdx
train_transforms = T.Compose([
T.Resize(target_size=512),
T.RandomHorizontalFlip(),
T.Normalize(
mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]),
])

eval_transforms = T.Compose([
T.Resize(target_size=512),
T.Normalize(
mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]),
])
5.2 定义数据集Dataset
实例分割使用SegDataset格式的数据集,因此采用pdx.datasets.SegDataset来加载数据集,该接口的介绍可参见文档pdx.datasets.SegDataset。

In [ ]
train_dataset = pdx.datasets.SegDataset(
data_dir=‘Source’,
file_list=‘Source/train.txt’,
label_list=‘Source/label_names.txt’,
transforms=train_transforms,
shuffle=True)
In [ ]
eval_dataset = pdx.datasets.SegDataset(
data_dir=‘./Source/’,
file_list=‘Source/val.txt’,
label_list=‘Source/label_names.txt’,
transforms=eval_transforms)
5.3 模型开始训练
使用本数据集在V100上训练,如有GPU,模型的训练过程预估为一个小时20分钟左右。更多训练模型的参数可参见文档paddlex.seg.DeepLabv3p。模型训练过程每间隔save_interval_epochs轮会保存一次模型在save_dir目录下,同时在保存的过程中也会在验证数据集上计算相关指标,具体相关日志参见文档。

AIStudio使用VisualDL查看训练过程中的指标变化

点击左边菜单图标的『可视化』;
设置logdir,logdir的路径为训练代码中save_dir指定的目录下的vdl_log目录,例如output/deeplab/vdl_log
点击下方『启动VisualDL服务按钮』,再『打开VisualDL』即可
In [ ]

需要Tesla A100,Video Mem:40GB,CPU:12 Core,RAM:96GB环境下运行,不然得修改train_batch_size,以确保性能满足,否则不能运行

num_classes = len(train_dataset.labels)
model = pdx.seg.DeepLabV3P(num_classes=num_classes)
model.train(
num_epochs=80,
train_dataset=train_dataset,
train_batch_size=10,
eval_dataset=eval_dataset,
learning_rate=0.01,
save_interval_epochs=1,
save_dir=‘outputDeepLabV3P/’,
use_vdl=True)
在37次epoch中达到最优值,miou达到了0.67,满足此项目需求。

category_iou=[0.7320994 0.8665909 0.3640704 0.81564426 0.78534275 0.5963331 0.557676 ],

oacc=0.870033

category_acc=[0.8356028 0.9212343 0.572601 0.8863896 0.9123213 0.7599818 0.71280867]

kappa=0.799426

category_F1-score=[0.8453318 0.92852798 0.53380005 0.89846266 0.8797669 0.74712864 0.71603595]

6、模型预测
使用模型进行预测,同时使用pdx.seg.visualize将结果可视化,可视化结果将保存到./outputDeepLabV3P下,其中weight代表原图的权重,即mask可视化结果与原图权重因子。

此处请在AIStudio Notebook页面的右上角菜单,选择重启执行器,以释放显存,重新加载训练好的模型

In [19]
import paddlex as pdx
model = pdx.load_model(‘outputDeepLabV3P/best_model’)
2022-06-02 16:22:41 [INFO] Model[DeepLabV3P] loaded.
In [20]
import matplotlib.pyplot as plt
import numpy as np
def preandshow(path):
image_name = path
result = model.predict(image_name)
mapl = result[‘label_map’]

fig,axes = plt.subplots(nrows=1,ncols=3,figsize=(30,10))

img = plt.imread(image_name)
axes[0].imshow(img)
axes[0].set_title('Original image')

redimg = np.zeros(img.shape,dtype=np.uint8)
redimg[:,:,0][mapl==1]=255

img= cv2.addWeighted(img,1.0,redimg,0.3,0)
axes[1].imshow(img)
axes[1].set_title('Prediction image')

img = plt.imread(image_name.replace('sat.jpg','mask.png'))
axes[2].imshow(img)
axes[2].set_title('Original Label')

# plt.show() # 有时候运行不显示,不知道为什么,故采用下面方式
plt.savefig('./preandshow.png')
Image.open("./preandshow.png")
# pdx.seg.visualize(image_name, result, weight=0.4, save_dir='./output/')

6.1、预测结果展示
In [27]
preandshow(‘Source/train/86805_sat.jpg’)
Image.open(“./preandshow.png”).resize((600, 200), Image.ANTIALIAS)

双击图片可以看大图在这里插入图片描述

In [28]
preandshow(‘Source/train/103665_sat.jpg’)
Image.open(“./preandshow.png”).resize((600, 200), Image.ANTIALIAS)

双击图片可以看大图在这里插入图片描述

In [29]
preandshow(‘Source/train/10452_sat.jpg’)
Image.open(“./preandshow.png”).resize((600, 200), Image.ANTIALIAS)

双击图片可以看大图在这里插入图片描述

7、心得及总结
这算是头一次接触飞桨框架,训练过程都集成好了,相关文档也都是开源的,文档也很详细操作起来简单方便,主要时间更多花在处理数据上面,花了一些时间去优化,从开始转换一张图片的标签耗时10分钟,后来导师指导优化方向,变成处理800来张图片耗时3分钟,效果非常明显,完成后便就很快了。

所以,我也希望读者能学习到下面几个点:

图像分割的流程
怎么样优化标签转换算法,使其更快完成
DeepLabV3+算法原理
8、致谢
本项目属于飞桨领航团AI workshop提供的项目,感谢百度平台为我们免费提供算力。

本项目导师为谢杰航,感谢谢老师一直及时的解决我们的一切疑问!

队伍:无敌

Logo

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

更多推荐