Camera2的使用【详细】

目录

1.获取权限

[2. 获取指定相机ID](#2. 获取指定相机ID)

(1)获取相机管理者CameraManager

(2)获取相机ID列表

(3)获取相机特征CameraCharacteristics

(4)获取相机朝向

3.获取相机输出尺寸

(1)根据相机ID获取相机特征

(2)获取输出流配置

(3)获取输出尺寸数组(参数为输出目标类)

(4)选择输出尺寸

[4. SurfaceView及其他控件的准备](#4. SurfaceView及其他控件的准备)

(1)获取视图

(2)根据相机输出尺寸宽高比设置SurfaceView宽高比尺寸

(3)添加回调

[5. 创建ImageReader用于处理捕获的图片](#5. 创建ImageReader用于处理捕获的图片)

(1)创建ImageReader

(2)设置图片可用监听器OnImageAvailableListener

[6. 创建相机设备状态回调、通道并发送循环预览请求](#6. 创建相机设备状态回调、通道并发送循环预览请求)

(1)创建相机设备状态回调CameraDevice.StateCallback

(2)创建捕获请求建造者CaptureRequest.Builder(参数:模板_预览)

(3)创建捕获请求目标集合

(4)创建捕获通道CaptureSession

(5)在捕获通道完成后要设置循环请求(预览捕获请求)

[7. 启动相机](#7. 启动相机)

[8. 拍摄照片](#8. 拍摄照片)

(1)创建捕获请求建造者CaptureRequest.Builder(参数:模板_静态捕获)

(2)捕获通道执行捕获拍照捕获请求

9.程序的停止和重启

(1)停止程序需关闭相机节省资源

(2)重启程序需再次启动相机

(3)重启程序重获取ImageReader的Surface

[10. 常见报错](#10. 常见报错)

[(1) CaptureRequest contains unconfigured Input/Output Surface!](#(1) CaptureRequest contains unconfigured Input/Output Surface!)

[(2)Surface was abandoned](#(2)Surface was abandoned)

[(3) MTK零延迟的Camera机制](#(3) MTK零延迟的Camera机制)


建议相机全部操作放在非主线程中,可以避免等待SurfaceView创建阻塞主线程ImageReader的Surface在主线程创建会在onStop()方法中被摧毁等多种情况。

1.获取权限

camera权限是必须申请的,如果需要保存图片还需要读写权限,并动态申请。

java 复制代码
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
java 复制代码
requestPermissions(new String[]{"android.permission.CAMERA","android.permission.READ_EXTERNAL_STORAGE","android.permission.WRITE_EXTERNAL_STORAGE"}, 2333);
java 复制代码
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == 2333) {
            for (int i = 0; i < permissions.length; i++) {
                if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
                    requestPermissions(permissions, requestCode);
                    break;
                }
            }
        }
    }

2. 获取指定相机ID

流程:

(1)获取相机管理者CameraManager

相机管理者中存储了相机ID列表、相机设备。

(2)获取相机ID列表

String cameraIdList[] = cameraManager.getCameraIdList();

(3)获取相机特征CameraCharacteristics

CameraCharacteristics中含有相机设备的信息。

(4)获取相机朝向

cameraCharacteristics.get(CameraCharacteristics.LENS_FACING);

java 复制代码
//相机ID
String cameraId=null;
//(1)获取相机管理者
CameraManager cameraManager = (CameraManager) getSystemService(CAMERA_SERVICE);
//(2)获取相机ID列表
String cameraIdList[] = cameraManager.getCameraIdList();
for (int i = 0; i < cameraIdList.length; i++) {
    //(3)获取相机特征
    CameraCharacteristics cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraIdList[i]);
    //(4)获取相机朝向---后置
    if (cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK) {
        //使用该相机ID
        cameraId = cameraIdList[i];
        break;
    }
 }

3.获取相机输出尺寸

流程:

(1)根据相机ID获取相机特征

(2)获取输出流配置

(3)获取输出尺寸数组(参数为输出目标类)

请注意部分相机不支持使用SurfaceView.class作为参数,可能会出现返回null,要对此做好处理。

(4)选择输出尺寸

java 复制代码
//获取相机支持尺寸
int output_width,output_height;
//(1)获取当前相机的特性
CameraCharacteristics cameraCharacteristics=cameraManager.getCameraCharacteristics(imageId);
//(2)获取输出流配置
StreamConfigurationMap map=cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
//(3)获取输出尺寸数组(参数为输出目标类)
Size[] outputSizes = map.getOutputSizes(ImageReader.class);
if(outputSizes.length!=null){
    //(4)选择输出尺寸
    for(int i=0;i<outputSizes.length;i++){
        //获取需要的输出尺寸
        //因为相机常为90°,所以将横竖调转
        output_height=outputSizes[i].getWidth();
        output_width=outputSizes[i].getHeight();
        //判断是否使用本尺寸代码
        //... ...
        break;
    }
}
else{
    //输出尺寸数组为null时设置输出宽高尺寸
    output_height=640*30;
    output_width=480*30;
}
java 复制代码
//例
//判断是否使用本尺寸代码
//即选择输出尺寸中最接近目标比例的尺寸
int output_width, output_height;//输出宽高比
double cha = 1000;// 实际宽高比与目标宽高比差值
double bili = 3 / 4;//目标宽/高
for (int i = 0; i < outputSize.length; i++) {
    int tempHeight = outputSize[i].getHeight();
    int tempWidth = outputSize[i].getWidth();
    double tempCha = tempWidth / tempHeight - bili;
    if (tempCha < 0) {
        tempCha = 0 - tempCha;
    }
    if (tempCha < cha) {
        cha = tempCha;
        output_width = tempWidth;
        output_height = tempHeight;
    }
}

4. SurfaceView及其他控件的准备

和Camera一样,Camera2也可以使用SurfaceView作为预览视图,但Camera2和Camera不同,他并不强制你拍摄前必须预览。

SurfaceView使用时请注意,一定要在SurfaceView创建完成后再进行预览的绑定,这一点和Camera、MediaPlayer等的操作是一样的。

但Camera绑定SurfaceView设置预览时使用的是SurfaceHolder,而Camera2使用的时Surface

SurfaceView用于显示捕获的图片,SurfaceView的Surface作为预览捕获请求的目标

流程:

(1)获取视图

(2)根据相机输出尺寸宽高比设置SurfaceView宽高比尺寸

(3)添加回调

surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {

请注意SurfaceView在onCreate()方法结束后创建完成并调用surfaceCreate()方法等待surfaceView创建完成的部分需要在非主线程**。**

java 复制代码
//(1)获取控件
SurfaceView surfaceView = findViewById(R.id.~);

//(2)根据相机捕获尺寸设置SurfaceView尺寸
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) surfaceView.getLayoutParams();
layoutParams.width = ~ ;
layoutParams.height = ~ ;
surfaceView.setLayoutParams(layoutParams);

//Camera2绑定SurfaceView预览时使用的是Surface
Surface surface_surfaceView=null;

//(3)SurfaceView回调
surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
    public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
        //构建成功
        //准备预览捕获请求的目标
        surface_surfaceView = surfaceHolder.getSurface();
    }
    public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder,int i,int i1,int i2) {
        //SurfaceView改变
    }
    public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {
        //构建摧毁
        surface_surfaceView = null;
    }
});


//拍照按钮
Button button_takePicture=null;

5. 创建ImageReader用于处理捕获的图片

ImageReader用于处理捕获的图片,ImageReader的Surface作为拍照捕获请求的目标。

流程:

(1)创建ImageReader

创建ImageView的前两个参数为捕获图像的宽高,请设置。第三个参数是表示在ImageReader 队列中同时最多可以有多少张图像。如果这个参数为 1,表示 ImageReader 只能容纳一张图像。当你获取一张新的图像时,如果队列已满,则会丢弃旧的图像。这有助于避免内存溢出或过多图像堆积的问题。

如果ImageReader是在主线程创建的,那么在关闭程序时,ImageReader的Surface会自动被摧毁,请在onRestart()重新获取!!!

(2)设置图片可用监听器OnImageAvailableListener

用ImageReader捕获图片后将调用其中的onImageAvailable()方法。

请注意onImageAvailable()方法的第二个参数为其使用的线程,不能为null

处理图像代码详解:

  • acquireNextImage() 方法用于从 ImageReader 中获取下一个可用的图像。acquireNextImage() 方法获取最新可用的图像,该图像会被移除并返回。如果没有可用的图像,此方法将会阻塞,直到有图像可用或者超时。返回的是一个 Image 对象,代表捕获到的图像数据。
  • Image 对象通过 getPlanes() 方法返回一个 Image.Plane[] 数组,每个平面包含了图像的一个通道(例如:红色、绿色、蓝色、透明度等)。通常,对于 JPEG 格式的图像,这里的数组中只有一个平面,即索引为 0 的平面。
  • ByteBuffer 是用来存储图像数据的容器。通过 getBuffer() 方法获取的 ByteBuffer 包含了图像数据。remaining() 方法返回当前位置与限制之间的元素数量,这里指的是剩余的可读取的字节数。
  • get(bytes) 方法从 ByteBuffer 中读取数据并将其存储到 bytes 数组中。
java 复制代码
//(1)创建ImageReader,请注意宽高
ImageReader imageReader = ImageReader.newInstance(output_width, output_height, ImageFormat.JPEG, 1);
//准备拍照捕获请求的目标
Surface surface_imageReader=imageReader.getSurface();
//(2)设置图片可用监听器
imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
    public void onImageAvailable(ImageReader reader) {
        
        // 处理图像数据
        Image image = reader.acquireNextImage();
        ByteBuffer byteBuffer=image.getPlanes()[0].getBuffer();
        byte[] bytes=new byte[byteBuffer.remaining()];
        byteBuffer.get(bytes);

        //存储
        File file = new File(Environment.getExternalStorageDirectory() + "/picture.jpg");
        try (OutputStream output = new FileOutputStream(file)) {
            output.write(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 处理完后记得释放资源
        image.close();
    }
}, handler_ui);

6. 创建相机设备状态回调、通道并发送循环预览请求

流程:

(1)创建相机设备状态回调CameraDevice.StateCallback

CameraDevice是指具体相体设备,即CameraDevice创建成功后

(2)创建捕获请求建造者CaptureRequest.Builder(参数:模板_预览)

捕获请求需要在相机启动成功后创建,即CameraDevice创建成功后。

添加目标为SurfaceView的Surface,需要确定此时的SurfaceView已经创建完成并已执行回调。

请注意SurfaceView在onCreate()方法结束后创建完成并调用surfaceCreate()方法等待surfaceView创建完成的部分需要在非主线程**。**

(3)创建捕获请求目标集合

++创建捕获通道的第一个参数为所有捕获请求的目标Surface集合++,包括用于预览的SurfaceView的Surface和用于拍照的ImageReader的Surface,不能将捕获请求的目标设为不在本集合中的Surface,否则会报错。

请确保创建通道的全部Surface保持存活!!!

如果在关闭应用时销毁了surface资源,请在重启时重新获取。

++如果ImageReader是在主线程创建的,那么在关闭程序时,ImageReader的Surface会自动被摧毁,请在onRestart()重新获取。++

(4)创建捕获通道CaptureSession

捕获通道需要在相机启动成功后创建,即CameraDevice创建成功后。

(5)在捕获通道完成后要设置循环请求(预览捕获请求)

cameraCaptureSession**.setRepeatingRequest(captureRequestBuilder_preview.build(), null, null)**;

java 复制代码
//捕获通道
CameraCaptureSession captureSession;

// (1) 创建相机设备状态回调-CameraDevice是指具体相体设备
CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
    public void onOpened(@NonNull CameraDevice cameraDevice) {
        //相机设备打开
        myCameraDevice=cameraDevice;
        //捕获请求建造者
        CaptureRequest.Builder captureRequestBuilder_preview;
        try {
            // (2) 创建捕获请求建设者,参数:模板_预览
            captureRequestBuilder_preview = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            //等待SurfaceView创建完成并回调后将它的Surface设为目标
            while (true) {
                if (surface != null) {
                    //添加目标
                    captureRequestBuilder_preview.addTarget(surface);
                    break;
                }
                Thread.sleep(40);
            }
        } catch (CameraAccessException e) {
                throw new RuntimeException(e);
        } catch (InterruptedException e) {
                throw new RuntimeException(e);
        }

        // (3) 创建捕获通道 - 捕获通道需要在相机启动后再创建
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            try {
                //创建目标合集
                List<Surface> list=new ArrayList();
                list.add(surface);
                list.add(imageReader.getSurface());
                cameraDevice.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() {
                    public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
                        //捕获通道配置完成
                        captureSession=cameraCaptureSession;
                        // (4) 配置完成,可以开始预览 - 设置循环请求
                        try {
                            cameraCaptureSession.setRepeatingRequest(captureRequestBuilder_preview.build(), null, null);
                        } catch (CameraAccessException e) {
                        throw new RuntimeException(e);
                        }
                    }
                    public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
                        //配置失败
                    }
                }, null);
            } catch (CameraAccessException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public void onDisconnected(@NonNull CameraDevice cameraDevice) {
        //相机设备断开
        if (myCameraDevice != null) {
            myCameraDevice.close();
            myCameraDevice = null;
        }
    }
    public void onError(@NonNull CameraDevice cameraDevice, int i) {
        //相机设备错误
        if (myCameraDevice != null) {
            myCameraDevice.close();
            myCameraDevice = null;
        }
    }
};

