OpenCV 4.0最近发布,其中一大亮点便是加入DNN;之前的文章中介绍了OpenCV 4.0的编译,本系列就通过GoogleNet的demo来窥探OpenCV 4.0的DNN。
首先需要准备GoogleNet的prototxt,caffemodel,和synset_words.txt;这个在网上很容易下载到。然后就是需要一张RGB或者BGR的测试图片,本文使用的是官方提供的经典图片space_shuttle.jpg。
我们使用的函数非常简单,主要涉及函数有以下几个:
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 )
以上是官方给出的函数原型。
以上的参数都很好理解,接下来是关于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)
{
...
以上是核心源代码节选。在代码中添加了注释,对源代码进行了简单梳理。
路径: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
可以看到name
是data
.
输入图像与mean,scalefactor的关系如下:
input(n,c,h,w) = scalefactor × (blob(n,c,h,w) - mean_c)
注意:公式中的input是指最终DNN的输入,blob则是我们程序输入的Image(我们提供给程序的输入图片)。
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层,name
是prob
;所以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
以上是关于核心几个函数的介绍,其中setInput和forward仅仅介绍了函数原型,没有介绍源码,是因为源码涉及的东西很多,所以会在接下来的文章中单独介绍这两个函数的源码,然后提供运行googleNet的demo。