VS2019 OpenCV的Windows工程到安卓的移植

前言

本篇是应一个读者的请求,而且这种需求应该还是蛮多的:刚入职或者刚进实验室的新人,接手一套C++算法工程,现在老板让你移植到Android上。全部用Java重写,当然是不现实的。本文将介绍一种尽可能简单的移植方法。

本文使用的软件版本如下:

  • VisualStudio 2019 16.3.5
  • AndroidStudio 3.5
  • NDK-r16B
  • Android SDK 21
  • Opencv 4.1.1(android & windows)

准备测试用的OpencvWindows项目

首先请参考 VS2017的C++开发心得(八)DLL动态链接——Opencv的使用建立一个简单的OpenCV VS解决方案,如下:

一个简单的cv::sum的使用。

开始进行安卓端的移植

1. 先创建一个VS的C++安卓.so项目,参考VS2019 C++的跨平台开发——Android .so开发

2. 一些基本的移植概念

所谓的移植,就是移除过去OpenCV对于Windows底层的依赖。

一听到Windows的底层依赖,你是不是感觉自己都是用的C++的标准库在编程,没有用到任何Windows的接口,应该不会有任何依赖。举个一个简单例子,比如你的项目在使用Opencvxxx.dll的时候,会用到LoadLibrary这个函数,而这个函数就是由Windows的user32.dll实现的。

在有全部源码的情况下可以使用安卓的NDK进行重新编译源码,然后要把所有使用的第三方库文件.dll和.lib,替换为安卓端的.so和.a。

3.使用NDK进行项目编译

第一种老实的做法,在步骤1.中建立的安卓项目下面把所有的.cpp和.h文件按照原来的项目结构拷贝过去。

第二种取巧的做法,直接修改“Opencv411Template.vcxproj”和“Opencv411Template.sln”为安卓项目。

(第三种不推荐的做法,在AndroidStudio的JNI中导入所有的.cpp和.h进行编译,用AndroidStudio开发C++实在不推荐)

这里简单介绍第二种做法,尝试之前请备份整个项目。

首先用文本工具打开“Opencv411Template.vcxproj”(准备的Opencv Windows项目)和“SharedObject7.vcxproj”(步骤1.中建立的安卓项目)。

先看下Windows的项目文件:

上图红色框内的就是需要进行编译的文件。除了这部分其他全部替换为“SharedObject7.vcxproj”(下图)中的内容就行,是不是很简单:

把两个红框标注的内容合并起来,记得还要把 "SharedObject7.cpp" "SharedObject7.h""pch.h" 这三个文件拷贝到Opencv411Template的对应目录下,合并结果如下:

接下来修改.sln文件,用右方的红框内的内容替换到左边:

重新打开Opencv411Template项目看看:

已经变成安卓项目,接下来的工作就是替换Windows工程的Opencv4.1.1为安卓的opencv-4.1.1-android-sdk。这里和Windows的主要区别在于加载的库不一样。

先简单说下Android库的头文件目录和库文件目录:

头文件位于:\opencv-4.1.1-android-sdk\OpenCV-android-sdk\sdk\native\jni\include

库文件位于:

\opencv-4.1.1-android-sdk\OpenCV-android-sdk\sdk\native\3rdparty\libs

\opencv-4.1.1-android-sdk\OpenCV-android-sdk\sdk\native\libs

\opencv-4.1.1-android-sdk\OpenCV-android-sdk\sdk\native\staticlibs

头文件很简单直接添加目录就行,没有变化。

关键是库文件,Windows工程只链接一个dll(或者静态编译几个.lib)。这里先必须链接一个libopencv_java4.so,其次根据你的需求要添加多个静态库进行编译。静态库的链接,懂的自然懂,还不懂的,就添加所有.a进去,多十几M的空间大家都还能接受。

下面是VS2019中安卓库的链接操作。首先把上面的三个路径添加到链接器的附加库目录里面,如下,注意红框的选择:

最后就是添加附加库:

由于Opencv安卓的库名没有Debug和Release的区别,也没有平台区别,而是用文件夹名称来区分库。这是比较好的命名方式,方便使用宏定义路径。

完整的库列表:

-lopencv_java4
-lopencv_calib3d
-lopencv_core
-lopencv_dnn
-lopencv_features2d
-lopencv_flann
-lopencv_highgui
-lopencv_imgcodecs
-lopencv_imgproc
-lopencv_ml
-lopencv_objdetect
-lopencv_photo
-lopencv_stitching
-lopencv_video
-lopencv_videoio
-lcpufeatures
-lIlmImf
-littnotify
-llibjasper
-llibjpeg-turbo
-llibpng
-llibprotobuf
-llibtiff
-llibwebp
-lquirc
-ltbb
-ltegra_hal
-lz
-ldl
-lm
-llog

然后信心满满的去编译,一堆错误:

不要怕,只是几个C++的设置而已。

再次编译项目:

成功了,注意从这里开始我都只示范Debug和ARM64配置下的编译.so的Android加载流程。

4.修改下.so的函数导出以便Java调用

修改导出文件SharedObject7.cpp如下(这个文件用来调用之前Windows项目下的函数接口,然后通过两种方式导出给JNI和Java类使用):

#include "SharedObject7.h"

