1. 程式人生 > >OpneCV和Android NDK

OpneCV和Android NDK

Android NDK 和 OpenCV 

這一節的主要內容是OpenCV在Android NDK開發中的應用,包括下面幾個方面的內容:

  • 如何實現Static Initialization從而不需要安裝OpenCV Manager執行含OpenCV library的app
  • 對十份論文和報告中的關於OpenCV和Android NDK開發的總結
  • 如何使用Android中的攝像頭,常見的問題有哪些?
  • OpenCV 和 Android NDK 整合開發的一般途徑

1.實現Static Initialization

實現Static Initialization就是指將OpenCV Library新增到app package中,不需要安裝OpenCV Manager這個app就能執行,

官方文件有介紹,但是不詳細,尤其是最後那句程式碼到底要放在什麼地方很多人都不清楚,其實並不需要像官方文件中介紹的那樣配置,我想在這裡介紹下如何修改FaceDetection專案的原始碼來做到這點。(最好是找一個包含jni程式碼的專案進行修改)

  • [1]開啟jni下的Android.mk檔案,修改OpenCV的那一部分,將off設定為on,並設定OpenCV_LIB_TYPESHARED,結果如下:
OpenCV_CAMERA_MODULES:=on
OpenCV_INSTALL_MODULES:=on
OpenCV_LIB_TYPE:=SHARED
include ${OpenCVROOT}/sdk/native
/jni/OpenCV.mk
  • [2]開啟FdActivity.java檔案,在其中新增一個靜態初始化塊程式碼,它是用來載入OpenCV_java庫的,由於FaceDetection中還用了另一個庫detection_based_tracker(用於人臉跟蹤),所以要在else子句中載入進來:
