OpenCV Android Studio

在Android Studio工程中使用Native的方式集成OpenCV

为什么要使用Native方式集成OpenCV

如果我们要处理的图片计算量不大,或者对处理速度不关注的时候,我们完全可以采用Java的来调用OpenCV。采用Java来调用OpenCV的集成方法非常简单,具体集成方法可以参考我的这个教程:https://panxsoft.coding.net/s/89438a3d-52b9-47e6-9c6b-23841f86cd8f

但是如果我们要进行实时处理,或者需要和其他的视觉库一起来使用,那么Java的处理方式就比较不友好了或者效率也不行(虽然OpenCV最终都是在C/C++上执行的,但是直接用C/C++来开发关键算法,明显会使得app的运行效率大大提升)。为了更快的执行效率,或者更好的与其他的视觉库融合,推荐采用Native的融合方式。

环境

Android Studio 3.2.1

NDK R16C

OpenCV 3.3.0

CMAKE 3.6.4

注意

关于NDK的安装

在国内由于网络的原因有时候通过Android Studio下载NDK的时候会造成下载不得部分文件缺失,这样NDK安装时候成功就不知道,在编译的时候回出现莫名其妙的错误(我就被这种错误深深的坑过)。所以安装NDK最好的办法是在Android官网上下载完整的安装包再解压到自己的电脑上。并在Android Studio中设置NDK的目录。

工程配置

更新Android SDK并安装NDK

下载OpenCV SDK的Android版本

去OpenCV官网下载OpenCV的Android版本:https://opencv.org/

创建一个新的Android Studio工程

选择Include C++ Support

选择一个Empty Activity

在C++ Support中勾选 -fexception和 -frtti

导入OpenCV Library Module

New -> Import Module

选择$(OpenCV for android SDK 所在目录)/sdk/java

一路next即可

修改OpenCV Library Module的build.gradle和你的app的build.gradle一致

例如:我的工程的的app的build.gradle的如下:

compileSdkVersion 28

defaultConfig {

minSdkVersion 18

targetSdkVersion 28

}

则应该将opencv library的build.gradle也修改为与app的build.gradle一致。下面是我修改之后

android {

compileSdkVersion 28 //与app的build.gradle一致

// buildToolsVersion "28.0.3" 这一行可以注视点

defaultConfig {

minSdkVersion 18 //与app的build.gradle一致

targetSdkVersion 28 //与app的build.gradle一致

}

buildTypes {

release {

minifyEnabled false

proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'

}

}

}

在 app module dependency中添加OpenCV Module

File -> Project structure -> Module app -> Dependencies tab -> New module dependency -> choose OpenCV library module

在工程的app/src/main目录下创建名为jniLibs的文件夹,并将$(OpenCV for android SDK 所在目录)/sdk/native/libs下面的文件夹拷贝到该文件夹路径下

为了是最终打包的APK尽可能的小,可以只拷贝对应ABI的文件.

将(OpenCV for android SDK 所在目录)/sdk/native/jni/include文件夹拷贝到app/src/main/cpp文件下

网上也有人说不用拷贝,在 CMakeLists.txt 设置就行了,但是我实际这样操作话会出现莫名其妙的错误,目前还不知道错误原因,等弄明白了再更新

如果在创建Android Studio工程的时候选择Include C++ Support,则在app/src/main文件夹下面会自动出现cpp文件夹。如果是自己手工添加C++ Support的话,存放C++文件的文件夹是自己定义的,总之就是将include文件放在你存放C++文件的文件夹下.

配置CMakeLists.txt文件

配置CMakeLists.txt有两种方法,选择一种适合你自己的

第一种:这种方式比较简单,一般不会出现什么错误,但是打包的*.so文件会稍微大一点,初学者推荐始终这种配置方式

cmake_minimum_required(VERSION 3.4.1)

# OpenCV 配置

include_directories(${CMAKE_SOURCE_DIR}/src/main/cpp/include)

add_library( lib_opencv SHARED IMPORTED )

set_target_properties(lib_opencv PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libopencv_java3.so)

# Creates and names a library, sets it as either STATIC

