Android 实现相机(Camera)预览

Posted AaVictory.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 实现相机(Camera)预览相关的知识,希望对你有一定的参考价值。

CameraX 是一个 Jetpack 库,旨在帮助您更轻松地开发相机应用。 对于新应用,我们建议从 CameraX 开始。它提供一致且易于使用的 API,适用于绝大多数 android 设备,并向后兼容 Android 5.0(API 级别 21)。

CameraX 支持大多数常见的相机用例:
  • 预览:在屏幕上查看图片。
  • 图片分析:无缝访问缓冲区中的图片以便在算法中使用,例如将其传递到机器学习套件。
  • 图片拍摄:保存图片。
  • 视频拍摄:保存视频和音频。

这篇文章讲的是 – 实现预览

在向应用添加预览时,请使用 PreviewView,这是一种可以剪裁、缩放和旋转以确保正确显示的 View。
当相机处于活动状态时,图片预览会流式传输到 PreviewView 中的 Surface。

效果图

配置:

  • 注意:CameraX 支持的最小SDK版本是21
  • 一、添加权限

1、清单文件中加入相机权限

<uses-permission android:name="android.permission.CAMERA" />

2、动态获取相机权限

  if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_DENIED) 
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) 
                requestPermissions(new String[]Manifest.permission.CAMERA, 11);
            
         else 
            //启动相机 
        
  • 二、在build.gradle中 添加CameraX依赖
    implementation "androidx.camera:camera-core:1.1.0-alpha10"
// CameraX Camera2 extensions[可选]拓展库可实现人像、HDR、夜间和美颜、滤镜但依赖于OEM
    implementation "androidx.camera:camera-camera2:1.1.0-alpha10"
// CameraX Lifecycle library[可选]避免手动在生命周期释放和销毁数据
    implementation "androidx.camera:camera-lifecycle:1.1.0-alpha10"
// CameraX View class[可选]最佳实践,最好用里面的PreviewView,它会自行判断用SurfaceView还是TextureView来实现
    implementation 'androidx.camera:camera-view:1.0.0-alpha23'

使用 PreviewView案例步骤:

1、(可选)配置 CameraXConfig.Provider。
2、 将 PreviewView 添加到布局。
3、 请求 ProcessCameraProvider。
4、 在创建 View 时,请检查 ProcessCameraProvider。
5、 选择相机并绑定生命周期和用例。

  • 将 PreviewView 添加到布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <FrameLayout
        android:layout_centerInParent="true"
        android:layout_width="500dp"
        android:layout_height="500dp">
        <androidx.camera.view.PreviewView
            android:id="@+id/previewView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            />
    </FrameLayout>
</RelativeLayout>
  • 步骤3、4、5、的处理
package com.example.myapplication;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;

import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.concurrent.ExecutionException;


public class MainActivity extends AppCompatActivity 
    
    private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
    private PreviewView previewView;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        previewView=findViewById(R.id.previewView);//初始化
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_DENIED) 
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) 
                requestPermissions(new String[]Manifest.permission.CAMERA, 11);
            
         else 
            //启动相机
            startCamera();
        
    

    private void startCamera() 
        // 请求 CameraProvider
        cameraProviderFuture = ProcessCameraProvider.getInstance(this);
        //检查 CameraProvider 可用性,验证它能否在视图创建后成功初始化
        cameraProviderFuture.addListener(() -> 
            try 
                ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
                bindPreview(cameraProvider);
             catch (ExecutionException | InterruptedException e) 
                // No errors need to be handled for this Future.
                // This should never be reached.
            
        , ContextCompat.getMainExecutor(this));
    
    //选择相机并绑定生命周期和用例
    private void bindPreview(@NonNull ProcessCameraProvider cameraProvider) 
        Preview preview = new Preview.Builder()
                .build();

        CameraSelector cameraSelector = new CameraSelector.Builder()
                .requireLensFacing(CameraSelector.LENS_FACING_BACK)
                .build();

        preview.setSurfaceProvider(previewView.getSurfaceProvider());

        Camera camera = cameraProvider.bindToLifecycle((LifecycleOwner)this, cameraSelector, preview);
    

  • 到bindPreview这一步,相机预览已经显示,请查验

