OpenCV4.0 DNN-googleNet

OpenCV4.0 DNN-googleNet

OpenCV 4.0最近发布,其中一大亮点便是加入DNN;之前的文章中介绍了OpenCV 4.0的编译,本系列就通过GoogleNet的demo来窥探OpenCV 4.0的DNN。

核心函数介绍

首先需要准备GoogleNet的prototxt,caffemodel,和synset_words.txt;这个在网上很容易下载到。然后就是需要一张RGB或者BGR的测试图片,本文使用的是官方提供的经典图片space_shuttle.jpg。
我们使用的函数非常简单,主要涉及函数有以下几个:

  • readNetFromCaffe
  • blobFromImage
  • setInput
  • forward

blobFormImage

函数原型

Mat cv::dnn::blobFromImage(
    InputArray 	image,
    double 	scalefactor = 1.0,
    const Size & 	size = Size(),
    const Scalar & 	mean = Scalar(),
    bool 	swapRB = false,
    bool 	crop = false,
    int 	ddepth = CV_32F )		

以上是官方给出的函数原型。

  • image:输入图像
  • scalefactor: multiplier for image values.
  • size:指的不是输入图像的尺寸,是指所需要的尺寸,也就是返回的Mat中数据的尺寸。
  • swapRB:是否交换R和B分量,这在之前的色彩空间转换的文章中介绍过
  • crop:输入图像大小与size不符的时候,是否需要裁剪
  • ddepth:图像的数据类型,目前仅支持32F和8U

以上的参数都很好理解,接下来是关于mean参数,如果之前没有深入研究过深度学习,这个还是不太好理解的。首先给出mean的数值:(104 117 123);数字从什么地方来的呢?这个是在googleNet训练的时候设定的,节选部分train_val.prototxt,可以看到在训练的时候transform_param中设置了mean。

name: "GoogleNet"
layer {
  name: "data"
  type: "Data"
  top: "data"
  top: "label"
  include {
    phase: TRAIN
  }
  transform_param {
    mirror: true
    crop_size: 224
    mean_value: 104
    mean_value: 117
    mean_value: 123
  }
  data_param {
    source: "examples/imagenet/ilsvrc12_train_lmdb"
    batch_size: 32
    backend: LMDB
  }
}
...