# or SHARED, and provides the relative paths to its source code.

# You can define multiple libraries, and CMake builds it for you.

# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.

native-lib

# Sets the library as a shared library.

SHARED

# Provides a relative path to your source file(s).

# Associated headers in the same location as their source

# file are automatically included.

src/main/cpp/native-lib.cpp )

# Searches for a specified prebuilt library and stores the path as a

# variable. Because system libraries are included in the search path by

# default, you only need to specify the name of the public NDK library

# you want to add. CMake verifies that the library exists before

# completing its build.

find_library( # Sets the name of the path variable.

log-lib

# Specifies the name of the NDK library that

# you want CMake to locate.

log )

# Specifies libraries CMake should link to your target library. You

# can link multiple libraries, such as libraries you define in the

# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.

native-lib

# OpenCV lib

lib_opencv

# Links the target library to the log library

# included in the NDK.

${log-lib} )

第二种:将opencv中的*.so文件一个一个指定打包。这种方法可以最大限度的减少打包的*.so文件的大小。但是对于OpenCV有一定的要求,初学者不建议采用这种�配置方式。

# NDK的最小版本

cmake_minimum_required(VERSION 3.4.1)

# 显示CMake Build的输出信息

set(CMAKE_VERBOSE_MAKEFILE on)

set(libs "${CMAKE_SOURCE_DIR}/src/main/jniLibs")

include_directories(${CMAKE_SOURCE_DIR}/src/main/cpp/include)

add_library(libopencv_java3 SHARED IMPORTED )

