【OpenCV for Android开发笔记】(二)相机的使用— —实时视频预览、拍照和相册读取

绪论

对于Android移动端的相机调用及数据读取,OpenCV是把原来C++的部分与本地AndroidSDK进行了整合,通过桥接的方式调用Android手机摄像头,最重要的一个类是JavaCameraView,它是OpenCV中调用Android手机摄像头的接口类,支持以代码和XMLView配置的方式使用,可以在Android设备中使用摄像头完成前置与后置摄像头的预览与拍照功能,下面就对这些内容逐一加以说明,并完成代码演示。



一、显示预览帧

1、修改AndroidManifest.xml文件:

  • 添加相机的相关权限:

因为在Android中使用SD卡、相机卡等本地硬件资源的时候会涉及授权问题,而且Android的低版本与高版本的授权方式有点不一样。这里首先需要说明一下Android在不同版本上的相机授权差别,Android在低版本中是通过向AndroidManifests.xml中添加文本的方式来完成授权的,添加内容如下:

<!--Allow to use a camera-->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
<uses-feature android:name="android.hardware.camera.front" android:required="false"/>
<uses-feature android:name="android.hardware.camera.front.autofocus" android:required="false"/>

这种方式在Android 6.0以下的版本上使用没有问题,但是在Android 6.0以上的版本中(包括6.0)就会出现问题,原因是Android系统的授权方式改变升级了,所以需要在onCreat中添加如下的代码进行授权:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            requestPermissions( new String[]{Manifest.permission.CAMERA}, 203);
        }
  • 设置应用的界面主题为没有顶部标题栏且全屏显示的,在application标签中添加:
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" <!--显示当前的帧率-->
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">  <!--Full screen mode-->
  • 设置横竖屏显示,在application-activity标签中添加:
 android:screenOrientation="landscape"     //屏幕的控制

2、为界面布局文件添加显示相机内容的组件:

打开res/layout下面的activity_main.xml布局文件,往布局中添加一个OpenCV的视觉组件JavaCameraView:

<RelativeLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:opencv="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 
    <org.opencv.android.JavaCameraView 
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:id="@+id/camera_view"
        opencv:show_fps="true"  
        opencv:camera_id="any"/>
 
</RelativeLayout>

3、在MainActivity中调用API

打开src目录下面的MainActivity,由于我们的目标是在应用中通过OpenCV的Java API实现打开相机全屏显示,并获取预览框,所以MainActivity需要实现CvCameraViewListener2接口,可以实现三个方法,分别是:onCameraViewStarted、onCameraViewStopped和onCameraFrame,关键的图像处理写在onCameraFrame函数中:

在这里插入图片描述
调用API
声明一个JavaCameraView对象,用于存放activity_main.xml中的JavaCameraView组件,并在OnCreate中实现绑定和添加事件监听:

mCVCamera = (JavaCameraView) findViewById(R.id.camera_view);
mCVCamera.setCvCameraViewListener(this);

修改public Mat onCameraFrame(CvCameraViewFrame inputFrame)回调函数的内容了,这个函数在相机刷新每一帧都会调用一次,而且每次的输入参数就是当前相机视图信息,我们直接获取其中的RGBA信息作为Mat数据返回给显示组件即可:

/**
	 * 图像处理都写在此处
	 */
	@Override
	public Mat onCameraFrame(CvCameraViewFrame inputFrame) {
		//直接返回输入视频预览图的RGBA数据并存在Mat数据中
		return inputFrame.rgba();
	}
       

以上操作中,我们在OnCreate函数中已经获取到mCVCamera对象,只有调用mCVCamera.enableView()之后,预览组件才会显示每一帧的Mat图像,但是在显示之前我们必须先确保OpenCV的库文件已经加载完成,所以调用此方法需要进行异步处理:

/**
 * 通过OpenCV管理Android服务,异步初始化OpenCV
 */
BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
	@Override
	public void onManagerConnected(int status){
		switch (status) {
		case LoaderCallbackInterface.SUCCESS:
			Log.i(TAG,"OpenCV loaded successfully");
			mCVCamera.enableView();
			break;
		default:
			break;
		}
	}
};

