OpenCV4 Android 调用摄像头

谁践踏了优雅 2023-02-18 08:10 135阅读 0赞

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 {

    1. private static final int CAMERA_PERMISSION_REQUEST_CODE = 200;
    2. protected List<? extends CameraBridgeViewBase> getCameraViewList() {
    3. return new ArrayList<CameraBridgeViewBase>();
    4. }
    5. protected void onCameraPermissionGranted() {
    6. List<? extends CameraBridgeViewBase> cameraViews = getCameraViewList();
    7. if (cameraViews == null) {
    8. return;
    9. }
    10. for (CameraBridgeViewBase cameraBridgeViewBase: cameraViews) {
    11. if (cameraBridgeViewBase != null) {
    12. cameraBridgeViewBase.setCameraPermissionGranted();
    13. }
    14. }
    15. }
    16. @Override
    17. protected void onStart() {
    18. super.onStart();
    19. boolean havePermission = true;
    20. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    21. if (checkSelfPermission(CAMERA) != PackageManager.PERMISSION_GRANTED) {
    22. requestPermissions(new String[]{CAMERA}, CAMERA_PERMISSION_REQUEST_CODE);
    23. havePermission = false;
    24. }
    25. }
    26. if (havePermission) {
    27. onCameraPermissionGranted();
    28. }
    29. }
    30. @Override
    31. @TargetApi(Build.VERSION_CODES.M)
    32. public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    33. if (requestCode == CAMERA_PERMISSION_REQUEST_CODE && grantResults.length > 0
    34. && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
    35. onCameraPermissionGranted();
    36. }
    37. super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    38. }

    }

实现要领

  1. 首先,要继承 CameraActivity,之前已经说过了。这个基类会去申请权限,然后通知 javaCameraView 已获取到权限,可以正常使用。
  2. 复写父类的 getCameraViewList 方法,将 javaCameraView 回送回去,这样当权限已被赋予时,就可以通知到预览界面开始正常工作了。
  3. OpenCV 已经为我们实现了 Camera 和 Camera2 的函数,如果应用最低版本 minSdkVersion > 5.0,建议使用 JavaCamera2View 的相关函数,否则使用 JavaCameraView。
  4. 在 onResume 时判断 opencv 库是否加载完毕,然后启用预览视图。在 onPause 时由于界面被遮挡,此时应该暂停摄像头的预览以节省手机性能和电量损耗。
  5. 切换前后摄像头时,要先禁用,设置完后启用才会生效。
  6. Camera2 和 Camera 的绝大部分差异 OpenCV 均已经为我们屏蔽在类的内部了,唯一的差别就是两者实现的 CvCameraViewListener 监听器里的预览函数 onCameraFrame 的参数略有不同。从下面的源码可以看出 CvCameraViewListener2 的 inputFrame 由 Mat 类型改为了 CvCameraViewFrame 类型,它额外提供了一个转化为灰度图的接口。

CvCameraViewListener

  1. public interface CvCameraViewListener {
  2. /**
  3. * This method is invoked when camera preview has started. After this method is invoked
  4. * the frames will start to be delivered to client via the onCameraFrame() callback.
  5. * @param width - the width of the frames that will be delivered
  6. * @param height - the height of the frames that will be delivered
  7. */
  8. public void onCameraViewStarted(int width, int height);
  9. /**
  10. * This method is invoked when camera preview has been stopped for some reason.
  11. * No frames will be delivered via onCameraFrame() callback after this method is called.
  12. */
  13. public void onCameraViewStopped();
  14. /**
  15. * This method is invoked when delivery of the frame needs to be done.
  16. * The returned values - is a modified frame which needs to be displayed on the screen.
  17. * TODO: pass the parameters specifying the format of the frame (BPP, YUV or RGB and etc)
  18. */
  19. public Mat onCameraFrame(Mat inputFrame);
  20. }

