您的位置:首页 > 移动开发 > Android开发

[置顶] 简单的调用OpenCV库的Android NDK开发 工具Android Studio

2018-01-23 14:16 1016 查看

前言

本博客写于2017/08/11, 博主非专业搞安卓开发, 只是工作的需要倒腾了下Android NDK相关的开发, 博文中有什么不正确、不严格的地方欢迎指正哈    本文后续也许还会有删改,
就这样。


一、工具、开发环境

博主的操作系统是Windows 10 x64位,虽然感觉Windows 7更适合用来搞开发, 但是用着Win 10也是挺好使的(没必要在操作系统上有很大纠结,Win 10和Win 7都可以)。


1.搭建并测试JAVA开发环境

首先要搭建Java开发环境,Java开发环境的搭建教程网上可以找到,这里不予赘述; 测试是否搭建成功的时候,需要打开命令提示符依次输入java、javac、javah命令进行测试,显示如下界面结果就成功了:



图一    java命令测试



 图二    javac命令测试



 图三    javah命令测试


2.搭建Android开发环境

很长一段时间内,Eclipse + ADT插件是Android开发的主流,不过现在流行的是Android Studio。 还记得Android Studio刚出来的时候很是不被看好, 不过现在它也是很好用的安卓开发工具了,此文介绍的所需要的工具只有Android Studio。

网上有介绍说设置ndk-build环境变量的, 但是要弄明白一件事, 他们设置这个变量是因为他们的"NDK"这个依赖包是单独下载的, 并没有在Android Studio里面下载, 本文的"NDK"依赖包是在Android Studio里面下载的, 因此并不需要设置ndk-build环境变量。

开始正式搭建环境,我用的是Android Studio 2.2.3版本。 安装完Android Studio之后, 选择"File" -> "Settings" -> "Appearence & Behavior" -> "System Settings" -> "Android SDK", 配置 "SDK Platforms"
和 "SDK Tools" :

(1)"SDK
Platforms":这个是选择安卓SDK版本,根据自己想法跟需要下载,我下载了安卓4.4、5.0、7.1.1




(2)"SDK Tools":这个是选择SDK相关开发工具,基础的"NDK"包(必选)是必须有的,刚安装上Android
Studio的时候默认有什么包我忘了,"Android SDK Build-Tools"这个包应该不用自己勾选,默认就有。NDK开发现在提供了CMake的方式来进行编译调试,通过下载"CMake"跟"LLDB"这俩(可选)就可以用CMake来开发了,本文未基于CMake的方式进行编译调试,而是采用的传统的gradle方式(相对而言,我其实更推荐使用CMake的方式)。

 


(3)正如(2)中所说的, 我采用的是gradle的方式进行的开发, 本文不介绍CMake的方式。 所以在采用gradle的基础上,才有了这第(3)步骤。
这一步就是为了更方便的进行gradle方式的开发而写的。 

可以将gradle经常用到的一些命令简化到菜单中,具体设置在"File" -> "Settings" -> "Tools" -> "External Tools"这里:



1) 添加javah,它用于生成头文件


Program:
$JDKPath$/bin/javah

Parameters: 
-d
../jni -jni $FileClass$

Working directory: 
$SourcepathEntry$\..\java


最后单击"OK"按钮进行保存。

2) 添加ndk-build,它用于构建so包


Program: 
你的NDK目录\build\ndk-build.cmd

Parameters: 什么都不用填
Working directory:
$ModuleFileDir$\src\main

 
最后单击"OK"按钮进行保存。

3) 添加ndk-build
clean,它用于清理so包


Program: 
你的NDK目录\build\ndk-build.cmd

Parameters: 
clean

Working directory:
$ModuleFileDir$\src\main

 
最后单击"OK"按钮进行保存。
3.来一个栗子

一个NDK开发项目总体就分为生成so库、调用so库两部分,本博客栗子就是先将基于OpenCV的C++代码编译生成so库,再通过jni接口来实现对安卓摄像头的灰度化处理。
(1)首先新建一个工程
1)选择新建项目:



 2)填入一些基本信息, 不勾选"Include C++ Support", 然后Next:



3)默认就可以了,继续Next:




 4)继续Next:




5)最后Finish:




 

