文章转载于微信公众号:GiantPandaCV
作者: 阿呆
开发环境选择
- 本文操作系统为Windows,因为Windows上的安卓模拟器选择较多,并且真机调试也比较方便;
- 交叉编译在Windows和Ubuntu上都进行了尝试,都可行,但是如果是Ubuntu上交叉编译之后再挪到Windows的话,容易出幺蛾子;
- 我目前使用的最稳定的工具版本组合是:ndk18、androidstudio4.1、cmake3.10、gradle6.5、MinGW(CodeBlocks自带)。
1. PyTorch模型转NCNN
这一小节是介绍如何将自己重新训练过的PyTorch模型转成ncnn,如果没有重训练需求的话,可以直接跳过这一节。
(1) 整体步骤
理想情况下,从PyTorch转到ncnn只需要下面两步:
- PyTorch转ONNX
torch.onnx._export(model,x,path,opset_version=11)
- ONNX转NCNN
./onnx2ncnnmodel.onnxmodel.parammodel.bin
遇到问题的适合解决思路如下:
convert.png
下面介绍一下我在做ChineseOCRLite中的PSENet模型转换的过程中遇到的问题。
(2)实际操作的时候可能会遇到各种问题
问题1:ReLU6不支持
概述:ReLU6算子在转换的时候容易出现不支持的情况,需要使用其他算子替代
解决:使用torch.clamp替代(虽然ReLU6可以通过组合ReLU的方式实现,但是组合得到的ReLU6在NCNN中容易转换失败,不建议使用。)
defrelu6(x,inplace=True):
returntorch.clamp(x,0,6)
问题2:Resize算子转换问题
概述:因为各个框架对Resize算子的支持都不尽相同,在转换过程中总会出现一些问题,pytorch中的interpolate算子转换成ONNX之后变成很多零散的算子,如cast、shape等,这些在ncnn里面不支持。你可以选择手动修改文件,也可以使用下面这个自动的方法:
解决:使用onnx/_simplifier对onnx模型进行简化,可以合并这些零散的算子。
python-monnxsimmodel.onnxmodel_sim.onnx
问题3:关于转ONNX及使用onnx/_simplifier过程中出现的一系列奇怪问题
概述:使用不同版本的ONNX可能会遇到不同的问题,比如提示conv层无输入等(具体错误名称记不清了)。
解决:下载最新ONNX源码编译安装(onnx/_simplifier中出现的一些错误也可以通过安装最新ONNX来解决)
gitclonehttps://github.com/onnx/onnx.git
sudoapt-getinstallprotobuf-compilerlibprotoc-dev
cdONNX
pythonsetup.pyinstall
问题4:模型输出结果的尺寸固定
概述:直接转换得到的onnx模型的Resize算子都是固定输出尺寸的,无论输入多大的图片都会输出同样大小的特征图,这无疑会影响到模型的精度及灵活性。
解决:修改NCNN模型的param文件,将Resize算子修改成按比例resize。
直接转换得到的param文件中的Interp算子是这样的:
Interp913119019130=21=1.000000e+002=1.000000e+003=6404=640
从下面的ncnn源码中可以看到,0代表resize/_type,1和2分别是高和宽的缩放比例,3和4分别是输出的高和宽。
intInterp::load_param(constParamDict&pd)
{
resize_type=pd.get(0,0);
height_scale=pd.get(1,1.f);
width_scale=pd.get(2,1.f);
output_height=pd.get(3,0);
output_width=pd.get(4,0);
return0;
}
我们只需将其修改成如下格式即可实现按比例resize:
Interp913119019130=11=4.000000e+002=4.000000e+00
问题5:NCNN模型输出结果与ONNX模型不同
解决:逐层对比NCNN与onnx模型的输出结果
使用onnxruntime(Python)和NCNN(C++)分别提取每个节点的输出,进行对比。对于ncnn比较简单,可以使用
extractor.extract(node_name,preds);
来提取不同节点的输出。
问题5衍生问题1:ONNX没有提供提取中间层输出的方法
解决:给要提取的层添加一个输出节点,代码如下:
deffind_node_by_name(graph,node_name):
fornodeingraph.node:
ifnode.output[0]==node_name:
returnnode
returnNone
defadd_extra_output_node(model,target_node,output_name):
extra_output=helper.make_empty_tensor_value_info(output_name)
target_output=target_node.output[0]
identity_node=helper.make_node("Identity",inputs=[target_output],outputs=[output_name],name=output_name)
model.graph.node.append(identity_node)
model.graph.output.append(extra_output)
returnmodel
修改模型之后再使用
out=sess.run([output_name],{"input.1":img.astype(np.float32)})
就可以获取到模型的中间层输出了。
问题5衍生问题2:发现最后一个Resize层的输出有差异
解决:参考chineseocr/_lite里面的代码把mode由bilinear改成了nearest(这里错误的原因可能是wenmuzhou/PSENet.pytorch中的模型最后一个F.interpolate中的align/_corners参数设置成了True。据说NCNN只实现了align/_corners为False的情况)。
这里修改之后的模型跟原模型之间是会有少许误差的,如果误差不可接受,就要重新训练才行。
2. 交叉编译opencv与ncnn
交叉编译工作可以在windows上进行,使用的是MinGW + cmkae3.10 + AndroidNDK18。可以参考Windows下编译OpenCV android(https://www.cnblogs.com/zhxmdefj/p/13094954.html
)
没有windows C++环境的话,也可以选择在linux上进行。
如果是在linux交叉编译,然后复制到windows的话,需要修改一下opencv中cmake配置文件中的路径。
(1)android ndk下载
最初选择的是r20b,因为和CMake之间的兼容问题,切换到了18b。
wgethttps://dl.google.com/android/repository/android-ndk-r18b-linux-x86_64.zip?hl=zh_cn
mvandroid-ndk-r18b-linux-x86_64.zip?hl=zh_cnandroid-ndk-r18b-linux-x86_64.zip
unzipandroid-ndk-r18b-linux-x86_64.zip
(2)编译opencv
利用android中提供的android.toolchain.cmake 工具链可以快速的编译opencv的arm版。
这里选择的arm平台是armeabi-v7a,便于在老旧手机上运行。
folde
if[[!-d"$folder"]];then
echo"$foldernotfound,creatingfolder..."
mkdirbuild_arm
fi
cdbuild_arm
cmake/
-DCMAKE_TOOLCHAIN_FILE=/
/home/dai/soft/android-ndk-r18b/build/cmake/android.toolchain.cmake/
-DANDROID_NDK=/home/dai/soft/android-ndk-r18b/
-DCMAKE_BUILD_TYPE=Release/
-DBUILD_ANDROID_PROJECTS=OFF/
-DBUILD_ANDROID_EXAMPLES=OFF/
-DANDROID_ABI=armeabi-v7a/
-DANDROID_NATIVE_API_LEVEL=21..
make-j4
(3)编译ncnn
编译选项参考ncnn wiki(https://github.com/Tencent/nc...
folde
if[[!-d"$folder"]];then
echo"$foldernotfound,creatingfolder..."
mkdirbuild_arm
fi
cdbuild_arm
cmake/
-DCMAKE_TOOLCHAIN_FILE=/
/home/dai/soft/android-ndk-r18b/build/cmake/android.toolchain.cmake/
-DANDROID_AB/
-DANDROID_ARM_NEON=ON/
-DANDROID_PLATFORM=android-14/
..
make-j4
(4)chineseocr/_lite的PC端测试
与ncnn有关的代码位于ncnn/_project目录下。在有opencv和ncnn库的基础上,可以先在pc端跑一下识别代码。
cdncnn_project/ocr
mkdirbuild_arm
cdbuild_arm
cmake..
make-j4
编译完成之后
./TextRecognition../test1.jpg
可以看到输出结果:
psenet前向时间:0.462291s
psenetdecode时间:0.0604791s
boxzie10
预测结果:
一
统
;名
称
丹正珍
类住
型
有限责
所
中山市
角度检测和文字识别总时间:1.52042s
3. NCNN模型的安卓端部署
因为代码较长,这一部分只介绍把PC端代码迁移到安卓端的思路,想看详细代码的同学请移步文末的Github地址。
迁移的整体思路如下图所示:
android/_flow.png
下面一一介绍图中内容
UI界面
这个demo的UI界面中至少应包含三个元件:
Button——用于选择相册图片
ImageView——用于展示图片及文本检测框
TextView——用于展示识别结果
界面截图如下(TextView在没有文字的时候是不显示的):
UI.jpg
界面res/layout/activity/_main.xml文件修改。
Java部分
模型推理是在C++中完成的,Java部分的代码主要是利用安卓的API实现图片读取、文本检测框绘制和结果展示等功能。
需要传入到C++函数的内容包括Bitmap图片和AssetManager对象。
从C++接收到的是一个包含文本框和识别结果的字符串。
C++部分
C++负责模型推理,推理代码与PC端无异,只是安卓端的文件读取与PC端不同,需要修改文件读取代码,比如crnn的模型加载代码就需要改成下面的样子:
intmodel::init(AAssetManager*mgr,conststd::stringcrnn_param,conststd::stringcrnn_bin)
{
intret1=crnn.load_param(mgr,crnn_param.c_str());
intret2=crnn.load_model(mgr,crnn_bin.c_str());
LOGI("ret1is%d,ret2is%d",ret1,ret2);
return(ret1||ret2);
}
另外还需要把Java部分传过来的Bitmap转换成cv::Mat,代码如下:
//convertbitmaptomat
int*data=NULL;
AndroidBitmapInfoinfo={0};
AndroidBitmap_getInfo(env,bitmap,&info);
AndroidBitmap_lockPixels(env,bitmap,(void**)&data);
//这里偷懒只写了RGBA格式的转换
LOGI("infoformatRGBA?%d",info.format==ANDROID_BITMAP_FORMAT_RGBA_8888);
cv::Mattest(info.height,info.width,CV_8UC4,(char*)data);//RGBA
cv::Matimg_bgr;
cvtColor(test,img_bgr,CV_RGBA2BGR);
最终识别结果
最终得到的demo识别结果如下图所示:
result.jpg
本项目完整代码请移步github:
https://github.com/Arctanxy/D...
推荐阅读
更多嵌入式AI技术干货请关注嵌入式AI专栏。
审核编辑:符乾江
-
AI
+关注
关注
87文章
31097浏览量
269420 -
深度学习
+关注
关注
73文章
5507浏览量
121266 -
pytorch
+关注
关注
2文章
808浏览量
13249
发布评论请先 登录
相关推荐
评论