OpenCV4 Android 調用攝像頭

OpenCV4 調用攝像頭黑屏問題

OpenCV 調用 Android 攝像頭這一塊,我之前研究了好幾天,都是一片黑,毫無頭緒。後來發現 OpenCV4 要想調用攝像頭,必須繼承自 OpenCV 的 CameraActivity !!!

CameraActivity.java 的源碼如下,可以看出大部分代碼都是爲了 Android M(6.0)以上請求權限而生的,只有兩個地方非常關鍵

  1. protected List<? extends CameraBridgeViewBase> getCameraViewList() { …… }
    子 Activity 在繼承 CameraActivity 後,需要複寫該函數,把 JavaCamera2View 或 JavaCameraView 送入 List 作爲返回值。

  2. cameraBridgeViewBase.setCameraPermissionGranted()
    相機視圖初始情況下是黑屏的,即不工作狀態。只有當權限授予完畢,調用了 setCameraPermissionGranted 之後,OpenCV 纔開始調用相機並把數據輸出到 SurfaceView 上。

public class CameraActivity extends Activity {

    private static final int CAMERA_PERMISSION_REQUEST_CODE = 200;

    protected List<? extends CameraBridgeViewBase> getCameraViewList() {
        return new ArrayList<CameraBridgeViewBase>();
    }

    protected void onCameraPermissionGranted() {
        List<? extends CameraBridgeViewBase> cameraViews = getCameraViewList();
        if (cameraViews == null) {
            return;
        }
        for (CameraBridgeViewBase cameraBridgeViewBase: cameraViews) {
            if (cameraBridgeViewBase != null) {
                cameraBridgeViewBase.setCameraPermissionGranted();
            }
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
        boolean havePermission = true;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (checkSelfPermission(CAMERA) != PackageManager.PERMISSION_GRANTED) {
                requestPermissions(new String[]{CAMERA}, CAMERA_PERMISSION_REQUEST_CODE);
                havePermission = false;
            }
        }
        if (havePermission) {
            onCameraPermissionGranted();
        }
    }

    @Override
    @TargetApi(Build.VERSION_CODES.M)
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        if (requestCode == CAMERA_PERMISSION_REQUEST_CODE && grantResults.length > 0
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            onCameraPermissionGranted();
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

實現要領

  1. 首先,要繼承 CameraActivity,之前已經說過了。這個基類會去申請權限,然後通知 javaCameraView 已獲取到權限,可以正常使用。
  2. 複寫父類的 getCameraViewList 方法,將 javaCameraView 回送回去,這樣當權限已被賦予時,就可以通知到預覽界面開始正常工作了。
  3. OpenCV 已經爲我們實現了 Camera 和 Camera2 的函數,如果應用最低版本 minSdkVersion > 5.0,建議使用 JavaCamera2View 的相關函數,否則使用 JavaCameraView。
  4. 在 onResume 時判斷 opencv 庫是否加載完畢,然後啓用預覽視圖。在 onPause 時由於界面被遮擋,此時應該暫停攝像頭的預覽以節省手機性能和電量損耗。
  5. Camera2 和 Camera 的絕大部分差異 OpenCV 均已經爲我們屏蔽在類的內部了,唯一的差別就是兩者實現的 CvCameraViewListener 監聽器裏的預覽函數 onCameraFrame 的參數略有不同。從下面的源碼可以看出 CvCameraViewListener2 的 inputFrame 由 Mat 類型改爲了 CvCameraViewFrame 類型,它額外提供了一個轉化爲灰度圖的接口。

CvCameraViewListener

public interface CvCameraViewListener {
   /**
     * This method is invoked when camera preview has started. After this method is invoked
     * the frames will start to be delivered to client via the onCameraFrame() callback.
     * @param width -  the width of the frames that will be delivered
     * @param height - the height of the frames that will be delivered
     */
    public void onCameraViewStarted(int width, int height);

    /**
     * This method is invoked when camera preview has been stopped for some reason.
     * No frames will be delivered via onCameraFrame() callback after this method is called.
     */
    public void onCameraViewStopped();

    /**
     * This method is invoked when delivery of the frame needs to be done.
     * The returned values - is a modified frame which needs to be displayed on the screen.
     * TODO: pass the parameters specifying the format of the frame (BPP, YUV or RGB and etc)
     */
    public Mat onCameraFrame(Mat inputFrame);
}

CvCameraViewListener2

public interface CvCameraViewListener2 {
   /**
     * This method is invoked when camera preview has started. After this method is invoked
     * the frames will start to be delivered to client via the onCameraFrame() callback.
     * @param width -  the width of the frames that will be delivered
     * @param height - the height of the frames that will be delivered
     */
    public void onCameraViewStarted(int width, int height);

    /**
     * This method is invoked when camera preview has been stopped for some reason.
     * No frames will be delivered via onCameraFrame() callback after this method is called.
     */
    public void onCameraViewStopped();

    /**
     * This method is invoked when delivery of the frame needs to be done.
     * The returned values - is a modified frame which needs to be displayed on the screen.
     * TODO: pass the parameters specifying the format of the frame (BPP, YUV or RGB and etc)
     */
    public Mat onCameraFrame(CvCameraViewFrame inputFrame);
};

/**
 * This class interface is abstract representation of single frame from camera for onCameraFrame callback
 * Attention: Do not use objects, that represents this interface out of onCameraFrame callback!
 */
public interface CvCameraViewFrame {

    /**
     * This method returns RGBA Mat with frame
     */
    public Mat rgba();

    /**
     * This method returns single channel gray scale Mat with frame
     */
    public Mat gray();
};

示例程序

下面使用 Camera2 來實現拍照功能( 注意:Camera2 只能用於 Android 5.0 以上的手機 )

Java代碼

public class OpencvCameraActivity extends CameraActivity {

    private static final String TAG = "OpencvCam";

    private OpencvCameraActivity activity = this;
    private JavaCamera2View javaCameraView;
    private CameraBridgeViewBase.CvCameraViewListener2 cvCameraViewListener2 = new CameraBridgeViewBase.CvCameraViewListener2() {
        @Override
        public void onCameraViewStarted(int width, int height) {
            Log.i(TAG, "onCameraViewStarted width=" + width + ", height=" + height);
        }

        @Override
        public void onCameraViewStopped() {
            Log.i(TAG, "onCameraViewStopped");
        }

        @Override
        public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {
            return inputFrame.rgba();
    	}
	}

    private BaseLoaderCallback baseLoaderCallback = new BaseLoaderCallback(this) {
        @Override
        public void onManagerConnected(int status) {
            Log.i(TAG, "onManagerConnected status=" + status + ", javaCameraView=" + javaCameraView);
            switch (status) {
                case LoaderCallbackInterface.SUCCESS: {
                    if (javaCameraView != null) {
                        javaCameraView.setCvCameraViewListener(cvCameraViewListener2);
                        // 禁用幀率顯示
                        javaCameraView.disableFpsMeter();
                        javaCameraView.enableView();
                    }
                }
                break;
                default:
                    super.onManagerConnected(status);
                    break;
            }
        }
    };

	// 複寫父類的 getCameraViewList 方法,把 javaCameraView 送到父 Activity,一旦權限被授予之後,javaCameraView 的 setCameraPermissionGranted 就會自動被調用。
    @Override
    protected List<? extends CameraBridgeViewBase> getCameraViewList() {
        Log.i(TAG, "getCameraViewList");
        List<CameraBridgeViewBase> list = new ArrayList<>();
        list.add(javaCameraView);
        return list;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_camera3);
        javaCameraView = findViewById(R.id.javaCameraView);
    }

    @Override
    public void onPause() {
        Log.i(TAG, "onPause");
        super.onPause();
        if (javaCameraView != null) {
            javaCameraView.disableView();
        }
    }

    @Override
    public void onResume() {
        Log.i(TAG, "onResume");
        super.onResume();
        if (OpenCVLoader.initDebug()) {
            Log.i(TAG, "initDebug true");
            baseLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);
        } else {
            Log.i(TAG, "initDebug false");
            OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION, this, baseLoaderCallback);
        }
    }
}