(2)项目创建完毕了,开始向项目中导入OpenCV库
1)去OpenCV官网下载OpenCV for Android版本,  我下载的是2.4.9版本,下载下来的是一个压缩包, 名字是"OpenCV-2.4.9-android-sdk", 我们将其解压放到某个位置, 待会会用到。 我将其解压缩到了D:\Usual\Android目录下面。
2)选择导入组件:



3)选择文件夹



在我这里, 就找到刚才解压位置OpenCV for Android包那里, 找到java文件夹然后"OK"开始确认。



接下来默认即可, "next"然后"finish"。

4)接下来还需要将OpenCV库添加到依赖:





 选择Project Structure之后, 弹出以下画面, 按图示进行操作来添加依赖:



 5)进行一些必要的小修改
切换到"Project"视图界面,





 将openCVLibrary249目录下的build.gradle向"app"->"src"下的build.gradle看齐, 也就是将前者的这四项参数修改的和后者一样:





 修改之后还需要单击"Sync Now"来更新一下:



 至此,算是正式导入成功OpenCV库。

(3)开始生成so库

首先分析一下项目, 整个项目要实现对安卓摄像头的实时灰度化, 我们在java类里面来实现摄像头画面的获取, 用C/C++代码来实现对一帧图像(一幅图像)的灰度化。
NDK开发将C/C++代码生成so库, 留一个对单幅图片进行灰度化的接口, 然后我们在Java层获取摄像头数据, 通过JNI调用所生成so库的接口, 对每一帧摄像头数据进行灰度化。
那么现在可以生成so库了。
 1)在app->src->main右键新建一个jni文件夹, 操作指示如图:



单击"JNI Folder"会出现一个界面来确认操作, 选择界面上的"Finish"结束即可。

2)新建一个类OpencvClass,负责与C/C++代码对接。



 


 3)开始生成C/C++头文件
打开新创建的类文件,添加一行代码:
public native static int convertGray(long matAddrRgba, long matAddrgray);

接下来,右键单击类OpencvClass,来生成C/C++头文件



 生成成功后会在main文件夹下多出来一个jni文件夹,里面有个头文件:

 


接下来新建一个.cpp文件,名字和该头文件一样,如下图所示操作:



 输入.cpp名字, 然后单击"OK"结束。



 4)开始写.c/.cpp源文件
上一步生成了头文件, 此头文件内容是:

#include <jni.h>

/* Header for class com_hoos_grayprocessing_OpencvClass */
#ifndef _Included_com_hoos_grayprocessing_OpencvClass
#define _Included_com_hoos_grayprocessing_OpencvClass
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class:     com_hoos_grayprocessing_OpencvClass
* Method:    convertGray
* Signature: (JJ)I
*/
JNIEXPORT jint JNICALL Java_com_hoos_grayprocessing_OpencvClass_convertGray
(JNIEnv *, jclass, jlong, jlong);
​
#ifdef __cplusplus
}
#endif
#endif


其中, JNIEXPORT是接口声明, 我们需要在cpp源文件里将它实现, 这里当然需要我们有C/C++基础, 可以写出cpp文件的框架了:

/ Created by HooS on 2017/8/14.

#include <com_hoos_grayprocessing_OpencvClass.h>

JNIEXPORT jint JNICALL Java_com_hoos_grayprocessing_OpencvClass_convertGray
(JNIEnv *, jclass, jlong, jlong){

}


在实现时, 接口的两个jlong类型需要用来传输OpenCV里面的Mat类型数据, 所以要略微修改一下那里:

// Created by HooS on 2017/8/14.
//
#include <com_hoos_grayprocessing_OpencvClass.h>
​
JNIEXPORT jint JNICALL Java_com_hoos_grayprocessing_OpencvClass_convertGray
(JNIEnv *, jclass, jlong addrRgba, jlong addrGray){

}
然后完善代码,完善JNI接口数据转换,实现单幅图像的灰度化功能:
// Created by HooS on 2017/7/25.
//
#include <com_hoos_ndkopencvtest_OpencvNativeClass.h>
​
JNIEXPORT jint JNICALL Java_com_hoos_ndkopencvtest_OpencvNativeClass_convertGray
(JNIEnv *, jclass, jlong addrRgba, jlong addrGray){
// 实现jlong类型到Mat类型的转换
Mat& mRgb = *(Mat*)addrRgba;
Mat& mGray = *(Mat*)addrGray;

// 定义 int 和 jint 类型来接收函数返回值
int conv;
jint retVal;
// 此处调用了toGray函数来对图片进行灰度化处理
conv = toGray(mRgb, mGray);

retVal = (jint)conv;
return retVal;
}
看注释可以知道, 我们还需要实现toGray函数来实现单幅图片的灰度化, 这个实现代码很简单, 为啥非得再麻烦些折腾些呢, 因为大项目往往会有好多源文件, 相互调用, 所以之前最好有多源文件编译的机会来锻炼一下, nao, 自己创造机会。

