OpenCV4 調用攝像頭黑屏問題
OpenCV 調用 Android 攝像頭這一塊,我之前研究了好幾天,都是一片黑,毫無頭緒。後來發現 OpenCV4 要想調用攝像頭,必須繼承自 OpenCV 的 CameraActivity !!!
CameraActivity.java 的源碼如下,可以看出大部分代碼都是爲了 Android M(6.0)以上請求權限而生的,只有兩個地方非常關鍵
-
protected List<? extends CameraBridgeViewBase> getCameraViewList() { …… }
子 Activity 在繼承 CameraActivity 後,需要複寫該函數,把 JavaCamera2View 或 JavaCameraView 送入 List 作爲返回值。 -
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);
}
}
實現要領
- 首先,要繼承 CameraActivity,之前已經說過了。這個基類會去申請權限,然後通知 javaCameraView 已獲取到權限,可以正常使用。
- 複寫父類的 getCameraViewList 方法,將 javaCameraView 回送回去,這樣當權限已被賦予時,就可以通知到預覽界面開始正常工作了。
- OpenCV 已經爲我們實現了 Camera 和 Camera2 的函數,如果應用最低版本 minSdkVersion > 5.0,建議使用 JavaCamera2View 的相關函數,否則使用 JavaCameraView。
- 在 onResume 時判斷 opencv 庫是否加載完畢,然後啓用預覽視圖。在 onPause 時由於界面被遮擋,此時應該暫停攝像頭的預覽以節省手機性能和電量損耗。
- 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);
}
}
}