static {
    Log.i(TAG, "OpenCV library load!");
    if (!OpenCVLoader.initDebug()) {
        Log.i(TAG, "OpenCV load not successfully");
    } else {
        System.loadLibrary("detection_based_tracker"
);// load other libraries } }
  • [3]刪除FdActivity.java的OnResume()方法的最後那句,不讓它去訪問OpenCV Manager。
@Override
public void onResume() {
    super.onResume();
//OpenCVLoader.initAsync(OpenCVLoader.OpenCV_VERSION_2_4_3, this, mLoaderCallback);//
}
  • [4]修改FdActivity.java的OnCreate()方法,從上面的private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this)程式碼塊中拷貝try-catch塊放到OnCreate的setContentView()之後,然後拷貝mOpenCVCameraView.enableView();放到mOpenCVCameraView = (CameraBridgeViewBase) findViewById(R.id.fd_activity_surface_view);之後,修改後的OnCreate()方法如下:
public void onCreate(Bundle savedInstanceState) {
    Log.i(TAG, "called onCreate");
    super.onCreate(savedInstanceState);
    getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

    setContentView(R.layout.face_detect_surface_view);

    //
    try {
        // load cascade file from application resources
        InputStream is = getResources().openRawResource(R.raw.lbpcascade_frontalface);
        File cascadeDir = getDir("cascade", Context.MODE_PRIVATE);
        mCascadeFile = new File(cascadeDir, "lbpcascade_frontalface.xml");
        FileOutputStream os = new FileOutputStream(mCascadeFile);

        byte[] buffer = new byte[4096];
        int bytesRead;
        while ((bytesRead = is.read(buffer)) != -1) {
            os.write(buffer, 0, bytesRead);
        }
        is.close();
        os.close();

        mJavaDetector = new CascadeClassifier(mCascadeFile.getAbsolutePath());
        if (mJavaDetector.empty()) {
            Log.e(TAG, "Failed to load cascade classifier");
            mJavaDetector = null;
        } else
            Log.i(TAG, "Loaded cascade classifier from " + mCascadeFile.getAbsolutePath());

        mNativeDetector = new DetectionBasedTracker(mCascadeFile.getAbsolutePath(), 0);// hujiawei

        cascadeDir.delete();

    } catch (IOException e) {
        e.printStackTrace();
        Log.e(TAG, "Failed to load cascade. Exception thrown: " + e);
    }

    //

    mOpenCVCameraView = (CameraBridgeViewBase) findViewById(R.id.fd_activity_surface_view);
    mOpenCVCameraView.enableView();//
    mOpenCVCameraView.setCvCameraViewListener(this);
}
  • [5]OK,解除安裝安裝好的OpenCV Manager,然後重新除錯執行FaceDetection試試,它已經可以自行運行了!

2.對十份論文和報告中的關於OpenCV和Android NDK開發的總結

這10篇文獻大部分[百度網盤下載地址]都還是停留如何在Android開發中使用OpenCV library,沒有牽涉到具體的實現領域。具體總結如下:

  • _利用OpenCV實現在Android系統下的人臉檢測

本文主要介紹瞭如何在底層通過OpenCV來對人臉部分進行檢測,得到的人臉位置資料通過JNI傳遞給Java層,詳細介紹了其中的JNI程式碼和共享庫的構建過程,對圖片是通過圖片的路徑來進行傳遞的,因為這裡的檢測只是對單張靜態的圖片進行檢測。

  • _Tutorial-2-OpenCV-for-Android-Setup-Macintosh-API11

本文主要是介紹了OpenCV和Android NDK開發環境的搭建,以及基於示例程式Face-Detection的演示。使用的方式是將OpenCV Library Project作為庫,然後呼叫OpenCV Android API。

  • _Android application for Face Recognition

這是一份詳細的專案介紹,實現了幾種基於Android平臺的人臉檢測和識別,包括Google API和OpenCV的,但是OpenCV的由於需要Library Project,而且演算法過於複雜,作者便自行開發了人臉檢測庫,有6大特性,其中包括了眼鏡和嘴巴的檢測。

  • _ECCV-2012-OpenCV4Android

這份報告寫得精簡但是內容豐富,有幾個重要點:
(1) 使用OpenCV的Android應用開發方式,對應不同的開發人群:Java developer / Native developer
(2) OpenCV4Android目前的侷限性,以及開發過程中對於提高效能和開發效率需要注意的事項

  • _Introduction to OpenCV for Android devices

本文設計的內容都很基礎,涉及到OpenCV和Android開發的環境搭建,亮點是最後的Using C++ OpenCV code,這裡是在Android ndk中使用OpenCV原生代碼的重要配置項。

  • _OpenCV-facedetection

這份報告講述了很多OpenCV的相關知識,另外還詳細講述了一個人臉檢測的演算法

  • _OpenCV on Android Platforms

這份報告內容也比較多,但是都很基礎。

  • _BDTI_ARMTechCon_2012_OpenCV_Android

這份報告講的是OpenCV在嵌入式裝置中的應用,其中介紹了OpenCV在Android上的開發,需要注意的是OpenCV2.4開始提供了native Android camera support!

  • _OpenCV Based Real-Time Video Processing Using Android Smartphone

這篇論文介紹了利用OpenCV對實時的視訊進行處理和純Android library進行處理的比較,發現利用OpenCV處理的結果更加準確,效率更快,而且更加省電。比較時使用的都是基本影象處理操作,例如灰度化,高斯模糊,Sobel邊緣檢測等等。

  • _Realtime Computer Vision with OpenCV

這篇文章比較有意思,大致看了下,介紹了OpenCV在移動終端的應用。

3.Android的攝像頭

  • 關於儲存預覽圖片:Android中的BitmapFactory.decodeByteArray只支援一定的格式,Camara預設的previewformat格式為NV21(對於大多數的Android裝置,即使修改CameraParameters的設定也還是不行),所以在獲得bitmap時,需要進行轉換,通過YuvImage類來轉換成JPEG格式,然後再儲存到檔案中。
    Google Group上的討論

  • 關於如何在預覽介面上新增一個矩形框,類似二維碼掃描那樣,原理很簡單,一個使用SurfaceView,另一個使用ImageVIew(或者SurfaceView也行),推薦文章:
    Android攝像頭中預覽介面新增矩形框

  • 關於如何進行和OpenCV有關的攝像頭開發:有了OpenCV的library之後,關於攝像頭的開發可謂是簡單了很多,可以參見OpenCV for Android中的三個Tutorial(CameraPreview, MixingProcessing和CameraControl),原始碼都在OpenCV-Android sdk的samples目錄下,這裡簡單介紹下:
    OpenCV Library中提供了兩種攝像頭,一種是Java攝像頭-org.OpenCV.Android.JavaCameraView,另一種是Native攝像頭-org.OpenCV.Android.NativeCameraView (可以執行CameraPreview這個專案來體驗下兩者的不同,其實差不多)。兩者都繼承自CameraBridgeViewBase這個抽象類,但是JavaCamera使用的就是Android SDK中的Camera,而NativeCamera使用的是OpenCV中的VideoCapture

  • 關於OpenCV的Camera在Layout檔案中的配置:OpenCV:show_fps在layout中如果設定為true的話顯示介面中會出現當前攝像頭幀率的資訊以及圖片的大小,OpenCV:camera_id的配置有三種front,back,any分別對應前置攝像頭,後置攝像頭和預設的攝像頭(其實也就是後置攝像頭)。

  • 關於CvCameraViewListener2介面:它可以方便的處理和攝像頭的互動,該介面只有三個函式,分別在Camera開啟(onCameraViewStarted),關閉(onCameraViewStopped)和預覽的圖片幀到了的時候(onCameraFrame)呼叫。其中OnCameraFrame這個方法很重要,如果要對圖片進行處理的話一般都是在這裡面處理的,這個函式的輸入引數是CvCameraViewFrame,需要注意的是,不要在這個方法的外面使用這個變數,因為這個物件沒有它自己的狀態,在回撥方法的外面它的行為是不可預測的!它提供了兩個有用的方法rgba()gray()分別得到影象幀的RGBA格式和灰度圖,OnCameraFrame的返回值是RGBA格式的影象,這個很重要!一定要保證處理了之後的影象是RGBA格式的Android系統才能正常顯示!來自OpenCV文件:Android Development with Android

Note Do not save or use CvCameraViewFrame object out of onCameraFrame callback. This object does not have its own state and its behavior out of callback is unpredictable!

  • 關於如何傳遞攝像頭預覽的影象資料給Native程式碼:這個很重要!我曾經試過很多的方式,大致思路有:

①傳遞圖片路徑:這是最差的方式,我使用過,速度很慢,主要用於前期開發的時候進行測試,測試Java層和Native層的互調是否正常。

②傳遞預覽影象的位元組陣列到Native層,然後將位元組陣列處理成RGB或者RGBA的格式[具體哪種格式要看你的影象處理函式能否處理RGBA格式的,如果可以的話推薦轉換成RGBA格式,因為返回的也是RGBA格式的。網上有很多的文章討論如何轉換:一種方式是使用一個自定義的函式進行編碼轉換(可以搜尋到這個函式),另一個種方式是使用OpenCV中的Mat和cvtColor函式進行轉換,接著呼叫影象處理函式,處理完成之後,將處理的結果儲存在一個整形陣列中(實際上就是RGB或者RGBA格式的影象資料),最後呼叫Bitmap的方法將其轉換成bitmap返回。這種方法速度也比較慢,但是比第一種方案要快了不少,具體實現過程可以看下面的推薦書籍。

③使用OpenCV的攝像頭:JavaCamera或者NativeCamera都行,好處是它進行了很多的封裝,可以直接將預覽影象的Mat結構傳遞給Native層,這種傳遞是使用Mat的記憶體地址(long型),Native層只要根據這個地址將其封裝成Mat就可以進行處理了,另外,它的回撥函式的返回值也是Mat,非常方便!這種方式速度較快。詳細過程可以檢視OpenCV-Android sdk的samples專案中的Tutorial2-MixedProcessing。

  • 關於攝像頭預覽介面倒置的問題:很多時候(一般是將應用設定為portrait模式之後)在呼叫了OpenCV的Camera之後,出現預覽內容倒置了90度的現象,原因是OpenCV的Camera預設情況下是以landscape模式執行的,一個可行但是不是很好的解決方案是修改OpenCV庫中的org.opencv.android.CameraBridgeViewBase類中的deliverAndDrawFrame方法,問題參考連結
protected void deliverAndDrawFrame(CvCameraViewFrame frame) {
        Mat modified;

        if (mListener != null) {
            modified = mListener.onCameraFrame(frame);
        } else {
            modified = frame.rgba();
        }

        boolean bmpValid = true;
        if (modified != null) {
            try {
                Utils.matToBitmap(modified, mCacheBitmap);
            } catch(Exception e) {
                Log.e(TAG, "Mat type: " + modified);
                Log.e(TAG, "Bitmap type: " + mCacheBitmap.getWidth() + "*" + mCacheBitmap.getHeight());
                Log.e(TAG, "Utils.matToBitmap() throws an exception: " + e.getMessage());
                bmpValid = false;
            }
        }

        if (bmpValid && mCacheBitmap != null) {
            Canvas canvas = getHolder().lockCanvas();
            if (canvas != 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);
//                }

                //ABC : Fixed for image rotation
                //TODO Why portrait is not opening in fulls creen
                Matrix matrix = new Matrix();
                int height_Canvas = canvas.getHeight();
                int width_Canvas = canvas.getWidth();

                int width = mCacheBitmap.getWidth();
                int height = mCacheBitmap.getHeight();

                float f1 = (width_Canvas - width) / 2;
                float f2 = (height_Canvas - height) / 2;
                matrix.preTranslate(f1, f2);
                if(getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT)
                matrix.postRotate(270f,(width_Canvas) / 2,(height_Canvas) / 2);
                canvas.drawBitmap(mCacheBitmap, matrix, new Paint());



                if (mFpsMeter != null) {
                    mFpsMeter.measure();
                    mFpsMeter.draw(canvas, 20, 30);
                }
                getHolder().unlockCanvasAndPost(canvas);
            }
        }
    }

3.OpenCV 和 OpenCV NDK 整合開發的一般途徑

在進行這類開發的時候,需要考慮如何在Android中使用OpenCV,並且如果需要呼叫攝像頭的話,要考慮以下內容:
首先,是否是在原有的C/C++程式碼上進行移植,如果是的話,那麼儘量考慮使用ndk開發,否則使用OpenCV for Android編寫Java程式碼進行開發,效率不會比native程式碼低多少;
其次,如果是需要OpenCV library,是否能夠容忍執行應用還需要安裝OpenCV Manager,如果不能的話,則在開發時要考慮將OpenCV binaries新增到應用中進行static initialization,但其實使用OpenCV Manager是有很多好處的,上面的論文和OpenCV官網都有相應的文件介紹它的好處和使用方式;
接著,是否需要呼叫攝像頭,如果需要的話,是使用原生Android的Camera還是使用OpenCV的Camera,如果是OpenCV Camera的話,是使用Java呼叫攝像頭還是Native呼叫攝像頭;
最後,圖片如何進行傳遞,如果是單張靜態圖片進行處理的話,只需要路徑就行了,但是如果是在視訊狀態下對圖片進行處理的話,那麼就只能傳遞影象資料了,這裡涉及到了Android中如何獲取預覽的影象資料以及如何將其傳遞到底層,又如何進行轉換(一般是YUV轉成RGB)使得OpenCV可以進行處理,處理完了之後,又如何將處理得到的圖片傳遞給Java層。

轉自:https://segmentfault.com/a/1190000000711105

謝謝作者:hujiawei