所以只有当mLoaderCallback收到LoaderCallbackInterface.SUCCESS消息的时候,才会打开预览显示,那么这个消息是从哪里发出来的呢,这就需要我们重写Activity的onRusume方法了,因为每次当前Activity激活都会调用此方法,所以可以在此处检测OpenCV的库文件是否加载完毕:

@Override
	public void onResume() {
		super.onResume();
		if (!OpenCVLoader.initDebug()) {
			Log.d(TAG,"OpenCV library not found!");
		} else {
			Log.d(TAG, "OpenCV library found inside package. Using it!");
			mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);
		}
	};

完整代码

  • MainActivity
package com.example.asus.opencvcamerademo;

import android.Manifest;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import org.opencv.android.BaseLoaderCallback;
import org.opencv.android.CameraBridgeViewBase;
import org.opencv.android.CameraBridgeViewBase.CvCameraViewFrame;
import org.opencv.android.CameraBridgeViewBase.CvCameraViewListener2;
import org.opencv.android.LoaderCallbackInterface;
import org.opencv.android.OpenCVLoader;
import org.opencv.android.JavaCameraView;

import org.opencv.core.Mat;


public class MainActivity extends AppCompatActivity implements CvCameraViewListener2 {

    private String TAG = "OpenCV_Test";
    private JavaCameraView mOpenCvCameraView;
    private int M_REQUEST_CODE = 203;
    private String[] permissions = {Manifest.permission.CAMERA};

    // Initialize OpenCV manager.
    private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
        @Override
        public void onManagerConnected(int status) {
            switch (status) {
                case LoaderCallbackInterface.SUCCESS: {

                    mOpenCvCameraView.enableView();
                    break;
                }
                default:
                    break;

            }
        }
    };
    @Override
    public void onResume() {
        super.onResume();
        if (!OpenCVLoader.initDebug()) {
            Log.d(TAG,"OpenCV library not found!");
        } else {
            Log.d(TAG, "OpenCV library found inside package. Using it!");
            mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // Set up camera listener.
        mOpenCvCameraView = (JavaCameraView)findViewById(R.id.CameraView);
        mOpenCvCameraView.setVisibility(CameraBridgeViewBase.VISIBLE);
        mOpenCvCameraView.setCvCameraViewListener(this);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            requestPermissions(permissions, M_REQUEST_CODE);
        }
    }
    // Load a network.
    public void onCameraViewStarted(int width, int height) {

    }
    public Mat onCameraFrame(CvCameraViewFrame inputFrame) {

        return inputFrame.rgba();
    }
    public void onCameraViewStopped() {}
  • AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.asus.opencvcamerademo">
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@android:style/Theme.NoTitleBar.Fullscreen">  <!--Full screen mode-->
        <activity android:name=".MainActivity"
            android:screenOrientation="landscape">  <!--Screen orientation-->
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <!--Allow to use a camera-->
    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-feature android:name="android.hardware.camera" android:required="false"/>
    <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
    <uses-feature android:name="android.hardware.camera.front" android:required="false"/>
    <uses-feature android:name="android.hardware.camera.front.autofocus" android:required="false"/>
</manifest>

二、横屏与竖屏切换

1、横竖屏切换

① 方法一:在AndroidManifest.xml中配置

假设不想让软件在横竖屏之间切换,最简单的办法就是在项目的AndroidManifest.xml中找到你所指定的activity中加上android:screenOrientation属性。

android:screenOrientation="landscape"是限制此页面横屏显示,  
android:screenOrientation="portrait"是限制此页面数竖屏显示。 
android:screenOrientation="landscape"是限制此页面横屏显示,
android:screenOrientation="portrait"是限制此页面数竖屏显示。

