\u200E
基于PaddleSeg模拟实现无人机巡航和智能环境监测
发布日期:2021-08-11T12:31:06.000+0000 浏览量:52次




项目背景




城市绿地作为城市空间环境的重要组成部分,是健康城市建设重要环节,对公众生理、心理、社会等方面健康有重要影响。同时也有研究表明,城市绿地建设与经济发展存在正向互动关系。加强监测和监管联动,将有利于城市绿地生态系统的保护和社会的健康发展。传统的绿地监测主要依靠卫星遥感技术,优点是感知范围大,技术成熟以及计算方便,但缺点是成本高、数据更新时间长。而基于无人机技术的监测方案能够做到细粒度更高、反馈更快而且成本更低,能够满足日常城市绿地监测任务需求。因此,本文提供一种基于PaddleSeg和Pyecharts的无人机智能环境监测的解决思路和方案。

本案例项目已经开源在AI Studio,可以一边看下面文档,一边在线运行体验学习。

项目地址:
https://aistudio.baidu.com/aistudio/projectdetail/2190766



数据集介绍及处理




本项目使用的数据集是来自卡内基梅隆大学一个计算机团队整理和标注的Aeroscapes的项目,里面包含了3269张由无人机在5-50米高度范围内拍摄的RGB照片和对应的标注图像,包含人、自行车、汽车、无人机、船、动物、障碍物、建筑、植物、道路和天空总共11个分类。在标注图像中,0代表背景,1-11分别对应上述11种分类。在数据集的压缩包中,包含4个文件夹,其中JPEGImages和SegmentationClass分别存储原图和标注图,ImageSets存储划分好的数据集和验证集列表。


我们第一步通常是先观察数据集的特征,以方便后面做数据增强和训练参数调优。我们先观察RGB图像,能直观看到的最大差异是拍摄时的光线条件,有阴天环境,也有晴天环境,转换成计算机语言就是图像的亮度、对比度和饱和度,这是我们后面做数据增强的调整方向。接下来还有更重要的一步,关注标注图像中的标签分布,我们用以下代码循环统计出每一分类的数量分布情况。

import os
import numpy as np
from PIL import Image
from tqdm import trange
from collections import Counter

label_dir = 'data/aeroscapes/SegmentationClass'
label_dirlist = os.listdir(label_dir)
label_count = Counter()

for l in trange(len(label_dirlist)): 
    label_image = Image.open(os.path.join(label_dir,label_dirlist[l]))
    label_array = Counter(np.array(label_image).flatten())
label_count += label_array

label_count

统计后的结果为:
Counter({0696388200,11137855496,4877424,91117021451,8133909912,716946045,10881708346,311114500,112114423,21596917,62524067,5653619})

观察结果我们发现标签数据分布是不均匀的,比如第4和第5分类的数量就严重偏少,因此我们后面需要进行参数优化使模型拟合效果更好。

了解了数据特征之后,我们根据ImageSets目录下提供训练集和验证集的文件名列表,生成符合PaddleSeg要求格式的列表文件。文件格式如下图:


下面代码可以把数据集自带的训练列表转换成要求格式,最后生成的训练集(train.txt)和验证集(val.txt)的图像数量分别是2621和648张。

