一点说明

前段时间根据gluon的教程动手学深度学习和同学项目实地拍摄的盲道图片完成了一个基于FCN的盲道语义分割程序,也是自己第一次做语义分割的项目。一方面发现深度学习在盲道这种具有简单纹理和颜色特征的识别上具有非常好的效果,在速度和效果上表现都还不错,另一方面这也是为了练习如何使用gluon接口完成一个语义分割项目。

由于盲道纹理比较简单,为了提高推理速度仅训练FCN32s网络不再继续训练16s和8s。分别使用ResNet18,MobileNetV2以及0.25倍剪枝版的MobileNetV2作为backbone做了实验。测试硬件为笔记本Core™ i5-5200U CPU以及GeForce 920M GPU。为了轻量化以达到实时性最后使用的是0.25倍剪枝版的MobileNetV2,在CPU 上能跑到10帧,笔记本920M上能到接近20帧。

虽然还有很大的优化空间(例如:更好的语义分割算法,尝试Focal Loss效果,模型量化为Int 8,将完整分割网络的剪枝等,随着后续的学习有时间会继续尝试),不过目前综合速度和精度上来看都远胜过网上大多数基于颜色和简单规则纹理人工设计的盲道识别算法。

基本原理

FCN原理上面链接教程里已经写得很清楚了,这里就不再赘述:

  1. 使用在ImageNet上与训练好的模型作为主干网络,观察网络结构,去掉最后的全连接层或者是全局平均池化层以及输出层;
  2. 在主干网络后接一个1x1卷积层进行通道整理,卷积核数量就是你的类别数,后续会在每个通道上分别预测像素类别的概率,此处有盲道和背景,自然就是两类;
  3. 后接一个转置卷积层,通常此时网络将原图像下采样了1/32,此处转置卷积就设置为把特征图上采样32倍以达到和原图相同大小,可以随机初始化一个ndarray输入看看是否如此;
  4. 将转置卷积初始化为双线性插值,利于上采样效果提高和快速收敛,gluon教程里提供了双线性插值的初始化方法;
  5. 使用labelme软件制作语义分割的mask标签;
  6. 划分训练集和验证集读入数据进行训练,收敛很快,大概训练10个epoch左右即可(5个epoch对直道,多数横向纵纹理和远处效果已经很好,由于训练集中横向横纹理的图片较少,需要继续训练提高拟合效果),训练监测acc。根据实验经验,训练集和验证集acc至少达到98以上后在测试集上分割效果较好误检率也较低。另外实验表明0.25倍剪枝版的MobileNet识别能力不如另外两个网络并且相对容易过拟合,误检率稍高,训练这个网络时候更需要注意测试误检情况;
  7. 拍摄数据集时候需要拍摄尽可能多角度,远近以降低过拟合。训练时更科学的做法是监测精确率P和召回率R以PR曲线或ROC曲线作为评估指标。这里类别没有严重不平衡所以监测acc也能起到效果;

测试集效果

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

数据标注

用labelme多边形标注500张盲道图片(当然数量和角度距离的变化越多越好,我标了500张测试下来效果不错 ),当然亲测labelme语义分割标注还是很方便的。
标注完保存会得到与原图同名的.json文件,使用以下命令可以生成同名文件夹,包含原图,mask标签图等文件

labelme_json_to_dataset <文件名>.json