android:screenOrientation设定该活动的方向,该值可以是任何一个下面的字符串:

  • “unspecified”:默认值 由系统来推断显示方向.判定的策略是和设备相关的,所以不同的设备会有不同的显示方向.
  • “landscape”:横屏显示(宽比高要长)
  • “portrait”:竖屏显示(高比宽要长)
  • “user”:用户当前首选的方向
  • “behind”:和该Activity以下的那个Activity的方向一致(在Activity堆栈中的)
  • “sensor”:有物理的感应器来决定。假设用户旋转设备这屏幕会横竖屏切换。
  • “nosensor”:忽略物理感应器。这样就不会随着用户旋转设备而更改了("unspecified"设置除外)。

② 方法二:在java代码中设置

设置横屏代码:setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);//横屏
设置竖屏代码:setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);//竖屏

3、竖屏预览图像自动旋转问题

Android的Camera相关应用开发中,有一个必须搞清楚的知识点,就是Camera的预览方向和拍照方向:

  • 图像的Sensor方向:手机Camera的图像数据都是来自于摄像头硬件的图像传感器(Image Sensor),这个Sensor被固定到手机之后是有一个默认的取景方向的,这个方向如下图所示,坐标原点位于手机横放时的左上角:
    在这里插入图片描述
  • Camera的预览方向:由于手机屏幕可以360度旋转,为了保证用户无论怎么旋转手机都能看到“正确”的预览画面(这个“正确”是指显示在UI预览界面的画面与你人眼看到的眼前的画面是一致的),Android系统底层根据当前手机屏幕的方向对图像Sensor采集到的数据进行了旋转处理,然后后才送给显示系统,因此,打开Camera应用后,无论怎么旋转手机,你都能看到“正确”的画面,Android系统提供一个API来手动设置Camera的预览方向,叫做setDisplayOrientation,默认情况下,这个值是0,与图像Sensor方向一致,所以对于横屏应用来说,就不需要更改这个Camera预览方向。但是,如果你的应用是竖屏应用,就必须通过这个API将Camera的预览方向旋转90,与手机屏幕方向一致,这样才会得到正确的预览画面。

问题

在这里插入图片描述
Camera的拍照方向:当你点击拍照按钮,得到的图片方向不一定与画面中预览的方向一致,这是因为拍摄的照片是将图像Sensor采集到的数据直接存储到SDCard上的,因此,Camera的拍照方向与上述的Camera的图像Sensor方向一致。
由此可见,如果横向拿手机拍照,由于正好与Camera的拍照方向一致,因此得到的照片是“正确”的;而竖着拿手机拍照的话,Camera的图像Sensor依然以上面描述的角度在采集图像并存储到SDCard上,所以得到的图片就是右图这样的,因为竖着拿手机正好与图像Sensor的方向相差了90度。由此,大家应该明白了为什么我们用手机拍出的照片经常需要旋转90度才能看到“正确”的画面了吧?

解决方法

① 首先在onCameraFrame方法中将每帧图像顺时针旋转90°以后再显示,代码如下:

@Override
public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {

    if (isFrontCamera) {
            Core.rotate(frame, frame, Core.ROTATE_90_COUNTERCLOCKWISE);
            Core.flip(frame, frame, 1);
       
        } else {
            Core.rotate(frame, frame, Core.ROTATE_90_CLOCKWISE);
        }
    return frame;

}

这里我们根据是否是前置摄像头,对帧所对应的矩阵进行旋转,其中后置摄像头需要将矩阵顺时针旋转90°得到竖向的图像,而前置摄像头则需要逆时针旋转90°,同时进行一次镜像翻转才能得到想要的图像。
PS:由于矩阵的乘法不满足交换律,所以此处前置摄像头的旋转与翻转顺序不可更改!!

public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {

        Mat frame= inputFrame.rgba();

        Core.rotate(frame,frame,Core.ROTATE_90_CLOCKWISE);

        return frame;

    }

② 当运行上述代码的时候就会发现PORTRAIT显示模式运行的时候,JavaCameraView没有实现相机预览显示功能,原因是其继承的基类CameraBridgeViewBase的deliverAndDrawFrame方法中有一个缓存图像对象,它的大小与onCameraFrame方法中旋转之后的frame大小不一致,导致无法填充缓冲区,实现绘制,这个时候可以在deliverAndDrawFrame方法中进行手动修改。

