java ndk编译opencv、opencv_contrib编译和使用相机Aruco姿态估计

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会更简洁、稳定。

  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 19
    评论
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

aworkholic

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值