计算机视觉——实时语义分割ENet算法原理与模型训练

1.概述

ENet(Efficient Neural Network)是一种专为实时语义分割任务设计的深度神经网络架构。与SegNet相比,ENet在尺寸和推理时间上都有显著的优势,同时能够提供相当或更好的准确性。

尺寸方面,ENet的设计注重减少模型的参数数量和计算复杂度。SegNet的架构相对对称,包含大量的参数,而ENet则采用了一种大型编码器与小型解码器的组合,这种设计哲学基于一个观点:解码器的主要作用是对编码器的输出进行上采样,并微调细节,而这并不需要一个复杂的结构。因此,ENet的参数数量大幅减少,仅为0.7MB,这使得整个网络可以适配在嵌入式处理器的快速片上内存中,从而显著降低了硬件要求。
在这里插入图片描述
论文地址:https://arxiv.org/abs/1606.02147
训练代码:https://download.csdn.net/download/matt45m/89113606

2.网络架构

在这里插入图片描述

ENet的网络架构是一个高度优化的结构,旨在实现高效的实时语义分割。它的设计考虑了计算效率和内存使用,同时保持了优秀的分割性能。下面是对ENet网络架构的详细描述:
在这里插入图片描述

2.1 Initial模块

Initial模块是整个网络的起始部分,它接收原始输入图像并开始构建特征图。这个阶段使用最大池化和卷积层来减少图像的空间维度,同时增加网络的深度。
在这里插入图片描述

2.2 Bottleneck模块

每个bottleneck模块由三个卷积层组成:

  1. 1×1投影卷积:这个卷积层用于减小通道数目,为后续的主卷积层做准备。
  2. 主卷积层:这是模块的核心,可以是标准卷积、空洞卷积或完全卷积。在原始论文中,这些卷积层被描述为具有3×3卷积核的conv操作。
  3. 1×1扩展卷积:这个卷积层用于将通道数目扩展回原来的大小,以便于与其他特征图进行合并。

在这些卷积层之间,使用批量归一化(Batch Normalization, BN)和PReLU激活函数。如果bottleneck模块负责下采样,那么会在主分支上添加一个最大池化层,并且第一个1×1投影卷积会被一个步长为2的2×2卷积替换,以匹配特征图的数量。

2.3 编码器和解码器

  • 编码器:由前三个阶段组成,每个阶段包含多个bottleneck模块。这些阶段的目的是通过连续的卷积和池化操作提取图像的特征。
  • 解码器:由后两个阶段组成,它们使用上采样和空间卷积操作来重建图像,并生成最终的分割结果。

2.4 特殊设计

  • 空间Dropout:为了正则化和防止过拟合,ENet在bottleneck2.0之前使用0.01的Dropout比率,在之后使用0.1的Dropout比率。
  • 无偏置的投影卷积:为了降低卷积的调用和内存溢出,投影卷积中没有偏置项,这一点对准确率没有影响。
  • max uppooling:在解码器中,最大池化被max uppooling操作所取代,这样可以与后续的空间卷积更好地结合。
  • 空间卷积:padding被空间卷积所取代,这有助于更好地控制上采样过程中的特征图大小。

2.5 性能优化

  • 最后一个全卷积模块:出于性能考虑,ENet决定仅将全卷积作为网络的最后一个模块。这个模块单独占用了解码器处理时间的相当一部分,但为了保持实时性能,这是一个必要的权衡。

3.设计选择

3.1 特征图分辨率的权衡

在语义分割任务中,特征图分辨率的调整是一个双刃剑。一方面,降低分辨率可以减少计算负担,提高处理速度;另一方面,这可能导致空间信息的损失,影响分割的精确度。为了平衡这两个方面,ENet借鉴了SegNet的策略,通过索引保存和稀疏上采样技术,有效地减少了内存消耗,同时尽可能地保留了空间细节。

3.2 空洞卷积的应用

空洞卷积(Dilated Convolution)作为一种能够增大感受野的技术,对于提高模型对上下文信息的捕捉能力至关重要。在复杂的视觉场景中,如道路环境中的行人和骑行者识别,空洞卷积使得模型能够更好地理解对象与其周围环境的关系。ENet的实验结果表明,空洞卷积在提高分割精度方面起到了关键作用。

3.3 早期下采样的策略

