Main Content

训练和部署用于语义分割的全卷积网络

此示例说明如何使用 GPU Coder™ 在 NVIDIA® GPU 上训练和部署全卷积语义分割网络。

语义分割网络对图像中的每个像素进行分类,从而生成按类分割的图像。语义分割的应用包括自动驾驶中的道路分割以及医疗诊断中的癌细胞分割。要了解详细信息,请参阅Getting Started with Semantic Segmentation Using Deep Learning (Computer Vision Toolbox)

为了说明训练过程,此示例将训练 FCN-8s [1],这是一种专门用于语义图像分割的卷积神经网络 (CNN)。其他类型的用于语义分割的网络包括全卷积网络,如 SegNet 和 U-Net。您也可以将此训练过程应用于这些网络。

此示例使用剑桥大学的 CamVid 数据集 [2] 进行训练。此数据集是包含驾驶时获得的街道级视图的图像集合。该数据集提供了 32 个语义类的像素级标签,包括汽车、行人和道路。

第三方前提条件

必需

  • 支持 CUDA® 的 NVIDIA GPU 和兼容驱动程序。

可选

验证 GPU 环境

使用 coder.checkGpuInstall (GPU Coder) 函数验证运行此示例所需的编译器和库是否已正确设置。

envCfg = coder.gpuEnvConfig("host");
envCfg.DeepLibTarget = "cudnn";
envCfg.DeepCodegen = 1;
envCfg.Quiet = 1;
coder.checkGpuInstall(envCfg);

设置

此示例创建的全卷积语义分割网络具有从 VGG-16 网络初始化的权重。vgg16 函数检查是否存在 Deep Learning Toolbox™ Model for VGG-16 Network 支持包,并返回预训练 VGG-16 模型。

vgg16();

下载 FCN 的预训练版本。借助此预训练模型,您无需等待训练完成,即可运行整个示例。doTraining 标志控制示例是使用示例的训练网络还是预训练的 FCN 网络来生成代码。

doTraining = false;
if ~doTraining
    pretrainedURL = "https://www.mathworks.com/supportfiles" + ...
        "/gpucoder/cnn_models/fcn/FCN8sCamVid.mat";
    disp("Downloading pretrained FCN (448 MB)...");
    websave("FCN8sCamVid.mat",pretrainedURL);
end
Downloading pretrained FCN (448 MB)...

下载 CamVid 数据集

从以下 URL 下载 CamVid 数据集。

imageURL = "http://web4.cs.ucl.ac.uk/staff/g.brostow/" + ...
    "MotionSegRecData/files/701_StillsRaw_full.zip";
labelURL = "http://web4.cs.ucl.ac.uk/staff/g.brostow/" + ...
    "MotionSegRecData/data/LabeledApproved_full.zip";

outputFolder = fullfile(pwd,"CamVid");

if ~exist(outputFolder, "dir")
   
    mkdir(outputFolder)
    labelsZip = fullfile(outputFolder,"labels.zip");
    imagesZip = fullfile(outputFolder,"images.zip");   
    
    disp("Downloading 16 MB CamVid dataset labels..."); 
    websave(labelsZip, labelURL);
    unzip(labelsZip, fullfile(outputFolder,"labels"));
    
    disp("Downloading 557 MB CamVid dataset images...");  
    websave(imagesZip, imageURL);       
    unzip(imagesZip, fullfile(outputFolder,"images"));    
end

数据下载时间取决于您的 Internet 连接。下载操作完成后,示例执行才会继续。您也可以使用 Web 浏览器先将数据集下载到本地磁盘。然后,使用 outputFolder 变量指向所下载文件的位置。

加载 CamVid 图像

使用 imageDatastore 加载 CamVid 图像。通过 imageDatastore 可将大量图像高效加载至磁盘。

imgDir = fullfile(outputFolder,"images","701_StillsRaw_full");
imds = imageDatastore(imgDir);

显示其中一个图像。

I = readimage(imds,25);
I = histeq(I);
imshow(I)

加载 CamVid 像素标注图像

