OpenCV深度学习应用与性能优化实践
上QQ阅读APP看书,第一时间看更新

2.3 API层

在OpenCV中,深度学习模块的原生接口基于C++语言,用户通过API层可以创建新的层类型,构建神经网络结构,加载不同框架的模型,获取网络参数,执行网络推理,获取推理结果。C++程序需要包含module/dnn/include/opencv2/dnn.hpp,它是对module/dnn/include/opencv2/dnn/dnn.hpp的封装,后者包含了API层的所有数据结构定义和函数声明,本节将讲解其中的关键类:Net、Layer、LayerParams,以及常用函数。

提示 在阅读OpenCV源码的过程中,经常会看到cv2或者opencv2的字样,这比较容易引起读者的困惑,这里做一下说明:2009年10月发布的OpenCV第2版,原生API从C切换成了C++,这是一次很大的版本改进,目录名也随之变成了opencv2,之后这个目录名沿用至今;还有的地方用cv2(例如,OpenCV的Python模块的名字就是cv2),也是出于这个原因。

2.3.1 Layer类及如何定制一个新的层类型

Layer类是所有层类型的父类,具体层类型在实现的时候需要继承该类。深度学习模块内置了30多种常用Layer类型的支持,但是在某些情况下,开发者为了运行自己设计的网络模型,需要针对性地定制自己网络中特有的Layer类型。下面以一个假设的FooLayer为例,讲解如何定制新的Layer类型。

第1步:在all_layers.hpp 参见https://github.com/opencv/opencv/tree/4.1.0/modules/dnn/include/opencv2/dnn/all_layers.hpp。中定义新的Layer类型。以下代码定义了一个名为FooLayer的层类型:


Class CV_EXPORTS FooLayer : public Layer
{
   public:
   int param1;
   int param2;

   static Ptr<FooLayer> create(const LayerParams& params);
}

其中,CV_EXPORTS是跨平台宏,表示后面的符号需要被动态库暴露出来。param1、param2是FooLayer类型的属性,按实际需要定义。create方法用来创建该类型的实例,每个定制Layer类型都要实现。

第2步:在modules/dnn/src/layers目录中创建foo_layer.cpp文件,实现基类定义的虚函数及create方法。必须实现的虚函数包括forward函数和getMemoryShapes函数。它们的原型如下面代码所示


virtual void forward(InputArrayOfArrays inputs, OutputArrayOfArrays outputs,
   OutputArrayOfArrays internals);
virtual bool getMemoryShapes(const std::vector<MatShape> &inputs,
    const int requiredOutputs,
    std::vector<MatShape> &outputs,
    std::vector<MatShape> &internals) const;

其中,forward函数是定制Layer最核心的部分,用来实现层的推理运算。inputs是输入数据。outputs是输出数据,即运算结果。internals是运算用到的中间数据。getMemoryShapes函数根据输入shape计算输出shape和内部数据的shape。DNN引擎根据计算出的shape分配内存。参数inputs表示层输入的形状。requiredOutputs表示该层输出张量对象的个数。outputs表示层输出shape。internals表示内部数据的形状。另外一个比较重要的虚函数如下:


virtual void finalize(const std::vector<Mat*> &input,
          std::vector<Mat> &output);

它的调用时机是DNN引擎分配好所有输入/输出内存之后,调用forward方法之前。可以通过这个函数做一些相关的初始化工作。例如,convolution层用来计算padding的宽和高。这个函数是可选的,根据Layer的具体逻辑决定是否实现。

接下来需要实现create方法,函数接口定义如下:


static Ptr<FooLayer> create(const LayerParams& params);

create的功能是创建FooLayer类型的对象,用智能指针保护,并返回智能指针。参数params表示层参数,具体定义如下:


    class CV_EXPORTS LayerParams : public Dict
    {
    public:
        std::vector<Mat> blobs; // 层的权重和偏置
        String name; // 层的名字
        String type; // 层的类型
    };

第3步:在init.cpp 参见https://github.com/opencv/opencv/tree/4.1.0/modules/dnn/src/init.cpp。中注册FooLayer。具体做法是在函数initializeLayerFactory中加入下面的代码:


CV_DNN_REGISTER_LAYER_CLASS(Foo, FooLayer);

其中,Foo表示层的名字,FooLayer表示类型。

至此,定制一个新的Layer类型所需的最少步骤已经完成。如果要实现更丰富的功能,则可根据实际需要实现Layer基类中的其他虚函数。

[1] 参见https://github.com/opencv/opencv/tree/4.1.0/modules/dnn/include/opencv2/dnn/all_layers.hpp。

[2] 参见https://github.com/opencv/opencv/tree/4.1.0/modules/dnn/src/init.cpp。

2.3.2 Net类

Net类提供了一个创建和管理网络的接口,下面讲解它的主要成员函数。

(1)readFromModelOptimizer

函数原型


CV_WRAP static Net readFromModelOptimizer(const String& xml, const String& bin);

函数功能:从Intel模型优化器格式的模型创建网络对象。2.3.3节会介绍readNet,那是一个更通用的加载各种类型网络模型的函数。

参数说明:

·xml:网络结构描述文件,采用的是XML格式。

·bin:训练好的网络权重值,采用的是二进制格式。

注意 Intel模型优化器输出的网络模型包括两个文件,一个是XML格式的网络结构描述文件,另一个是二进制格式的权重文件。

(2)addLayer

函数原型


int addLayer(const String &name, const String &type, LayerParams &params);

函数功能:向网络中添加一个层对象。

参数说明:

·name:层对象名字。

·type:层对象类型。

·params:层对象参数。

·返回值:层对象id,如果创建失败则返回-1。

(3)connect

函数原型:该函数有两个版本,原型定义如下。


1    void connect(String outPin, String inpPin);
2    void connect(int outLayerId, int outNum, int inpLayerId, int inpNum);

1)void connect(String outPin,String inpPin)。

函数功能:连接层的某个输出和另一个层的某个输入。

参数说明:

·outPin:层的某个输出。

·inpPin:层的某个输入。

2)void connect(int outLayerId,int outNum,int inpLayerId,int inpNum)。

函数功能:连接层的某个输出和另一个层的某个输入。和上面connect函数的区别是,输入参数不同。

参数说明:

·outLayerId:输出层id。

·outNum:输出层的输出端口id。

·inpLayerId:输入层id。

·inpNum:输入层的输入端口id。

注意 每个层的输出端口可能有多个,用输出id来确定,多数情况下只用到第一个输出端口。每个层的输入端口可能有多个,用输入id来确定。

(4)addLayerToPrev

函数原型


int addLayerToPrev(const String &name, const String &type,
LayerParams &params);

函数功能:向网络中添加一个层对象,并将它的第一个输入端口和前一层的第一个输出端口相连。这是addLayer函数和connect函数的结合,方便层对象的添加和连接。

参数说明:具体参数和返回值同addLayer函数。

(5)forward

函数原型:该函数有4个版本,原型定义如下。


1    CV_WRAP Mat forward (const String& outputName=String ());
2    CV_WRAP void forward (OutputArrayOfArrays outputBlobs,
                          const String& outputName=String());
3    CV_WRAP void forward (OutputArrayOfArrays outputBlobs,
                          const std::vector<String>& outBlobNames);
4    CV_WRAP_AS (forwardAndRetrieve) void forward (
                          CV_OUT std::vector<std::vector<Mat> >& outputBlobs,
                          const std::vector<String>& outBlobNames);

1)Mat forward(const String&outputName=String())。

函数功能:网络计算进行到指定层。如果不指定层的名字,则计算整个网络。

参数说明:

·outputName:指定层对象的名字。

·返回值:层对象第一个输出端口的输出数据。

2)void forward(OutputArrayOfArrays outputBlobs,const String&outputName=String())。

函数功能:网络计算到指定层,并返回该层的所有输出。如果不指定层的名字,则计算整个网络,返回最后一层的所有输出。

参数说明:

·outputBlobs:输出参数,存放层的所有输出数据。

·outputName:输出层的名字。

3)void forward(OutputArrayOfArrays outputBlobs,const std::vector<String>&outBlobNames)。

函数功能:网络计算到指定的一组层,并返回每个层的第一个输出端口数据。

