如何将 OpenCV 整合 Android JNI 开发 C++ 代码并打包成 aar lib 给其他工程使用

a0e6bbcb7d5bf378c96bfbbc0920eaab.jpeg

动机

  • 本身是作影像视觉相关的工作,多数时候都使用 Python 作为开发语言,但OpenCV 本身是C++开发,学 C++ 应该有帮助。

  • 公司有 Android/iOS Team ,有些功能需要与他们整合,多了解对方领域可以减少沟通成本。

  • 网络上有关 OpenCV 与 Android Studio 整合的教学零散文章,这次整合成功后,把这些碎片化资讯整理起来记录,避免之后有同样需求时又辛苦一次,这次整合采了太多坑,如果不做个记录,三个月后就会忘了。

环境

  • Android Studio Chipmunk | 2021.2.1 Patch 1 (Ubuntu 环境)

  • JNI 有 cmake 和ndk-build两种方法,我是用Cmake (版本3.22.1 )如果你参考的教学有用到 Android.mk 或Application.mk ,那这篇有关 build 的方式会很不一样。

  • OpenCV 4.5.5 / 4.6.0 

Android Studio 连结 JNI

开启新工程,使用Base Activity 即可。

0b0d3b035ea195818d818fb1f9815eed.png

取名叫MyApp,方便之后模拟使用这个lib 的人。

40e5f60b40698d93eef542978236328c.png

由于我们最终目的是要做一个 aar lib 。所以 myapp 用来当作 application 使用,另外,再新增一个Module 来作为 C++ lib 开发。

点选 [Files]>[New]>[New Module…] 新增 MyOpenCv。

f387debdbade764c93bece703b23e736.png

这时的目录结构如下,除了 app 以外,多了MyOpenCv 这个Module,有CMakeLists.txt, myopencv.cpp, NativeLib三个档案。

8a8782f27fdeeea294349798859f8e19.png

此时,到 app 的 build.gradle 新增刚刚的module 作为依赖并执行gradle sync。

c593db6d45d211334e05b2f8f6937eb2.png

sync 后,可以到 app 的MainActivity.java 测试是不是可以抓到 JNI 的信息。

07d37b58756a321e7451978991c14c5a.pngb1190ada5f464cd317938e8254186337.png

这阶段就完成了从Android 呼叫C++ Lib部分。

备注1: 这边 CMakeLists.txt 能与Android 连接是通过build.gradle 里的这段:

externalNativeBuild {
     cmake {
         path "src/main/cpp/CMakeLists.txt" 
        version "3.18.1" 
    } 
}

备注2: MyOpenCv Module 和app 可以相连是通过settings.gradle 后两行的include。

pluginManagement {
     repositories {
         gradlePluginPortal() 
        google() 
        mavenCentral() 
    } 
}
 dependencyResolutionManagement {
     repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 
    repositories {
         google() 
        mavenCentral() 
    } 
}
 rootProject.name = "MyApp" 
include ':app' 
include ':MyOpenCv'

Cmake 与OpenCV 依赖