使用 pixelLabelDatastore (Computer Vision Toolbox) 加载 CamVid 像素标注图像数据。pixelLabelDatastore 将像素标签数据和标签 ID 封装到类名映射中。

按照 SegNet 论文 [3] 中描述的训练方法,将 CamVid 中的 32 个原始类分组为 11 个类。指定这些类。

classes = [
    "Sky"
    "Building"
    "Pole"
    "Road"
    "Pavement"
    "Tree"
    "SignSymbol"
    "Fence"
    "Car"
    "Pedestrian"
    "Bicyclist"
    ];

要将 32 个类减少为 11 个类,需要将原始数据集中的多个类组合在一起。例如,"Car" 是 "Car"、"SUVPickupTruck"、"Truck_Bus"、"Train" 和 "OtherMoving" 的组合。使用 camvidPixelLabelIDs 支持函数返回分组的标签 ID。

labelIDs = camvidPixelLabelIDs();

使用类和标签 ID 创建 pixelLabelDatastore

labelDir = fullfile(outputFolder,"labels");
pxds = pixelLabelDatastore(labelDir,classes,labelIDs);

读取一个像素标注图像,并将其叠加在图像上方显示。

C = readimage(pxds,25);
cmap = camvidColorMap;
B = labeloverlay(I,C,"ColorMap",cmap);
imshow(B)
pixelLabelColorbar(cmap,classes);

没有颜色叠加的区域没有像素标签,在训练过程中不被使用。

分析数据集统计信息

要查看 CamVid 数据集中类标签的分布,请使用 countEachLabel (Computer Vision Toolbox)。此函数按类标签计算像素数。

tbl = countEachLabel(pxds)
tbl=11×3 table
         Name         PixelCount    ImagePixelCount
    ______________    __________    _______________

    {'Sky'       }    7.6801e+07      4.8315e+08   
    {'Building'  }    1.1737e+08      4.8315e+08   
    {'Pole'      }    4.7987e+06      4.8315e+08   
    {'Road'      }    1.4054e+08      4.8453e+08   
    {'Pavement'  }    3.3614e+07      4.7209e+08   
    {'Tree'      }    5.4259e+07       4.479e+08   
    {'SignSymbol'}    5.2242e+06      4.6863e+08   
    {'Fence'     }    6.9211e+06       2.516e+08   
    {'Car'       }    2.4437e+07      4.8315e+08   
    {'Pedestrian'}    3.4029e+06      4.4444e+08   
    {'Bicyclist' }    2.5912e+06      2.6196e+08   

按类可视化像素计数。

frequency = tbl.PixelCount/sum(tbl.PixelCount);

bar(1:numel(classes),frequency)
xticks(1:numel(classes)) 
xticklabels(tbl.Name)
xtickangle(45)
ylabel("Frequency")

理想情况下,所有类都有相同数量的观测值。CamVid 中的类是不平衡的,这是街景汽车数据集的常见问题。此类场景的天空、建筑物和道路像素比行人和骑车人像素多,因为天空、建筑物和道路覆盖了图像中的更多区域。如果处理不当,这种不平衡可能对学习过程不利,因为学习会偏向于占主导的类。在此示例的稍后部分,您将使用类权重来处理此问题。

调整 CamVid 数据的大小

CamVid 数据集中的图像大小为 720×960。为减少训练时间和内存使用量,请使用 resizeCamVidImagesresizeCamVidPixelLabels 支持函数将图像和像素标签图像的大小调整为 360×480。

imageFolder = fullfile(outputFolder,"imagesResized",filesep);
imds = resizeCamVidImages(imds,imageFolder);

labelFolder = fullfile(outputFolder,"labelsResized",filesep);
pxds = resizeCamVidPixelLabels(pxds,labelFolder);

准备训练集和测试集

使用数据集中 60% 的图像训练 SegNet。其余图像用于测试。以下代码将图像和像素标签数据随机分成训练集和测试集。

[imdsTrain,imdsTest,pxdsTrain,pxdsTest] = partitionCamVidData(imds ...
    ,pxds);

60/40 拆分产生以下数量的训练和测试图像:

numTrainingImages = numel(imdsTrain.Files)
numTrainingImages = 421
numTestingImages = numel(imdsTest.Files)
numTestingImages = 280

