本文介绍了一种使用opencv + c++ 可以在生产环境下调用tensorflow pb文件进行预测的方法。
该方法不需要在生产环境下搭建python运行环境。 流程上相对简单清晰。如果要将神经网络/机器学习添加到几年前项目中,也比较简单可行。
一.pb文件的生成
本人使用的RsNet模型完全参考github上官方给出的demo训练生成。网址https://github.com/tensorflow/models
训练入口文件official/resnet/imagenet_main.py (主线版本 v1.8.1)
只定义了2个分类,具体的训练方法就不再累
训练完成后会在根目录生成一个imagenet_model文件夹,里面存储了模型文件(ckpt文件)。接下来需要将模型文件固化成pb文件。为了让opencv可以使用这个pb文件,需要定义好神经网络input入口与output出口
import tensorflow as tf
from tensorflow.python.framework import graph_util
import cv2
import numpy as np
import imagenet_main #训练的入口文件
MODEL_SAVE_PATH="imagenet_model/"
#三通道图片默认的数据,我只用了一个通道的值测试。真实应用环境下,值应该是图片的平均辉度
_R_MEAN = 123.68/255
_G_MEAN = 116.78/255
_B_MEAN = 103.94/255
#入口位置 图片维度格式 NHWC chanel在最后一个维度
x_cv_imput = tf.placeholder(tf.float32,[1,448,448,1],name='cv-input')
model = imagenet_main.ImagenetModel(34, 'channels_last', version=2)
logits = model(x_cv_imput, False)
#定义了网络出口 各分类的概率 概率最大的分类就是神经网络判定的结果
result = tf.nn.softmax(logits,name = 'cv-out-probabilities')
#定义了一个train.Saver 用来保存pb文件
saver = tf.train.Saver()
#开始session 并重新载入训练过的ckpt
sess = tf.Session()
init = tf.global_variables_initializer()
sess.run(init)
ckpt = tf.train.get_checkpoint_state(MODEL_SAVE_PATH)
saver.restore(sess, ckpt.model_checkpoint_path)
#定义pb的输出参数 其中 output_node_names = ['cv-out-probabilities'] 为出口
output_graph_def = graph_util.convert_variables_to_constants(sess, sess.graph_def,output_node_names = ['cv-out-probabilities'])
#输入一个图片进行预测,并输出结果。为了与在opencv中的结果进行对比
#图片resize到448×448,单通道,float类型,并进行归一化处理
img = cv2.imread("resize.png")
img = cv2.resize(img, (448,448), interpolation=cv2.INTER_AREA)
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img = np.asarray(img, dtype='float')
img = img/255.0
img = img - [_R_MEAN]
_result,_y_cv = sess.run([result,logits],feed_dict={x_cv_imput:np.reshape(img, [1,448,448,1])})
print _result
#保存pb文件为renet.pn
tf.train.write_graph(output_graph_def, '', 'rsnet.pb', as_text=False)
输出结果:
[[ 9.99989152e-01 1.08901504e-05]
二.使用opencv导入pb文件进行预测
opencv使用的版本为3.4.2 曾经使用3.3版本进行过测试,但错误太多,主要是很多神经网络运算不支持,印象中有FusedBatchNorm等
代码如下:
#include <opencv2/core.hpp>
#include "opencv2/dnn.hpp"
using namespace cv;
using namespace cv::dnn;
using namespace std;
int main(int argc, char *argv[])
{
net = readNetFromTensorflow("rsnet.pb");
if (net.empty()) {//导入失败,返回
return;
}
net.setPreferableBackend(DNN_BACKEND_OPENCV);//如果编译opencv时使用了mkl等加速,可以修改为其他的dnn_backend
//使用下面的代码,可以看到除入口外的Layer名称
std::vector<String> names = net.getLayerNames();
for(auto iter1=names.begin(); iter1!=names.end(); ++iter1 ){
cout<<*iter1<<endl;
}
//载入图片并reszie和归一化处理
Mat img = cv::imread("resize.png",cv::IMREAD_GRAYSCALE);
resize(img ,img ,Size(448,448),0,0,INTER_AREA);
img.convertTo(img, CV_32F);
img= img/ 255.0 - 123.68 / 255.0;
//opencv进行预测前,专门有个函数将图片Mat转换为4维Mat
Mat inputBlob = blobFromImage(img);
net.setInput(blob, "cv-input");//网络入口
Mat out = net.forward("cv-out-probabilities");//预测,并定义了网络出口
float* data = out.ptr<float>(0);
cout<<out.size<<endl;//输出结果Mat的大小
cout<<data[0]<<endl;//第一个分类的概率
cout<<data[1]<<endl;//第二个分析的概率
}
输出结果:
Pad
conv2d/Conv2D
max_pooling2d/MaxPool
batch_normalization/FusedBatchNorm
Relu
conv2d_2/Conv2D
conv2d_3/Conv2D
batch_normalization_2/FusedBatchNorm
Relu_1
……
1 x 2
0.999989
1.08901e-0
结果与python下的计算一样
三.注意点 和 困惑的地方
1.测试时出现了以下错误
OpenCV(3.4.2) Error: Unspecified error (Can’t create layer “ExpandDims” of type “ExpandDims”) in cv::dnn::experimental_dnn_v5::LayerData::getLayerInstance, file …….
应该是tf.expanddims这个函数暂不支持,修改了python文件,屏蔽了 tf.expanddims 这个函数就好了
2.神经网络输入的维度
上述代码中,python定义的input网络维度为(1,448,448,1)。而如果打印出 opencv blobFromImage函数返回的inputBlob维度,会发现是 1×1×448×448 。两者的维度是不一样的,理论上应该会报错。但输出的结果是正确的。。。
我也尝试修改 inputBlob 维度 为 1×448×448×1,运行后反而报错了,维度不正确。。
查看opencv源码,在modules\dnn\src\tensorflow\tf_importer.cpp中找到一段对 “Pad”的处理,有维度交换的操作,可能与此有关。在Pad运行时,进行了维度调整。
else if (type == "Pad")
{
Mat paddings = getTensorContent(getConstBlob(layer, value_id, 1));
CV_Assert(paddings.type() == CV_32SC1);
if (paddings.total() == 8)
{
// Perhabs, we have NHWC padding dimensions order.
// N H W C
// 0 1 2 3 4 5 6 7
std::swap(*paddings.ptr<int32_t>(0, 2), *paddings.ptr<int32_t>(0, 6));
std::swap(*paddings.ptr<int32_t>(0, 3), *paddings.ptr<int32_t>(0, 7));
// N C W H
// 0 1 2 3 4 5 6 7
std::swap(*paddings.ptr<int32_t>(0, 4), *paddings.ptr<int32_t>(0, 6));
std::swap(*paddings.ptr<int32_t>(0, 5), *paddings.ptr<int32_t>(0, 7));
// N C H W
// 0 1 2 3 4 5 6 7
}
layerParams.set("paddings", DictValue::arrayInt<int*>((int*)paddings.data, paddings.total()));
int id = dstNet.addLayer(name, "Padding", layerParams);
layer_id[name] = id;
connect(layer_id, dstNet, parsePin(layer.input(0)), id, 0);
}
Pingback引用通告: opencv 中使用 cudnn 预测CNN网络 | YangYouji's WebSite