失踪人口回归!距离上次承诺的更新,一下子鸽了半年,从文章的点击量来看,大家还是比较钟情OpenCV相关的开发的,那么在今天这个1024程序猿节日(等审核过了估计就1025了- -b),我也给广大刚刚入行的开发者送上一份微薄的礼物,那就是新的一篇关于OpenCV在安卓开发中的使用教程!
首先强调一点,这篇博文只是针对于刚刚入门的新手的,本身没有比较深的难度,更不具有研究性质,本人水平也比较有限,只是希望能帮助一些需要帮助的同学,再日后会逐渐提升博文的深度和广度,和大家一起进步。
关于我之前人脸识别的博文,其中只介绍了如何使用OpenCV的Java层做相关的开发,但众所周知OpenCV原本是C/C++编写的,在Java层的支持实际上是非常有限的,如果想进行一些更加高级的操作,还是需要用C++来进行开发,那么我就来介绍一下怎么在NDK的开发中使用OpenCV,如果你对NDK开发还不太熟悉,请先浏览我之前的博文,这些教程是一个循序渐进的过程。
今天我们不写什么高大上的功能,当然也不能只写一个Hello World级别的程序来糊弄大家,就简单写一下如何使用OpenCV给图片加文字水印吧,其实这个功能不是重点,重点是分享一些我平时总结的JNI和Java之间的图片传递问题的解决方案,我会用多种方法来实现这个功能,具体使用哪种,大家就根据性能和需求来自己决定吧。(如果你是为了添加水印而来的可能要失望了,这个不是本文的重点,而且这里的水印也不支持中文字符...)
首先我们先去官网下载一个OpenCV最新的安卓开发包,截止本文发稿时最新版为3.3,下载完成后将其解压到一个无中文的路径下,解压完毕后,打开OpenCV330\sdk\native\libs目录,将其中各种处理器类型的libopencv_java3.so全部提取出来,放入jniLibs中,如图所示:
此时我们就将OpenCV的so库引入了项目,但不要着急,想在NDK中使用它,还需要重要的一步,就是在CMake文件中进行配置,引入方法也很简单,只需要两行:
//添加一个名称为lib_opencv的library
add_library(lib_opencv SHARED IMPORTED )
//设置lib_opencv的为刚才引入的so文件,包含了一些相对路径的写法
set_target_properties(lib_opencv PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libopencv_java3.so)
当然引入这些还是不够的,so文件里面只是具体的函数实现,我们还需要引入所有函数的头文件才可以正常调用,于是再在Cmake中添加:
include_directories(D:/OpenCV330/sdk/native/jni/include)
cmake_minimum_required(VERSION 3.4.1)
include_directories(D:/OpenCV330/sdk/native/jni/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)
add_library(native-lib SHARED src/main/cpp/native-lib.cpp )
find_library(log-lib log )
target_link_libraries(native-lib lib_opencv ${log-lib} )
到此,准备工作算是全部完成了,我们终于可以在cpp文件里大展拳脚啦!由于上次已经介绍过NDK开发的基础知识,本次就不再详细说明,只先贴一些关键代码来带过。
在Java层,我们直接声明3个native方法,在C++中写具体的实现:
public class NdkLoader {
static {
System.loadLibrary("native-lib");
}
public static native int[] addText2Picture(int[] pixels_, int width, int height, String content);
public static native int addText2Picture2(long output, String content);
public static native int addText2Picture3(String content, String input, String output);
}
本次我们将以3种数据传输方式来完成这个功能,分别对应了3种不同的函数:
第一种是在Java层把一个Bitmap的像素数据以int[]传入JNI,JNI处理后再返回一个int[]的像素数据。(Java层无需引入OpenCV)
第二种是Java层调用Java层的OpenCV,创建一个空的Mat对象,将此对象的地址传入JNI,JNI处理后,形参改变了实参。(Java层需要引入OpenCV)
第三种是Java层只传入输入图片在手机中的路径和输出的字符串路径,JNI去读取该文件,并将新的图片输出为新的文件。(Java层无需引入OpenCV)
方案一
现在我们先来介绍第一种方法,关于水印部分,每种方法里的实现也都一样,所以也只在这里进行讲解。我们先在Java层读取一个Bitmap对象,然后提取它的像素值传入JNI:
Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.mipmap.zelda);
int w = bmp.getWidth();
int h = bmp.getHeight();
int[] pixels = new int[w * h];
bmp.getPixels(pixels, 0, w, 0, 0, w, h);
String content = editText.getText().toString();
int[] resultInt = NdkLoader.addText2Picture(pixels, w, h, content);
实现方法比较简单,获取bitmap对象的宽高,然后申请长度为长x宽的数组,使用bitmap.getPixels方法提取像素。addText2Picture的第一个参数是像素数组,第二个参数为宽度,第三个参数为高度,最后一个参数是从输入框获取的水印内容。
然后我们重点来看C++代码:
JNIEXPORT jintArray JNICALL
Java_com_lbw_opencvaddtextmark_NdkLoader_addText2Picture(
JNIEnv *env,
jobject, jintArray pixels_,
jint w, jint h, jstring textString) {
const char *text = env->GetStringUTFChars(textString, 0);
string content = text;
jint *pixels = env->GetIntArrayElements(pixels_, NULL);
if (pixels == NULL) {
return NULL;
}
Mat src(h, w, CV_8UC4, pixels);
int width = src.cols;
int height = src.rows;
int margin = 10;
int baseline;
Size srcSize = getTextSize(content, FONT_HERSHEY_COMPLEX, 2, 2, &baseline);
cv::Point point;
point.x = width - srcSize.width - margin;
point.y = height - margin;
//Scalar BGR
putText(src, content, point, FONT_HERSHEY_COMPLEX, 2, cv::Scalar(94, 206, 165, 255), 2, 8, 0);
int size = w * h;
jintArray result = env->NewIntArray(size);
env->SetIntArrayRegion(result, 0, size, pixels);
env->ReleaseIntArrayElements(pixels_, pixels, 0);
env->ReleaseStringUTFChars(textString, text);
return result;
}
首先接收了传进来的字符串内容,然后定义一个jint的指针,指向传入的jintArray,然后调用Mat(int rows,int cols,int type,void *data)构造方法,这四个参数的意义比较重大,下面详解一下:第一个参数要求传rows(行数),而我传的是高度,这里稍微用膝盖思考一下就会发现,一个矩阵的行数就是高度- -b,同理列数就是宽度,这个参数是非常常见的,希望大家可以记住这些常识。。。然后是type,我们传入的是CV_8U4C,就代表这是一个8位无符号4通道的带透明色的RGB图像,那么一会儿我们设置字体颜色时也要有四个颜色参数。*data我们传入的是pixels,就是jintarray的指针,这个没有什么好解释的。这样我们就创建了一个包含图像的Mat矩阵,Mat是OpenCV中非常非常常用的对象,希望大家多多掌握它的用法。
在添加水印之前,为了确保图片足够容纳下文字,我们必须先计算出水印的宽高,再决定在什么坐标上进行绘制,如果你看到了这里,对坐标的计算应该是没有太大问题,因为OpenCV的坐标系和安卓是一样的,左上角为原点坐标,右为X轴正方向,下为Y轴正方向。然后我们调用getTextSize方法,指定文字内容,字体,单位尺寸,线条宽度等元素后获得计算结果,用Size对象接收。
然后我们调用putText函数,先传入要添加水印的mat对象,然后传入文字内容,再传入坐标点、文字字体、单位尺寸、颜色、线条宽度等参数,坐标点指的是文字的左下角,要特别注意,我准备将水印放在整个图片的又下角,像新浪微博一样,point的创建应该无需过多解释,稍微做过一些自定义控件应该都会明白的。这里的参数范围大家可以自己去实验一下,至于字体这里支持7,8种,很遗憾这些有限的字体里并不支持中文,所以水印是无法使用中文的,如果在windows上开发,你会查到很多让它支持中文的方法,比如使用FreeType,当然现在是在安卓上做的开发,我并未研究怎么去编译并引入FreeType的源码,所以本次就先不讨论怎么在这里让它支持中文,如果你有好的办法可以在下面留言分享给大家。重点要强调的是颜色的设置,需要创建一个Scalar对象,因为我们刚才设置的图片为4通道,这里也需要4个参数,前三位是颜色,第四位为透明度,但你一定要扭转日常开发的思维,这里的前三位颜色并不是RGB顺序,而是BGR顺序,至于原因估计就是作者这种超级大神的个人习惯问题吧 - -。最后我们再创建一个jintArray,并将其赋值为修改后的像素值返回,Java层接收后做简单处理就可以显示了。
int[] resultInt = NdkLoader.addText2Picture(pixels, w, h, content);
if (resultInt != null) {
Bitmap resultImg = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
resultImg.setPixels(resultInt, 0, w, 0, 0, w, h);
imageView.setImageBitmap(resultImg);
}
方案二
方案二比起方案一,是一种效率更高的办法,但是实际写起来会麻烦一些,因为在Java层必须也引入openCV,首先还是老套路,将OpenCV330\sdk\java作为Module引入,并为主工程添加依赖,但这次不同的是我们要使用Java层的Mat对象,官方给的做法是先安装他们指定的APK,作为一个外联库用,这个当然是绝对不可以接受的,这里我教大家怎么摆脱掉那个额外的APK。首先创建一个OpenCV的BaseLoaderCallback回调,用于得知so库是否加载成功
private BaseLoaderCallback mOpenCVCallBack = new BaseLoaderCallback(this) {
@Override
public void onManagerConnected(int status) {
switch (status) {
case LoaderCallbackInterface.SUCCESS: {
button2.setEnabled(true);
}
break;
default: {
super.onManagerConnected(status);
}
break;
}
}
};
@Override
protected void onResume() {
super.onResume();
if (OpenCVLoader.initDebug()) {
System.loadLibrary("opencv_java3");
mOpenCVCallBack.onManagerConnected(LoaderCallbackInterface.SUCCESS);
}
}
java:
Mat src = new Mat();
Bitmap bmp2 = BitmapFactory.decodeResource(getResources(), R.mipmap.ciri);
Utils.bitmapToMat(bmp2, src);
int i = NdkLoader.addText2Picture2(src.getNativeObjAddr(), editText.getText().toString());
if (i == 1) {
Utils.matToBitmap(src, bmp2);
imageView.setImageBitmap(bmp2);
}
C++:
JNIEXPORT jint JNICALL
Java_com_lbw_opencvaddtextmark_NdkLoader_addText2Picture2(JNIEnv *env, jclass type, jlong output,
jstring textString) {
const char *text = env->GetStringUTFChars(textString, 0);
string content = text;
Mat src = *(Mat *) output;
int width = src.cols;
int height = src.rows;
int margin = 10;
int baseline;
Size srcSize = getTextSize(content, FONT_HERSHEY_COMPLEX, 2, 2, &baseline);
cv::Point point;
point.x = width - srcSize.width - margin;
point.y = height - margin;
putText(src, content, point, FONT_HERSHEY_COMPLEX, 2, Scalar(129, 58, 98, 255), 2, 8, 0);
env->ReleaseStringUTFChars(textString, text);
return 1;
}
方案三
这种方案纯粹在C++中进行IO操作,可以一直读写文件,如果只要文件不展示结果则Java层无需处理其他事物。如果你的代码想在更多平台上跑,如iOS,Windows等,这样写也很容易移植。
Java:
File file = new File(Environment.getExternalStorageDirectory().getPath() + "/lara.jpg");
if (!file.exists()) {
copyFilesFromAssets(getApplicationContext(), "lara.jpg", Environment.getExternalStorageDirectory().getPath() + "/lara.jpg");
}
int reusult = NdkLoader.addText2Picture3(editText.getText().toString(),
Environment.getExternalStorageDirectory().getPath() + "/lara.jpg",
Environment.getExternalStorageDirectory().getPath() + "/lara_new.jpg");
if (reusult == 1) {
Bitmap bmp3 = BitmapFactory.decodeFile(Environment.getExternalStorageDirectory().getPath() + "/lara_new.jpg");
imageView.setImageBitmap(bmp3);
}
C++:
JNIEXPORT jint JNICALL
Java_com_lbw_opencvaddtextmark_NdkLoader_addText2Picture3(JNIEnv *env, jclass type,
jstring textString, jstring input,
jstring output) {
const char *content = env->GetStringUTFChars(textString, 0);
const char *inputPath = env->GetStringUTFChars(input, 0);
const char *outputPath = env->GetStringUTFChars(output, 0);
Mat src = imread(inputPath);
if (src.data == NULL) {
return 0;
}
int width = src.cols;
int height = src.rows;
int margin = 10;
int baseline;
Size srcSize = getTextSize(content, FONT_HERSHEY_COMPLEX, 2, 2, &baseline);
cv::Point point;
point.x = width - srcSize.width - margin;
point.y = height - margin;
putText(src, content, point, FONT_HERSHEY_COMPLEX, 2, Scalar(255, 255, 255, 255), 2, 8, 0);
imwrite(outputPath, src);
env->ReleaseStringUTFChars(textString, content);
env->ReleaseStringUTFChars(input, inputPath);
env->ReleaseStringUTFChars(output, outputPath);
return 1;
}
尾声
到这里,本期的分享也就结束了,可能内容有些枯燥,所以建议大家编程之余多玩玩游戏放松一下,我也就选择了几张游戏女神图片作为案例,工作压力大也一定要学会怎么去放松。之前每期的最后我都会立一个flag,提醒自己一定要继续更新博客,这次也不例外,下回我会分享一些好玩的不枯燥的东西,同时又包含着一些技术点,那就是教大家如何使用Nintendo Switch 的JoyCon手柄在一台没有蓝牙的PC上玩游戏!将通过Android和JavaSE来实现,我会尽快写出来的.对于第一期迟迟没上传的源码我在这里表示抱歉,年代久远我已经找不到了,评论好多朋友都炸锅了...也许日后有空我会重新写一遍补上吧,现在确实是太忙.....
最后附上本期内容的Demo源码,大家自行下载(由于空间问题,demo里只保留了armv7的环境,并且删除了opencv java层的module,请大家自行在官网下载引入)
OpenCV下载地址
Demo下载
pan点baidu点com/s/1qY0sflq
密码:jpkd