7. 启动相机

使用相机管理者CameraManager的openCamera启动指定相机,参数为相机ID、相机状态回调、Handler。请注意第三个参数Handler用于指定回调方法在哪个线程上执行。通常情况下,相机相关的操作需要在主线程(UI 线程)执行,以确保与 UI 的交互是安全的。该参数不能为null

因启动相机会调用stateCallback中的onOpened()方法,如果该方法中存在等待surfaceView创建完成的部分,需要将openCamera()放在非主线程

java 复制代码
//Handler
Handler handler_ui = new Handler() {
    public void handleMessage(@NonNull Message msg) {
    
    }
};

//启动相机
cameraManager.openCamera(cameraId, stateCallback, handler_ui);


//关闭相机可使用如下代码
if (myCameraDevice != null) {
    myCameraDevice.close();
    myCameraDevice = null;
}

8. 拍摄照片

流程:

(1)创建捕获请求建造者CaptureRequest.Builder(参数:模板_静态捕获)

捕获请求需要在相机启动成功后创建,即CameraDevice创建成功后。

(2)捕获通道执行捕获拍照捕获请求

java 复制代码
button_take.setOnClickListener(new View.OnClickListener() {
    public void onClick(View view) {
        try {

            //(1)创建拍照的捕获请求,参数:模板_静态_捕获
            CaptureRequest.Builder captureRequestBuilder = myCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
            //用于保存拍照的Surface(在通道创建时已在目标集合中)
            Surface captureSurface = imageReader.getSurface(); 
            captureRequestBuilder.addTarget(captureSurface);

            //(2)捕获通道执行捕获请求
            captureSession.capture(captureRequestBuilder.build(), new CameraCaptureSession.CaptureCallback() {
                public void onCaptureStarted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, long timestamp, long frameNumber) {
                    super.onCaptureStarted(session, request, timestamp, frameNumber);
                }
            },null);

        } catch (CameraAccessException e) {
            throw new RuntimeException(e);
        }
    }
});