#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "SharedObject7", __VA_ARGS__))
#define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, "SharedObject7", __VA_ARGS__))
extern float TestOpencv(float* buf, int len); //假设这是过去Opencv工程的导出接口
extern "C" {
	float ExternTestOpencv(float* buf, int len)//这个用来导出给Android JNI使用
	{
		return TestOpencv(buf,len);
	}
	//C++导出给Java类使用的命名规范
	//Java_packagename_classname_functionname
	//第一个传参总是JNIEnv* env
	//第二个传参 如果是static成员函数就是jclass type,
	//		    如果是非static成员函数就是jobject thiz,
	//第三个传参才是真正的参数
	JNIEXPORT jfloat JNICALL
		Java_com_jniexample_JNIInterface_CVTestSum(JNIEnv* env, jclass type, jfloatArray buf) //这个用来导出给Java使用
	{
		auto len = env->GetArrayLength(buf);
		jboolean notcopy = JNI_FALSE;
		float* fptr = env->GetFloatArrayElements(buf, &notcopy);//从Java内存转换到native指针
		return TestOpencv(fptr, len);
	}
	JNIEXPORT jfloat JNICALL
		Java_com_jniexample_JNIInterface_TestSum(JNIEnv* env, jclass type, jfloatArray buf)//这个用来导出给Java使用
	{
		auto len = env->GetArrayLength(buf);
		jboolean notcopy = JNI_FALSE;
		float* fptr = env->GetFloatArrayElements(buf, &notcopy);
		float sum = 0;
		for (size_t i = 0; i < len; i++)
		{
			sum += fptr[i];
		}
		return sum;
	}
	/*此简单函数返回平台 ABI,此动态本地库为此平台 ABI 进行编译。*/
	const char * SharedObject7::getPlatformABI()
	{
	#if defined(__arm__)
	#if defined(__ARM_ARCH_7A__)
	#if defined(__ARM_NEON__)
		#define ABI "armeabi-v7a/NEON"
	#else
		#define ABI "armeabi-v7a"
	#endif
	#else
		#define ABI "armeabi"
	#endif
	#elif defined(__i386__)
		#define ABI "x86"
	#else
		#define ABI "unknown"
	#endif
		LOGI("This dynamic shared library is compiled with ABI: %s", ABI);
		return "This native library is compiled with ABI: %s" ABI ".";
	}

	void SharedObject7()
	{
	}

	SharedObject7::SharedObject7()
	{
	}

	SharedObject7::~SharedObject7()
	{
	}
}

至此,C++端的准备完成。

创建Android Studio下的JNI项目

AS我也不是什么专家,就简单贴下步骤:

创建完成,切换到项目视图:

第一件事,在上图中蓝色的文件夹java中添加一个Java类。

这个类用来对接.so中的两个导出函数:Java_com_jniexample_JNIInterface_CVTestSum和Java_com_jniexample_JNIInterface_TestSum。

所以,接下来就要创建这两个函数:

接着把生成.so放到工程下面:

请按照Opencv的文件夹结构来存放.so(我这里只演示了arm64-v8a的版本,其他架构文件名参考下图):

到这里,Java导入.so的部分也就结束了。再补充一个在AndroidStudio的JNI中导入.so的使用方法。

在JNI中导入即在AndroidStudio的.cpp文件中使用.so中的导出函数。

首先修改native-lib.cpp(自动生成的文件):

然后修改CMakeLists.txt(AS是使用的Cmake,所以多多少少要会点Cmake的语法):

 以上两步就是jni对外部的.so库的使用和链接。

这一步很重要,修改build.gradle(无论是JNI调用还是Java调用都需要修改这步)

苦闷的工作终于结束了,下面增加一点界面上的元素,用于显示.so的计算结果:

先双击activity_main.xml进行UI编辑:

增加两个button用于调用两个导出的Java函数,增加一个Textview用来显示计算结果:

修改UI的后端代码:

package cn.com.inxpar.nativeproject;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

import com.jniexample.JNIInterface;

import org.w3c.dom.Text;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
        System.loadLibrary("SharedObject7");
    }

    private TextView sumText;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());

        sumText=findViewById(R.id.textView);
        findViewById(R.id.button).setOnClickListener(this);
        findViewById(R.id.button2).setOnClickListener(this);
    }
    float[] a=new float[]{1,2,2,3,3,4};
    float sum=0;
    @Override
    public void onClick(View v) {

        switch (v.getId())
        {
            case R.id.button:
                sum= JNIInterface.CVTestSum(a);
                sumText.setText("OpenCV Sum:"+Float.toString(sum));
                break;
            case R.id.button2:
                sum= JNIInterface.TestSum(a);
                sumText.setText("Raw Sum:"+Float.toString(sum));
                break;
            default:
                break;
        }
        }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();
}

至此,大功告成。

编译apk,分析apk:

如果分析的apk里面有这几个.so库那就对了:

最后在arm64的安卓设备上运行:

看,是不是很简单。

提示

1.AndroidStudio中的虚拟机默认是使用的x86的安卓系统,所以应该用x86编译下的.so文件。

2.apk安装后一运行就提示xxx已停止工作(这就是安卓里面的崩溃),一般情况下是.so找不到,需要使用logcat自己排查问题。

3.app运行后点击某个按键后提示xxx已停止工作,logcat显示崩溃在xxxxx函数没有实现,一般错误是那两个导出给安卓的函数名不正确(是否为静态,传参是否正确),认真检查。

4.VS2019在使用Opencv4.1.1的安卓native sdk后,如果项目属性里选择的是 llvm-libc++静态库,那么会出现编译错误:undefined reference to `strtof_l'. 具体原因我也不清楚,但是由于Opencv使用libc++_shared,所以这里使用static本身也不合理,改成llvm-libc++共享库后就可以编译成功。

5.在进行大项目移植时,请先建立最小的opencv项目测试成功后再开始。

6.一定要会使用logcat

7.事已至此,请静下来学习一点Java和Android的开发知识,不要什么都直接去百度,最后拼凑出一个刚好能使用的项目。

安卓的官方文档:https://developer.android.com/studio/projects/add-native-code.html

https://developer.android.com/ndk/guides

猜你喜欢

转载自blog.csdn.net/luoyu510183/article/details/102710080