参数说明:

·outputBlobs:输出参数,存放每个指定层的第一个输出端口数据。

·outBlobNames:一组输出层的名字。

4)void forward(CV_OUT std::vector<std::vector<Mat>>&outputBlobs,const std::vector<String>&outBlobNames)。

函数功能:指定的一组层,网络计算到这些层为止,并返回每个层的所有输出数据。

参数说明:

·outputBlobs:输出参数,存放每个指定层的所有输出端口数据。

·outBlobNames:一组输出层的名字。

(6)setPreferableBackend

函数原型:


CV_WRAP void setPreferableBackend(int backendId);

函数功能:设置后端类型。

参数说明:

·backendId:后端id。下面代码段定义了Backend枚举类型,它给出了DNN支持的所有后端类型,每种后端的具体讲解请看2.5.2节。


enum Backend
{
       DNN_BACKEND_DEFAULT,
       DNN_BACKEND_HALIDE,
       DNN_BACKEND_INFERENCE_ENGINE,
       DNN_BACKEND_OPENCV,
       DNN_BACKEND_VKCOM
};

(7)setPreferableTarget

函数原型


CV_WRAP void setPreferableTarget(int targetId);

函数功能:设置目标运算设备的类型。

参数说明:

·targetId:目标运算设备类型id。定义如下。每种Target类型的具体讲解请参考2.5.1节。


enum Target
{
        DNN_TARGET_CPU,
        DNN_TARGET_OPENCL,
        DNN_TARGET_OPENCL_FP16,
        DNN_TARGET_MYRIAD,
        DNN_TARGET_VULKAN,
        DNN_TARGET_FPGA
 };

(8)setInput

函数原型


CV_WRAP void setInput(InputArray blob, const String& name="",
                          double scalefactor=1.0, const Scalar& mean=Scalar());

函数功能:设置网络输入。

参数说明:

·blob:网络输入数据,格式必须是CV_32F或者CV_8U。

·name:输入层的名字。

·scalefactor:缩放因子,用于对输入数据进行缩放。

·mean:均值,用于对输入数据进行减去均值操作。

以上列出了Net类的常用成员函数,已经足够覆盖日常使用。完整接口定义请参考源代码modules/dnn/include/opencv2/dnn/dnn.hpp。

2.3.3 常用函数

除了Net类提供的功能之外,DNN还提供了一些常用函数以方便使用。

(1)模型导入函数

函数原型


CV_EXPORTS_W Net readNet(const String& model, const String& config="",
                          const String& framework="");

函数功能:将不同深度学习框架训练的模型导入DNN模块的Net对象中,并返回Net对象。目前支持的模型格式有Dartnet、TensorFlow、Caffe、Torch、ONNX和Intel OpenVINO。该函数的主要逻辑是根据模型参数文件或者模型描述文件推断出框架类型,调用相应的导入器加载网络模型。

参数说明:

·model:模型权重文件路径。

·config:模型配置文件路径。

·framework:DNN框架,可省略,DNN模块会自动推断框架种类。

(2)图片到模型输入的转化函数

函数原型


void blobFromImage(InputArray image, OutputArray blob, double scalefactor,
                          const Size& size, const Scalar& mean, bool swapRB,
                          bool crop);

函数功能:将图片数据转化成神经网络的输入数据。

参数说明:

·image:输入的图像数据。

·blob:经过缩放、裁剪、去均值图之后的图像数据。它是一个4维数据,布局一般用N、C、H、W表示。其中,N表示batch size,即一次输入几张图片;C表示图片通道数,如RGB图片通道数为3;H表示图片高度;W表示图片宽度。

·scalefactor:对mean数据进行缩放的比例。具体运算是对mean的每个像素值乘以scalefactor。

·size:模型输入数据的宽度和高度。

·mean:模型训练时用到的图像集的均值,可选。

·swapRB:是否需要将mean的R通道和B通道进行交换。当模型接受的通道顺序和mean的通道顺序不一致时,swapRB需要设置成true。

·crop:当image大小和size不一致时,是通过裁剪方式还是通过缩放方式对image数据进行调整。true表示通过裁剪方式调整,false表示通过缩放方式调整。