先至 OpenCV 官网(https://opencv.org/releases/)下载Andoird 版本并解压缩到想要的文件夹,这边示例是在/home/user/Documents/

f51e9b63058f55007fcf6c65431f8a03.png

接着在CMakeLists.txt 加入OpenCV 依赖并执行Gradle Sync。

其中 jnigraphics-lib 是等会将图片格式由Android Bitmap 转成OpenCV Mat 格式时会用到的。

cmake_minimum_required(VERSION 3.18.1)

set(OpenCV_DIR "/home/jason9075/Documents/OpenCV-android-sdk/sdk/native/jni")

find_package(OpenCV REQUIRED)

project("myopencv")

add_library(myopencv
            SHARED
            myopencv.cpp)

include_directories(${OpenCV_INCLUDE_DIRS})

find_library(log-lib
             log)

# For Android Bitvert to cv::Mat   
find_library(jnigraphics-lib jnigraphics)

target_link_libraries(myopencv
                      ${OpenCV_LIBS}
                      ${jnigraphics-lib} # For Android Bitmap Covert to cv::Mat
                      ${log-lib})

这时你会发现Sync 失败, 问题出在CMakeLists.txt 第五行没有抓到OpenCV 套件。

337d0dd93951fc1814b1f094aa4a960c.png

检视警告页面提示为:无法找到abi binary,位置在OpenCVConfig.cmake:47

17426ddc7b21c45c75087012ee83b6ab.png

然后…经过我漫长的寻找…不断的在cmake file 里用message() 确认各个变数,终于发现在OpenCVConfig.cmake 这个档案里的第39行中ANDROID_NDK_ABI_NAME 的值都是空的!

理论上它应该会是:[arm64-v8a, armeabi-v7a, x86, x86_64] ,各代表在不同环境的CPU架构。

b3b5fdfe23cd449fd37d2724c7b79632.png

OpenCVConfig.cmake

尝试注解掉原本的ANDROID_NDK_ABI_NAME 后,替换成 ANDROID_ABI 就可以成功的sync。

5eeee1485454d3313d0085e1cc874dfb.png

等 sync 完成后,就可以在myopencv.cpp 档案里引用OpenCV 而不会出错。

3c178f1b5f2ea84b5ece4ea0d4910090.png

备注:如果你自己的电脑本身有安装OpenCV ,不使用官网下载的OpenCV_DIR ,它可能会自己跑去找系统的版本(/usr/local/lib/cmake/opencv4),而发生错误。

我自己有发生set(OpenCV_DIR path) 路径打错,跑去抓/usr/local 底下的版本,然后不断出现:

C/C++: CMakeFiles/cvmodule.dir/cvmodule.cpp.o(.data+0x0): error: undefined reference to 'typeinfo for cv::Exception'

的错误。

OpenCV bash 开发

  1. 开发图片转灰度功能

新增一个将图片转成灰度的功能测试,这边要注意的是在JNI 使用上,必须定义好function 的signature,像名称要和java 的package name 对应。

extern "C" JNIEXPORT void JNICALL
Java_com_jason9075_myopencv_NativeLib_toGrey(
        JNIEnv *env, 
        jobject, 
        jobject bitmapIn, 
        jobject bitmapOut) {
    Mat src, greyOut;
    bitmapToMat(env, bitmapIn, src, false);
    cvtColor(src, greyOut, CV_BGR2GRAY);
    matToBitmap(env, greyOut, bitmapOut, false);
}

然后在NativeLib.java 里要宣告与C++对应的function。

public class NativeLib {

    static {
        System.loadLibrary("myopencv");
    }

    public native String stringFromJNI();

    public native void toGrey(Bitmap bitmapIn, Bitmap bitmapOut);
}

为了测试我们到应用程式app文件夹,把Android绿色机器人图片放到drawable 文件夹,然后在MainActivity 转成灰色。

6a1096acb1510fcb2759700b9d3b50ac.png

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    binding = ActivityMainBinding.inflate(getLayoutInflater());
    setContentView(binding.getRoot());

    setSupportActionBar(binding.toolbar);

    NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
    appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build();
    NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
    ImageView iv = findViewById(R.id.imageView);

    binding.fab.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                    .setAction("Action", null).show();
        }
    });

    // Test Hello World From MyOpenCv
    NativeLib cv = new NativeLib();
    System.out.println(cv.stringFromJNI());

    Bitmap image = BitmapFactory.decodeResource(getResources(), R.drawable.android);
    cv.toGrey(image, image);
    iv.setImageBitmap(image);
}

7fad210336de3c93487cf9cf6c6fd1bc.png

  1. 开发读取图片宽高功能

比如说今天Android Team 有个需求是传一张图片,我们分析图片宽高等资讯,而且希望他们可以直接拿到 Java Class。

我们先在Module 里新增一个DTO 叫ImageInfo。

package com.jason9075.myopencv;

public class ImageInfo {
    private final int width;
    private final int height;

    public ImageInfo(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }

    @Override
    public String toString() {
        return "ImageInfo{" +
                "width=" + width +
                ", height=" + height +
                '}';
    }
}

在 NativeLib.java 宣告相对应的 function getInfo()

package com.jason9075.myopencv;

import android.graphics.Bitmap;

public class NativeLib {

    static {
        System.loadLibrary("myopencv");
    }

    public native String stringFromJNI();

    public native void toGrey(Bitmap bitmapIn, Bitmap bitmapOut);