ENet的早期下采样策略体现了对计算资源的精细管理。通过在网络的初始阶段大幅度减小输入尺寸,ENet有效地降低了后续层的计算负担。这种策略基于一个核心假设:视觉信息在空间上存在高度的冗余性,可以通过更紧凑的表示形式进行编码。这一策略的成功实施,为后续的特征提取和上采样操作奠定了坚实的基础。

3.4 编码器与解码器的结构优化

ENet的编码器-解码器架构经过精心设计,以确保编码器能够充分提取图像特征,而解码器则专注于恢复这些特征以实现精确的像素级预测。这种不对称的结构设计反映了两个部分在任务中的不同重要性,同时也减少了模型的参数数量,提高了整体的运行效率。

3.5 非线性激活函数的优化

在ENet中,传统的ReLU激活函数被PReLU所取代,这一改变基于对网络行为的深入分析。PReLU允许每个特征图独立学习非线性斜率,提供了更大的灵活性和适应性。这种激活函数的选择不仅提高了模型的表达能力,还有助于在不同的网络层中实现更精细的信息处理。

3.6 信息保存维度的创新

ENet在信息保存维度上采用了创新的策略,通过并行执行池化和卷积操作,并连接结果特征图,显著提高了模型的推理速度。这种策略的实施,不仅优化了信息流,还减少了信息的丢失,提高了分割的准确性。

3.7 分解卷积的计算优化

ENet通过将大的卷积核分解为多个小的卷积核,有效地减少了模型的参数数量和计算复杂度。这种分解策略不仅提高了模型的运行速度,还通过增加非线性层,增强了模型的表达能力。

3.8 空洞卷积的性能提升

空洞卷积在不增加计算负担的情况下,显著提高了模型的感受野。这种技术的应用使得ENet在保持分辨率的同时,能够捕捉到更广泛的上下文信息,从而提高了分割的精度。

3.9 正则化技术的探索

在面对小规模数据集时,ENet通过空间Dropout技术有效地防止了过拟合。这种正则化方法不仅提高了模型的泛化能力,还通过随机丢弃部分特征图,增加了模型的鲁棒性。

4.数据集处理

数据集的标注用labelme,使用的数据是书本,为了提取书本边缘:
在这里插入图片描述
在这里插入图片描述

5.模型训练

5.1 环境安装

conda create --name enet python=3.7
source activate enet
conda install pytorch torchvision cudatoolkit=10.2 -c pytorch
pip install cython matplotlib tqdm opencv-python scipy pillow

5.2 数据处理

import os
import sys
import glob
import json
import math
import uuid
import random

import numpy as np
import PIL.Image
import PIL.ImageDraw
from tqdm import tqdm

def shape_to_mask(img_shape, points, shape_type=None,
                  line_width=10, point_size=5):
    mask = np.zeros(img_shape[:2], dtype=np.uint8)
    mask = PIL.Image.fromarray(mask)
    draw = PIL.ImageDraw.Draw(mask)
    xy = [tuple(point) for point in points]
    if shape_type == 'circle':
        assert len(xy) == 2, 'Shape of shape_type=circle must have 2 points'
        (cx, cy), (px, py) = xy
        d = math.sqrt((cx - px) ** 2 + (cy - py) ** 2)
        draw.ellipse([cx - d, cy - d, cx + d, cy + d], outline=1, fill=1)
    elif shape_type == 'rectangle':
        assert len(xy) == 2, 'Shape of shape_type=rectangle must have 2 points'
        draw.rectangle(xy, outline=1, fill=1)
    elif shape_type == 'line':
        assert len(xy) == 2, 'Shape of shape_type=line must have 2 points'
        draw.line(xy=xy, fill=1, width=line_width)
    elif shape_type == 'linestrip':
        draw.line(xy=xy, fill=1, width=line_width)
    elif shape_type == 'point':
        assert len(xy) == 1, 'Shape of shape_type=point must have 1 points'
        cx, cy = xy[0]
        r = point_size
        draw.ellipse([cx - r, cy - r, cx + r, cy + r], outline=1, fill=1)
    else:
        assert len(xy) > 2, 'Polygon must have points more than 2'
        draw.polygon(xy=xy, outline=1, fill=1)
    mask = np.array(mask, dtype=bool)
    return mask

