飞桨常规赛:dreamkwc的团队 - 12月第2名方案

飞桨常规赛:遥感影像地块分割 - 11月第8名方案

1. 比赛介绍

1.1 比赛页面传送门:常规赛:遥感影像地块分割

1.2 赛题介绍

本赛题由 2020 CCF BDCI 遥感影像地块分割 初赛赛题改编而来。遥感影像地块分割, 旨在对遥感影像进行像素级内容解析,对遥感影像中感兴趣的类别进行提取和分类,在城乡规划、防汛救灾等领域具有很高的实用价值,在工业界也受到了广泛关注。现有的遥感影像地块分割数据处理方法局限于特定的场景和特定的数据来源,且精度无法满足需求。因此在实际应用中,仍然大量依赖于人工处理,需要消耗大量的人力、物力、财力。本赛题旨在衡量遥感影像地块分割模型在多个类别(如建筑、道路、林地等)上的效果,利用人工智能技术,对多来源、多场景的异构遥感影像数据进行充分挖掘,打造高效、实用的算法,提高遥感影像的分析提取能力。 赛题任务 本赛题旨在对遥感影像进行像素级内容解析,并对遥感影像中感兴趣的类别进行提取和分类,以衡量遥感影像地块分割模型在多个类别(如建筑、道路、林地等)上的效果。

1.3 数据说明

本赛题提供了多个地区已脱敏的遥感影像数据,各参赛选手可以基于这些数据构建自己的地块分割模型。

训练数据集
样例图片及其标注如下图所示:

飞桨常规赛:dreamkwc的团队 - 12月第2名方案
飞桨常规赛:dreamkwc的团队 - 12月第2名方案

飞桨常规赛:dreamkwc的团队 - 12月第2名方案
飞桨常规赛:dreamkwc的团队 - 12月第2名方案

训练数据集文件名称:train_and_label.zip

包含2个子文件,分别为:训练数据集(原始图片)文件、训练数据集(标注图片)文件,详细介绍如下:

训练数据集(原始图片)文件名称:img_train

包含66,653张分辨率为2m/pixel,尺寸为256 * 256的JPG图片,每张图片的名称形如T000123.jpg。

训练数据集(标注图片)文件名称:lab_train

包含66,653张分辨率为2m/pixel,尺寸为256 * 256的PNG图片,每张图片的名称形如T000123.png。

备注: 全部PNG图片共包括4种分类,像素值分别为0、1、2、3。此外,像素值255为未标注区域,表示对应区域的所属类别并不确定,在评测中也不会考虑这部分区域。

测试数据集
测试数据集文件名称:img_test.zip,详细介绍如下:

包含4,609张分辨率为2m/pixel,尺寸为256 * 256的JPG图片,文件名称形如123.jpg。

2. 思路介绍

说明,这个版本主要是为了学习使用PaddleSeg,并熟悉AiStudio平台,所以并没有对模型进行特别的修改,也没有针对数据集的特点进行分析。

为了快速上手,从讨论区找了官方提供的基于PaddleSeg的baseline

首先按照教程准备环境和数据。

环境安装

!git clone https://gitee.com/paddlepaddle/PaddleSeg.git

# 安装所需依赖项
!pip install -r PaddleSeg/requirements.txt

解压数据集

数据存放在data路径下,每次关闭notebook都会被删除,所以每次都需要重新解压。

# 按照自己的路径名修改“data80164”
!unzip -q data/data80164/train_and_label.zip
!unzip -q data/data80164/img_test.zip

数据处理

将训练集数据分为train和val两部分,并将文件名写入txt,后续数据读取配置是从txt文件中读取。可以一次写入,后续重复使用,这样也便于比较观察模型改动有没有提升。

import os
import numpy as np

datas = []
image_base = 'img_train'   # 训练集原图路径
annos_base = 'lab_train'   # 训练集标签路径

# 读取原图文件名
ids_ = [v.split('.')[0] for v in os.listdir(image_base)]

# 将训练集的图像集和标签路径写入datas中
for id_ in ids_:
    img_pt0 = os.path.join(image_base, '{}.jpg'.format(id_))
    img_pt1 = os.path.join(annos_base, '{}.png'.format(id_))
    datas.append((img_pt0.replace('/home/aistudio', ''), img_pt1.replace('/home/aistudio', '')))
    if os.path.exists(img_pt0) and os.path.exists(img_pt1):
        pass
    else:
        raise "path invalid!"

# 打印datas的长度和具体存储例子
print('total:', len(datas))
print(datas[0][0])
print(datas[0][1])
print(datas[10][:])
import numpy as np

# 四类标签,这里用处不大,比赛评测是以0、1、2、3类来对比评测的
labels = ['建筑', '耕地', '林地',  '其他']

# 将labels写入标签文件
with open('labels.txt', 'w') as f:
    for v in labels:
        f.write(v+'\n')

# 随机打乱datas
np.random.seed(5)
np.random.shuffle(datas)

# 验证集与训练集的划分,0.05表示5%为训练集,95%为训练集
split_num = int(0.05*len(datas))