创建网络

使用 fcnLayers (Computer Vision Toolbox) 创建使用 VGG-16 权重初始化的全卷积网络层。fcnLayers 函数执行网络变换以从 VGG-16 传递权重,并添加语义分割所需的其他层。fcnLayers 函数的输出是一个表示 FCN 的 LayerGraph 对象。LayerGraph 对象封装了网络层及各层之间的连接。

imageSize = [360 480];
numClasses = numel(classes);
lgraph = fcnLayers(imageSize,numClasses);

图像大小的选择基于数据集中的图像大小。类数量的选择基于 CamVid 中的类。

使用类权重平衡类

CamVid 中的类并不平衡。为了改进训练,您可以使用先前通过 countEachLabel (Computer Vision Toolbox) 函数计算的像素标签计数,计算具有中位数频率的类的权重 [3]。

imageFreq = tbl.PixelCount ./ tbl.ImagePixelCount;
classWeights = median(imageFreq) ./ imageFreq;

使用 pixelClassificationLayer (Computer Vision Toolbox) 指定类权重。

pxLayer = pixelClassificationLayer("Name","labels","Classes", ...
    tbl.Name,"ClassWeights",classWeights)
pxLayer = 
  PixelClassificationLayer with properties:

            Name: 'labels'
         Classes: [11×1 categorical]
    ClassWeights: [11×1 double]
      OutputSize: 'auto'

   Hyperparameters
    LossFunction: 'crossentropyex'

通过删除当前 pixelClassificationLayer 并添加新层,来更新具有新 pixelClassificationLayer 的 SegNet 网络。当前 pixelClassificationLayer 命名为 pixelLabels。使用 removeLayers 函数将其删除,使用 addLayers 函数添加一个新层,然后使用 connectLayers 函数将新层连接到网络的其余部分。

lgraph = removeLayers(lgraph,"pixelLabels");
lgraph = addLayers(lgraph, pxLayer);
lgraph = connectLayers(lgraph,"softmax","labels");

选择训练选项

训练的优化算法是 Adam(派生自自适应矩估计)。使用 trainingOptions 函数指定用于 Adam 的超参数。

options = trainingOptions("adam", ...
    "InitialLearnRate",1e-3, ...
    "MaxEpochs",100, ...  
    "MiniBatchSize",4, ...
    "Shuffle","every-epoch", ...
    "CheckpointPath", tempdir, ...
    "VerboseFrequency",2);

大小为 4 的 MiniBatchSize 可减少训练时的内存使用量。您可以根据系统中的 GPU 内存量增大或减小此值。

CheckpointPath 设置为临时位置。此名称-值对组让您能够在每轮训练结束时保存网络检查点。如果由于系统故障或停电而导致训练中断,您可以从保存的检查点处恢复训练。确保 CheckpointPath 指定的位置有足够的空间来存储网络检查点。

数据增强

数据增强可通过在训练期间随机变换原始数据来提高网络准确度。通过使用数据增强,您可以为训练数据添加更多变化,而不必增加带标签的训练样本的数量。要对图像和像素标签数据应用相同的随机变换,请使用数据存储组合和变换。首先,合并 imdsTrainpxdsTrain

dsTrain = combine(imdsTrain, pxdsTrain);

接下来,使用数据存储变换应用在支持函数 augmentImageAndLabel 中定义的所需数据增强。此处使用随机左/右翻转和随机 X/Y 平移 +/- 10 个像素来进行数据增强。

xTrans = [-10 10];
yTrans = [-10 10];
dsTrain = transform(dsTrain, @(data)augmentImageAndLabel(data, ...
    xTrans,yTrans));

请注意,数据增强不适用于测试数据和验证数据。理想情况下,测试数据和验证数据应代表原始数据并且保持不变,以便进行无偏置的评估。

开始训练

如果 doTraining 标志为 true,则使用 trainNetwork 开始训练。否则,请加载预训练的网络。