接下来,新建gray.h、gray.cpp文件来实现toGray这个函数。按照刚才新建.c/.cpp文件的操作来新建:



 gray.h  和 gray.cpp里面就是跟纯C/C++编程一样了, 写一个对单个图像进行灰度化的函数:
#include <stdio.h>
#include <opencv2/opencv.hpp>
​
using namespace cv;
using namespace std;
​
bool toGray(Mat img, Mat& gray);


#include "gray.h"

bool toGray(Mat img, Mat& gray)
{
if (img.channels()  == 3)
cvtColor(img, gray, CV_BGR2GRAY);
else if (img.channels() == 4)
cvtColor(img, gray, CV_BGRA2GRAY);
else if (img.channels() == 1)
gray = img;
if(gray.rows == img.rows && gray.cols == img.cols)
return true;
else
return false;
}


然后再修改下com_hoos_ndkopencvtest_OpencvNativeClass.h文件就好了,添加如下代码(在哪里添加应该都懂):

using namespace cv;
using namespace std;


至此,C/C++源文件搞定。
5)写 Android.mk 和 Application.mk 文件

因为本文采用的gradle的方式来进行NDK开发的, 所以我们会用到 Android.mk 和 Application.mk文件,开始新建这两个文件:





 创建Application.mk文件的方法也是这样,我就不予赘述了。两个文件内容如下,需要修改的地方都注释了:
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

#opencv
OPENCVROOT:= D:/Usual/Android/OpenCV-2.4.9-android-sdk  #这句代码等号右边写上自己的OpenCV安卓开发包的位置
OPENCV_CAMERA_MODULES:=on
OPENCV_INSTALL_MODULES:=on
OPENCV_LIB_TYPE:=SHARED
​
#思考下,参考我的修改一下就好了
include D:/Usual/Android/OpenCV-2.4.9-android-sdk/sdk/native/jni/OpenCV.mk
​
#这里写上自己的开发需要的源文件, 我这里需要两个
LOCAL_SRC_FILES := com_hoos_grayprocessing_OpencvClass.cpp gray.cpp
​
​
#设置生成的so库的名字  前面的lib和后缀名不用写
LOCAL_MODULE := MyLibs
​
LOCAL_LDLIBS += -llog
​
include $(BUILD_SHARED_LIBRARY)
APP_STL := gnustl_static
APP_CPPFLAGS := -frtti -fexceptions
​
#这句是设置生成的cpu指令类型,可以根据自己需求来设置生成的平台, 一般这两个就够了
APP_ABI := armeabi-v7a x86
​
APP_PLATFORM := android-8    #这句是设置最低安卓平台,可以不弄

6)开始生成so库



 可以看到,多出来了一个libs文件夹,如图:


 至此,so库生成完成。

(4)JNI调用so库

这一部分将大体分析一下, 毕竟实现不同的功能类有不同的写法, 本文主要提供一个思路, 以待能够举一反三写出其他NDK开发实例来。这是这一部分的五个关键文件:



1)activity_main.xml
源内容如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.hoos.grayprocessing.MainActivity">
​
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />
</RelativeLayout>


根据实际情况, 我们不需要TextView, 而需要一个JavaCameraView来显示摄像头帧, 控件ID为"java_camera_view", 修改如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.hoos.grayprocessing.MainActivity">
​
<org.opencv.android.JavaCameraView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/java_camera_view"
/>
</RelativeLayout>


2)AndroidManifest.xml

原内容是:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.hoos.grayprocessing">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
​
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
​
</manifest>


因为应用需要摄像头权限, 所以需要在这个文件添加摄像头权限:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.hoos.grayprocessing">