# 划分训练集和验证集
train_data = datas[:-split_num]
valid_data = datas[-split_num:]

# 写入训练集list
with open('train_list.txt', 'w') as f:
    for img, lbl in train_data:
        f.write(img + ' ' + lbl + '\n')

# 写入验证集list
with open('valid_list.txt', 'w') as f:
    for img, lbl in valid_data:
        f.write(img + ' ' + lbl + '\n')

# 打印训练集和测试集大小
print('train:', len(train_data))
print('valid:', len(valid_data))

模型训练

需要自己修改的配置文件有两个。

为了快速实验,我把deeplabv3+的backbone换成了mobilenetv2,同样使用预训练模型。考虑到这次使用的图像尺寸为256*256像素,将ASPP的空洞卷积卷积率进行了一定调整,空洞率从[12,24,36]减小为{1,3,6],并将新结构命名为DSPP。

为了充分利用GPU显存,将batch size拉到最大,240基本就是极限了,同时增加了训练迭代次数。

deeplabv3p_mobilenetv2_g.yml

_base_: '../_base_/cityscapes.yml'

batch_size: 240
iters: 60000

model:
  type: DeepLabV3P_DSPP
  backbone:
    type: MobileNetV2
    channel_ratio: 1.0
    min_channel: 16
    pretrained: https://bj.bcebos.com/paddleseg/dygraph/mobilenetv2.tar.gz
    # output_stride: 16
    # multi_grid: [1, 1, 2]
  num_classes: 4
  backbone_indices: [0, 3]
  dspp_ratios: [1, 3, 6]
  # mid_channels: 256
  dspp_out_channels: 256
  align_corners: False
  pretrained: null

pred_dataset:
  num_classes: 4
  transforms:
    - type: Normalize

第二个配置文件时基准文件,主要是修改传入的txt路径,地物类别数。

简单尝试了不同transforms的效果,去掉色彩和对比度的效果更好。同时发现了11月没注意到的一个点,随机缩放裁切的默认值太大了,图像本身才256256,缩放到了1080512,难怪之前训练那么慢,batchsize稍微大一点儿就爆显存。修改为256*256后batchsize扩大了十倍,效果提升明显。

batch_size: 2
iters: 80000

train_dataset:
  type: Dataset
  dataset_root: /home/aistudio
  train_path: /home/aistudio/train_list.txt
  num_classes: 4
  transforms:
    - type: ResizeStepScaling
      min_scale_factor: 0.5
      max_scale_factor: 2.0
      scale_step_size: 0.25
    - type: RandomPaddingCrop
      crop_size: [256, 256]
    - type: RandomHorizontalFlip
    # - type: RandomDistort
    #   brightness_range: 0.4
    #   contrast_range: 0.4
    #   saturation_range: 0.4
    - type: Normalize
  mode: train

val_dataset:
  type: Dataset
  dataset_root: /home/aistudio
  val_path: /home/aistudio/valid_list.txt
  num_classes: 4
  transforms:
    - type: Normalize
  mode: val


optimizer:
  type: sgd
  momentum: 0.9
  weight_decay: 4.0e-5

lr_scheduler:
  type: PolynomialDecay
  learning_rate: 0.01
  end_lr: 0
  power: 0.9

loss:
  types:
    - type: CrossEntropyLoss
  coef: [1]

接着就可以开始训练了。

!python PaddleSeg/train.py \
        --config PaddleSeg/configs/deeplabv3p/deeplabv3p_mobilenetv2.yml \
        --use_vdl \
        --do_eval \
        --save_interval 1000 \
        --save_dir output \
        --num_workers 4 \
        --learning_rate 0.1

模型训练结束后使用保存的best_model进行预测。为了方便提交结果,稍微修改了一下预测文件,添加了符合提交要求的输出方式。

# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os
import math

import cv2
import numpy as np
import paddle
from paddle.fluid.layers.tensor import save

from paddleseg import utils
from paddleseg.core import infer
from paddleseg.utils import logger, progbar


def mkdir(path):
    sub_dir = os.path.dirname(path)
    if not os.path.exists(sub_dir):
        os.makedirs(sub_dir)


def partition_list(arr, m):
    """split the list 'arr' into m pieces"""
    n = int(math.ceil(len(arr) / float(m)))
    return [arr[i:i + n] for i in range(0, len(arr), n)]


