Press "Enter" to skip to content

超详细!使用 OpenCV 深度学习模块在图像分类下的应用实践

深度学习理论的广泛研究促进了其在不同场景的应用。在计算机视觉领域,图像分类、目标检测、语义分割和视觉风格变换等基础任务的性能也因为采用了深度学习的方法而有了飞跃性的提升。本章将为读者梳理深度学习方法在这些基本应用场景的应用情况,并结合OpenCV深度学习模块的示例程序,从源代码和实际运行两个层面进行讲解。 下文对书中图像分类部分内容进行摘录:

 

图像分类是计算机视觉领域的基础任务之一,在各种基于视觉的人工智能应用中,图像分类都扮演着重要的角色。例如,在智能机器人应用中,我们需要对所采集的视频中的每一帧进行主要物体的检测和分类,并以此作为进一步决策的基础。

 

近些年,图像分类与深度学习的飞速发展有着密不可分的关系。在2012年的ILSVRC (ImageNet Large Scale Visual Recognition Competition,ImageNet大规模视觉识别挑战赛)大赛上,AlexNet横空出世,以压倒性优势战胜了传统图像分类算法而夺得冠军,开启了计算机视觉领域的深度学习革命。2015年,ResNet首次在图像分类准确度上战胜人类。2017年,随着SENet的夺冠,最后一届ILSVRC大赛落下帷幕。下面为大家梳理一下历届ILSVRC大赛中出现的经典网络结构。

 

图像分类经典网络结构

 

自2012年ILSVRC大赛AlexNet夺冠以来,直至2017年最后一届SENet夺冠,所有冠军都被各种深度神经网络所摘得。历届ILSVRC大赛的经典网络结构及其特点如表9-1所示。

 

 

这些网络结构不仅可应用在图像分类中,而且可作为其他计算机视觉任务(如目标检测、语义分割和视觉风格变换)的骨干(backbone)网络,用来提取图像特征。因此,它们对整个计算机视觉技术的发展有着深远的影响。

 

下面我们摘录OpenCV官方Wiki上的DNN模块运行效率统计表,看一下AlexNet、GoogLeNet和ResNet-50在OpenCV DNN模块中的运行效率。

 

测试系统软硬件配置如下:

 

 

各软件组件的版本信息如表9-2所示。

 

 

CPU实现的运行时间如表9-3所示,该时间取的是50次运行的中位数时间,中位数时间可以排除多次推理运算中某些过于异常的值对平均值的干扰。另外,所有网络模型都采用32位浮点数据格式进行计算。在神经网络的推理计算中,可以采用量化方法把32位浮点精度的模型参数降低到16位浮点精度以节省数据读取带宽提高运算效率,但是并不是所有算法都支持针对16位浮点精度的实现,为了便于比较,测试都采用32位浮点精度。

 

 

GPU实现的运行时间如表9-4所示。

 

 

从上面的数据可以看到,DNN模块的OpenCL实现跟原生C++实现性能相近,而Intel-Caffe MKLDNN的加速性能最好,原因是多方面的。首先MKLDNN是针对Intel CPU进行高度优化的神经网络计算库,能够充分发挥Intel CPU的性能。其次,该测试使用的CPU硬件性能比较强劲(8核心,4.0GHz运行频率),而集成的GPU是中低配置。最后,测试的3种网络模型的运算量不算太大,未能充分发挥GPU的并发特性。

 

接下来,我们以GoogLeNet(Inception-v1)为例,详细讲解其网络结构和设计原理,然后结合OpenCV中的图像分类示例程序讲解GoogLeNet模型的实际使用。

 

GoogLeNet

 

GoogLeNet自2014年提出以来,总共演进了4个版本,由于第1版是后续几个版本的基础,本节主要介绍2014年的第1版,即GoogLeNet v1。

 

GoogLeNet v1是2014年ILSVRC大赛的冠军模型,它延续了自LeNet以来的典型卷积网络结构,即多个卷积层前后堆叠,然后通过全连接层输出最终的特征值。GoogLeNet的结构如图9-1所示。

 

 

下面对图9-1中各列进行解释。type列表示层或者模块的类型,其中inception代表一个Inception模块,GoogLeNet中总共堆叠了9个Inception模块,convolution表示卷积层,max pool表示最大池化层,avg pool表示平均池化层,dropout代表随机裁剪操作,linear是全连接层,softmax表示最后对输出特征值进行sotfmax操作。patch size列表示卷积核大小,stride表示卷积运算的步进值。output size列表示输出特征图的长、宽和通道数。Depth列表示该层或者模块重复连接的次数。#1×1,#3×3,#5×5列分别表示Inception模块中的1×1,3×3,5×5卷积核大小的卷积分支的输出通道数。pool proj列表示池化投影的输出通道数。#3×3 reduce和#5×5 reduce列表示Inception结构中3×3和5×5卷积核卷积之前的1×1卷积的输出通道数。params列表示参数数目。ops列表示运算量。Inception模块是GoogLeNet的最大创新点,它的初衷是增加卷积核尺寸种类的同时降低训练参数数量,下面对Inception v1模块进行讲解,它的结构如图9-1所示。

 