labelme的一个不方便之处是没有自带批量生成,为了不浪费时间需要写shell脚本完成批量生成标签文件夹。参考博客(https://blog.csdn.net/lyxleft/article/details/82222452)创建shell脚本可以批量处理,亲测高效可行!

  1.     #!/bin/bash
        echo "Now begin to search json file..."
        for file in ./*
        do
            if [ "${file##*.}"x = "json"x ]
            then
            filename=`basename $file`
            temp_filename=`basename $file  .json`
            suf=_json
            new_filename=${temp_filename}${suf}
        #    echo $new_filename
            cmd="labelme_json_to_dataset ${filename} -o ${new_filename}"
            eval $cmd
            fi
        #    printf "no!\n "
        done
    

训练

基本设置

照搬教程的双线性初始化函数,其余的设置务必在理解教程的基础上根据自己项目的实际情况修改。注意颜色转换表的颜色应该与用labelme生成的mask的颜色相对应。

  1. import os
    import d2lzh as d2l
    from mxnet import gluon, image, nd, init
    from mxnet.gluon import nn, model_zoo, data as gdata, utils as gutils, loss as gloss
    import sys
    import numpy as np
    import matplotlib.pyplot as plt
    import mxnet as mx
    
    #双线性插值初始化函数
    def bilinear_kernel(in_channels, out_channels, kernel_size):
        factor = (kernel_size + 1) // 2
        if kernel_size % 2 == 1:
            center = factor - 1
        else:
            center = factor - 0.5
        og = np.ogrid[:kernel_size, :kernel_size]
        filt = (1 - abs(og[0] - center) / factor) * \
               (1 - abs(og[1] - center) / factor)
        weight = np.zeros((in_channels, out_channels, kernel_size, kernel_size),
                          dtype='float32')
        weight[range(in_channels), range(out_channels), :, :] = filt
        return nd.array(weight)
    
    #读取图像和标签图列表,简单划分训练集和验证集(此处使用前50张作验证集,可自行修改k折测试)
    def read_br_images():
        images = [i[:-4] for i in os.listdir('data/img')]#名字
        images_extra = [i[:-4] for i in os.listdir('data/extra/img')]
        
        features, labels = [None] * (len(images)+len(images_extra)), [None] * (len(images)+len(images_extra))
        
        for i, fname in enumerate(images):
            #print(fname)
            features[i] = image.imread('data/img/'+images[i]+'.jpg')
            labels[i] = image.imread('data/label/'+images[i]+'_json/label.png')
            
        for i, fname in enumerate(images_extra):
            #print(fname)
            features[i+len(images)] = image.imread('data/extra/img/'+images_extra[i]+'.jpg')
            labels[i+len(images)] = image.imread('data/extra/'+images_extra[i]+'_json/label.png')
            
        return features[50:], labels[50:]
    
    def read_br_images_validation():
        images = [i[:-4] for i in os.listdir('data/img')]#名字
        images_extra = [i[:-4] for i in os.listdir('data/extra/img')]
        
        features, labels = [None] * (len(images)+len(images_extra)), [None] * (len(images)+len(images_extra))
        
        for i, fname in enumerate(images):
            #print(fname)
            features[i] = image.imread('data/img/'+images[i]+'.jpg')
            labels[i] = image.imread('data/label/'+images[i]+'_json/label.png')
            
        for i, fname in enumerate(images_extra):
            #print(fname)
            features[i+len(images)] = image.imread('data/extra/img/'+images_extra[i]+'.jpg')
            labels[i+len(images)] = image.imread('data/extra/'+images_extra[i]+'_json/label.png')
            
        return features[:50], labels[:50]
    
    #彩色标签按颜色表转换为类别标签图
    def label_indices(colormap, colormap2label):
        colormap = colormap.astype('int32')
        idx = ((colormap[:, :, 0] * 256 + colormap[:, :, 1]) * 256
               + colormap[:, :, 2])
        return colormap2label[idx]
    
    #随机裁剪
    def rand_crop(feature, label, height, width):
        feature, rect = image.random_crop(feature, (width, height))
        label = image.fixed_crop(label, *rect)
        return feature, label
    
    #预设颜色转换表和类别
    BR_COLORMAP = [[0,0,0],[128,0,0]]
    BR_COLORCLASSES = ['background', 'BR']
    

读入数据

在教程基础上改写的数据类

class BRSegDataset(gdata.Dataset):
    def __init__(self, is_train, crop_size, colormap2label):
        self.rgb_mean = nd.array([0.485, 0.456, 0.406])
        self.rgb_std = nd.array([0.229, 0.224, 0.225])
        self.crop_size = crop_size
        if is_train ==True:
            features, labels = read_br_images()
        else:
            features, labels = read_br_images_validation()
        self.features = [self.normalize_image(feature)
                         for feature in self.filter(features)]
        self.labels = self.filter(labels)
        self.colormap2label = colormap2label
        print('read ' + str(len(self.features)) + ' examples')

    def normalize_image(self, img):
        return (img.astype('float32') / 255 - self.rgb_mean) / self.rgb_std

    def filter(self, imgs):
        return [img for img in imgs if (
            img.shape[0] >= self.crop_size[0] and
            img.shape[1] >= self.crop_size[1])]

    def __getitem__(self, idx):
        feature, label = rand_crop(self.features[idx], self.labels[idx],
                                       *self.crop_size)
        return (feature.transpose((2, 0, 1)),label_indices(label, self.colormap2label))

    def __len__(self):
        return len(self.features) 

当数据集中有图片尺寸不一时需要随机裁剪,生成数据迭代器

#数据迭代器
#裁剪大小与转化表
crop_size, batch_size, colormap2label = (480, 640), 32, nd.zeros(256**3)

for i, cm in enumerate(BR_COLORMAP):
    colormap2label[(cm[0] * 256 + cm[1]) * 256 + cm[2]] = i

num_workers = 0 if sys.platform.startswith('win32') else 4

train_iter = gdata.DataLoader(
    BRSegDataset(True, crop_size, colormap2label), batch_size,
    shuffle=True, last_batch='discard', num_workers=num_workers)

test_iter = gdata.DataLoader(
    BRSegDataset(False, crop_size, colormap2label), batch_size,
     last_batch='discard', num_workers=num_workers)

网络构建

教程上是ResNet18去掉最后两块,这里以mobilenet为例,观察结构应该去掉最后一块,实际上包括了一个全局平均池化,一个1x1卷积和一个Flatten。之后根据类别数加上一个1x1卷积整理通道和转置卷积层上采样。

pretrained_net = model_zoo.vision.mobilenet_v2_1_0(pretrained=True)
net = nn.HybridSequential()
for layer in pretrained_net.features[:-1]:
    net.add(layer)
#背景与盲道
num_classes = 2
net.add(nn.Conv2D(num_classes, kernel_size=1),
        nn.Conv2DTranspose(num_classes, kernel_size=64, padding=16,
                           strides=32))
#初始化通道整理层和转置卷积层
net[-1].initialize(init.Constant(bilinear_kernel(num_classes, num_classes,
                                                 64)))
net[-2].initialize(init=init.Xavier())                           

开始训练

ctx = d2l.try_all_gpus()
loss = gloss.SoftmaxCrossEntropyLoss(axis=1)
net.collect_params().reset_ctx(ctx)
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.1,
                                                      'wd': 1e-3})
d2l.train(train_iter, test_iter, net, loss, trainer, ctx, num_epochs=10)

测试

下面是一份完整测试和测速代码,分别测试gpu和cpu推理的速度。
需要注意的是MXNet是异步运行,python前端将命令交给C++后端执行不等待结果就会执行之后的python命令,在实际使用中这样可以节省前端调用时间。为了测试速度需要nd.waitall()命令强制前后端同步。调用net.hybridize()转为静态图可以节省一部分内存/显存,并有少量加速。另外gpu测试时程序内第一次调用cuda会包含大量初始化。

#!/usr/bin/env python
# coding: utf-8

from mxnet import gluon, image, nd, init
from mxnet.gluon import nn, model_zoo
import numpy as np
#import matplotlib.pyplot as plt
import mxnet as mx
import time
import cv2

ctx = [mx.cpu()]
#ctx = [mx.gpu()]
#构建网络

pretrained_net = model_zoo.vision.mobilenet_v2_1_0(pretrained=True)#, ctx = mx.gpu())
#pretrained_net = model_zoo.vision.resnet18_v2(pretrained=True)#, ctx = mx.gpu())
#pretrained_net = model_zoo.vision.mobilenet_v2_0_25(pretrained=True)#, ctx = mx.gpu())

net = nn.HybridSequential()
for layer in pretrained_net.features[:-2]:
    net.add(layer)
num_classes = 2
net.add(nn.Conv2D(num_classes, kernel_size=1),
        nn.Conv2DTranspose(num_classes, kernel_size=64, padding=16,
                           strides=32))


net.load_parameters('mobileBR.params', ctx = ctx[0])
net.hybridize()

def normalize_image(img):
    rgb_mean = mx.nd.array([0.485, 0.456, 0.406])
    rgb_std = mx.nd.array([0.229, 0.224, 0.225])
    return (img.astype('float32') / 255 - rgb_mean) / rgb_std

def predict(img):
    X = normalize_image(img)
    X = X.transpose((2, 0, 1)).expand_dims(axis=0)
    pred = nd.argmax(net(X.as_in_context(ctx[0])), axis=1)
    return pred.reshape((pred.shape[1], pred.shape[2]))

#预热
nd.waitall()
img = image.imread('data/extra/img/BM_right24.jpg')
pred = predict(img)
nd.waitall()

t1 = time.time()
img = image.imread('data/extra/img/BM_right23.jpg')
pred = predict(img)
#result = pred.asnumpy().astype(np.uint8)*255
nd.waitall()
#cv2.imwrite('result.jpg',result)
print(time.time()-t1)