开始之前需要的:
本文总体的思路:本文将按Native C++编程的3个层面,native C++层,jni层,managed层这3个层面的顺序去完成该demo。native层是C++的具体方法,jni层提供了接口功能的实现,通常借助native层的方法来实现,从而让上层能调用jni层的函数实现对应的功能, jni层代码写在cpp文件中。Managed层也就是应用层,这里需要对jni层的函数进行声明才能与jni层的函数对应起来,并在此对函数进行调用,通常是JAVA或者Kotlin代码。3层的关系如下图所示。
正式开始-创建项目
新建project, 选择native c++, 点击next,如下所示,
输入有意义的项目名字,并选择语言为java或者kotlin, 本文选择kotlin,点击next,
这一页C++标准选择可以默认,点击finish,就完成了创建。
添加C++源文件
在安卓视图下,app下有个cpp文件夹,在该文件夹下有默认的两个文件,CMakeLists.txt和native-lib.cpp。当然,我们可以在native-lib.cpp中写所有的C++代码和JNI层代码,但是为了让C++代码看起来更有组织和方便理解,我们只在native-lib.cpp文件中写JNI层代码,其他的图像处理过程写在别的文件中。下面添加两个文件。
右击cpp文件夹,选择New -> C/C++ Header File, 命名为opencv-utils。
右击cpp文件夹,选择New -> C/C++ Source File, 命名为opencv-utils。
添加完上述两个文件后,在安卓视图下cpp文件夹下的文件应该看起来如下图。此时这两个文件内容是空的。
配置Opencv的构建(即配置CMakeLists.txt)
打开CMakeLists.txt, 并在文件的上面添加下面的代码,添加在“cmake_minimum_required”或“project”的下面,注意将openCV_DIR替换成自己下载的opencv库的路径。见下图,
# opencv
set(OpenCV_STATIC ON)
set(OpenCV_DIR /Users/xxx/Downloads/OpenCV-android-sdk/sdk/native/jni)
find_package(OpenCV REQUIRED)
将源文件opencv-utils.cpp添加到add_library中,这样才能编译到库中,
为了在native code中操作Bitmap, 在“target_link_libraries”之前增加如下的代码,
# jnigraphics lib from NDK is used for Bitmap manipulation in native code
find_library(jnigraphics-lib jnigraphics)
最后,还要把opencv和jnigraphics库包含进链接进程中,在“target_link_libraries”里增加下面代码:
${OpenCV_LIBS}
${jnigraphics-lib}
Sync & Build
1.首先进行同步,点击 File -> Sync project with Gradle Files
2.接着进行build, 点击build -> Make Project, 如果成功了,则祝贺。
如果失败,可以参考:(我这里是成功的,因此下面的方法没有验证过)
接下来开始用OpenCV
利用OpenCV实现图像的翻转和模糊两个函数
1. 在头文件opencv-utils.h中添加以下代码:
#pragma once
#include
using namespace cv;
void myFlip(Mat& src);
void myBlur(Mat& src, float sigma);
这里opencv2头文件会报错,先不用管,只要构建完这个项目之后就能自动解决报错。
2. 在源文件opencv-utils.cpp中写上函数的实现代码:
#include "opencv-utils.h"
#include
void myFlip(Mat& src) {
flip(src, src, 0);
}
void myBlur(Mat& src, float sigma) {
GaussianBlur(src, src, Size(), sigma);
}
暴露native 方法给manage层
1. 首先在native-lib.cpp文件中暴露上述两个函数的接口:(注意将函数名中包名到类名这部分替换成自己的)
extern "C" JNIEXPORT void JNICALL
Java_com_demo_opencv_1basic_MainActivity_flip(JNIEnv* env, jobject p_this, jobject bitmapIn, jobject bitmapOut) {
Mat src;
bitmapToMat(env, bitmapIn, src, false);
// NOTE bitmapToMat returns Mat in RGBA format, if needed convert to BGRA using cvtColor
myFlip(src);
// NOTE matToBitmap expects Mat in GRAY/RGB(A) format, if needed convert using cvtColor
matToBitmap(env, src, bitmapOut, false);
}
extern "C" JNIEXPORT void JNICALL
Java_com_demo_opencv_1basic_MainActivity_blur(JNIEnv* env, jobject p_this, jobject bitmapIn, jobject bitmapOut, jfloat sigma) {
Mat src;
bitmapToMat(env, bitmapIn, src, false);
myBlur(src, sigma);
matToBitmap(env, src, bitmapOut, false);
}
2. 在JNI层需要实现将Android的Bitmap转换成OpenCV的Mat格式,增加下面两个方法:
void bitmapToMat(JNIEnv *env, jobject bitmap, Mat& dst, jboolean needUnPremultiplyAlpha)
{
AndroidBitmapInfo info;
void* pixels = 0;
try {
CV_Assert( AndroidBitmap_getInfo(env, bitmap, &info) >= 0 );
CV_Assert( info.format == ANDROID_BITMAP_FORMAT_RGBA_8888 ||
info.format == ANDROID_BITMAP_FORMAT_RGB_565 );
CV_Assert( AndroidBitmap_lockPixels(env, bitmap, &pixels) >= 0 );
CV_Assert( pixels );
dst.create(info.height, info.width, CV_8UC4);
if( info.format == ANDROID_BITMAP_FORMAT_RGBA_8888 )
{
Mat tmp(info.height, info.width, CV_8UC4, pixels);
if(needUnPremultiplyAlpha) cvtColor(tmp, dst, COLOR_mRGBA2RGBA);
else tmp.copyTo(dst);
} else {
// info.format == ANDROID_BITMAP_FORMAT_RGB_565
Mat tmp(info.height, info.width, CV_8UC2, pixels);
cvtColor(tmp, dst, COLOR_BGR5652RGBA);
}
AndroidBitmap_unlockPixels(env, bitmap);
return;
} catch(const cv::Exception& e) {
AndroidBitmap_unlockPixels(env, bitmap);
jclass je = env->FindClass("java/lang/Exception");
env->ThrowNew(je, e.what());
return;
} catch (...) {
AndroidBitmap_unlockPixels(env, bitmap);
jclass je = env->FindClass("java/lang/Exception");
env->ThrowNew(je, "Unknown exception in JNI code {nBitmapToMat}");
return;
}
}
void matToBitmap(JNIEnv* env, Mat src, jobject bitmap, jboolean needPremultiplyAlpha)
{
AndroidBitmapInfo info;
void* pixels = 0;
try {
CV_Assert( AndroidBitmap_getInfo(env, bitmap, &info) >= 0 );
CV_Assert( info.format == ANDROID_BITMAP_FORMAT_RGBA_8888 ||
info.format == ANDROID_BITMAP_FORMAT_RGB_565 );
CV_Assert( src.dims == 2 && info.height == (uint32_t)src.rows && info.width == (uint32_t)src.cols );
CV_Assert( src.type() == CV_8UC1 || src.type() == CV_8UC3 || src.type() == CV_8UC4 );
CV_Assert( AndroidBitmap_lockPixels(env, bitmap, &pixels) >= 0 );
CV_Assert( pixels );
if( info.format == ANDROID_BITMAP_FORMAT_RGBA_8888 )
{
Mat tmp(info.height, info.width, CV_8UC4, pixels);
if(src.type() == CV_8UC1)
{
cvtColor(src, tmp, COLOR_GRAY2RGBA);
} else if(src.type() == CV_8UC3){
cvtColor(src, tmp, COLOR_RGB2RGBA);
} else if(src.type() == CV_8UC4){
if(needPremultiplyAlpha) cvtColor(src, tmp, COLOR_RGBA2mRGBA);
else src.copyTo(tmp);
}
} else {
// info.format == ANDROID_BITMAP_FORMAT_RGB_565
Mat tmp(info.height, info.width, CV_8UC2, pixels);
if(src.type() == CV_8UC1)
{
cvtColor(src, tmp, COLOR_GRAY2BGR565);
} else if(src.type() == CV_8UC3){
cvtColor(src, tmp, COLOR_RGB2BGR565);
} else if(src.type() == CV_8UC4){
cvtColor(src, tmp, COLOR_RGBA2BGR565);
}
}
AndroidBitmap_unlockPixels(env, bitmap);
return;
} catch(const cv::Exception& e) {
AndroidBitmap_unlockPixels(env, bitmap);
jclass je = env->FindClass("java/lang/Exception");
env->ThrowNew(je, e.what());
return;
} catch (...) {
AndroidBitmap_unlockPixels(env, bitmap);
jclass je = env->FindClass("java/lang/Exception");
env->ThrowNew(je, "Unknown exception in JNI code {nMatToBitmap}");
return;
}
}
如上图所示,并添加上必要的头文件。
在Managed层调用Native层代码
1. 首先在页面布局上,增加ImageView, Button和SeekBar,分别用来显示图片,点击按钮,调节滑动条。页面布局的调整在安卓视图的app -> res -> layout下的activity_main.xml中。
调整好的页面如下图所示:
2. 在MainActivity中,声明JNI层的函数:
external fun blur(bitmapIn: Bitmap, bitmapOut: Bitmap, sigma: Float)
external fun flip(bitmapIn: Bitmap, bitmapOut: Bitmap)
在创建Native C++文件时默认在MainActivity中导入了库,如果没有的话,就在MainActivity.kt加上下面的代码,
companion object {
// Used to load the 'opencv_basic' library on application startup.
init {
System.loadLibrary("opencv_basic")
}
}
MainActivity.kt的整个内容如下:
在MainActivity中处理Android Bitmap
1. 创建2个bitmaps, 分别叫做srcBitmap和dstBitmap,并将dstBitmap显示在屏幕上,
class MainActivity : AppCompatActivity() {
var srcBitmap: Bitmap? = null
var dstBitmap: Bitmap? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
srcBitmap = BitmapFactory.decodeResource(this.resources, R.drawable.mountain)
// Create and display dstBitmap in image view, we will keep updating
// dstBitmap and the changes will be displayed on screen
dstBitmap = srcBitmap!!.copy(srcBitmap!!.config, true)
imageView.setImageBitmap(dstBitmap)
...
2. 设置对seekbar的监听,每当用户滑动滑杆的时候,读取滑杆的Sigma值作为模糊函数的参数。
a. 在MainActivity的onCreat函数上,加上sldSigma.setOnSeekBarChangeListener(this)
点击this并选择第2个让MainActivity实现接口,并将3个接口函数都选上,如下图。
b. 写上滑杆的响应函数:
fun doBlur() {
// The SeekBar range is 0-100 convert it to 0.1-10
val sigma = max(0.1F, sldSigma.progress / 10F)
// This is the actual call to the blur method inside native-lib.cpp
this.blur(srcBitmap!!, dstBitmap!!, sigma)
}
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
this.doBlur()
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
3. 设置对Button的监听,每当用户点击按钮,即调用对应的响应函数。
a. 在MainActivity中设置Button的响应函数,如下
fun btnFlip_click(view: View) {
this.flip(srcBitmap!!, dstBitmap!!)
}
b. 将Button与其响应函数进行关联,当点击该Button时,即调用该响应。在布局文件activity_main.xml中点击btnFlip,并在Common Attributes的onClick中选择btnFlip_click。
额外补充:
如果出现unresolved reference: imageView,只需要在app下的build.gradle里增加
id 'kotlin-android'和id 'kotlin-android-extensions',如下所示,
plugins {
id 'com.android.application'
// id 'org.jetbrains.kotlin.android'
id 'kotlin-android'
id 'kotlin-android-extensions'
}
然后点击Sync now,最后点击imageView,会出现红色import提示,点击import即可。
最终结果:
1. 原始图片
2. 图片翻转
3. 图片模糊
参考资料:
OpenCV in Android native using C++
THE END !