Tensorflow模型部署到昇腾310虚拟平台进行推理运算
Tensorflow代码准备与任务分析
由于华为应用案例中有不少例子了,咱们今天做一个不一样的。
前几天课程作业需要做一个车牌识别的项目,今天我们就把它部署到华为昇腾310平台上推理一下试试吧
因为懒惰,我直接从github上找了一个车牌识别的项目网址。效果非常好。
训练步骤大致分为分割车牌-车牌矫正-识别字符,具体细节就不说了,可以自己看一下代码。
推理阶段大致分为四部分
- 利用分割网络分割出车牌mask
- 车牌矫正(需要用C++重写)
- 将矫正后的车牌输入CNN网络进行字符识别
- 最后在图片上打印识别车牌结果(原程序做了个界面,我就直接用C++实现自己的功能了)
环境配置
我使用的是华为的云平台,具体环境是有视频讲解的,我就不赘述了
模型转换
程序是Tensorflow训练的,并且是用Keras输出的模型,就是两个.h5文件。保险起见,我们先把他们转换成.pb。
from tensorflow.keras.models import load_model
import tensorflow as tf
import os
from tensorflow.keras import backend as K
#转换函数
def h5_to_pb(h5_model,output_dir,model_name,out_prefix = "output_",log_tensorboard = True):
if os.path.exists(output_dir) == False:
os.mkdir(output_dir)
out_nodes = []
for i in range(len(h5_model.outputs)):
out_nodes.append(out_prefix + str(i + 1))
tf.identity(h5_model.output[i],out_prefix + str(i + 1))
sess = K.get_session()
from tensorflow.python.framework import graph_util,graph_io
init_graph = sess.graph.as_graph_def()
main_graph = graph_util.convert_variables_to_constants(sess,init_graph,out_nodes)
graph_io.write_graph(main_graph,output_dir,name = model_name,as_text = False)
if log_tensorboard:
from tensorflow.python.tools import import_pb_to_tensorboard
import_pb_to_tensorboard.import_to_tensorboard(os.path.join(output_dir,model_name),output_dir)
if __name__ == "__main__":
# 路径参数
weight_file_path = "./cnn.h5"
#输出路径
output_dir = "./"
#加载模型
h5_model = load_model(weight_file_path)
h5_to_pb(h5_model,output_dir = output_dir,model_name = "cnn.pb")
print('model saved')
转换以后得到两个.pb文件,分别为 unet.pb 和 cnn.pb,分别对应着分割网络与字符识别网络。
将两个.pb文件上传到云平台,进行模型转换,输出两个.om文件,使第三方框架的模型可以在昇腾平台跑通。
废话不多说,直接上代码
在模型所在的文件夹中,命令行执行:
atc --model="./unet.pb" --framework=3 --output="unet" --soc_version=Ascend310 --input_format=NHWC --input_shape="input_1:1,512,512,3"
atc --model="./cnn.pb" --framework=3 --output="cnn" --soc_version=Ascend310 --input_format=NHWC --input_shape="input_1:1,80,240,3"
我们来仔细看一下这些参数
–model:输入模型的路径
–framework:第三方框架的代号,比如Caffe是0,mindspore是1,tensorflow是3,ONNX是5
–soc_version:处理器的版本,可以是Ascend310或者Ascend910,根据自己的硬件填写
–input_format:当原始框架是TensorFlow时,支持NCHW、NHWC、ND、NCDHW、NDHWC五种输入格式,默认为NHWC。
–input_shape:模型输入的shape信息,例如:“input_name1:n1,c1,h1,w1;input_name2:n2,c2,h2,w2”。指定的节点必须放在双引号中,节点中间使用英文分号分隔。input_name必须是转换前的网络模型中的节点名称。节点名称可以使用可视化工具netron来看,我自己是使用在线工具:
Netron
最后得到两个模型文件:unet.om,cnn.om
推理代码适配
华为有很多sample,给个链接
我们随便挑一个拉下来修改
华为工程师已经帮我们写好框架了哈哈,不嫖白不嫖,给黑白图片上色
先看一下代码结构:
├App名称
├── model //该目录下存放模型转换相关的配置文件
│ ├── xxx.cfg
├── data
│ ├── xxx.jpg //测试数据
├── inc //该目录下存放声明函数的头文件
│ ├── xxx.h
├── out //该目录下存放输出结果
├── src //该目录下存放系统初始化的配置文件、编译脚本、函数的实现文件
│ ├── xxx.json //系统初始化的配置文件
│ ├── CMakeLists.txt //编译脚本
│ ├── xxx.cpp //实现文件
我们来模仿着写一个。
首先确定.h文件,构建推理类 Class PlateRec,需要实现的功能及流程如下:
- 需要进行AscendCL的初始化: init()
- 申请运行管理资源:init_resources()
- 数据处理(车牌矫正与标记车牌):plate_correct(), plate_draw()
- 推理:inference()
- 运行资源释放,AscendCL去初始化:destroy_resource()
class PlateRec {
public:
PlateRec(const char* modelPath, uint32_t modelWidth, uint32_t modelHeight);
~PlateRec();
AtlasError init();
AtlasError init_resource();
AtlasError inference(std::vector<InferenceOutput>& inferOutputs);
AtlasError plate_correct(const std::string& imageFile, std::vector<InferenceOutput>& modelOutput);
AtlasError plate_draw(const std::string& imageFile, std::vector<InferenceOutput>& modelOutput);
void destroy_resource();
};
这肯定是不够的
第二步没必要public,用init()来调用就可以了,第五步由析构函数调用就可以了
第二步中需要申请Device,Context,Stream。Device好理解,Context作为一个容器,管理了所有对象(包括Stream、Event、设备内存等)的生命周期。Stream用于维护一些异步操作的执行顺序,确保按照应用程序中的代码调用顺序在Device上执行。最后,获取当前昇腾AI软件栈的运行模式runMode_,根据不同的运行模式,后续的接口调用方式不同
第三步中也需要一些必要的参数
还要准备数据输入输出的数据结构
改过后的代码:
/**
* PlateRec
*/
class PlateRec {
public:
PlateRec(const char* modelPath, uint32_t modelWidth, uint32_t modelHeight);
~PlateRec();
AtlasError init();
AtlasError inference(std::vector<InferenceOutput>& inferOutputs);
AtlasError plate_correct(const std::string& imageFile, std::vector<InferenceOutput>& modelOutput);
AtlasError plate_draw(const std::string& imageFile, std::vector<InferenceOutput>& modelOutput);
private:
AtlasError init_resource();
AtlasError create_input(size_t inputDataSize);
void save_image(const std::string& origImageFile, cv::Mat& image);
void destroy_resource();
private:
int32_t deviceId_;
aclrtContext context_;
aclrtStream stream_;
AtlasModel model_;
const char* modelPath_;
uint32_t modelWidth_;
uint32_t modelHeight_;
uint32_t inputDataSize_;
void* inputBuf_;
aclrtRunMode runMode_;
bool isInited_;
};
现在来实现:
构造与析构函数:
PlateRec::PlateRec(const char* modelPath, uint32_t modelWidth, uint32_t modelHeight)
:deviceId_(0), context_(nullptr), stream_(nullptr), inputBuf_(nullptr),
modelWidth_(modelWidth), modelHeight_(modelHeight), isInited_(false){
modelPath_ = modelPath;
}
PlateRec::~PlateRec() {
destroy_resource();
}
申请运行资源时,我们隐式调用了Context和Stream。aclrtSetDevice()申请了三个资源
AtlasError PlateRec::init_resource() {
// open device
ret = aclrtSetDevice(deviceId_);
if (ret != ACL_ERROR_NONE) {
ATLAS_LOG_ERROR("Acl open device %d failed", deviceId_);
return ATLAS_ERROR;
}
ATLAS_LOG_INFO("Open device %d success", deviceId_);
ret = aclrtGetRunMode(&runMode_);
if (ret != ACL_ERROR_NONE) {
ATLAS_LOG_ERROR("acl get run mode failed");
return ATLAS_ERROR;
}
return ATLAS_OK;
}
未完待续、、