    public native ImageInfo getInfo(Bitmap bitmap);
}

然后在myopencv.cpp 新增C++ 实做方式。

须注意的一点是,因为我们最终要回传 Java Object,所以在C++这边要定义clsPath ,要找你预期回传的 Java Class 长的怎么样,还有这个Class 的Constructor 需要什么样的signature (这边 width 和 height 都是 Int 所以是(II)V)。

extern "C"
JNIEXPORT jobject JNICALL
Java_com_jason9075_myopencv_NativeLib_getInfo(JNIEnv *env, jobject thiz, jobject bitmap) {
    Mat src;
    bitmapToMat(env, bitmap, src, false);
    int width = src.cols;
    int height = src.rows;

    // return java object
    const char *clsPath = "com/jason9075/myopencv/ImageInfo";
    jclass cls = env->FindClass(clsPath);
    jmethodID constructor = env->GetMethodID(cls, "<init>", "(II)V");
    return env->NewObject(cls, constructor, width, height);
}

完成后,再回到 app 里实际测试。

// Test Hello World From MyOpenCv
NativeLib cv = new NativeLib();
System.out.println(cv.stringFromJNI());

Bitmap image = BitmapFactory.decodeResource(getResources(), R.drawable.android);
cv.toGrey(image, image);
iv.setImageBitmap(image);

System.out.println(">>> " + cv.getInfo(image));

我们可以成功读取到宽高分别为2688 和3197。(这边和原图宽高不同的原因是Android 的Drawable 会自动缩放,若想测试原图可以改放Asset文件夹)

bc06eb6b0fbf791e413d7b94e478399e.png

打包成 aar 给其他工程使用

上面的工程主要有两个部分,一个是 app 拥有Activity 来模拟使用这个lib的情况,令一个部分是 MyOpenCv 这个module 为实际lib 的内容,这是我们接下来要介绍的,如何将这个module 转成aar。

首先,我们先点击 [Build]>[Select Build Varient] 开启选单,再生成 aar 时可以选择 debug 或是 release 来发布,这边我选用release 做示范。

c3a02c0ff70e52e3bc3db1d9fae18b3c.png

设成release 之后,再去[Build]>[Rebuild Project]让工程建构一下,完成后会在build 文件夹的outputs 找到我们要的aar。

45be264361ddcc03d9c24da3eb2764f4.png

打开MyOpenCv-release.aar 可以看到在jni 里面存放各个不同架构的.so,代表正常。

c7e38ab1aa1f0a4c491e08d6bf2d7b52.png

我们开启令一个Android Project 叫AnotherApp ,并把MyOpenCv-release.aar 放入app/libs 里。

89b749c3c06f7e66f4f5882370462a94.png

然后在AnotherApp 的build.gradle 加入依赖。

f996f7f97baca15e27c61404539c033a.png

然后回到AnotherApp 的MainActivity我们加入这四行,测试自己撰写的aar lib 能不能成功使用。

65082632db7c50c9e37c18fc02d7dfdc.png

执行结果是程序有抓到图片在画面上的宽高。

1c52108747eb0c3a158e723ff8223b0c.png

以上就完成了运用JNI 连结OpenCV 开发C++,并打包成aar lib 的使用教学。

代码:

MyApp: https://github.com/jason9075/Android_with_OpenCV_Module

温馨提醒:使用时module 的CMakeLists.txt 请将OpenCV_DIR换成自己的sdk路径。

AnotherApp: https://github.com/jason9075/Android_use_OpenCV_AAR_lib

Ref

https://developer.android.com/studio/projects/configure-cmake

https://github.com/ValYouW/AndroidOpenCVDemo

https://stackoverflow.com/questions/9433257/how-to-specify-array-of-class-in-getmethodid-method-signature-parameter

https://stackoverflow.com/questions/22300848/return-object-from-java-native-method

https://stackoverflow.com/questions/51107185/how-to-create-new-android-aar-in-android-studio

☆ END ☆

如果看到这里,说明你喜欢这篇文章,请转发、点赞。微信搜索「uncle_pn」,欢迎添加小编微信「 woshicver」,每日朋友圈更新一篇高质量博文。

扫描二维码添加小编↓

9e2f81a60cde3a645f1fe2399f249a3a.jpeg

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值