在这里插入图片描述

请在455行的AllocateCache方法中将 mFrameHeight, mFrameWidth对调成如下:

protected void AllocateCache()
{   
    mCacheBitmap = Bitmap.createBitmap( mFrameHeight, mFrameWidth,Bitmap.Config.ARGB_8888);
}

【注:如果是横屏显示,则不能对调。】

三、前后置摄像头切换

对于大多数的Android移动设备来说都有前置与后置摄像头,JavaCameraView通过setCameraIndex方法根据所输入摄像头的索引值来决定是开启前置还是后置摄像头,具体如下。

  • 0:开启后置摄像头。
  • 1:开启前置摄像头。

在View定义的XML文件中添加如下两个RadioButton对象作为开启前置或者后置摄像头的选项,相关的XML定义如下:

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:opencv="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <org.opencv.android.JavaCameraView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/CameraView"
        opencv:show_fps="true"
        opencv:camera_id="any"/>

    <RadioGroup
        android:id="@+id/radioGroup"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <RadioButton
            android:id="@+id/backCameraBtn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="后置摄像头"
            android:paddingTop="10dp"
            android:checked="true"/>

        <RadioButton
            android:id="@+id/frontCameraBtn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="前置摄像头"
            android:paddingTop="10dp"/>
    </RadioGroup>

</FrameLayout>

当横屏显示的时候,若使用的是前置摄像头,则需要在返回之前完成图像的镜像变换,否则图像会呈镜像显示。最后在选择前后摄像头、横屏与竖屏相互切换的时候,我们应该在destory方法中禁用JavaCameraView对象,然后在resume方法中再次启用它,不然就会导致切换之后显示不正确。

 @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // Set up camera listener.
        mOpenCvCameraView = (JavaCameraView)findViewById(R.id.CameraView);
        mOpenCvCameraView.setVisibility(CameraBridgeViewBase.VISIBLE);
        mOpenCvCameraView.setCvCameraViewListener(this);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            requestPermissions( new String[]{Manifest.permission.CAMERA}, 203);
        }

        RadioGroup radioGroup=findViewById(R.id.radioGroup);
        RadioButton frontOption = (RadioButton) findViewById(R.id.frontCameraBtn);
        RadioButton backOption = (RadioButton) findViewById(R.id.backCameraBtn);
        radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(RadioGroup group, int checkedId) {
                switch (checkedId){
                    case R.id.frontCameraBtn:
                        cameraIndex=1;

                        break;
                    case R.id.backCameraBtn:
                        cameraIndex=0;

                        break;
                }
                mOpenCvCameraView.setCameraIndex(cameraIndex);
                mOpenCvCameraView.disableView();
                mOpenCvCameraView.enableView();
            }
        });
    }
  • 若是竖屏显示,则
 android:screenOrientation="portrait">  <!--Screen orientation-->

在onCameraFrame方法中加入

@Override
public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {

    Mat frame= inputFrame.rgba();
    if(cameraIndex==1) {
        Core.rotate(frame, frame, Core.ROTATE_90_CLOCKWISE);
        //倒过来
        Core.flip(frame,frame,0);
        return frame;
    }else {
        Core.rotate(frame, frame, Core.ROTATE_90_CLOCKWISE);
        return frame;
    }

}
  • 若是横屏显示,则
 android:screenOrientation="landscape">  <!--Screen orientation-->

记得修改第455行中的AllocateCache()方法
在这里插入图片描述
在onCameraFrame方法中加入

@Override
public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {

    Mat frame= inputFrame.rgba();
    if(cameraIndex==1) {
        Core.flip(frame,frame,1);
        //镜像翻转
        return frame;
    }else {
        return frame;
    }


}

四、拍照和读取相册

参考

发布了28 篇原创文章 · 获赞 2 · 访问量 2803

猜你喜欢

转载自blog.csdn.net/Jarvis_lele/article/details/105353923