set_target_properties(libopencv_java3 PROPERTIES

IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libopencv_java3.so")

add_library(libopencv_calib3d STATIC IMPORTED )

set_target_properties(libopencv_calib3d PROPERTIES

IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libopencv_calib3d.a")

add_library(libopencv_core STATIC IMPORTED )

set_target_properties(libopencv_core PROPERTIES

IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libopencv_core.a")

add_library(libopencv_features2d STATIC IMPORTED )

set_target_properties(libopencv_features2d PROPERTIES

IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libopencv_features2d.a")

add_library(libopencv_flann STATIC IMPORTED )

set_target_properties(libopencv_flann PROPERTIES

IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libopencv_flann.a")

add_library(libopencv_highgui STATIC IMPORTED )

set_target_properties(libopencv_highgui PROPERTIES

IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libopencv_highgui.a")

add_library(libopencv_imgcodecs STATIC IMPORTED )

set_target_properties(libopencv_imgcodecs PROPERTIES

IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libopencv_imgcodecs.a")

add_library(libopencv_imgproc STATIC IMPORTED )

set_target_properties(libopencv_imgproc PROPERTIES

IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libopencv_imgproc.a")

add_library(libopencv_ml STATIC IMPORTED )

set_target_properties(libopencv_ml PROPERTIES

IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libopencv_ml.a")

add_library(libopencv_objdetect STATIC IMPORTED )

set_target_properties(libopencv_objdetect PROPERTIES

IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libopencv_objdetect.a")

add_library(libopencv_photo STATIC IMPORTED )

set_target_properties(libopencv_photo PROPERTIES

IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libopencv_photo.a")

add_library(libopencv_shape STATIC IMPORTED )

set_target_properties(libopencv_shape PROPERTIES

IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libopencv_shape.a")

add_library(libopencv_stitching STATIC IMPORTED )

set_target_properties(libopencv_stitching PROPERTIES

IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libopencv_stitching.a")

add_library(libopencv_superres STATIC IMPORTED )

set_target_properties(libopencv_superres PROPERTIES

IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libopencv_superres.a")

add_library(libopencv_video STATIC IMPORTED )

set_target_properties(libopencv_video PROPERTIES

IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libopencv_video.a")

add_library(libopencv_videoio STATIC IMPORTED )

set_target_properties(libopencv_videoio PROPERTIES

IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libopencv_videoio.a")

add_library(libopencv_videostab STATIC IMPORTED )

set_target_properties(libopencv_videostab PROPERTIES

IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libopencv_videostab.a")

add_library(libopencv_ts STATIC IMPORTED )

set_target_properties(libopencv_ts PROPERTIES

IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libopencv_ts.a")

add_library( # Sets the name of the library.

native-lib

# Sets the library as a shared library.

SHARED

# Provides a relative path to your source file(s).

# Associated headers in the same location as their source

# file are automatically included.

src/main/cpp/native-lib.cpp )

find_library( # Sets the name of the path variable.

log-lib

# Specifies the name of the NDK library that

# you want CMake to locate.

log)

target_link_libraries(native-lib android log

libopencv_java3 libopencv_calib3d libopencv_core libopencv_features2d libopencv_flann libopencv_highgui libopencv_imgcodecs

libopencv_imgproc libopencv_ml libopencv_objdetect libopencv_photo libopencv_shape libopencv_stitching libopencv_superres

libopencv_video libopencv_videoio libopencv_videostab

${log-lib}

)

如何指定编译的ABI可以在app的build.gradle中指定

apply plugin: 'com.android.application'

android {

compileSdkVersion 28

defaultConfig {

applicationId "com.xxxxx.xxxx"

minSdkVersion 18

targetSdkVersion 28

versionCode 1

versionName "1.0"

testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

externalNativeBuild {

cmake {

cppFlags "-std=c++11 -frtti -fexceptions" //CMake编译支持

abiFilters 'armeabi-v7a', 'x86', 'x86_64', 'arm64-v8a', 'armeabi' //指定需要编译的ABI

}

}

}

buildTypes {

release {

minifyEnabled false

proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

}

}

externalNativeBuild {

cmake {

path "CMakeLists.txt"

}

}

sourceSets{

main{

jniLibs.srcDirs = ['src/main/jniLibs']

}

}

}

dependencies {

implementation fileTree(include: ['*.jar'], dir: 'libs')

implementation 'com.android.support:appcompat-v7:28.0.0'

implementation 'com.android.support.constraint:constraint-layout:1.1.3'

testImplementation 'junit:junit:4.12'

androidTestImplementation 'com.android.support.test:runner:1.0.2'

androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

implementation project(':openCVLibrary330')

}

Demo检查

要检查我们是否配置成功,可以先Build一下,如果一切正常则配置成功。否则根据提示修正错误即可。如果一切正常,我们来做一个从工程:打开摄像头并使用OpenCV提供Canndy算子来检测摄像头画面中对象轮廓。效果图如下:

效果图

为app的AndroidManifest.xml文件添加摄像头访问权限

修改activity_main.xml文件添加摄像头画面SurfaceView

xmlns:app="http://schemas.android.com/apk/res-auto"

xmlns:tools="http://schemas.android.com/tools"

android:layout_width="match_parent"

android:layout_height="match_parent"

tools:context=".MainActivity">

android:id="@+id/camera_surface"

android:layout_width="match_parent"

android:layout_height="match_parent" />

修改app/src/main/cpp下的native-lib.cpp文件,添加Canndy处理方法

#include

#include

#include

#include

#include

using namespace std;

using namespace cv;

extern "C" JNIEXPORT jstring JNICALL Java_com_seventythree_cvtest_MainActivity_stringFromJNI(

JNIEnv *env,

jobject /* this */) {

std::string hello = "Hello from C++";

return env->NewStringUTF(hello.c_str());

}

/**

* @brief 使用Canndy算子检测图像中的对象轮廓

* @param matAddrGray, Mat图像的内存地址

*/

extern "C" JNIEXPORT void JNICALL Java_com_seventythree_cvtest_MainActivity_CanndyDetect(

JNIEnv *env,

jobject thiz,

jlong matAddrGray) {

Mat &grayMat = *(Mat *) matAddrGray;

Canny(grayMat, grayMat, 50, 100);

}

修改MainActivity.java

package com.你自己的包名;

import android.Manifest;

import android.content.pm.PackageManager;

import android.support.annotation.NonNull;

import android.support.v4.app.ActivityCompat;

import android.support.v7.app.AppCompatActivity;

import android.os.Bundle;

import android.util.Log;

import android.view.SurfaceView;

import android.view.WindowManager;

import android.widget.TextView;

import android.widget.Toast;

import org.opencv.android.BaseLoaderCallback;

import org.opencv.android.CameraBridgeViewBase;

import org.opencv.android.LoaderCallbackInterface;

import org.opencv.android.OpenCVLoader;

import org.opencv.core.Mat;

public class MainActivity extends AppCompatActivity implements CameraBridgeViewBase.CvCameraViewListener2 {

private static final String TAG = "MainActivity";

private CameraBridgeViewBase mCameraBridgeViewBase;

private BaseLoaderCallback _baseLoaderCallback = new BaseLoaderCallback(this)

{

@Override

public void onManagerConnected(int status)

{

switch (status)

{

case LoaderCallbackInterface.SUCCESS:

{

Log.i(TAG, "OpenCV库加载成功");

// opencv初始化之后加载ndk打包的模块

System.loadLibrary("native-lib");

mCameraBridgeViewBase.enableView();

}break;

default:

{

super.onManagerConnected(status);

}break;

}

}

};

// // Used to load the 'native-lib' library on application startup.

// static {

// System.loadLibrary("native-lib");

// }

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

// 设置windows保持常量

getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

setContentView(R.layout.activity_main);

// 在Android 6.0 +上可以

ActivityCompat.requestPermissions(MainActivity.this,

new String[]{Manifest.permission.CAMERA},

1);

mCameraBridgeViewBase = (CameraBridgeViewBase) findViewById(R.id.camera_surface);

mCameraBridgeViewBase.setVisibility(SurfaceView.VISIBLE);

mCameraBridgeViewBase.setCvCameraViewListener(this);

}

/**

* 重写onPause

*/

@Override

protected void onPause() {

super.onPause();

disableCamera();

}

/**

* 重写onResume

*/

@Override

protected void onResume() {

super.onResume();

if (!OpenCVLoader.initDebug())

{

Log.d(TAG, "应用内无法找到OpenCV库,使用OpenCV Manager进行初始化!");

OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION, this, _baseLoaderCallback);

}

else

{

Log.d(TAG, "使用应用内的OpenCV库进行初始化");

_baseLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);

}

}

/**

* 重修onDestroy

*/

@Override

protected void onDestroy() {

super.onDestroy();

disableCamera();

}

/**

* 摄像头授权回调

* @param requestCode

* @param permissions

* @param grantResults

*/

@Override

public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {

super.onRequestPermissionsResult(requestCode, permissions, grantResults);

switch (requestCode)

{

case 1:

{

// 如果用户取消授权,则result数组为空

if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)

{

// 授权成功可以做一些你爱做的事了ˇˍˇ

}

else

{

// 授权失败了

Toast.makeText(MainActivity.this, "一定要授权才能使用呀", Toast.LENGTH_SHORT).show();

}

return;

}

// 其他的case大家根据自己的实际需求写吧

}

}

/**

* 禁用摄像头

*/

public void disableCamera()

{

if (mCameraBridgeViewBase !=null)

mCameraBridgeViewBase.disableView();

}

@Override

public void onCameraViewStarted(int width, int height) {

}

@Override

public void onCameraViewStopped() {

}

@Override

public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame)

{

Mat matGray = inputFrame.gray();

CanndyDetect(matGray.getNativeObjAddr());

return matGray;

}

/**

* A native method that is implemented by the 'native-lib' native library,

* which is packaged with this application.

*/

public native String stringFromJNI();

/**

* 对图像进行Canndy边缘检测

* @param matAddr 灰度图像的Mat地址

*/

public native void CanndyDetect(long matAddr);

}

常见问题

1. OpenCV JavaCameraView的画面旋转问题

今天晚上突然发现一个很严重的问题,使用本方法修改之后onCameraFrame中的Mat突然无法与布局中的JavaCameraView绑定了,具体原因正在排查中...

这是由于OpenCV的SDK中的JavaViewCamera.java中未对屏幕方向进行处理的原型。可以自己写适配屏幕方向的方法,这里仅以portrait为例演示如何旋转摄像头画面旋转90°的方法

首先采用反射的方法在JavaCameraView类中添加旋转摄像头的方法

/**

* 使用反射的方法调用摄像头的旋转方法

* @param camera 摄像头对象

* @param angle 旋转角度,逆时针为正,顺时针为负

*/

private void setDisplayOrientation(Camera camera, int angle)

{

Method setOrientation;

try {

setOrientation = camera.getClass().getMethod("setDisplayOrientation", new Class[]{int.class});

if (setOrientation != null)

setOrientation.invoke(camera, new Object[]{angle});

}catch (Exception e1) {}

}

修改JavaCameraView类的initializeCamera方法

首先定位到initializeCamera方法中如下代码块

/* Finally we are ready to start the preview */

Log.d(TAG, "startPreview");

mCamera.startPreview();

将上面的代码块修改成如下样子

/* Finally we are ready to start the preview */

Log.d(TAG, "startPreview");

// 修正摄像头画面 Added By shawnzhang

// mCamera.setDisplayOrientation(90); // 不采用仿射的方法

setDisplayOrientation(mCamera, 90); //旋转摄像头

mCamera.setPreviewDisplay(getHolder()); // 刷新Canvas

// 修正摄像头画面 Added By shawnzhang

mCamera.startPreview();

此时摄像头画面就在portrait方式下正常了,当然你也可以不采用反射的方法,直接采用注释掉的方法来处理,不过OpenCV的Java源码多采用反射的处理方式,这样做也是符合OpenCV的方式而已。

2. OpenCV的摄像头预览画面变形

默认情况下使用JavaCameraView直接打开摄像头的画面是变形的,这是由于JavaCameraView类的initializeCamera方法中调用的calculateCameraFrameSize方法的问题,OpenCV的原始代码如下:

if (sizes != null) {

/* Select the size that fits surface considering maximum size allowed */

Size frameSize = calculateCameraFrameSize(sizes, new JavaCameraSizeAccessor(), width, height);

/* Image format NV21 causes issues in the Android emulators */

if (Build.FINGERPRINT.startsWith("generic")

calculateCameraFrameSize在CameraBridgeViewBase.java中实现,参数sizes是摄像头支持的previewSize, 参数width和height是显示摄像头画面的Frame的宽和高,由调用initializeCamera方法的对象传入,

这样计算最佳FrameSize的参数都有了。在JavaViewCamera中创建一个根据摄像头支持的previewSize和显示摄像头画面的SurefaceView选择最佳FrameSize的方法,代码如下:

/**

* 根据显示摄像头画面的SurfaceView的尺寸选择最合适的FrameSize

* @param supportedSizes 摄像头支持的previewSize列表

* @param surfaceWidth 显示摄像头画面的SurfaceView的宽度

* @param surfaceHeight 显示摄像头画面的SurfaceView的高度

* @return 注意返回值的Size采用的是org.opencv.core.Size 而不是android.haraware.Camera.Size

*/

private Size getBestCameraFrameSize(List supportedSizes,int surfaceWidth, int surfaceHeight)

{

float tmp = 0.0f;

float minDiff = 100.0f;

int bestWidth = 0;

int bestHeight = 0;

float x_d_y = (float)surfaceWidth/(float)surfaceHeight;

Size best = null;

for (android.hardware.Camera.Size size : supportedSizes)

{

tmp = Math.abs(((float)size.height/(float)size.width)-x_d_y);

if (tmp < minDiff)

{

minDiff = tmp;

bestWidth = size.width;

bestHeight = size.height;

}

}

return new Size(bestWidth, bestHeight);

}

然后修改initializeCamera方法中计算最佳FrameSize的代码块如下所示:

if (sizes != null) {

/* Select the size that fits surface considering maximum size allowed */

// Size frameSize = calculateCameraFrameSize(sizes, new JavaCameraSizeAccessor(), width, height);

// 选择最适合的Frame的Size Add By Shawnzhang

Size frameSize = getBestCameraFrameSize(sizes, width, height);

// 选择最适合的Frame的Size Add By Shawnzhang

/* Image format NV21 causes issues in the Android emulators */

if (Build.FINGERPRINT.startsWith("generic")

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