背景:将MTCNN部署在FPGA上需要将其代码设计为C代码,c代码的网络结构需要与python代码保持一致。
目的:将MTCNN的c代码网络结构转为与python代码一致。
一、相关代码与含义
1.1 相关知识
类对象
https://blog.csdn.net/qq_32583189/article/details/52412369
http://www.baike.com/wiki/%E7%B1%BB%E5%92%8C%E5%AF%B9%E8%B1%A1
实例化一个对象就是通过new运算符为对象分配空间(类属于复合数据类型,在声明对象时,系统并没有为对象分配空间,用户需要应用new完成分配空间的任务)。既可以在声明对象时实例化(创建)对象,也可以先声明对象,然后再创建。
this指针
https://baike.baidu.com/item/C++this%E6%8C%87%E9%92%88/637012
this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。
1.2 与网络结构相关的数据
pBox.h程序之中,均为struct结构体
- pBox 理解为三维的picture box,包含了数据指针,宽,高,channel
- pRelu 相关prelu的斜率参数,包含了数据指针,宽度
- Weight 权重参数,包含了两个数据指针:1指向卷积核的数据,2与卷积核的偏置,其他参数为lastChannel上一层的featureMap数量,selfChannel本层输出的featureMap数量,卷积核大小,步长,pad信息
- Bbox,bounding box, 包含了得分score,四个顶点的坐标,area,exist,关键点的坐标ppoint[2*NumPoint],和regrecoord
- orderScore,包含了指向结构的指针,score以及初始的顺序。
1.3 相关的函数及含义
在network.cpp之中,
- 所有的init函数都是malloc相应的空间,然后数据指针指向的部分全部置0.
- 所有的__toMatirx函数都是将相应的数据转为矩阵形式。
- 具体名字的函数是具体的前馈运算
具体系列的函数
- initConvAndFc函数,初始化卷积层与Fc层,把相应的权重参数传给Weight结构体,然后返回值该层权重的个数,指针指向的权重值先置为0。
Pnet::run
image2Matrix(image, this->rgb);
feature2Matrix(this->rgb, this->conv1_matrix, this->conv1_wb);
convolution(this->conv1_wb, this->rgb, this->conv1, this->conv1_matrix);
- image2Matrix,将Mat格式读入的图像转为pBox格式的图像
- featurePad, 将pBox格式的图像进行pad
- feature2Matrix,将featureMap转为矩阵形式用于后面的矩阵乘
- convolution,第一个参数为权重(权重自读取进来之后就始终没有变化,矩阵形式的权重与三维权重大小相同,而featureMap的大小在转换时就需要发生相应的变化),第二个参数的目的仅仅是传递网络结构给卷积函数。最后一个参数为矩阵形式的featureMap。
二、与网络结构定义相关的语句
2.1 mtcnn.h
定义了Pnet,Rnet,Onet的class,class中定义了网络的结构
mtcnn.cpp
2.2 Pnet::Pnet
定义Pnet::Pnet函数,在函数中实例化Pnet的class,将其中指针指向具体的struct 的pBox,weight
初始化conv与fc层
根据层参数初始化权重指针组,读取权重
2.3 Pnet::~Pnet
释放Pnet之前的指针指向的内存
2.4 Pnet::run
Init系函数,转化为matrix系列函数,卷积与池化init系列函数
然后是网络具体的前馈计算
2.5 Rnet::,Onet::系列相关
与上面这些一致
2.6 mtcnn.cpp中与网络结构无关的函数
Pnet::generateBbox,根据相应score生成备选框,无需更改
mtcnn::detectObject应该不涉及关于网络结构的更改相关的问题。
三、Pnet结构的更改
3.1 python代码前后结构
Pnet原始结构
Feature size |
name | Kernel size |
Stride |
Padding |
12*12*3 |
conv1 PReLU1 |
3*3*10 |
1 |
Valid |
10*10*10 |
pool1 | Maxpool 2*2 |
2 |
Same |
5*5*10 |
conv2 PReLU2 |
3*3*16 |
1 |
Valid |
3*3*16 |
conv3 PReLU3 |
3*3*32 |
1 |
Valid |
1*1*32 |
Pnet 最终结构,只有3×3的卷积(为保证输出的得分图与输入的映射,需要same与valid)
Feature size |
name | Kernel size |
Stride |
Padding |
12*12*3 |
conv1 PReLU1 |
3*3*10 |
1 |
Valid |
10*10*10 |
pool1_conv1 pool1_PReLU1 |
3*3*16 |
2 |
Same |
5*5*16 |
conv2 PReLU2 |
3*3*32 |
2 |
Valid |
3*3*32 |
conv3 PReLU3 |
3*3*32 |
1 |
Valid |
1*1*32 |
3.2 c代码中变量及含义
原始结构:mtcnn.h与mtcnn.cpp
layer name | input and output | weight |
conv1 PReLU1 |
rgb:矩阵格式的rgb图像 conv1_matrixI_in conv1 |
conv1_wb prelu_gmma1 |
pool1 | maxPooling1 |
|
conv2 PReLU2 |
maxPooling_matrix conv2 |
conv2_wb prelu_gmma2 |
conv3 PReLU3 |
conv3_matrix conv3 |
conv3_wb prelu_gmma3 |
post conv layers |
3.3 相关函数参数
init系列
image2MatrixInit(输入mat格式图片,输出rgb格式图片)
feature2MatrixInit(该层输入,该层需转化为的matrix格式的输入,该层权重)
convolutionInit(该层权重,该层输入,该层输出,该层matrix格式的输入)
maxPoolingInit(该层输入,该层输出,kernelSize,Stride)
运算系列
feature2matrix(该层输入,该层需转化的matrix格式的输入,该层权重)
convolution(该层权重,该层输入,该层输出,该层marix格式输入)
prelu(该层输入输出,上层卷积的偏置,权重斜率)
maxpooling(该层输入,该层输出,kernelSize,Stride)
3.4 更改顺序
mtcnn.h中pnet中的private成员定义
mtcnn.cpp中
Pnet函数中this指针指向的量
dataNumber中数值的值
pointTeam中数值指针值
readData的读入的值
~Pnet中的free的指针值
四、卷积运算相关代码
原版的代码之中只有形式为valid的卷积,我们需要加入padding以及stride的卷积。代码之中关于卷积以及stride和padding的代码在network.cpp之中,我们需要搞懂此代码。
4.1 pad运算
原版的pad是左右两边都加相同的pad,并且嵌套在convolution之中,顺序并不对。
原始的pad代码是两边都pad相同的尺寸,我们需要更改尺寸,我们加入leftPad与rightPad。(此处留有隐患就是左右顺序是否是正确的,但我们可以大致看出,feature的排列顺序是先行后列后channel)
//network.cpp in featurePad
for (int row = 0; row < outpBox->channel*outpBox->height;row++){
if ((row%outpBox->height) <leftPad || (row % outpBox->height >(outpBox->height-rightPad-1))){
p += outpBox->width;
continue;
}
p += leftPad;
memcpy(p, pIn, pbox->width*sizeof(mydataFmt));
p += pbox->width + rightPad;
pIn += pbox->width;
}
据此可以大致看出循环是for channel,for row,for col
4.2 feature转为矩阵形式
卷积的矩阵乘法运用的是二维矩阵乘,所以需要将feature转换为二维形式与权重对应。以下为图像转为矩阵乘的参考。
//image2Matrix network.cpp
for (int rowI = 0; rowI < image.rows; rowI++){
for (int colK = 0; colK < image.cols; colK++){
*p = (image.at<Vec3b>(rowI, colK)[0] - 127.5)*0.0078125;//opencvµÄͨµÀÅÅÐòÊÇRGB
*(p + image.rows*image.cols) = (image.at<Vec3b>(rowI, colK)[1] - 127.5)*0.0078125;
*(p + 2*image.rows*image.cols) = (image.at<Vec3b>(rowI, colK)[2] - 127.5)*0.0078125;
p++;
}
}
读取图像时的形式,将二维图像读取为线性的存储。
//network.cpp feature2matrix
for (int row = 0; row< h_out; row ++){
for (int col = 0; col < w_out; col++){
pIn = pboxIn->pdata + row*stride*pboxIn->width + col*stride;
for (int channel = 0; channel < pboxIn->channel; channel++){
ptemp = pIn + channel*pboxIn->height*pboxIn->width;
for (int kernelRow = 0; kernelRow < kernelSize; kernelRow++){
memcpy(p, ptemp, kernelSize*sizeof(mydataFmt));//from ptemp to p
p += kernelSize;
ptemp += pboxIn->width;
}
}
}
}
将feature转为矩阵形式,
4.3 convolution
// -------------convulution in 2D matrix format-------------------
// input kernel matrix * input feature matrix(Trans) = output feature matrix
// height (outChannels) height (3D_KernelSize) height (outChannels)
// width (3D_KernelSize) width (outFeatureSize) width (outFeatureSize)
//C=αAB + βC : outpBox=weightIn*matrixIn(T)
// RowMajor(c code) A no trans B trans
cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasTrans, \
//A row C row B col C col A col B row alpha
weightIn->selfChannel,matrixIn->height,matrixIn->width, 1,\
//A* A'col B* B'col beta C* C'col
weightIn->pdata,matrixIn->width,matrixIn->pdata,matrixIn->width,0,outpBox->pdata,matrixIn->height);
cblas_segmm的参数
https://blog.csdn.net/u012235274/article/details/52769682
cblas_sgemm(order, transA, transB, M, N, K, ALPHA, A, LDA, B, LDB, BETA, C, LDA);
第一个参数的函数是存储的有限性,有行优先和列优先(c语言是行优先)
第二个参数和第三个参数是是否转置
A矩阵经过transA之后的维度是M×K
B矩阵经过transB之后的维度是K×N
C矩阵的维度是M×N
LDA和LDB是对应矩阵还没变换之前,在主维度方向的维度。(如果是行优先就是列数)。