9.程序的停止和重启

(1)停止程序需关闭相机节省资源

java 复制代码
protected void onStop() {
    if (myCameraDevice != null) {
        myCameraDevice.close();
        myCameraDevice = null;
    }
    super.onStop();
}

(2)重启程序需再次启动相机

程序onStop()时相机关闭,在onRestart()中请重新openCamera()。

java 复制代码
 protected void onRestart() {
        try {
            cameraManager.openCamera(cameraId, stateCallback, handler_ui);
        } catch (CameraAccessException e) {
            throw new RuntimeException(e);
        }
        super.onRestart();
}

(3)重启程序重获取ImageReader的Surface

如果ImageReader是在主线程创建的,那么在关闭程序时,ImageReader的Surface会自动被摧毁,请在onRestart()重新获取!!!

java 复制代码
 protected void onRestart() {
        surface_imageReader = imageReader.getSurface();
      
        try {
            cameraManager.openCamera(cameraId, stateCallback, handler_ui);
        } catch (CameraAccessException e) {
            throw new RuntimeException(e);
        }
        super.onRestart();
}

10. 常见报错

(1) CaptureRequest contains unconfigured Input/Output Surface!

出现这个错误的原因是我们在一次捕获capture(也就是拍照的时候),cameraCaptureSession.capture(captureRequestBuilder.build(),CaptureCallback,Handler);这个captureRequestBuilder的目标Surface必须在创建通道时,是创建通道的第一个参数(目标List<Surface>集合)中的子集,否则就报异常了。