def shapes_to_label(img_shape, shapes, label_name_to_value):
    cls = np.zeros(img_shape[:2], dtype=np.int32)
    ins = np.zeros_like(cls)
    instances = []
    for shape in shapes:
        points = shape['points']
        label = shape['label']
        group_id = shape.get('group_id')
        if group_id is None:
            group_id = uuid.uuid1()
        shape_type = shape.get('shape_type', None)

        cls_name = label
        instance = (cls_name, group_id)

        if instance not in instances:
            instances.append(instance)
        ins_id = instances.index(instance) + 1
        cls_id = 1
        # cls_id = label_name_to_value[cls_name]

        mask = shape_to_mask(img_shape[:2], points, shape_type)
        cls[mask] = cls_id
        ins[mask] = ins_id

    return cls, ins

def lblsave(filename, lbl):
    if os.path.splitext(filename)[1] != '.png':
        filename += '.png'
    # Assume label ranses [-1, 254] for int32,
    # and [0, 255] for uint8 as VOC.
    if lbl.min() >= 0 and lbl.max() <= 255:
        lbl_pil = PIL.Image.fromarray(lbl.astype(np.uint8), mode='L')
        lbl_pil.save(filename)
    else:
        raise ValueError(
            '[%s] Cannot save the pixel-wise class label as PNG. '
            'Please consider using the .npy format.' % filename
        )

if __name__ == '__main__':
    data_path = sys.argv[1]
    out_path = sys.argv[2]
    if not os.path.exists(out_path):
        os.makedirs(out_path)
    label_name_to_value = {
        '_background_': 0,
        'a': 1,
    }
    json_fns = glob.glob(os.path.join(data_path, '**/*.json'), recursive=True)
    out_lst = []
    for json_fn in tqdm(json_fns):
        with open(json_fn, 'r') as f:
            data = json.load(f)
        img_shape = (data['imageHeight'], data['imageWidth'])
        lbl, _ = shapes_to_label(img_shape, data['shapes'], label_name_to_value)
        image_fn = json_fn.replace('.json', '.jpg')
        label_fn = json_fn.replace('.json', '_label.png')
        if not os.path.exists(image_fn):
            print(image_fn + ' not exists')
            continue
        else:
            img = PIL.Image.open(image_fn)
            mask = PIL.Image.open(label_fn)
            if img.size != mask.size:
                print(image_fn, img.size, mask.size)
                continue
        lblsave(label_fn, lbl)
        out_lst.append(image_fn + ',' + label_fn)
    random.shuffle(out_lst)
    trn_num = int(len(out_lst) * 0.9)
    with open(os.path.join(out_path, 'train.txt'), 'w') as f:
        f.write('\n'.join(out_lst[:trn_num]))
    with open(os.path.join(out_path, 'val.txt'), 'w') as f:
        f.write('\n'.join(out_lst[trn_num:]))


执行以下命令:

python generate_label.py /path/to/ datasets

结果将在datasets目录下生成train.txt和val.txt,分别表示训练集和验证集。
参数说明:第一个参数为训练图像目录,第二个参数为输出文件夹

5.3 训练

运行以下命令:

python main.py -m train --save-dir save/ENet_Card --name ENet --dataset card --dataset-dir datasets --epochs 100 --height 512 --width 512 --print-step

参数说明:
-- save-dir:训练模型保存路径
–dataset:训练数据类型
–dataset-dir:train.txt和val.txt所在路径
–epochs:训练总epoch数
–height:输入高度
–width:输入宽度
–print-step:是否打印每个step的loss

5.4 测试

运行以下命令:

python demo.py demo_imgs save/ENet_Card/ENet results

参数说明:
第一个参数:输入图像路径
第二个参数: 训练得到的模型
第三个参数:输出文件夹路径

5.5 转成onnx

运行以下命令:

python convert_to_onnx.py --input save/ENet_Card/ENet --output save/ENet_Card/ENet.onnx

参数说明:
–input:训练得到的模型
–output: 输出onnx模型,可以采用opencv的dnn调用

6.模型推理

#include <iostream>
#include <string>
#include <vector>
#include <fstream>
#include <sstream>
#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>

void show_img(std::string name, const cv::Mat& img) {
	cv::namedWindow(name, 0);
	int max_rows = 500;
	int max_cols = 600;
	if (img.rows >= img.cols) {
		cv::resizeWindow(name, cv::Size(img.cols * max_rows / img.rows, max_rows));
	}
	else {
		cv::resizeWindow(name, cv::Size(max_cols, img.rows * max_cols / img.cols));
	}
	cv::imshow(name, img);
}

