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