(2)Surface was abandoned

这个异常表明在使用相机API时,一个Surface对象被放弃(abandoned)。在Camera2 API中,Surface通常用于显示相机预览或保存照片。如果在关闭应用时销毁了surface资源,请在重启时重新获取。如果ImageReader是在主线程创建的,那么在关闭程序时,ImageReader的Surface会自动被摧毁,请在onRestart()重新获取。

(3) MTK零延迟的Camera机制

预览与拍照模式传给hal层的metadata键值对的key必须保持同步,也就是说,在同一次session会话中,你给预览传了什么样的metadata键值对的key,在拍照时同样也要传这个key,哪怕你不用这个metadata去调用底层工作,value可以传0或者任何底层不接受的值,切记这个key是必须传,否则打开相机的回调函数CameraDevice.StateCallback中onError就会被底层调用,从而导致相机无法打开。

相关推荐
吾日三省吾码8 分钟前
JVM 性能调优
java
Estar.Lee14 分钟前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
温辉_xh36 分钟前
uiautomator案例
android
弗拉唐1 小时前
springBoot,mp,ssm整合案例
java·spring boot·mybatis
oi772 小时前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器
工业甲酰苯胺2 小时前
MySQL 主从复制之多线程复制
android·mysql·adb
少说多做3432 小时前
Android 不同情况下使用 runOnUiThread
android·java
知兀2 小时前
Java的方法、基本和引用数据类型
java·笔记·黑马程序员
蓝黑20202 小时前
IntelliJ IDEA常用快捷键
java·ide·intellij-idea