int main(int argc, char* argv[]) 
{
	std::string path = "D:/data/DataSet/book/";
	std::vector<std::string> filenames;
	cv::glob(path, filenames, false);

	for (auto img_name : filenames)
	{
		cv::Mat img = cv::imread(img_name);
		cv::Size reso(512, 512);
		cv::Mat blob = cv::dnn::blobFromImage(img, 1.0 / 255, reso,
			cv::Scalar(0, 0, 0), false, false);
		cv::dnn::Net net = cv::dnn::readNet("model/ENet.onnx");
		net.setInput(blob);
		auto t0 = cv::getTickCount();
		cv::Mat out = net.forward();

		std::cout << out << std::endl;
		auto t1 = cv::getTickCount();
		std::cout << "elapsed time: " << (t1 - t0) * 1000.0 / cv::getTickFrequency() << "ms" << std::endl;
		cv::Mat segm = cv::Mat::zeros(out.size[2], out.size[3], CV_8UC1);
		for (int i = 0; i < out.size[2] * out.size[3]; ++i)
		{
			if (out.ptr<float>(0, 0)[i] < out.ptr<float>(0, 1)[i])
			{
				segm.data[i] = 255;
			}
		}
		cv::resize(segm, segm, img.size(), 0.0, 0.0, cv::INTER_NEAREST);
		show_img("img", img);
		show_img("out", segm);
		cv::waitKey();
	}
	return 0;
}

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

7.实验结果

推理时间:
在这里插入图片描述
硬件要求:
在这里插入图片描述

基准测试:
Cityscapes:
在这里插入图片描述
ENet在类别IoU(类别间交叉验证)、iIoU(实例间交叉验证)以及整体类别IoU方面均优于SegNet。
在这里插入图片描述
CamVid:
在这里插入图片描述
SUN RGB-D:准确性的差异不应掩盖这两个网络之间巨大的性能差距。ENet可以实时处理图像,比嵌入式平台上的SegNet快近20倍。

这些性能指标的比较揭示了ENet在语义分割任务的几个关键评估领域中展现出更优的性能。具体来说,ENet在类别IoU方面的表现超过了SegNet,这意味着ENet在识别和分割不同类别的像素时更为准确。类别IoU是一个衡量模型在特定类别上分割精度的指标,它通过计算预测的分割区域与真实标注之间的重叠程度来评估模型的性能。

此外,ENet在iIoU方面也展现了更好的结果,这表明ENet在处理实例级别的分割任务时同样表现出色。实例分割不仅需要识别图像中的不同类别,还需要区分属于同一类别的不同实例,这对于理解复杂场景中的个体对象非常重要。

最后,整体类别IoU的提升进一步证实了ENet在整体分割性能上的优势。这一指标综合考虑了所有类别的分割结果,提供了模型整体性能的一个全面评估。

这些结果表明,ENet在处理语义分割任务时,不仅在单个类别上表现出色,而且在整体分割精度上也超越了SegNet。这使得ENet成为实时语义分割应用中一个非常有力的竞争者,尤其是在需要高精度分割结果的场景中。

8.总结

ENet所提出的这一创新神经网络架构,专为语义分割任务量身打造,其核心设计理念在于充分发挥嵌入式设备上有限资源的潜力。在这一目标的指引下,ENet的工作取得了显著的成效,不仅成功实现了与现有技术相比更为高效的计算性能,而且在某些情况下,还实现了与之相匹配甚至更优的结果,即便这些现有技术对计算能力和内存的需求远高于ENet的设计。

ENet在NVIDIA TX1硬件平台上的应用展示了其实时、便携的嵌入式解决方案的强大实力。这一平台的高效性能不仅适用于移动设备,ENet的实验还表明,即使在如NVIDIA Titan X这样的高端GPU上,ENet同样能够发挥出色的效果。这一点在需要处理大量高分辨率图像的数据中心级应用中尤为重要,因为ENet能够以更快、更高效的方式执行大规模的计算任务,从而可能为企业节省大量的成本。
ENet不仅在嵌入式设备上提供了实时语义分割的可能性,而且在高性能计算平台上也展现了其强大的计算能力。这种跨平台的高效性能,使得ENet能够适应各种不同的应用场景,从移动设备到数据中心,都能提供准确、快速的语义分割服务。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知来者逆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值