CvCameraViewListener2

  1. public interface CvCameraViewListener2 {
  2. /**
  3. * This method is invoked when camera preview has started. After this method is invoked
  4. * the frames will start to be delivered to client via the onCameraFrame() callback.
  5. * @param width - the width of the frames that will be delivered
  6. * @param height - the height of the frames that will be delivered
  7. */
  8. public void onCameraViewStarted(int width, int height);
  9. /**
  10. * This method is invoked when camera preview has been stopped for some reason.
  11. * No frames will be delivered via onCameraFrame() callback after this method is called.
  12. */
  13. public void onCameraViewStopped();
  14. /**
  15. * This method is invoked when delivery of the frame needs to be done.
  16. * The returned values - is a modified frame which needs to be displayed on the screen.
  17. * TODO: pass the parameters specifying the format of the frame (BPP, YUV or RGB and etc)
  18. */
  19. public Mat onCameraFrame(CvCameraViewFrame inputFrame);
  20. };
  21. /**
  22. * This class interface is abstract representation of single frame from camera for onCameraFrame callback
  23. * Attention: Do not use objects, that represents this interface out of onCameraFrame callback!
  24. */
  25. public interface CvCameraViewFrame {
  26. /**
  27. * This method returns RGBA Mat with frame
  28. */
  29. public Mat rgba();
  30. /**
  31. * This method returns single channel gray scale Mat with frame
  32. */
  33. public Mat gray();
  34. };

示例程序

下面使用 Camera2 来实现拍照功能( 注意:Camera2 只能用于 Android 5.0 以上的手机 )

Java代码

  1. public class OpencvCameraActivity extends CameraActivity {
  2. private static final String TAG = "OpencvCam";
  3. private JavaCamera2View javaCameraView;
  4. private Button switchCameraBtn;
  5. private int cameraId = JavaCamera2View.CAMERA_ID_ANY;
  6. private CameraBridgeViewBase.CvCameraViewListener2 cvCameraViewListener2 = new CameraBridgeViewBase.CvCameraViewListener2() {
  7. @Override
  8. public void onCameraViewStarted(int width, int height) {
  9. Log.i(TAG, "onCameraViewStarted width=" + width + ", height=" + height);
  10. }
  11. @Override
  12. public void onCameraViewStopped() {
  13. Log.i(TAG, "onCameraViewStopped");
  14. }
  15. @Override
  16. public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {
  17. return inputFrame.rgba();
  18. }
  19. }
  20. private BaseLoaderCallback baseLoaderCallback = new BaseLoaderCallback(this) {
  21. @Override
  22. public void onManagerConnected(int status) {
  23. Log.i(TAG, "onManagerConnected status=" + status + ", javaCameraView=" + javaCameraView);
  24. switch (status) {
  25. case LoaderCallbackInterface.SUCCESS: {
  26. if (javaCameraView != null) {
  27. javaCameraView.setCvCameraViewListener(cvCameraViewListener2);
  28. // 禁用帧率显示
  29. javaCameraView.disableFpsMeter();
  30. javaCameraView.enableView();
  31. }
  32. }
  33. break;
  34. default:
  35. super.onManagerConnected(status);
  36. break;
  37. }
  38. }
  39. };
  40. //复写父类的 getCameraViewList 方法,把 javaCameraView 送到父 Activity,一旦权限被授予之后,javaCameraView 的 setCameraPermissionGranted 就会自动被调用。
  41. @Override
  42. protected List<? extends CameraBridgeViewBase> getCameraViewList() {
  43. Log.i(TAG, "getCameraViewList");
  44. List<CameraBridgeViewBase> list = new ArrayList<>();
  45. list.add(javaCameraView);
  46. return list;
  47. }
  48. @Override
  49. protected void onCreate(Bundle savedInstanceState) {
  50. super.onCreate(savedInstanceState);
  51. setContentView(R.layout.activity_camera);
  52. findView();
  53. setListener();
  54. }
  55. private void findView() {
  56. javaCameraView = findViewById(R.id.javaCameraView);
  57. switchCameraBtn = findViewById(R.id.switchCameraBtn);
  58. }
  59. private void setListener() {
  60. switchCameraBtn.setOnClickListener(view -> {
  61. switch (cameraId) {
  62. case JavaCamera2View.CAMERA_ID_ANY:
  63. case JavaCamera2View.CAMERA_ID_BACK:
  64. cameraId = JavaCamera2View.CAMERA_ID_FRONT;
  65. break;
  66. case JavaCamera2View.CAMERA_ID_FRONT:
  67. cameraId = JavaCamera2View.CAMERA_ID_BACK;
  68. break;
  69. }
  70. Log.i(TAG, "cameraId : " + cameraId);
  71. //切换前后摄像头,要先禁用,设置完再启用才会生效
  72. javaCameraView.disableView();
  73. javaCameraView.setCameraIndex(cameraId);
  74. javaCameraView.enableView();
  75. });
  76. }
  77. @Override
  78. public void onPause() {
  79. Log.i(TAG, "onPause");
  80. super.onPause();
  81. if (javaCameraView != null) {
  82. javaCameraView.disableView();
  83. }
  84. }
  85. @Override
  86. public void onResume() {
  87. Log.i(TAG, "onResume");
  88. super.onResume();
  89. if (OpenCVLoader.initDebug()) {
  90. Log.i(TAG, "initDebug true");
  91. baseLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);
  92. } else {
  93. Log.i(TAG, "initDebug false");
  94. OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION, this, baseLoaderCallback);
  95. }
  96. }
  97. }

布局文件

布局文件很简单,核心就是这个 JavaCamera2View 视图

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. xmlns:app="http://schemas.android.com/apk/res-auto"
  4. android:layout_width="match_parent"
  5. android:layout_height="match_parent">
  6. <org.opencv.android.JavaCamera2View
  7. android:id="@+id/javaCameraView"
  8. android:layout_width="match_parent"
  9. android:layout_height="match_parent"
  10. app:show_fps="true"
  11. app:camera_id="any" />
  12. <Button
  13. android:id="@+id/switchCameraBtn"
  14. android:layout_width="match_parent"
  15. android:layout_height="wrap_content"
  16. android:text="切换摄像头"
  17. app:layout_constraintBottom_toBottomOf="parent" />
  18. </androidx.constraintlayout.widget.ConstraintLayout>

全屏预览

虽然使用了上述方法,但相机的预览视图还是只占了屏幕的一小丢丢,而且还是头朝左的。

此时需要修改 OpenCV 的源码里 CameraBridgeViewBase.java 中的 deliverAndDrawFrame 方法,对图像进行旋转缩放。

  1. /**
  2. * 获取屏幕旋转角度
  3. */
  4. private int rotationToDegree() {
  5. WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
  6. int rotation = windowManager.getDefaultDisplay().getRotation();
  7. int degrees = 0;
  8. switch(rotation) {
  9. case Surface.ROTATION_0:
  10. if(mCameraIndex == CAMERA_ID_FRONT) {
  11. degrees = -90;
  12. } else {
  13. degrees = 90;
  14. }
  15. break;
  16. case Surface.ROTATION_90:
  17. break;
  18. case Surface.ROTATION_180:
  19. break;
  20. case Surface.ROTATION_270:
  21. if(mCameraIndex == CAMERA_ID_ANY || mCameraIndex == CAMERA_ID_BACK) {
  22. degrees = 180;
  23. }
  24. break;
  25. }
  26. return degrees;
  27. }
  28. /**
  29. * 计算得到屏幕宽高比
  30. */
  31. private float calcScale(int widthSource, int heightSource, int widthTarget, int heightTarget) {
  32. if(widthTarget <= heightTarget) {
  33. return (float) heightTarget / (float) heightSource;
  34. } else {
  35. return (float) widthTarget / (float) widthSource;
  36. }
  37. }
  38. /**
  39. * This method shall be called by the subclasses when they have valid
  40. * object and want it to be delivered to external client (via callback) and
  41. * then displayed on the screen.
  42. * @param frame - the current frame to be delivered
  43. */
  44. protected void deliverAndDrawFrame(CvCameraViewFrame frame) {
  45. Mat modified;
  46. if (mListener != null) {
  47. modified = mListener.onCameraFrame(frame);
  48. } else {
  49. modified = frame.rgba();
  50. }
  51. boolean bmpValid = true;
  52. if (modified != null) {
  53. try {
  54. Utils.matToBitmap(modified, mCacheBitmap);
  55. } catch(Exception e) {
  56. Log.e(TAG, "Mat type: " + modified);
  57. Log.e(TAG, "Bitmap type: " + mCacheBitmap.getWidth() + "*" + mCacheBitmap.getHeight());
  58. Log.e(TAG, "Utils.matToBitmap() throws an exception: " + e.getMessage());
  59. bmpValid = false;
  60. }
  61. }
  62. if (bmpValid && mCacheBitmap != null) {
  63. Canvas canvas = getHolder().lockCanvas();
  64. if (canvas != null) {
  65. canvas.drawColor(0, android.graphics.PorterDuff.Mode.CLEAR);
  66. if (BuildConfig.DEBUG) Log.d(TAG, "mStretch value: " + mScale);
  67. //TODO 额外添加,让预览框达到全屏效果
  68. int degrees = rotationToDegree();
  69. Matrix matrix = new Matrix();
  70. matrix.postRotate(degrees);
  71. Bitmap outputBitmap = Bitmap.createBitmap(mCacheBitmap, 0, 0, mCacheBitmap.getWidth(), mCacheBitmap.getHeight(), matrix, true);
  72. if (outputBitmap.getWidth() <= canvas.getWidth()) {
  73. mScale = calcScale(outputBitmap.getWidth(), outputBitmap.getHeight(), canvas.getWidth(), canvas.getHeight());
  74. } else {
  75. mScale = calcScale(canvas.getWidth(), canvas.getHeight(), outputBitmap.getWidth(), outputBitmap.getHeight());
  76. }
  77. if (mScale != 0) {
  78. canvas.scale(mScale, mScale, 0, 0);
  79. }
  80. Log.d(TAG, "mStretch value: " + mScale);
  81. canvas.drawBitmap(outputBitmap, 0, 0, null);
  82. /*
  83. if (mScale != 0) {
  84. canvas.drawBitmap(mCacheBitmap, new Rect(0,0,mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),
  85. new Rect((int)((canvas.getWidth() - mScale*mCacheBitmap.getWidth()) / 2),
  86. (int)((canvas.getHeight() - mScale*mCacheBitmap.getHeight()) / 2),
  87. (int)((canvas.getWidth() - mScale*mCacheBitmap.getWidth()) / 2 + mScale*mCacheBitmap.getWidth()),
  88. (int)((canvas.getHeight() - mScale*mCacheBitmap.getHeight()) / 2 + mScale*mCacheBitmap.getHeight())), null);
  89. } else {
  90. canvas.drawBitmap(mCacheBitmap, new Rect(0,0,mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),
  91. new Rect((canvas.getWidth() - mCacheBitmap.getWidth()) / 2,
  92. (canvas.getHeight() - mCacheBitmap.getHeight()) / 2,
  93. (canvas.getWidth() - mCacheBitmap.getWidth()) / 2 + mCacheBitmap.getWidth(),
  94. (canvas.getHeight() - mCacheBitmap.getHeight()) / 2 + mCacheBitmap.getHeight()), null);
  95. }
  96. */
  97. if (mFpsMeter != null) {
  98. mFpsMeter.measure();
  99. mFpsMeter.draw(canvas, 20, 30);
  100. }
  101. getHolder().unlockCanvasAndPost(canvas);
  102. }
  103. }
  104. }

发表评论

表情:
评论列表 (有 0 条评论,135人围观)

还没有评论,来说两句吧...

相关阅读