OpenCV -Android Studio 中使用(opencv-3.4.0-android-sdk)

源码:https://github.com/huangshuyuan/OpenCvDemo-Master/

一、导入程序

1. 确定SdkVersion

在导入程序之前,我们需要先确定待会的OpenCv工程中的一些和SdkVersion有关的配置,最好的办法就是先用AS建一个HelloWorld,也可以顺便熟悉一下Android Studio的开发流程。在工程中打开build.gradle,可以得到我们需要的信息:

这个随意,和开发的APP的兼容性有关`
  compileSdkVersion 26
    buildToolsVersion "26.0.2"

    defaultConfig {
        applicationId "org.opencv.samples.facedetect"
        minSdkVersion 14
        targetSdkVersion 26

        ndk {
            moduleName "detection_based_tracker"
            abiFilters "armeabi-v7a"
        }
    }

2.导入samples\face-detection

第一步:



第二步:


找到opencv下载的demo


打开,直接下一步就行,我这边已经导入过了


此时,我们在AS左侧的Project窗口的打开如下的配置文件:
openCVSamplefacedetection->src->build.gradle

修改如下4个配置信息:

compileSdkVersion 26
buildToolsVersion 26.0.2
minSdkVersion 14
targetSdkVersion  26

在修改完成之后,需要重新进行”Gradle project sync”,点击Tools->Android->Sync Project with Gradle Files:

image.png

原因在于,工程openCVSamplefacedetection依赖于库工程openCVLibrary340,而库工程openCVLibrary340的build.gradle配置也需要修改。这里不再赘述,找到openCVLibrary340下的build.gradle进行修改即可。

修改完成后再次进行”Gradle project sync”,这一次”Gradle Sync”没有报错,随后AS会进行”Gradle build”,也顺利完成。

3. jni的配置

怎样解决上面出现的问题?
首先,我们需要先配置NDK的路径,点击File->Project Structure,在如下的界面上配置NDK的路径。

然后,在左侧的项目窗口中选中openCVSamplefacedetection,右键点击”Link C++ Project with Gradle”,在弹出的窗口中按照下图选择,点击”OK”。


右击

打开Android.mk文件


ok,即可,会在gradle里面生成以下代码


 externalNativeBuild {
        ndkBuild {
            path 'src/main/jni/Android.mk'
        }
    }

接下来,在左右的项目窗口中的“External Build Files”下,选择Android.mk,修改OpenCV.mk的路径:

include ../../sdk/native/jni/OpenCV.mk

修改后的路径为,当然你需要根据自己电脑上的OpenCV4Android路径进行配置:

 include D:\OpenCV-android-sdk\sdk\native\jni\OpenCV.mk

在第12行的ndk部分加入以下的声明:

  abiFilters "armeabi-v7a"

最终得到:

  ndk {
            moduleName "detection_based_tracker"
            abiFilters "armeabi-v7a"
        }

至此,整个jni部分的配置就全部完成,这时再点击”构建“,可以发现已经可以生成成功。

二、剔除OpenCV Manager依赖

在上一小节中,我们已经可以成功地配置fd工程,并且编译生成都成功,此时我们可以将apk部署到手机上进行运行。但是等等,你还想被OpenCV Manager所困扰吗?说实话,每次我想要找到适合自己手机的OpenCV Manager,都要上网查一大堆资料,费时又费劲。

那么接下来我就基于face-detection工程,给大家分享一个去掉OpenCV Manager依赖的方法。

1、把OpenCV sdk for Android文件下F:\OpenCV-android-sdk\sdk\native下的libs文件夹拷贝到你的安卓项目下,即自己的项目\src\main下面,并且将libs改名为 jniLibs(需要)
2、将OpenCV-android-sdk\samples\image-manipulations\res\layout下的xml文件拷贝到自己的项目\src\main\res下面(不需要也行)
3、将OpenCV-android-sdk\samples\image-manipulations\src\org\opencv\samples\imagemanipulations下的java文件拷到自己的项目\src\main\java\你 ,MainActivity所在的包名,即和MainActivity同级目录(我改的是demo,没加)
4、在项目清单文件中为刚才导入的java文件进行配置,加上相应的权限(看demo)

gradle:

 sourceSets.main {
        jniLibs.srcDir 'src/main/libs' //set .so files directory to libs
        jni.srcDirs = [] //disable automatic ndk-build call
    }

三、常见问题

1、openCV默认是横屏,改成竖屏不全屏,竖屏无法识别人脸
2、openCV Demo 里面没有获取图片的地方
3、OpenCV Android 打开前置后置摄像头

1、摄像头竖屏全屏的设置

在CameraBridgeViewBase.java 文件修改 deliverAndDrawFrame()函数中,修改以下部分

if (canvas != null) {  
    canvas.rotate(90,0,0);  
    float scale = canvas.getWidth() / (float)mCacheBitmap.getHeight();  
    float scale2 = canvas.getHeight() / (float)mCacheBitmap.getWidth();  
    if(scale2 > scale){  
        scale = scale2;  
    }  
    if (scale != 0) {  
        canvas.scale(scale, scale,0,0);  
    }  
    canvas.drawBitmap(mCacheBitmap, 0, -mCacheBitmap.getHeight(), null);  
   // canvas.drawColor(0, android.graphics.PorterDuff.Mode.CLEAR);  
    Log.d(TAG, "mStretch value: " + mScale);  
  
  /*  if (mScale != 0) { 
        canvas.drawBitmap(mCacheBitmap, new Rect(0,0,mCacheBitmap.getWidth(), mCacheBitmap.getHeight()), 
                new Rect((int)((canvas.getWidth() - mScale*mCacheBitmap.getWidth()) / 2), 
                        (int)((canvas.getHeight() - mScale*mCacheBitmap.getHeight()) / 2), 
                        (int)((canvas.getWidth() - mScale*mCacheBitmap.getWidth()) / 2 + mScale*mCacheBitmap.getWidth()), 
                        (int)((canvas.getHeight() - mScale*mCacheBitmap.getHeight()) / 2 + mScale*mCacheBitmap.getHeight())), null); 
    } else { 
        canvas.drawBitmap(mCacheBitmap, new Rect(0,0,mCacheBitmap.getWidth(), mCacheBitmap.getHeight()), 
                new Rect((canvas.getWidth() - mCacheBitmap.getWidth()) / 2, 
                        (canvas.getHeight() - mCacheBitmap.getHeight()) / 2, 
                        (canvas.getWidth() - mCacheBitmap.getWidth()) / 2 + mCacheBitmap.getWidth(), 
                        (canvas.getHeight() - mCacheBitmap.getHeight()) / 2 + mCacheBitmap.getHeight()), null); 
    }*/  

因为opencv要在横屏时才能得到较好的结果,那么我可以先把竖屏时得到的图像顺时针旋转90度,这样就和横屏时一样了,然后我在把得到识别绿框的图像逆时针旋转90度,再输出这样就能做到竖屏时实现人脸检测了。
所以修改FaActivity中的onCameraViewStarted和onCameraFrame()函数修改如下

   Mat Matlin, gMatlin;
    int absoluteFaceSize;

    public void onCameraViewStarted(int width, int height) {
        //        mGray = new Mat();
        //        mRgba = new Mat();

        mRgba = new Mat(width, height, CvType.CV_8UC4);
        mGray = new Mat(height, width, CvType.CV_8UC4);
        Matlin = new Mat(width, height, CvType.CV_8UC4);
        gMatlin = new Mat(width, height, CvType.CV_8UC4);

        absoluteFaceSize = (int) (height * 0.2);
    }

    int faceSerialCount = 0;

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    @Override
    public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame InputFrame) {
        mGray = InputFrame.gray();
        mRgba = InputFrame.rgba();
        int rotation = mOpenCvCameraView.getDisplay().getRotation();

        //使前置的图像也是正的
        Core.flip(mRgba, mRgba, 1);
        Core.flip(mGray, mGray, 1);

        //MatOfRect faces = new MatOfRect();

        if (rotation == Surface.ROTATION_0) {
            MatOfRect faces = new MatOfRect();
            Core.rotate(mGray, gMatlin, Core.ROTATE_90_CLOCKWISE);
            Core.rotate(mRgba, Matlin, Core.ROTATE_90_CLOCKWISE);
            if (mJavaDetector != null) {
                mJavaDetector.detectMultiScale(gMatlin, faces, 1.1, 2, 2, new Size(absoluteFaceSize, absoluteFaceSize), new Size());
            }

            Rect[] faceArray = faces.toArray();
    
            for (int i = 0; i < faceArray.length; i++)

                Imgproc.rectangle(Matlin, faceArray[i].tl(), faceArray[i].br(), new Scalar(0, 255, 0, 255), 2);
            Core.rotate(Matlin, mRgba, Core.ROTATE_90_COUNTERCLOCKWISE);

        } else {
            MatOfRect faces = new MatOfRect();
            if (mJavaDetector != null)
                mJavaDetector.detectMultiScale(mGray, faces, 1.1, 2, 2, // TODO: objdetect.CV_HAAR_SCALE_IMAGE
                        new Size(mAbsoluteFaceSize, mAbsoluteFaceSize), new Size());

            Rect[] faceArray = faces.toArray();

       
            for (int i = 0; i < faceArray.length; i++)

                Imgproc.rectangle(mRgba, faceArray[i].tl(), faceArray[i].br(), new Scalar(0, 255, 0, 255), 2);
        }

        return mRgba;
    }

2、捕获人脸后自动拍照

捕获人脸后自动拍照,这个需求可能是最最常见的了,那在OpenCV里要如何实现呢?首先我们来观察一下JavaCameraView这个类,它继承自CameraBridgeViewBase

这个类,再往下翻会发现一个非常熟悉的Camera对象,没错这个类里其实是使用了Android原生的API构造了一个相机对象(还好是原生的,至今还没忘却被JNI相机

一旦实现了PreviewCallback接口,肯定会有onPreviewFrame(byte[] frame,Camera camera)这个回调函数,这里面的字节数组frame对象,就是当前的视频帧,注意这里是视频帧,是YUV编码的,并不能直接转换为Bitmap。这个回调函数在预览过程中会一直被调用,那么只要确定了哪一帧有人脸,只需要在这里获取就行,代码如下:

private boolean takePhotoFlag = false;  
    @Override  
    public void onPreviewFrame(byte[] frame, Camera arg1) {  
        if (takePhotoFlag){  
            Camera.Size previewSize = mCamera.getParameters().getPreviewSize();  
            BitmapFactory.Options newOpts = new BitmapFactory.Options();  
            newOpts.inJustDecodeBounds = true;  
            YuvImage yuvimage = new YuvImage(  
                    frame,  
                    ImageFormat.NV21,  
                    previewSize.width,  
                    previewSize.height,  
                    null);  
            ByteArrayOutputStream baos = new ByteArrayOutputStream();  
            yuvimage.compressToJpeg(new Rect(0, 0, previewSize.width, previewSize.height), 100, baos);  
            byte[] rawImage = baos.toByteArray();  
            BitmapFactory.Options options = new BitmapFactory.Options();  
            options.inPreferredConfig = Bitmap.Config.RGB_565;  
            Bitmap bmp = BitmapFactory.decodeByteArray(rawImage, 0, rawImage.length, options);  
            try {  
                BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(fileName));  
                bmp.compress(Bitmap.CompressFormat.JPEG, 100, bos);  
                bos.flush();  
                bos.close();  
            } catch (IOException e) {  
                e.printStackTrace();  
            }  
            bmp.recycle();  
            takePhotoFlag = false;  
        }  
        synchronized (this) {  
            mFrameChain[mChainIdx].put(0, 0, frame);  
            mCameraFrameReady = true;  
            this.notify();  
        }  
        if (mCamera != null)  
            mCamera.addCallbackBuffer(mBuffer);  
    }  

这里我们先在外层声明一个布尔类型的变量,在无限的回调过程中,一旦次布尔值为真,就对该视频帧保存为文件,上面的代码就将YUV视频帧转换为Bitmap对象的方法,然后将Bitmap存成文件,当然这也的结构不太合理,我只是为了展示方便这样书写。

现在已经可以抓取照片了,那么如何才能判断是不是有人脸来修改这个布尔值呢,我们再定义这样一个方法:</pre>

public void takePhoto(String name){  
       fileName = name;  
       takePhotoFlag = true;  
   }  

一旦调用了takePhoto这个方法,传入一个保存路径,就能修改此布尔值,完成拍照,我们离完成越来越接近了。那么回到我们一开始的Activity,这里面包含刚刚修改的

JavaCameraView对象,可以对他进行操作。然后找到Activity的onCameraFrame回调函数,修改为:

int faceSerialCount = 0;  
   @Override  
   public Mat onCameraFrame(Mat aInputFrame) {  
       Imgproc.cvtColor(aInputFrame, grayscaleImage, Imgproc.COLOR_RGBA2RGB);  
       MatOfRect faces = new MatOfRect();  
       if (cascadeClassifier != null) {  
           cascadeClassifier.detectMultiScale(grayscaleImage, faces, 1.1, 2, 2,  
                   new Size(absoluteFaceSize, absoluteFaceSize), new Size());  
       }  
       Rect[] facesArray = faces.toArray();  
       int faceCount = facesArray.length;  
       if (faceCount > 0) {  
           faceSerialCount++;  
       } else {  
           faceSerialCount = 0;  
       }  
       if (faceSerialCount > 6) {  
           openCvCameraView.takePhoto("sdcard/aaa.jpg");  
           faceSerialCount = -5000;           
       }  
       for (int i = 0; i < facesArray.length; i++) {  
           Imgproc.rectangle(aInputFrame, facesArray[i].tl(), facesArray[i].br(), new Scalar(0, 255, 0, 255), 3);  
       }  
       return aInputFrame;  
   }  

一旦该变量大于0,就让faceSerialCount自增,else的话就清零,如果faceSerialCount>6就调用刚才我们定义的takePhoto方法进

3、OpenCV Android 打开前置后置摄像头

  mOpenCvCameraView.setCameraIndex(CameraBridgeViewBase.CAMERA_ID_FRONT);
//前置摄像头 CameraBridgeViewBase.CAMERA_ID_BACK为后置摄像头  

兼容性适配

名词解析:
NDK:Native Development Kit
JNI:Java Native Interface
ABI: Application Binary Interface 应用二进制接口

1、Android Studio使用so库

1、使用和eclipse一样在libs目录下新建armeabi目录的方式

需要在build.gradle中添加指定jni库目录的语句

sourceSets {
   main.jniLibs.srcDirs = ['libs']  //指定libs为jni的存放目录
}
2、使用AS默认的位置:src/main/jniLibs

直接在src/main/下新建jniLibs目录,将armeabi等目录放到该目录下即可
备注:AS可以直接右键新建同目录下的jniLibs目录,但该目录不是编译好的库文件目录,而是未编译的本地代码文件的目录(这里指的是与java同级的jni目录,放置cpp代码的)


android支持的cpu架构(目前是七种)

cpu
armeabi 第5代 ARM v5TE,使用软件浮点运算,兼容所有ARM设备,通用性强,速度慢
armeabi-v7a 第7代 ARM v7,使用硬件浮点运算,具有高级扩展功能
arm64-v8a 第8代,64位,包含AArch32、AArch64两个执行状态对应32、64bit
x86 intel 32位,一般用于平板
x86_64 intel 64位,一般用于平板
mips 少接触
mips64 少接触
安装时的兼容性检查:

安装到系统中后,so文件会被提取在:data/app/com.xxxxxxxx.app-x/lib/目录下(5.0版本)、/data/app-lib/目录下(4.2版本),其中armeabi和armeabi-v7a会生成arm目录,arm64-v8a会生成arm64目录。
安装app的时候,如果app使用了so文件,而不存在适合本机cpu架构的so文件,会报如下错误:
Installation failed with message INSTALL_FAILED_NO_MATCHING_ABIS.
例如:在x86模拟器上就必须有x86版本的so文件夹。不然无法安装成功。

运行时的兼容性检查:

1、检查目标目录下是否存在的so库文件
2、检查存在的so文件是否符合当前cpu架构。
对于情况一,一般规避的做法是:保证jnilibs目录下x86、x84_64、armeabi、armeabi-v7a、arm64-v8a等目录下的文件名称数量是一致的。
例如:项目中使用了A、B、C三个第三方库。其中A、B提供了armebi以及arm64-v8a版本的库文件,而C只提供了armebi、armebi-v7a版本的库文件。这时候只能够删除原有的arm64-v8a目录,保留armeabi目录,一般arm64的手机都能兼容使用armeabi版本的库。或者复制一份armeabi的so文件到缺少的目录中(推荐)。

生成so文件:

NDK交叉编译时选定APP_ABI := armeabi x86 ...可以生成支持相应芯片的so文件。APP_ABI := all生成支持所有芯片指令集(目前七种)so文件。

Android加载so文件规则:

当你只提供了armeabi目录时,armeabi-v7a、arm64-v8a架构的程序都会去armeabi里寻找,而当你同时也提供了armeabi-v7a、armeabi-v8a目录,而里面又不存在对应的so库时,系统就不会再去armeabi里面寻找了,直接找不到报错。其他平台也是如此。这里我踩了不少的坑,切记。
一般来说,一些比较有名的第三方库都会提供armeabi、armeabi-v7a、x86这三种类型的so文件,同时拥有这三种版本的app可以在所有机型上运行。另外,越来越多的SDK会同时提供arm64-v8a版本。只包含armeabi的项目也可以在所有设备上运行。


2、ABI

Application.mk 文件如下

APP_STL := gnustl_static
APP_CPPFLAGS := -frtti -fexceptions
APP_ABI := armeabi-v7a       #这句是设置生成的cpu指令类型,提示,目前绝大部分安卓手机支持armeabi,libs下太多类型,编译进去 apk 包会过大
APP_PLATFORM := android-8    #这句是设置最低安卓平台,可以不弄 

3、关于abiFilters的使用

在app的gradle的defaultConfig里面加上这么一句

ndk {
    abiFilters  "armeabi-v7a"  // 指定要ndk需要兼容的架构(这样其他依赖包里mips,x86,armeabi,arm-v8之类的so会被过滤掉)
}

这句话的意思就是指定ndk需要兼容的架构,把除了v7a以外的兼容包都过滤掉,只剩下一个v7a的文件夹。

以上是一种ABI的添加,多种如下

APP_ABI :=armeabi-v7a arm64-v8a armeabi  mips mips64 x86 x86_64 //Application.mk中空格分开

gradle中逗号分开

 ndk {
            moduleName "detection_based_tracker"
            abiFilters "armeabi-v7a","arm64-v8a","armeabi","mips","mips64","x86","x86_64"
        }

官方说

参考文章

Android - OpenCV library
Android Camera动态人脸识别+人脸检测基于OpenCV(无需OpenCVManager) - CSDN博客
Android Studio-—使用OpenCV的配置方法和demo以及开发过程中遇到的问题解决 - 奋斗者—cyf - 博客园
关于opencv的人脸识别的Demo配置(android端不需要Manager) - 简书
Android Studio配置并运行OpenCV4Android的face-detection - Android移动开发技术文章_手机开发 - 红黑联盟
Android NDK 入门与实践
Android.mk | Android Developers
使用AndroidStudio编译NDK的方法及错误解决方案 - 炉火纯青 - 博客园
NDK各版本下载 - CSDN博客
OpenCV Android 打开前置后置摄像头 - CSDN博客
OpenCV学习笔记(七)—— OpenCV for Android实时图像处理 - CSDN博客
【安卓随笔】使用OpenCV进行人脸跟踪和自动拍照 - CSDN博客
Android OpenCV 实例笔记3 -- 摄像头竖屏全屏的设置,更新完整代码 - CSDN博客
OpenCV on Android 开发 (4)竖屏预览图像问题解决方法-续 - 简书

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容