Inception模块使用1×1卷积对前层数据进行降维处理并分成多路,然后用3×3,5×5卷积对降维后的分支进行卷积运算,同时将各个卷积结果和3×3最大池化的结果按通道进行连接。这种创新的结构使得网络参数大大降低的同时保留了很好的特征表达能力,达到了深度和参数数量的双赢。

 

为什幺使用多种尺寸的卷积核有助于提高特征表达能力呢?我们以图9-3为例,最左边的狗占据了图的大部分,中间的狗占了图的一部分,而最右边的狗占了图的很小一部分。采用多种尺寸的卷积核可以学习到不同尺度的特征,使网络具有更好的特征适应性。

 

 

接下来,我们结合DNN模块图像分类示例程序看一下图像分类应用的具体实现。

 

图像分类程序源码分析

 

我们借助OpenCV的示例程序来介绍图像分类应用的主要步骤。OpenCV DNN模块示例程序囊括了各种不同应用场景,它们有着相似的代码结构和流程,如图9-4所示。各种示例应用源代码的区别主要体现在最后一步:推理结果的解析和可视化。本节将详细讲解代码的每个步骤,之后各节的源码分析将重点聚焦于应用特定的参数及推理结果的解析和可视化。

 

 

下面分析图像分类示例程序源码。

 

首先引入必要的头文件,参见代码清单9-1。其中,fstream和sstream是C++标准库头文件,用于文件读取和文本处理。dnn.hpp、imgproc.hpp、highgui.hpp提供OpenCV API声明,common.hpp提供了一些DNN示例程序通用的函数,例如,查找输入文件位置,从模型配置文件中读取默认的运行时参数等。

 

9-1 引入必要的头文件

 

#include <fstream>
#include <sstream>
#include <opencv2/dnn.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include "common.hpp"

 

代码清单9-2定义了命令行参数,下面逐一讲解。

 

 

 

9-2 命令行参数定义

 

std::string keys=
    "{ help  h | | Print help message. }"
    "{ @alias  | | An alias name of model to extract preprocessing parameters from models.yml file. }"
    "{ zoo  | models.yml | An optional path to file with preprocessing parameters }"
    "{ input i  | | Path to input image or video file. Skip this argument to capture frames from a camera.}"
    "{ framework f | | Optional name of an origin framework of the model. Detect it automatically if it does not set. }"
    "{ classes  | Optional path to a text file with names of classes. }"
    "{ backend  | 0 | Choose one of computation backends: "
                     "0: automatically (by default), "
                     "1: Halide language (http://halide-lang.org/), "
                     "2: Intel's Deep Learning Inference Engine (https://software.intel.com/openvino-toolkit), "
                    "3: OpenCV implementation }"
    "{ target  | 0 | Choose one of target computation devices: "
                     "0: CPU target (by default), "
                     "1: OpenCL, "
                     "2: OpenCL fp16 (half-float precision), "
                     "3: VPU }";

 

接下来引用命名空间,参见代码清单9-3。我们的代码用到了cv和dnn命名空间 中的API,通过显式声明命名空间,方便后续的API调用。

 

9-3 声明命名空间及定义全局变量

 

using namespace cv;
using namespace dnn;

 

接下来定义用于存放类别名称的变量classes:

 

std::vector<std::string> classes;

 

下面进入主函数。

 

首先,解析命令行参数,参见代码清单9-4。

 

9-4 主函数(解析命令行参数)

 