利用Android Camera2 的照相机api 实现 实时的图像采集与预览

   最近想要做一个客户端往服务器推送实时画面的功能,首先可以考虑到两种思路,一种是在客户端进行视频流的推送,主要利用RTSP等流媒体协议进行传输,而另外一种是通过摄像头获取当前画面,将每一帧作为对象单独传输。

   项目想要实现的功能最终目的是对实时画面的每一帧进行处理,可以考虑客户端推流到服务器,再在服务器进行帧解析的操作,但由于目前很多的流媒体推送框架在推流端或者服务端都或多或少存在限制,很少有完全开源的项目,再加上传送画面的同时需要附带部分的数据,仍然需要另外建立连接进行传输,所以暂时搁置这一方案。选择第二种思路,获取每一帧的画面,单独传输。

   要想获取实时画面,我们必须通过对安卓设备上的摄像头进行调用。

   从API21开始,安卓引入了android.hardware.camera2这个包,来替代原有的camera类,原有的camera类已经不再建议使用了。camera2中最重要的变化是,摄像头的调用不再是简单地进行实例化,而是用一种类似服务申请的方式来进行调用。通过CameraManager来管理摄像服务,需要通过建立CameraCaptureSession来建立一个调用摄像设备CameraDevices的会话,来实现对摄像头的调用。而CaptureRequest.Builder类用于建立实际的调用请求,具体的参数设置也可以通过这个类来实现(而不是对camera设备进行直接设置),这样做的目的是把对摄像头的控制与摄像头本身分离开来,用户可以通过不同的session根据不同的配置来使用摄像头。

   我们可以结合具体的代码来分析新api中摄像头调用的过程。

   首先我们想要对摄像设备进行操作,需要获得CameraManager的实例

    CameraManager cameraManager;
    cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);

   我们可以调用openCamera函数打开摄像头设备

    cameraManager.openCamera(cameraId, cameraCallback, mainHandler);

   这里需要传入三个参数,cameraId是设备编号,cameraCallback控制摄像服务的回调,最后一个参数指定HandlerThread对象 

     cameraId = Integer.toString(CameraCharacteristics.LENS_FACING_FRONT);
     
     private CameraDevice.StateCallback cameraCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(CameraDevice camera) {
            Log.d("CameraCallback", "Camera Opened");
            cameraDevice = camera;
            takePreview();
        }

        @Override
        public void onDisconnected(CameraDevice cameraDevice) {
            Log.d("CameraCallback", "Camera Disconnected");
            closeCameraDevice();
        }

        @Override
        public void onError(CameraDevice cameraDevice, int i) {
            Log.d("CameraCallback", "Camera Error");
            Toast.makeText(PusherSurface.this, "摄像头开启失败", Toast.LENGTH_SHORT).show();
        }
    };

 回调函数用于指定连接摄像头设备时不同状态的操作。在这里,我们在摄像头成功连接的时候调用  takePreview()函数开启摄像头画面的预览。

private void takePreview() {
    try {
        final CaptureRequest.Builder previewRequestBuilder
                = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        previewRequestBuilder.addTarget(surfaceHolder.getSurface());
        previewRequestBuilder.addTarget(previewReader.getSurface());
        cameraDevice.createCaptureSession(Arrays.asList(surfaceHolder.getSurface(),
                previewReader.getSurface(),
                imageReader.getSurface()), new CameraCaptureSession.StateCallback() {
            @Override
            public void onConfigured(CameraCaptureSession cameraCaptureSession) {
                if (cameraDevice == null) return;
                mCameraCaptureSession = cameraCaptureSession;

                try {
                    previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                            CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
                    previewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                            CaptureRequest.CONTROL_AE_MODE_OFF);

                    CaptureRequest previewRequest = previewRequestBuilder.build();
                    mCameraCaptureSession.setRepeatingRequest(previewRequest, null, childHandler);
                } catch (CameraAccessException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {
                Toast.makeText(PusherSurface.this, "配置失败", Toast.LENGTH_SHORT).show();
            }
        }, childHandler);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

要从摄像设备中获取图像,我们首先需要建立一个camera capture session。函数

createCaptureSession(List, CameraCaptureSession.StateCallback, Handler)的第一个参数传入了我们想要绘制的视图列表,第二个参数传入的是建立摄像会话的状态回调函数,第三个参数传入相应的handler处理器。然后,我们需要利用capturerequest来定义摄像头捕获图像时候的具体参数,比如是否开启摄像头,是否自动对焦等。最后通过CamraCaptureSession.setRepeatingRequest来开启请求。这样我们就可以从capturesession传入的list中的surface列表获得连续的图像。留意到

previewRequestBuilder.addTarget(surfaceHolder.getSurface());
previewRequestBuilder.addTarget(previewReader.getSurface());

这里除了传入xml界面布局中的surfaceHolder的surface外,还传入了一个previewReader的surface。

previewReader是一个自定义的ImageReader对象。

previewReader = ImageReader.newInstance(1080, 1920, ImageFormat.YUV_420_888, 2);
        previewReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
            @Override
            public void onImageAvailable(ImageReader imageReader) {
                Image image = null;
                try {
                    image = imageReader.acquireLatestImage();
                    Log.d("PreviewListener", "GetPreviewImage");
                    if (image == null) {
                        return;
                    }
                    byte[] bytes = ImageUtil.imageToByteArray(image);
                    if (pushFlag == false)
                        uploadImg(bytes);
                } finally {
                    if (image != null) {
                        image.close();
                    }
                }

            }
        }, mainHandler);

ImageReader是一个可以让我们对绘制到surface的图像进行直接操作的类。在这里我们从摄像设备中传入了连续的预览图片,也就是我们在屏幕上看到的画面,它们的格式都是未经压缩的YUV_420_888类型的(同样的如果要操作拍摄后的图片,就要设置成jpeg格式)。我们调用imageReader.acquireLatestImage或者acquireNextImage来获取图像队列中的图片。并进行操作。在这里我利用一个函数将图像压缩后转化成byte[]格式,并调用uploadImg函数上传至服务器。这样,整个摄像头的调用到预览图像的处理也就完成了。想要实现拍照功能也是大同小异,在这里我就不一一贴出了。

  欢迎更多安卓开发者一同交流。

本文出自 “DavidWillo的博客” 博客,请务必保留此出处http://davidwillo.blog.51cto.com/12613091/1908166

以上是关于Android 实现相机(Camera)预览的主要内容,如果未能解决你的问题,请参考以下文章

轻松实现相机预览 | Camera Viewfinder 全新上线

利用Android Camera2 的照相机api 实现 实时的图像采集与预览

Android:更改屏幕上的相机预览大小

Android Camera API/Camera2 API 相机预览及滤镜贴纸等处理

Android Camera API/Camera2 API 相机预览及滤镜贴纸等处理

Android 自定义相机 Camera 预览变形拉伸问题