1. 程式人生 > >OpenCV -Android Studio 中使用(opencv-3.4.0-android-sdk)

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

一、匯入程式

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存成檔案,當然這也的結構不太合理,我只是為了展示方便這樣書寫。

現在已經可以抓取照片了,那麼如何才能判斷是不是有人臉來修改這個布林值呢,我們再定義這樣一個方法:

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架構(目前是七種)
這裡寫圖片描述

安裝時的相容性檢查:

安裝到系統中後,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"
        }

官方說

參考文章