训练在具有 12 GB GPU 内存的 NVIDIA Titan Xp 上进行了验证。如果您的 GPU 内存较少,则可能内存不足。如果系统的内存不足,请尝试将 trainingOptions 中的 MiniBatchSize 属性降低到 1。根据您的 GPU 硬件情况,训练此网络需要大约 5 个小时或更长时间。

doTraining = false;
if doTraining    
    [net, info] = trainNetwork(dsTrain,lgraph,options);
    save("FCN8sCamVid.mat","net");
end

将 DAG 网络对象保存为名为 FCN8sCamVid.mat 的 MAT 文件。在代码生成过程中将使用此 MAT 文件。

执行 MEX 代码生成

fcn_predict 函数以图像作为输入,并使用保存在 FCN8sCamVid.mat 文件中的深度学习网络对图像执行预测。该函数将 FCN8sCamVid.mat 中的网络对象加载到持久变量 mynet 中,并在后续的预测调用中重用该持久性对象。

type("fcn_predict.m")
function out = fcn_predict(in)
%#codegen
% Copyright 2018-2019 The MathWorks, Inc.

persistent mynet;

if isempty(mynet)
    mynet = coder.loadDeepLearningNetwork('FCN8sCamVid.mat');
end

% pass in input
out = predict(mynet,in);

为 MEX 对象生成一个 GPU 配置对象以将目标语言设置为 C++。使用 coder.DeepLearningConfig (GPU Coder) 函数创建一个 cuDNN 深度学习配置对象,并将其赋给 GPU 代码配置对象的 DeepLearningConfig 属性。运行 codegen (MATLAB Coder) 命令以指定输入大小为 360×480×3。此大小对应于 FCN 的输入层。

cfg = coder.gpuConfig("mex");
cfg.TargetLang = "C++";
cfg.DeepLearningConfig = coder.DeepLearningConfig("cudnn");
codegen -config cfg fcn_predict -args {ones(360,480,3,"uint8")} -report
Code generation successful: View report

运行生成的 MEX

加载并显示输入图像。

im = imread("testImage.png");
imshow(im);

对输入图像调用 fcn_predict_mex 来运行预测。

predict_scores = fcn_predict_mex(im);

predict_scores 变量是一个三维矩阵,它具有 11 个通道,分别对应于每个类的像素级预测分数。使用最高预测分数计算通道以获得像素级标签。

[~,argmax] = max(predict_scores,[],3);

在输入图像上叠加分割标签并显示分割区域。

classes = [
    "Sky"
    "Building"
    "Pole"
    "Road"
    "Pavement"
    "Tree"
    "SignSymbol"
    "Fence"
    "Car"
    "Pedestrian"
    "Bicyclist"
    ];

cmap = camvidColorMap();
SegmentedImage = labeloverlay(im,argmax,"ColorMap",cmap);
figure
imshow(SegmentedImage);
pixelLabelColorbar(cmap,classes);

支持函数

function data = augmentImageAndLabel(data, xTrans, yTrans)
% Augment images and pixel label images using random reflection and
% translation.

for i = 1:size(data,1)
    
    tform = randomAffine2d(...
        "XReflection",true,...
        "XTranslation", xTrans, ...
        "YTranslation", yTrans);
    
    % Center the view at the center of image in the output space while
    % allowing translation to move the output image out of view.
    rout = affineOutputView(size(data{i,1}), tform, "BoundsStyle", ...
        "centerOutput");
    
    % Warp the image and pixel labels using the same transform.
    data{i,1} = imwarp(data{i,1}, tform, "OutputView", rout);
    data{i,2} = imwarp(data{i,2}, tform, "OutputView", rout);
    
end
end

参考资料

[1] Long, J., E. Shelhamer, and T. Darrell."Fully Convolutional Networks for Semantic Segmentation."Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition, 2015, pp. 3431–3440.

[2] Brostow, G. J., J. Fauqueur, and R. Cipolla."Semantic object classes in video:A high-definition ground truth database."Pattern Recognition Letters.Vol. 30, Issue 2, 2009, pp 88-97.

[3] Badrinarayanan, V., A. Kendall, and R. Cipolla."SegNet:A Deep Convolutional Encoder-Decoder Architecture for Image Segmentation." arXiv preprint arXiv:1511.00561, 2015.