但是为什么这么做呢?关于这么做的原因有两个,其一,数据规整化或者说标准化;熟悉PCA(主成分分析)算法的同学清楚,在PCA中需要对输入数据的特征向量进行规整化,通过预估均值和方差,将其规整化为0均值和单位方差。由于深度学习中,对图片取样很小例如16*16等,各位置方差相差不大。所以方差的规整化意义不大,所以仅对均值进行规整化。
其二,构造0中心分布的数据,使梯度下降算法更有效。均值一般反应为照度,但是图像识别中,对照度一般不敏感,因此去除照度,构造0中心分布的数据,有利于梯度沿最优路线下降。(参考:http://cs231n.github.io/neural-networks-1/)

以上是关于mean参数的解释。

函数源码

这一部分是通过函数源码简单看一下该函数都做了什么工作。
路径:opencv/modules/dnn/src/dnn.cpp +104

//申请blob
Mat blobFromImage(InputArray image, double scalefactor, const Size& size,
                  const Scalar& mean, bool swapRB, bool crop, int ddepth)
{
    CV_TRACE_FUNCTION();
    Mat blob;
    blobFromImage(image, blob, scalefactor, size, mean, swapRB, crop, ddepth);
    return blob;
}

//把输入图像写入vector
void blobFromImage(InputArray image, OutputArray blob, double scalefactor,
                   const Size& size, const Scalar& mean, bool swapRB, bool crop, int ddepth)
{
    CV_TRACE_FUNCTION();
    std::vector images(1, image.getMat());
    blobFromImages(images, blob, scalefactor, size, mean, swapRB, crop, ddepth);
}

//核心计算函数,接收的输入是vector,输出是Mat
void blobFromImages(InputArrayOfArrays images_, OutputArray blob_, double scalefactor,
                    Size size, const Scalar& mean_, bool swapRB, bool crop, int ddepth)
{
    //检测输入参数的合法性
    CV_TRACE_FUNCTION();
    CV_CheckType(ddepth, ddepth == CV_32F || ddepth == CV_8U, "Blob depth should be CV_32F or CV_8U");
    if (ddepth == CV_8U)
    {
        CV_CheckEQ(scalefactor, 1.0, "Scaling is not supported for CV_8U blob depth");
        CV_Assert(mean_ == Scalar() && "Mean subtraction is not supported for CV_8U blob depth");
    }

    
    std::vector images;
    images_.getMatVector(images);
    CV_Assert(!images.empty());

    //对输入图像的size,depth,swapRB,mean,scalefactor做处理
    for (int i = 0; i < images.size(); i++)
    {
        //处理输入Image的尺寸
        Size imgSize = images[i].size();
        if (size == Size())
            size = imgSize;
        if (size != imgSize)
        {
            if(crop)
            {
            //计算缩放系数图像宽和高的缩放系数,按照较大的缩放
              float resizeFactor = std::max(size.width / (float)imgSize.width,
                                            size.height / (float)imgSize.height);
            //按照缩放系数做双线性插值
              resize(images[i], images[i], Size(), resizeFactor, resizeFactor, INTER_LINEAR);

            //裁剪图像保证中心点对齐
              Rect crop(Point(0.5 * (images[i].cols - size.width),
                              0.5 * (images[i].rows - size.height)),
                        size);
              images[i] = images[i](crop);
            }
            else
            //如果没有设置crop,则直接进行双线性插值,缩放输入图像到目标尺寸
              resize(images[i], images[i], size, 0, 0, INTER_LINEAR);
        }
        //如果输入图像数据类型与目标类型不一致,进行数据类型转换
        if(images[i].depth() == CV_8U && ddepth == CV_32F)
            images[i].convertTo(images[i], CV_32F);
        Scalar mean = mean_;

        //交换RB
        if (swapRB)
            std::swap(mean[0], mean[2]);

        //处理mean和scalefactor(注意直接操作的是图像,运算符应该在Mat中重载了)
        images[i] -= mean;
        images[i] *= scalefactor;
    }

    //保证图像满足NCHW数据格式
    size_t i, nimages = images.size();
    Mat image0 = images[0];
    int nch = image0.channels();
    CV_Assert(image0.dims == 2);
    Mat image;
    if (nch == 3 || nch == 4)
    {
    ...

以上是核心源代码节选。在代码中添加了注释,对源代码进行了简单梳理。

setInput

路径:opencv/modules/dnn/src/dnn.cpp +2824

函数原型

void cv::dnn::Net::setInput	(	
    InputArray 	blob,
    const String & 	name = "",
    double 	scalefactor = 1.0,
    const Scalar & 	mean = Scalar() )		

这几个参数,看起来很眼熟。首先这个blob,就是上文中介绍的blobFromImage的返回值。剩下的除了name,其他两个跟上文blobFromImage中的意义相同。这个name指的是inputlayer的名字。
一下是googleNet prototxt的inputlayer:

input: "data"
input_dim: 1
input_dim: 3
input_dim: 224
input_dim: 224

可以看到namedata.
输入图像与mean,scalefactor的关系如下:

input(n,c,h,w) = scalefactor × (blob(n,c,h,w) - mean_c)

注意:公式中的input是指最终DNN的输入,blob则是我们程序输入的Image(我们提供给程序的输入图片)。

forward

函数原型

forward的函数原型有4个,分别提供了不同的功能:

  • 第一个
Mat cv::dnn::Net::forward(const String & outputName = String())

这个函数只需要提供layer的name即可;函数返回一个Mat变量,返回值是指输入的layername首次出现的输出。

默认输出整个网络的运行结果。

googleNet的输出层如下:

layer {
  name: "prob"
  type: "Softmax"
  bottom: "loss3/classifier"
  top: "prob"
}

这是一个softmax层,nameprob;所以forward的输入为prob即可。

  • 第二个
void cv::dnn::Net::forward(OutputArrayOfArrays outputBlobs,
const String & outputName = String())	

该函数的返回值是void,通过OutputArrayOfArrays类型提供计算结果,类型为blob。这个outputName依然是layer的name,outputBlobs不是首次layer的输出了,而是layername指定的layer的全部输出;多次出现,就提供多个输出。

  • 第三个
void cv::dnn::Net::forward(OutputArrayOfArrays outputBlobs,
const std::vector & outBlobNames)	

该函数返回值为void,outBlobNames是需要提供输出的layer的name,类型为vector,也就是说可以提供多个layer的那么;它会将每个layer的首次计算输出放入outputBlobs。

  • 第四个
void cv::dnn::Net::forward(std::vector> & outputBlobs, 
const std::vector & outBlobNames 
)	

该函数的功能是最强大的,返回值为void;输入outBlobNames是vector类型,outputBlobs是vector>类型;该函数可以输入多个layer的name;它会输出每个layer的全部输出到outputBlobs中。

以上是关于核心几个函数的介绍,其中setInput和forward仅仅介绍了函数原型,没有介绍源码,是因为源码涉及的东西很多,所以会在接下来的文章中单独介绍这两个函数的源码,然后提供运行googleNet的demo。

欢迎关注公众号:计算机视觉与高性能计算(to_know)

OpenCV4.0 DNN-googleNet_第1张图片

你可能感兴趣的:(OpenCV源码杂记)