佈局文件

佈局文件很簡單,核心就是這個 JavaCamera2View 視圖

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <org.opencv.android.JavaCamera2View
        android:id="@+id/javaCameraView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:camera_id="back"
        app:show_fps="true" />
</FrameLayout>

全屏預覽

雖然使用了上述方法,但相機的預覽視圖還是隻佔了屏幕的一小丟丟,而且還是頭朝左的。

此時需要修改 OpenCV 的源碼裏 CameraBridgeViewBase.java 中的 deliverAndDrawFrame 方法,對圖像進行旋轉縮放。

/**
  * This method shall be called by the subclasses when they have valid
  * object and want it to be delivered to external client (via callback) and
  * then displayed on the screen.
  * @param frame - the current frame to be delivered
  */
 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);
             if (BuildConfig.DEBUG) Log.d(TAG, "mStretch value: " + mScale);

             //TODO 額外添加,讓預覽框達到全屏效果
             int degrees = rotationToDegree();
             Matrix matrix = new Matrix();
             matrix.postRotate(degrees);
             Bitmap outputBitmap = Bitmap.createBitmap(mCacheBitmap, 0, 0, mCacheBitmap.getWidth(), mCacheBitmap.getHeight(), matrix, true);

             if (outputBitmap.getWidth() <= canvas.getWidth()) {
                 mScale = calcScale(outputBitmap.getWidth(), outputBitmap.getHeight(), canvas.getWidth(), canvas.getHeight());
             } else {
                 mScale = calcScale(canvas.getWidth(), canvas.getHeight(), outputBitmap.getWidth(), outputBitmap.getHeight());
             }

             if (mScale != 0) {
                 canvas.scale(mScale, mScale, 0, 0);
             }
             Log.d(TAG, "mStretch value: " + mScale);

             canvas.drawBitmap(outputBitmap, 0, 0, null);

             /*
             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);
             }
             */

             if (mFpsMeter != null) {
                 mFpsMeter.measure();
                 mFpsMeter.draw(canvas, 20, 30);
             }
             getHolder().unlockCanvasAndPost(canvas);
         }
     }
 }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章