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 -Android Studio 中使用(opencv-3.4.0-android-sdk)_第1张图片

第二步:


OpenCV -Android Studio 中使用(opencv-3.4.0-android-sdk)_第2张图片

找到opencv下载的demo

OpenCV -Android Studio 中使用(opencv-3.4.0-android-sdk)_第3张图片

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


OpenCV -Android Studio 中使用(opencv-3.4.0-android-sdk)_第4张图片

此时,我们在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:

OpenCV -Android Studio 中使用(opencv-3.4.0-android-sdk)_第5张图片
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的路径。

OpenCV -Android Studio 中使用(opencv-3.4.0-android-sdk)_第6张图片

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


OpenCV -Android Studio 中使用(opencv-3.4.0-android-sdk)_第7张图片
右击

OpenCV -Android Studio 中使用(opencv-3.4.0-android-sdk)_第8张图片
打开Android.mk文件

OpenCV -Android Studio 中使用(opencv-3.4.0-android-sdk)_第9张图片

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


OpenCV -Android Studio 中使用(opencv-3.4.0-android-sdk)_第10张图片
 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存成文件,当然这也的结构不太合理,我只是为了展示方便这样书写。

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

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)竖屏预览图像问题解决方法-续 -

你可能感兴趣的:(OpenCV -Android Studio 中使用(opencv-3.4.0-android-sdk))