int main(int argc, char** argv)
{
    CommandLineParser parser(argc, argv, keys);
    const std::string modelName=parser.get<String>("@alias");
    const std::string zooFile=parser.get<String>("zoo");
    keys +=genPreprocArguments(modelName, zooFile);
    parser=CommandLineParser(argc, argv, keys);
    parser.about("Use this script to run classification deep learning networks using OpenCV.");
    if (argc==1 || parser.has("help"))
    {
        parser.printMessage();
        return 0;
    }
    float scale=parser.get<float>("scale");
    Scalar mean=parser.get<Scalar>("mean");
    bool swapRB=parser.get<bool>("rgb");
    int inpWidth=parser.get<int>("width");
    int inpHeight=parser.get<int>("height");
    String model=findFile(parser.get<String>("model"));
    String config=findFile(parser.get<String>("config"));
    String framework=parser.get<String>("framework");
    int backendId=parser.get<int>("backend");
    int targetId=parser.get<int>("target");

 

如果命令行参数提供了类别文件路径,则解析类别文件并将类别名称存储到全局变量classes,参见代码清单9-5。

 

9-5 主函数(类别文件解析)

 

if (parser.has("classes"))
{
    std::string file=parser.get<String>("classes");
    std::ifstream ifs(file.c_str());
    if (!ifs.is_open())
        CV_Error(Error::StsError, "File " + file + " not found");
    std::string line;
    while (std::getline(ifs, line))
    {
        classes.push_back(line);
    }
}

 

接下来进行异常情况检查,包括命令行参数异常,以及缺失模型文件异常,参加代码清单9-6。

 

9-6 主函数(异常情况检查)

 

if (!parser.check())
    {
        parser.printErrors();
        return 1;
    }
    CV_Assert(!model.empty());

 

加载网络模型,创建DNN模块网络对象,并设置加速后端和目标运算设备,参加代码清单9-7。

 

9-7 主函数(初始化网络并创建显示窗口)

 

Net net=readNet(model, config, framework);
net.setPreferableBackend(backendId);
net.setPreferableTarget(targetId);

 

接下来,创建用于显示结果的窗口对象。代码如下:

 

static const std::string kWinName="Deep learning image classification in OpenCV";
namedWindow(kWinName, WINDOW_NORMAL);

 

然后,创建图像输入对象cap,用于读取指定的图片、视频文件,参见代码清单9-8。如果没有指定图片或视频文件,则从摄像头读取视频帧。

 

9-8 主函数(创建图像输入对象)

 

VideoCapture cap;
if (parser.has("input"))
cap.open(parser.get<String>("input"));
else
    cap.open(0);

 

接下来进入图像处理循环,循环起始部分通过cap对象读取一帧图像,参见代码清单9-9。

 

9-9 图像处理循环(读取一帧图像)

 

Mat frame, blob;
while (waitKey(1) < 0)
{
    cap >> frame;
    if (frame.empty())
    {
        waitKey();
        break;
    }

 

然后调用blobFromImage()函数将读入的图像转换成网络模型的输入(blob),并设置网络对象,参见代码清单9-10。blobFromImage()函数会对图像进行一系列的预处理,包括调整大小、减均值、交换红蓝颜色通道等,最终返回一个一维数组(N、C、H、W)。其中,N代表批大小,实时应用中通常为1,即一次处理一帧图像数据;C代表图像通道数,一般为3,即R、G、B三种颜色;H、W分别代表图像的高度和宽度。

 

9-10 图像处理循环(设置网络输入)

 

blobFromImage(frame, blob, scale, Size(inpWidth, inpHeight),
mean, swapRB, false);
net.setInput(blob);

 

接下来运行网络模型推理,代码如下:

 

Mat prob=net.forward();

 

网络推理的输出数据对象prob包含1000个概率值,分别对应1000个图像类别。

 

至此,网络推理运算部分结束,接下来进行推理结果的解析和可视化,参见代码清单9-11和代码清单9-12。

 

9-11 图像处理循环(解析网络推理输出)

 

Point classIdPoint;
double confidence;
// 找到概率值最大的类别id,该类别为图像所属分类
minMaxLoc(prob.reshape(1, 1), 0, &confidence, 0, &classIdPoint);
int classId=classIdPoint.x;

 

9-12 图像处理循环(可视化推理结果)

 

// 获取网络推理运算耗时,并叠加到原始图像上
std::vector<double> layersTimes;
double freq=getTickFrequency() / 1000;
double t=net.getPerfProfile(layersTimes) / freq;
std::string label=format("Inference time: %.2f ms", t);
putText(frame, label, Point(0, 15), FONT_HERSHEY_SIMPLEX,
          0.5, Scalar(0, 255, 0));
// 将图像类别标签和概率值叠加到原始图像上
label=format("%s: %.4f", (classes.empty() ?
                                 format("Class #%d", classId).c_str() :
                                 classes[classId].c_str()),confidence);
putText(frame, label, Point(0, 40), FONT_HERSHEY_SIMPLEX,
           0.5, Scalar(0, 255, 0));
// 显示图像
imshow(kWinName, frame);
    }
    // 循环结束,退出主函数
    return 0;
}

 

以上内容摘自 《OpenCV深度学习应用与性能优化实践》 一书。

Be First to Comment

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注