<uses-permission android:name="android.permission.CAMERA"/>

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
​
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
​
</manifest>


3)gradle.propertise

需要添加对以前版本的支持,在此文件内容的最后添加一行:

android.useDepredcatedNdk=true


4)build.gradle
原内容:

android {
compileSdkVersion 26
buildToolsVersion "26.0.1"
defaultConfig {
applicationId "com.hoos.grayprocessing"
minSdkVersion 21
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
​
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
​
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:26.0.0-alpha1'
testCompile 'junit:junit:4.12'
compile project(':openCVLibrary249')
}


添加内容, 指明生成的so库的路径:

android {
compileSdkVersion 26
buildToolsVersion "26.0.1"
defaultConfig {
applicationId "com.hoos.grayprocessing"
minSdkVersion 21
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
​
sourceSets.main{
jniLibs.srcDirs = ['src/main/libs']
jni.srcDirs = []
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
​
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:26.0.0-alpha1'
testCompile 'junit:junit:4.12'
compile project(':openCVLibrary249')
}


5)MainActivity.java
部分注释在代码中,

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
​
import org.opencv.android.BaseLoaderCallback;
import org.opencv.android.CameraBridgeViewBase;
import org.opencv.android.JavaCameraView;
import org.opencv.android.LoaderCallbackInterface;
import org.opencv.android.OpenCVLoader;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
​
public class MainActivity extends AppCompatActivity implements CameraBridgeViewBase.CvCameraViewListener2{

private static String TAG = "MainActivity";
// 初始化一个实例,用来获取摄像头帧
JavaCameraView javaCameraView;
Mat mRgba;  // 用来存储原始摄像头数据
Mat mGray;  // 用来存储灰度化后的帧数据
​
// 静态加载之前生成的so库——Mylins
static{
System.loadLibrary("MyLibs");
}
​
// 回调开初始化帧数据
BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
@Override
public void onManagerConnected(int status) {
switch(status){
case BaseLoaderCallback.SUCCESS:
javaCameraView.enableView();
break;
default:
super.onManagerConnected(status);
break;
}
super.onManagerConnected(status);
}
};
​
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
​
// 与java_camera_view控件绑定
javaCameraView = (JavaCameraView)findViewById(R.id.java_camera_view);
javaCameraView.setVisibility(View.VISIBLE);
javaCameraView.setCvCameraViewListener(this);
}
​
@Override
protected void onPause(){
super.onPause();
if(javaCameraView!=null)
javaCameraView.disableView();
}
​
protected void onDestroy(){
super.onDestroy();
if(javaCameraView!=null)
javaCameraView.disableView();
}
​
// 判断是否加载成功OpenCV库
protected void onResume(){
super.onResume();
if(OpenCVLoader.initDebug()){
Log.i(TAG, "Opencv loaded successfully");
mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);
}
else{
Log.i(TAG, "Opencv not loaded");
OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_2_4_9, this, mLoaderCallback);
}
}
​
@Override
public void onCameraViewStarted(int width, int height) {
// 初始化这两个Mat类型
mRgba = new Mat(height, width, CvType.CV_8UC4);
mGray = new Mat(height, width, CvType.CV_8UC1);
}
​
@Override
public void onCameraViewStopped() {
mRgba.release();
}
​
@Override
public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {
// 得到摄像头原始帧数据
mRgba = inputFrame.rgba();
// 调用OpencvClass类的方法来对帧数据进行灰度化
OpencvClass.convertGray(mRgba.getNativeObjAddr(), mGray.getNativeObjAddr());
​
return mGray;
}
}


此外, 出现了如下提示"Gradle files have changed ...", 请单击右边的"Sync Now"来进行更新。



(4)在x86的Android 5.0的虚拟机器上运行了下, 木有问题



当然了,想要虚拟安卓机能用电脑或者usb外置摄像头需要修改虚拟机一点参数:
1)鼠标左键单击下图红色标记按钮:


 2)鼠标左键单击下图红色标记按钮(前提是你已经创建了一个虚拟机,
建议创建x86的虚拟机):

3) 鼠标左键单击下图红色标记按钮:

 


4)将参数 "Front" 或者 "Back" 的其中任意一个改成Webcam0就可以了, 这样只要电脑内置或者外置摄像头可用虚拟机就可以调用了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: