前言
前两天照着OpenCV官方的How to run deep networks on Android device教程,在自己的小米手机上进行了测试,可能是由于手机的API兼容问题,使用OpenCV封装的JavaCameraView控件不能正常打开摄像头,始终是黑屏的状态,起初我以为是SELinux在作妖,直到后面使用Camera2 API才顺利捕获到摄像头帧完成测试。此外,使用OpenCV的官方代码会始终提示要安装OpenCV Manager,即使我已经加载OpenCV的动态链接库,后来直接跳过OpenCV Manager的初始化,程序才运行正常。这篇文章简单记录了下OpenCV使用DNN模块加载SSD模型涉及的用法,作为自己的一点积累。
DNN模型加载
自OpenCV4.2.0版本后,DNN模块的使用逐渐完善。目前,DNN模块支持包括OpenCL、OpenVINO、CUDA等后端,可加载包括Caffe、TensorFlow、ONNX等格式的模型进行推理,可以说功能全面,非常值得一试。使用OpenCV Java接口加载SSD模型的代码可参考官方例程,本文仅对一些需要注意的地方做一些记录。
加载OpenCV运行库
官方提供的示例代码会提示需要额外安装OpenCV Manager APP,但对于有强迫症的开发人员来说,这是不可能的。。因此在确保程序正常运行的同时,需要做一些简单处理,跳过OpenCV Manager的初始化,示例代码如下:
public void openCVInit() {
// Initialize OpenCV manager.
BaseLoaderCallback loaderCallback = new BaseLoaderCallback(this) {
@Override
public void onManagerConnected(int status) {
switch (status) {
case LoaderCallbackInterface.SUCCESS: {
Log.i(TAG, "OpenCV loaded successfully");
break;
}
default: {
super.onManagerConnected(status);
break;
}
}
}
};
if (OpenCVLoader.initDebug()) {
System.loadLibrary("opencv_java4");
loaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);
}
else {
OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION, this, loaderCallback);
}
}
上面的openCVInit函数同官方代码不同的地方在于,跳过了OpenCV Manager的初始化,同时手动加载OpenCV 运行库。在编写安卓APP时,可在类构造函数或Activity的onResume函数中调用此函数完成OpenCV的初始化。
设置OpenCV 后端
在官方代码中使用的是默认的后端,在实际应用中,可能会尝试在不同的后端运行,以确定效果最好的运行后端。可在官方loadModel函数中添加设置后端的代码,具体代码如下:
public void loadModel() {
String proto = getPath("MobileNetSSD_deploy.prototxt", context);
String weights = getPath("MobileNetSSD_deploy.caffemodel", context);
net = Dnn.readNetFromCaffe(proto, weights);
net.setPreferableBackend(Dnn.DNN_BACKEND_OPENCV);
net.setPreferableTarget(Dnn.DNN_TARGET_CPU);
Log.i(TAG, "Network loaded successfully");
}
上面的setPreferableTarget即指定模型在什么设备上运行,查看官方源码可以看到,可选的运行设备有如下:
public static final int
DNN_TARGET_CPU = 0,
DNN_TARGET_OPENCL = 0+1,
DNN_TARGET_OPENCL_FP16 = 0+2,
DNN_TARGET_MYRIAD = 0+3,
DNN_TARGET_VULKAN = 0+4,
DNN_TARGET_FPGA = 0+5,
DNN_TARGET_CUDA = 0+6,
DNN_TARGET_CUDA_FP16 = 0+7;
如果设备支持,应选择DNN_TARGET_CUDA_FP16 或DNN_TARGET_OPENCL_FP16使用半精度浮点数计算,可在精度无损失的前提下进一步提升推理实时性。
Bitmap转Mat
如果不使用OpenCV提供的视频预览控件,那么会涉及到如何将捕获到的Bitmap对象转换为Mat的问题。当然,OpenCV已经准备好相关的接口,用户只需调用函数即可完成转换,具体的代码如下:
import org.opencv.android.Utils;
import org.opencv.imgproc.Imgproc;
public Mat convertBitmapToMat(Bitmap inputFrame) {
Mat frame = new Mat();
Utils.bitmapToMat(inputFrame, frame);
Imgproc.cvtColor(frame, frame, Imgproc.COLOR_RGBA2RGB);
return frame;
}
在上面的代码中,bitmapToMat函数负责具体的Bitmap转Mat工作,传入的Mat对象可以为空。通常Bitmap都是RGBA的格式,而模型输入则是RGB格式,因此还需要调用cvtColor函数对转换后的Mat进行格式转换。
模型推理
在经过OpenCV初始化、模型加载和数据获取后,即可进行模型推理的步骤,使用DNN模块进行模型推理的方法也是非常简单,具体的代码如下:
final int IN_WIDTH = 300;
final int IN_HEIGHT = 300;
final double IN_SCALE_FACTOR = 0.007843;
final double MEAN_VAL = 127.5;
Mat blob = Dnn.blobFromImage(
frame,
IN_SCALE_FACTOR,
new Size(IN_WIDTH, IN_HEIGHT),
new Scalar(MEAN_VAL, MEAN_VAL, MEAN_VAL),
/*swapRB*/false,
/*crop*/false
);
net.setInput(blob);
Mat detections = net.forward();
可以看出DNN的推理流程与TensorFlow较为相似,可理解为先定义模型的输入占位符,接着是传入图像张量开始推理,模型推理的结果最终以Mat的形式返回。
结语
得益于OpenCV社区的推进,使用OpenCV进行模型推理的步骤已经相当简单,对于OpenCV的用户而言,使用Mat作为模型输入输出简直不能太友好,同时OpenCV也支持多样的后端设备,使得它的实用性进一步增强。不过在官方示例中需要吐槽的是,提供的模型性能太烂,且在安卓设备上使用DNN_TARGET_OPENCL或者DNN_TARGET_OPENCL_FP16选项推理时几乎没有看到实时性的提升,可能是未适配安卓设备的OPENCL接口的原因,处理一帧图片大约需要220ms,跟TensorFlow-Lite官方示例一比简直是被按在地上摩擦,后者仅需大约50ms,只能说还有相当大的进步空间。