def predict(model,
            model_path,
            transforms,
            image_list,
            image_dir=None,
            save_dir='output',
            aug_pred=False,
            scales=1.0,
            flip_horizontal=True,
            flip_vertical=False,
            is_slide=False,
            stride=None,
            crop_size=None):
    """
    predict and visualize the image_list.

    Args:
        model (nn.Layer): Used to predict for input image.
        model_path (str): The path of pretrained model.
        transforms (transform.Compose): Preprocess for input image.
        image_list (list): A list of image path to be predicted.
        image_dir (str, optional): The root directory of the images predicted. Default: None.
        save_dir (str, optional): The directory to save the visualized results. Default: 'output'.
        aug_pred (bool, optional): Whether to use mulit-scales and flip augment for predition. Default: False.
        scales (list|float, optional): Scales for augment. It is valid when `aug_pred` is True. Default: 1.0.
        flip_horizontal (bool, optional): Whether to use flip horizontally augment. It is valid when `aug_pred` is True. Default: True.
        flip_vertical (bool, optional): Whether to use flip vertically augment. It is valid when `aug_pred` is True. Default: False.
        is_slide (bool, optional): Whether to predict by sliding window. Default: False.
        stride (tuple|list, optional): The stride of sliding window, the first is width and the second is height.
            It should be provided when `is_slide` is True.
        crop_size (tuple|list, optional):  The crop size of sliding window, the first is width and the second is height.
            It should be provided when `is_slide` is True.

    """
    utils.utils.load_entire_model(model, model_path)
    model.eval()
    nranks = paddle.distributed.get_world_size()
    local_rank = paddle.distributed.get_rank()
    if nranks > 1:
        img_lists = partition_list(image_list, nranks)
    else:
        img_lists = [image_list]

    added_saved_dir = os.path.join(save_dir, 'added_prediction')
    pred_saved_dir = os.path.join(save_dir, 'pseudo_color_prediction')
    org_saved_dir = os.path.join(save_dir, 'result')

    logger.info("Start to predict...")
    progbar_pred = progbar.Progbar(target=len(img_lists[0]), verbose=1)
    with paddle.no_grad():
        for i, im_path in enumerate(img_lists[local_rank]):
            im = cv2.imread(im_path)
            ori_shape = im.shape[:2]
            im, _ = transforms(im)
            im = im[np.newaxis, ...]
            im = paddle.to_tensor(im)

            if aug_pred:
                pred = infer.aug_inference(
                    model,
                    im,
                    ori_shape=ori_shape,
                    transforms=transforms.transforms,
                    scales=scales,
                    flip_horizontal=flip_horizontal,
                    flip_vertical=flip_vertical,
                    is_slide=is_slide,
                    stride=stride,
                    crop_size=crop_size)
            else:
                pred = infer.inference(
                    model,
                    im,
                    ori_shape=ori_shape,
                    transforms=transforms.transforms,
                    is_slide=is_slide,
                    stride=stride,
                    crop_size=crop_size)
            pred = paddle.squeeze(pred)
            pred = pred.numpy().astype('uint8')

            # get the saved name
            if image_dir is not None:
                im_file = im_path.replace(image_dir, '')
            else:
                im_file = os.path.basename(im_path)
            if im_file[0] == '/' or im_file[0] == '\\':
                im_file = im_file[1:]

            # save added image
            added_image = utils.visualize.visualize(im_path, pred, weight=0.6)
            added_image_path = os.path.join(added_saved_dir, im_file)
            mkdir(added_image_path)
            cv2.imwrite(added_image_path, added_image)

            # save pseudo color prediction
            pred_mask = utils.visualize.get_pseudo_color_map(pred)
            pred_saved_path = os.path.join(
                pred_saved_dir,
                os.path.splitext(im_file)[0] + ".png")
            mkdir(pred_saved_path)
            pred_mask.save(pred_saved_path)

            # 此处添加了用于提交的输出
            org_saved_path = os.path.join(
                org_saved_dir, 
                os.path.splitext(im_file)[0] + ".png"
            )
            mkdir(org_saved_path)
            cv2.imwrite(org_saved_path, pred)

            # pred_im = utils.visualize(im_path, pred, weight=0.0)
            # pred_saved_path = os.path.join(pred_saved_dir, im_file)
            # mkdir(pred_saved_path)
            # cv2.imwrite(pred_saved_path, pred_im)

            progbar_pred.update(i + 1)

!python PaddleSeg/predict.py \
        --config PaddleSeg/configs/deeplabv3p/deeplabv3p_mobilenetv2.yml \
        --model_path output/best_model/model.pdparams \
        --image_path data/img_testA \
        --aug_pred

3. 使用经验分享

12月继续熟悉代码,主要看了看图像输入预处理的部分。代码整体跑下来两点体会。

  1. 找到合适的预处理组合对分类精度有明显提升。
  2. 尽可能地使用大batch size,充分利用显存,提高效率的同时提高分类精度。

对于初次接触PaddleSeg的人来说,按照官方给的示例,可以比较轻松地跑通流程。

流程简单总结就是:

  • 下载PaddleSeg
    
  • 配置环境(这一步好像可以跳过)
    
  • 解压数据,构建train和val列表txt文件
    
  • 修改模型配置文件中的相应路径和地物类别数
    
  • 训练和预测
    
  • 提交结果
    

PaddleSeg中模型的配置都是使用的yml文件,没用过的人还是需要花时间熟悉一下的。模型都是模块化设计,通过配置文件替换不同模块比较方便,也可以自己写新的模块,总的来说简单的改动还是很容易的。自己尝试修改了多尺度模块ASPP,可以很轻松的嵌入到现有模型中。

上一篇:Android开发(12):Fragment的使用


下一篇:这大致是继高考后失眠后的又一次失眠