with open('data/train.txt','a+'as t:
    for tr_line in open('data/aeroscapes/ImageSets/trn.txt'):
        tr_line = tr_line.replace('\n','')
        t.write('JPEGImages/%s.jpg SegmentationClass/%s.png\n'%(tr_line,tr_line))
print('生成训练集成功!')

with open('data/val.txt','a+'as v:
    for val_line in open('data/aeroscapes/ImageSets/val.txt'):
        val_line = val_line.replace('\n','')
        v.write('JPEGImages/%s.jpg SegmentationClass/%s.png\n'%(val_line,val_line))
print('生成验证集成功!')



PaddleSeg 

API介绍与模型训练




PaddleSeg是基于飞桨PaddlePaddle开发的端到端图像分割开发套件,涵盖了大量高精度、轻量级的分割模型,通过模块化的设计,提供了配置化驱动和API调用两种应用方式,辅助开发者便捷快速地完成从训练到部署的全流程图像分割应用。

这次 使用的是API调用的方式,与配置化驱动的区别是只需要一行命令安装PaddleSeg库即可调用,无需下载和打包整个PaddleSeg项目。但配置基本相同,主要分为数据变换(数据增强)、数据集处理、模型准备、优化器、训练5个部分。

首先,安装PaddleSeg库

pip install paddleseg

数据增强 ,采用的策略是调整照片的亮度、对比度和饱和度,并通过水平翻转增加样本数量。

import paddleseg.transforms as T

train_transforms = [
    T.ResizeStepScaling(min_scale_factor=0.75,
                        max_scale_factor=2.0
                        scale_step_size=0.25
    ),
    T.RandomPaddingCrop(crop_size=(10241024)),
    T.RandomHorizontalFlip(),
    T.RandomVerticalFlip(),
    T.RandomDistort(
        brightness_range = 0.4,
        contrast_range = 0.4,
        saturation_range = 0.4
    ),
    T.Normalize(
        mean = [0.4850.4560.406],
        std = [0.2290.2240.225],
    )
]

val_transforms = [
    T.Normalize(
        mean = [0.4850.4560.406],
        std = [0.2290.2240.225],
    )
]

数据集处理

from paddleseg.datasets import Dataset

dataset_dir = 'data/aeroscapes'

train_dataset = Dataset(
    dataset_root = dataset_dir,
    train_path = 'data/train.txt',
    num_classes = 12,
    transforms = train_transforms,
    edge = True,
    mode = 'train'
)

val_dataset = Dataset(
    dataset_root = dataset_dir,
    val_path = 'data/val.txt',
    num_classes = 12,
    transforms = val_transforms,
    mode = 'val'
)

模型准备 ,在PaddleSeg的模型库中,SFNet和BiSeNet V2都是相对较高效率的模型,BiSeNet V2速度更快,但SFNet的精度更优,在精度和效率的权衡下,最终选择了SFNet。从网络结构图中能看出Feature extraction部分应该使用了骨干网络,论文原文使用的骨干网络是ResNet101,但为了节约计算资源,加快训练速度,使用的是ResNet50。

SFNet网络结构图



from paddleseg.models import SFNet
from paddleseg.models.backbones import ResNet50_vd

backbone = ResNet50_vd(
            output_stride = 8,
            pretrained = 'https://bj.bcebos.com/paddleseg/dygraph/resnet50_vd_ssld_v2.tar.gz'
        )

model = SFNet(
            num_classes = 12,
            backbone = backbone,
            backbone_indices = [0123],
            pretrained = 'https://bj.bcebos.com/paddleseg/dygraph/cityscapes/sfnet_resnet50_os8_cityscapes_1024x1024_80k/model.pdparams'
        )

优化器和损失函数 ,优化器选了比较常用的带动量的SGD,同时由于数据分类不均衡,损失函数将CrossEntropyLoss改成了LovaszSoftmaxLoss。

from paddle import optimizer
from paddleseg.models.losses import OhemCrossEntropyLoss,LovaszSoftmaxLoss

lr = optimizer.lr.PolynomialDecay(
    learning_rate = 0.01,
    decay_steps = 10000,
    end_lr = 0,
    power=0.9,
)

optimizer = optimizer.Momentum(lr, parameters=model.parameters(), momentum=0.9, weight_decay=4.0e-5)

losses = {}
losses['types'] = [LovaszSoftmaxLoss()]
losses['coef'] = [1]
print(losses)

开始训练

from paddleseg.core import train

train(
    model=model,
    train_dataset=train_dataset, # 训练集的dataset
    val_dataset=val_dataset, # 验证集的dataset
    optimizer=optimizer, # 优化器
    save_dir='output'# 保存路径
    iters=80000# 训练次数
    batch_size=2,
    save_interval=1400# 保存的间隔次数
    log_iters=10# 日志打印间隔
    num_workers=0,
    losses=losses,
    use_vdl=True)


预测统计与图表生成

准备了一段无人机飞行的视频,首先对它进行了分帧处理,把视频任务转化成常规的图像预测任务,视频约43s,最后分成了1309帧。

视频分帧

import cv2

def save_rgb(path):
    # 获得视频的格式
    rgb = path + 'videoplayback.avi'
    print(rgb) 
videoCapture = cv2.VideoCapture(rgb)

    # 获得码率及尺寸
    fps = videoCapture.get(cv2.CAP_PROP_FPS)
    size = (int(videoCapture.get(cv2.CAP_PROP_FRAME_WIDTH)),
            int(videoCapture.get(cv2.CAP_PROP_FRAME_HEIGHT)))

    fNUMS = videoCapture.get(cv2.CAP_PROP_FRAME_COUNT)
print(fps, size, fNUMS)

    # 读帧
    success, frame = videoCapture.read()
    i = 0
    while success:
        cv2.imwrite(path + 'origin_jpg/%d.jpg' % i, frame)
        i = i + 1
        success, frame = videoCapture.read()
videoCapture.release()

save_rgb('./video/')

图像批量预测

%mkdir ./video/predict_png

import paddleseg.transforms as T
from ppseg_predict import predict
from ppseg_predict import get_image_list

transforms = T.Compose([
    T.Normalize()
])

image_list,image_dir = get_image_list('video/origin_jpg')

predict(
        model,
        model_path='output/best_model/model.pdparams'# 模型路径
        transforms=transforms, # transform.Compose, 对输入图像进行预处理
        image_list=image_list, # list,待预测的图像路径列表。
        image_dir=image_dir, # str,待预测的图片所在目录
        save_dir='video/predict_png' # str,结果输出路径
)

预测完成后统计每一帧真值图中每种分类的数量,并可视化成统计图表,这一步需要用到pyecharts和phantomjs,需要先安装以下两个库。


pip install pyecharts
pip install snapshot-phantomjs

然后我们循环读取和统计生成后的图像,并逐帧生成图表。这里代码有点长,而且由于调用了phantomjs的库, 不支持notebook运行,需要在本地运行。

%mkdir video/chart_bar
%mkdir video/chart_meter

import os
import numpy as np
from PIL import Image
from tqdm import trange
from collections import Counter
from pyecharts.charts import Bar
from pyecharts.charts import Gauge
from pyecharts import options as opts
from snapshot_phantomjs import snapshot
from pyecharts.render import make_snapshot

def count_pixel(img,mode='bar'):
    img = np.array(Image.open(img)).flatten()
    img_len = len(img)
img_pixels = Counter(img)

    if mode == 'bar':
        return img_pixels,img_len
    if mode == 'meter':
        return round(img_pixels[9]/img_len*100,2)

def create_bar_image(img_dir,img_basename):
    img_path = os.path.join(img_dir,img_basename)
img_data, total_pixels = count_pixel(img_path,mode='bar')

    c = (
        Bar(init_opts=opts.InitOpts(width="610px",height="380px"))
        .set_global_opts(legend_opts=opts.LegendOpts(is_show=False),yaxis_opts=opts.AxisOpts(min_=0,max_='1'))
        .add_xaxis([''])
        .add_yaxis(
            "Unlabeled",
            [round(img_data[0]/total_pixels,2)],
            itemstyle_opts=opts.ItemStyleOpts(color='rgb(0, 0, 0)'),
        )
        .add_yaxis(
            "Person",
            [round(img_data[1]/total_pixels,2)],
            itemstyle_opts=opts.ItemStyleOpts(color='rgb(192,128,128)'),
        )
        .add_yaxis(
            "Bike",
            [round(img_data[2]/total_pixels,2)],
            itemstyle_opts=opts.ItemStyleOpts(color='rgb(0,128,0)'),
        )
        .add_yaxis(
            "Car",
            [round(img_data[3]/total_pixels,2)],
            itemstyle_opts=opts.ItemStyleOpts(color='rgb(128,128,128)'),
        )
        .add_yaxis(
            "Drone",
            [round(img_data[4]/total_pixels,2)],
            itemstyle_opts=opts.ItemStyleOpts(color='rgb(128,0,0)'),
        )
        .add_yaxis(
            "Boat",
            [round(img_data[5]/total_pixels,2)],
            itemstyle_opts=opts.ItemStyleOpts(color='rgb(0,0,128)'),
        )
        .add_yaxis(
            "Animal",
            [round(img_data[6]/total_pixels,2)],
            itemstyle_opts=opts.ItemStyleOpts(color='rgb(192,0,128)'),
        )
        .add_yaxis(
            "Obstacle",
            [round(img_data[7]/total_pixels,2)],
            itemstyle_opts=opts.ItemStyleOpts(color='rgb(192,0,0)'),
        )
        .add_yaxis(
            "Construction",
            [round(img_data[8]/total_pixels,2)],
            itemstyle_opts=opts.ItemStyleOpts(color='rgb(192,128,0)'),
        )
        .add_yaxis(
            "Vegetation",
            [round(img_data[9]/total_pixels,2)],
            itemstyle_opts=opts.ItemStyleOpts(color='rgb(0,64,0)'),
        )
        .add_yaxis(
            "Road",
            [round(img_data[10]/total_pixels,2)],
            itemstyle_opts=opts.ItemStyleOpts(color='rgb(128,128,0)'),
        )
        .add_yaxis(
            "Sky",
            [round(img_data[11]/total_pixels,2)],
            itemstyle_opts=opts.ItemStyleOpts(color='rgb(0,128,128)'),
        )
)

make_snapshot(snapshot,c.render(),os.path.join('./video/chart_bar',img_basename).replace('jpg','png'))

def create_meter_image(img_dir,img_basename):
    img_path = os.path.join(img_dir,img_basename)
green_pixels = count_pixel(img_path,mode='meter')

    c = (
        Gauge(init_opts=opts.InitOpts(width="350px", height="350px"))
        .add(series_name="", data_pair=[["", green_pixels]],pointer=opts.GaugePointerOpts(length='70%',width=4))
        .set_global_opts(
            legend_opts=opts.LegendOpts(is_show=False),
        )
        .set_series_opts(
            axisline_opts=opts.AxisLineOpts(
                linestyle_opts=opts.LineStyleOpts(
                    color=[[0.15"#fd666d"], [0.25"#fddd60"], [1"#91cc75"]], width=15
                )
            )
        )
    )
make_snapshot(snapshot,c.render(),os.path.join('./video/chart_meter',img_basename).replace('jpg','png'))

predict_img_dir = 'video/predict_png/grey_prediction/'
predict_imgs = os.listdir(predict_img_dir)

for pi in trange(len(predict_imgs)):
    create_bar_image(predict_img_dir,predict_imgs[pi])
    create_meter_image(predict_img_dir,predict_imgs[pi])

视频合成

生成图表后,先将原图、预测图和生成的柱状图仪表盘结合背景图片逐帧对应合成数据大屏。

%mkdir video/board

import numpy as np
from PIL import Image
from tqdm import trange

NUM_FRAMES = 1309 #总帧数

for pid in trange(NUM_FRAMES):
background_board = Image.open('board.jpg')

origin_frame = Image.open('video/origin_jpg/{}.jpg'.format(pid)).resize((660,370))

predict_frame = Image.open('video/predict_png/pseudo_color_prediction/{}.png'.format(pid)).resize((660,370))

bar_chart = Image.open('video/chart_bar/{}.png'.format(pid)).resize((620,340))

meter_chart = Image.open('video/chart_meter/{}.png'.format(pid)).resize((400,400))

    bar_r,bar_g,bar_b,bar_a = bar_chart.split()
meter_r,meter_g,meter_b,meter_a = meter_chart.split()

    background_board.paste(origin_frame,(120,230))
    background_board.paste(predict_frame,(1150,230))
    background_board.paste(bar_chart,(160,700),mask=bar_a)
background_board.paste(meter_chart,(1300,700),mask=meter_a)

    background_board.save('video/board/{}.png'.format(pid))

最后我们将1309张数据大屏的图像,按照30fps的帧率重新合成视频,即可实现文章开头的效果。

import os
import cv2

def images_to_video(path,width,height,output_name):
    fps = 30  # 帧率
    num_frames = NUM_FRAMES
    img_width = width
    img_height = height
    out = cv2.VideoWriter(output_name, cv2.VideoWriter_fourcc(*"mp4v"), fps,(img_width,img_height))

    for i in trange(num_frames):
        filepath = path +str(i) + ".png"
        out.write(cv2.imread(filepath))

out.release()

images_to_video('video/board/',1920,1080,'borad.mp4')



总结




由于时间所限,本次仅用无人机视频替代实际的无人机飞行,模型训练精度也不足,具体落地可以借助DJI提供的Windows版本SDK进行二次开发,SFNet和BiSeNet V2都是支持FPS较高的模型,支持实时图传分割,可以考虑使用。项目内也提供了模型,算力充裕的开发者可以继续以提升至更高精度。另外,数据集的标注较粗,如果需要应用到真实业务,请替换或者自行标注更高精度和更多类型的数据集。

欢迎关注作者AI Studio账号~
https://aistudio.baidu.com/aistudio/personalcenter/thirdview/168825


如有飞桨相关技术问题,欢迎在飞桨论坛中提问交流:
http://discuss.paddlepaddle.org.cn/

欢迎加入官方QQ群获取最新活动资讯:793866180

如果您想详细了解更多飞桨的相关内容,请参阅以下文档。

·飞桨官网地址·
https://www.paddlepaddle.org.cn/

·飞桨开源框架项目地址·
GitHub: https://github.com/PaddlePaddle/Paddle 
Gitee: https://gitee.com/paddlepaddle/Paddle

长按上方二维码立即star!


飞桨(PaddlePaddle)以百度多年的深度学习技术研究和业务应用为基础,集深度学习核心训练和推理框架、基础模型库、端到端开发套件和丰富的工具组件于一体,是中国首个自主研发、功能丰富、开源开放的产业级深度学习平台。飞桨企业版针对企业级需求增强了相应特性,包含零门槛AI开发平台EasyDL和全功能AI开发平台BML。EasyDL主要面向中小企业,提供零门槛、预置丰富网络和模型、便捷高效的开发平台;BML是为大型企业提供的功能全面、可灵活定制和被深度集成的开发平台。


END