OpenCV3.1时代开始,Android平台就已经有官方提供的OpenCV库了,理论上我们是不需要再自行编译的。而且OpenCV的官方建议也是直接使用OpenCV4Android库(也就是预编译的libopencv_java3.so),并提供了两套使用方法:
- 利用OpenCV提供的全套Java接口, 在Android Java层调用。
- 利用OpenCV提供的C/C++ 接口, 在JNI层使用(就跟在PC端VC++下使用OpenCV一样一样的)。
是由于在实际的应用中难免会遇到一些问题,比如在Android工程中如果要同时使用SNPE(一个高性能神经网络加速库)和OpenCV时,由于SNPE使用的STL链接的是libc++,而OpenCV默认使用的是gnu_stl,所以会导致gradle不管怎么配置都无法正常编译过的情况。
这种情况下如果gradle中选择arguments '-DANDROID_STL=c++_shared’的话SNPE可以正常编译,但是在使用像imwrite这样的OpenCV函数时就会报链接错误。相反如果gradle中选择arguments '-ANDROID_STL=gnu_stl’则SNPE无法编译通过。
另外一方面,官方预编译好的OpenCV4Android库是不带contrib模块的,所以无法使用像是xfeatures2d、ArUco这样的库。
这里在windows下使用Andorid NDK编译,后期可以直接在Android 中使用或者部署。
文章目录
1、源码编译
(1)windows开发环境
win 10操作系统,CMake 3.21,Android Studio 2020.3的有关sdk信息如下截图
(2)下载源码
这里使用v4.5.4版本,两部分源码下载地址
opencv源码: https://github.com/opencv/opencv/releases/tag/4.5.4
opencv_contrib源码: https://github.com/opencv/opencv_contrib/releases/tag/4.5.4
两个包解压到如D:\opencv\opencv4.5.4下
(3)编译
可以在任意位置执行编译脚本,这里选择D:\opencv\opencv4.5.4目录,执行脚本为
python ./sources/platforms/android/build_sdk.py \
--extra_modules_path=D:/opencv/opencv4.5.4/opencv_contrib-4.5.4/modules/ \
--config ./sources/platforms/android/ndk-22.config.py
当提示 work_dir、opencv_dir或者ndk_path、sdk_path错误,需要再添加对应目录,可以直接查看 build_sdk.py的解释,例如添加ndk或者sdk目录 --sdk_path=E:/AndroidProjects/SDK
。
默认编译 arm64-v8a、 armeabi-v7a、x86、x86_64四个版本。根据需要修改ndk-22.config.py脚本即可,不做说明。
编译成功将在指定目录下生成OpenCV-android-sdk文件夹,内部文件夹为
2、Aruco的jni代码项目
参考前面的博文,仅使用opencv4java module,或者仅使用opencv native jni,或者两者都使用。这里演示Aruco姿态估计使用。
导入opencv module有多种方式,可以参考OpenCV-android-sdk下build.gradle的注释说明部分。
2.1、使用opencv的CameraActivity获取相机画面
为使用方便,直观看到检测效果,在项目中导入opencv module,使用CameraActivity实现相机画面的预览、能够实时通过回调获取相机画面的rgba格式的Mat对象。
注意给予app相机权限,这里的代码可以参考opencv sdk下的sample项目。
public class MainActivity extends CameraActivity implements CvCameraViewListener2 {
private static final String TAG = "cvCameraJni";
private CameraBridgeViewBase mOpenCvCameraView;
private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
@Override
public void onManagerConnected(int status) {
switch (status) {
case LoaderCallbackInterface.SUCCESS: {
Log.i(TAG, "OpenCV loaded successfully");
mOpenCvCameraView.enableView();
} break;
default: {
super.onManagerConnected(status);
} break;
}
}
};
public MainActivity() {
Log.i(TAG, "Instantiated new " + this.getClass());
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mOpenCvCameraView = (CameraBridgeViewBase) findViewById(R.id.javaCameraView);
mOpenCvCameraView.setVisibility(SurfaceView.VISIBLE);
mOpenCvCameraView.setCvCameraViewListener(this);
}
@Override
public void onPause()
{
super.onPause();
if (mOpenCvCameraView != null)
mOpenCvCameraView.disableView();
}
@Override
public void onResume()
{
super.onResume();
if (!OpenCVLoader.initDebug()) {
Log.d(TAG, "Internal OpenCV library not found. Using OpenCV Manager for initialization");
OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_0_0, this, mLoaderCallback);
} else {
Log.d(TAG, "OpenCV library found inside package. Using it!");
mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);
}
}
@Override
protected List<? extends CameraBridgeViewBase> getCameraViewList() {
return Collections.singletonList(mOpenCvCameraView);
}
public void onDestroy() {
super.onDestroy();
if (mOpenCvCameraView != null)
mOpenCvCameraView.disableView();
}
public void onCameraViewStarted(int width, int height) {
Log.d(TAG, "start size " + width + "x" + height);
}
public void onCameraViewStopped() {
}
public Mat onCameraFrame(CvCameraViewFrame inputFrame) {
return inputFrame.rgba(); // 必须返回rgba格式, CvCameraViewFrame还封装了对应的gray格式
}
}
后面是应用,我们可以在 onCameraFrame()
回调函数中对实时画面进行操作、并将处理后的画面返回预览显示的app中。
2.2、实现jni部分代码
2.2.1、Java中封装类CvAurcoWrapper
为了在Java中简单使用,封装一个CvAurcoWrapper.java
类,并定义需要的native
函数,如下
public class CvAurcoWrapper {
static {
System.loadLibrary("cvcamerajni");
}
public CvAurcoWrapper(){}
public boolean init(String detectorParamsFile, String cameraParamsFile) {
mNativeObj = nativeCreateAurco(detectorParamsFile, cameraParamsFile);
return mNativeObj != 0;
}
public boolean detect(Mat imageGray, Mat pos) {
return nativeDetect(mNativeObj, imageGray.getNativeObjAddr(), pos.getNativeObjAddr());
}
public void release() {
nativeDestroyAurco(mNativeObj);
mNativeObj = 0;
}
private long mNativeObj = 0; // 用来保存c++下对象的指针
// 初始化、返回c++对象指针
private static native long nativeCreateAurco(String detectorParamsFile, String cameraParamsFile);
// 通过传递c++对象指针进行资源释放
private static native void nativeDestroyAurco(long thiz);
// 通过传递c++对象指针,以及图像和接收结果的指针,进行Aruco姿态估计
private static native boolean nativeDetect(long thiz, long inputImage, long pos);
}
2.2.1、jni代码native函数的实现
在实现上面三个native函数前,先说明c++下的Aruco模块使用类封装,给出公有函数申明如下
/// #include "DetectMarkersApi.h"
#pragma once
#include "opencv2\aruco.hpp"
class DetectMarkersApi
{
public:
DetectMarkersApi();
~DetectMarkersApi();
/**
* 初始化检测参数、相机参数
* @param detectorParamsFile detector_params.yml文件目录
* @param cameraParamsFile camera_params.xml文件目录
*/
bool init(const std::string detectorParamsFile, const std::string cameraParamsFile);
/**
* 检测、姿态估计
* @param src 待处理图像
* @param tvec 姿态计算结果,单位米
* @param rvec 旋转向量
* @return 是否检测到有效 Marker
*/
bool getPosition(const cv::Mat &src, cv::Vec3d &tvec, cv::Vec3d &rvec);
// draw results
void draw(cv::Mat &src, cv::Vec3d tvec, cv::Vec3d rvec);
}
native的jni部分的函数名必须为 “包名_函数名”,参数为对应jni类型;由于native使用c语言分离实现的方式,将一个类的成员函数功能调用全部通过对象指针方式实现。
注意以下代码使用了异常,需要在app的build.gradle的android.defaultConfig下添加支持
externalNativeBuild {
cmake {
cppFlags "-std=c++11 -frtti -fexceptions"
}
}
jni代码如下所示:
#include "CvAurcoWrapper_jni.h"
#include "DetectMarkersApi.h"
#include <string>
#include <vector>
#include <android/log.h>
#include <opencv2/imgproc.hpp>
#define LOG_TAG "CvAurcoWrapper_nativeCreateAurco"
#define LOGD(...) ((void)__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__))
#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__))
JNIEXPORT jlong JNICALL Java_com_example_cvcamerajni_CvAurcoWrapper_nativeCreateAurco
(JNIEnv *env, jclass clazz, jstring detector_params_file, jstring camera_params_file)
{
LOGD("CvAurcoWrapper_nativeCreateAurco enter");
const char *jdetctcorstr = env->GetStringUTFChars(detector_params_file, NULL);
const char *jcamerastr = env->GetStringUTFChars(camera_params_file, NULL);
std::string detectorParamsFile(jdetctcorstr);
std::string cameraParamsFile(jcamerastr);
jlong result = 0;
try{
DetectMarkersApi *ptr = new DetectMarkersApi();
if( ptr->init(detectorParamsFile, cameraParamsFile) ){
result = (jlong)ptr;
}else{
LOGE("================== DetectMarkersApi init failed. !!!!!!!!!!!!!!!!");
delete ptr;
}
}
catch(const cv::Exception &e){
jclass je = env->FindClass("org/opencv/core/CvException");
if(!je)
je = env->FindClass("java/lang/Exception");
env->ThrowNew(je, e.what());
}
catch (...)
{
jclass je = env->FindClass("java/lang/Exception");
env->ThrowNew(je, "Unknown exception in JNI code of Java_com_example_cvcamerajni_CvAurcoWrapper_nativeCreateAurco");
return 0;
}
return result;
}
JNIEXPORT void JNICALL Java_com_example_cvcamerajni_CvAurcoWrapper_nativeDestroyAurco
(JNIEnv *env, jclass clazz, jlong thiz)
{
LOGD("CvAurcoWrapper_nativeDestroyAurco");
try{
if(thiz != 0){
delete (DetectMarkersApi*)thiz;
}
}
catch (...)
{
LOGE("nativeDestroyObject caught unknown exception");
jclass je = env->FindClass("java/lang/Exception");
env->ThrowNew(je, "Unknown exception in JNI code of CvAurcoWrapper_nativeDestroyAurco()");
}
LOGD("Java_com_example_cvcamerajni_CvAurcoWrapper_nativeDestroyAurco exit");
}
JNIEXPORT jboolean JNICALL Java_com_example_cvcamerajni_CvAurcoWrapper_nativeDetect
(JNIEnv *env, jclass clazz, jlong thiz, jlong input_image, jlong pos)
{
jboolean result = false;
try {
cv::Vec3d rvec;
cv::Vec3d tvec;
// // 仅返回结果
// result = ((DetectMarkersApi*)thiz)->getPosition(*((cv::Mat *) input_image), tvec, rvec);
// if(result) {
// ((cv::Mat *)pos)->at<cv::Vec3d>(0,0) = tvec;
// ((DetectMarkersApi *) thiz)->draw( *((cv::Mat *) input_image), tvec, rvec);
// }
// 将结果绘制在Mat上用于显示
cv::Mat rgb;
cv::cvtColor(*((cv::Mat*)input_image), rgb, cv::COLOR_RGBA2BGR);
result = ((DetectMarkersApi*)thiz)->getPosition(rgb, tvec, rvec); // 检测只能是rgb或gray图
if(result) {
((cv::Mat *)pos)->at<cv::Vec3d>(0,0) = tvec; // 仅返回位移
((DetectMarkersApi *) thiz)->draw(rgb, tvec, rvec); //叠加检测结果
cv::cvtColor(rgb, *((cv::Mat *) input_image), cv::COLOR_BGR2RGBA);
}
}
catch(const cv::Exception& e)
{
LOGD("CvAurcoWrapper_nativeDetect caught cv::Exception: %s", e.what());
jclass je = env->FindClass("org/opencv/core/CvException");
if(!je)
je = env->FindClass("java/lang/Exception");
env->ThrowNew(je, e.what());
}
catch (...)
{
LOGD("nativeDetect caught unknown exception");
jclass je = env->FindClass("java/lang/Exception");
env->ThrowNew(je, "Unknown exception in JNI code CvAurcoWrapper_nativeDetect()");
}
return result;
}
2.3、java和jni中的代码逻辑实现
整体逻辑为:
- 在java中定义一个类型为
CvAurcoWrapper
成员变量 - 在
onCreate()
中进行初始化 - 在
onCameraFrame()
进行检测
初始化需要的检测参数文件、相机标定参数文件放在外存中,初始化读取时需要授权读写权限。
String modelPath = getExternalStorageDirectory().getAbsolutePath();
String detectorParamsFile = modelPath + "/detector_params.yml";
String cameraParamsFile = modelPath + "/out_camera_data.xml";
mCvAurcoWrapper = new CvAurcoWrapper();
if(!mCvAurcoWrapper.init(detectorParamsFile, cameraParamsFile)){
Log.e(TAG, "onCreate: mCvAurcoWrapper null");
}
对于检测,将结果绘制带实时预览画面,修改onCameraFrame()函数如下
public Mat onCameraFrame(CvCameraViewFrame inputFrame) {
// Log.d(TAG, "onCameraFrame: " + inputFrame.rgba().size());
// return inputFrame.rgba();
Mat frame = inputFrame.rgba();
if(mCvAurcoWrapper != null) {
Mat pos = new Mat(); // 接收姿态,目前仅位移x、y、z
pos.create(1,3, CvType.CV_64FC1);
if(mCvAurcoWrapper.detect(frame, pos)) {
double[] posXYZ = new double[3];
pos.get(0, 0, posXYZ);
Log.d(TAG, "res: " + posXYZ[0] + ", " + posXYZ[1] + ", " + posXYZ[2]);
}
}
return frame;
}
如果只希望得到结果,修改jni中从Java_com_example_cvcamerajni_CvAurcoWrapper_nativeDetect的实现,并修改onCameraFrame()函数如下
public Mat onCameraFrame(CvCameraViewFrame inputFrame) {
// Log.d(TAG, "onCameraFrame: " + inputFrame.rgba().size());
// return inputFrame.rgba();
if(mCvAurcoWrapper != null) {
Mat pos = new Mat(); // 接收姿态,目前仅位移x、y、z
pos.create(1,3, CvType.CV_64FC1);
if(mCvAurcoWrapper.detect(inputFrame.gray(), pos)) {
double[] posXYZ = new double[3];
pos.get(0, 0, posXYZ);
Log.d(TAG, "res: " + posXYZ[0] + ", " + posXYZ[1] + ", " + posXYZ[2]);
}
}
return inputFrame.rgba();
}
编译运行即可。
3、机体坐标系转换
aruco的marker坐标系检测结果,和机体坐标系不一致。无人机z轴为roll轴,x轴为pitch轴,y轴为yaw轴,右手法则对应的红色方向为正向。无人机镜头垂直向下,aruco mark坐标系红色为x轴(向右),绿色为y轴(向上),蓝色为z轴(屏幕向外)。
相机坐标系向前为z,向右为x,向上为y,与aruco marker的坐标系仅x轴方向相同,其他轴方向相反。当无人进行roll操作时,对应在相机的 z 操作;无人机进行pitch操作,对应相机的 x 操作;无人机进行yaw操作,对应相机的 y 操作。相机欧拉角与无人机欧拉角存在顺序差异。
因此,aruco检测的得到的姿态转换到无人机坐标系下,需要进行翻转、顺序调整,代码如下:
vec -- Rodrigues --> R
R -> flip y + flip z -> Rc
Rc -> euler Angle[1,3] -> UAV Eular { angle[2], angle[0], angle[1] }
4、示例
c++中提供两个Aruco marker,当检测到两个marker时仅返回小的marker结果。视频结果如下:
opencv aruco姿态估计
DJI无人机msdk/osdk上使用Aruco进行姿态估计,实现无人机引导精准降落。下面视频中无人机距离地面4M,2个marker打印在A4纸上,app上实时图传演示如下:
aruco引导无人机降落
4.1 实际使用
marker引导无人机降落会使用多个marker,如果使用aruco库的姿态估计方法,会计算多个相对于相机的姿态,需要反复转换计算无人机最终的调整偏移。这种方法复杂,实测仅使用aruco marker的检测方法,再使用pnp方法一次性求解r、t会更简洁、稳定。