Camera2的使用【详细】

目录

1.获取权限

2. 获取指定相机ID

(1)获取相机管理者CameraManager

(2)获取相机ID列表

(3)获取相机特征CameraCharacteristics

(4)获取相机朝向

3. 获取相机输出尺寸

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

(2)获取输出流配置StreamConfigurationMap

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

(4)选择输出尺寸

4. SurfaceView及其他控件的准备

(1)获取视图

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

(3)添加回调

(4)纹理视图TextureView

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

(1)创建ImageReader

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

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

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

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

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

(4)创建捕获通道CaptureSession

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

7. 启动相机

8. 拍摄照片

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

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

9.程序的停止和重启

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

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

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

10. 开启闪光灯、关闭闪光灯

11. 常见报错

(1) CaptureRequest contains unconfigured Input/Output Surface!

(2) Surface was abandoned

(3) MTK零延迟的Camera机制

12. 请求建造者常用参数

13. 录像


建议相机全部操作放在非主线程(即子线程)中,可以避免等待SurfaceView创建阻塞主线程等多种情况;同时建议将输出用的ImageReader设为全局变量。

1.获取权限

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

<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"/>
requestPermissions(new String[]{"android.permission.CAMERA","android.permission.READ_EXTERNAL_STORAGE","android.permission.WRITE_EXTERNAL_STORAGE"}, 2333);
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);

//相机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. 获取相机输出尺寸

可直接设置:如宽480*30高640*30

流程:

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

(2)获取输出流配置StreamConfigurationMap

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

输出尺寸参数建议选择ImageReader.class。

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

(4)选择输出尺寸

//获取相机支持尺寸
int output_width,output_height;
//(1)获取当前相机的特征
CameraCharacteristics cameraCharacteristics=cameraManager.getCameraCharacteristics(cameraId);
//(2)获取输出流配置
StreamConfigurationMap map=cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
//(3-1)获取输出尺寸数组(参数为输出目标类)
Size[] outputSizes = map.getOutputSizes(ImageReader.class);
//(4-1)选择输出尺寸
for(int i=0;i<outputSizes.length;i++){
    //获取需要的输出尺寸
    //因为相机常为90°,所以将横竖调转
    output_height=outputSizes[i].getWidth();
    output_width=outputSizes[i].getHeight();
    //判断是否使用本尺寸代码
    //... ...
    break;
}



//(3-2)获取输出尺寸数组(参数为输出目标类)
Size[] outputSizes = map.getOutputSizes(SurfaceView.class);
if(outputSizes.length!=null){
    //(4-2)选择输出尺寸
    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;
}
//例
//判断是否使用本尺寸代码
//即选择输出尺寸中最接近目标比例的尺寸
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 = (double) tempWidth /(double) 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创建完成的部分需要在非主线程

//(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;

(4)纹理视图TextureView

表面视图SurfaceView在使用Camera的一般情况下是够使用了,但是有一些限制。因为表面视图不是通过onDraw方法和 dispatchDraw方法进行绘图,所以无法使用View的基本视图方法;各种视图变化方法均无法奏效,包括透明度变化方法setAlpha、平移方法setTranslation、缩放方法setScale、旋转方法 setRotation等,甚至连最基础的背景图设置方法setBackground都失效了。
为了解决表面视图的不足之处,Android在4.0之后引入了纹理视图TextureView。与表面视图相比,纹理视图并没有创建一个单独的绘图表面用来绘制,可以像普通视图一样执行变换操作,也可以正常设置背景图。 (详见TextureView部分)

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

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

流程:

(1)创建ImageReader

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

如果ImageReader是非主线程的局部变量(即未在MainActivity类中声明变量名),那么在关闭程序时,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 数组中。
//(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是指具体相体设备;用于创建对应捕获请求、捕获通道及关闭相机。

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

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

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

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

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

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

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

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

如果ImageReader是非主线程的局部变量(即未在MainActivity类中声明变量名),那么在关闭程序时,ImageReader的Surface会自动被摧毁,请在onRestart()重新获取。

(4)创建捕获通道CaptureSession

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

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

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

//捕获通道
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_surfaceView);
                    break;
                }
                Thread.sleep(40);
            }
        } catch (CameraAccessException e) {
                throw new RuntimeException(e);
        } catch (InterruptedException e) {
                throw new RuntimeException(e);
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            try {
                // (3) 创建目标合集
                List<Surface> list=new ArrayList();
                list.add(surface_surfaceView);
                list.add(surface_imageReader);
                // (4) 创建捕获通道 - 捕获通道需要在相机启动后再创建
                cameraDevice.createCaptureSession(list, new CameraCaptureSession.StateCallback() {
                    public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
                        //捕获通道配置完成
                        captureSession=cameraCaptureSession;
                        // (5) 配置完成,可以开始预览 - 设置循环请求
                        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()放在非主线程

//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)捕获通道执行捕获拍照捕获请求

button_take.setOnClickListener(new View.OnClickListener() {
    public void onClick(View view) {
        try {

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

            //(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)停止程序需关闭相机节省资源

使用相机设备(CameraDevice).close()方法关闭相机

protected void onStop() {
    if (myCameraDevice != null) {
        myCameraDevice.close();
        myCameraDevice = null;
    }
    super.onStop();
}

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

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

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

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

 如果ImageReader是非主线程的局部变量(即未在MainActivity类中声明变量名),那么在关闭程序时,ImageReader的Surface会自动被摧毁,请在onRestart()重新获取!!!

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

10. 开启闪光灯、关闭闪光灯

在Camera2中,我们一般在预览的捕获请求中设置开启或关闭闪光灯,然后向捕获通道设置新的捕获请求

开启闪光灯:

captureRequestBuilder .set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH);

关闭闪光灯:

captureRequestBuilder .set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);

Button torch = findViewById(R.id.torch);
torch.setOnClickListener(new View.OnClickListener() {
    int i = 0;
    public void onClick(View view) {
        if (i == 0) {
            CaptureRequest.Builder captureRequestBuilder_preview = myCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                            
            //设置开启闪光灯
            captureRequestBuilder_preview.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH);
                            
            captureRequestBuilder_preview.addTarget(surface_surfaceView);
            //通道设置新循环捕获请求
            myCameraCaptureSession.setRepeatingRequest(captureRequestBuilder_preview.build(),null,null);
            i=1;
        } else if (i==1) {
            CaptureRequest.Builder captureRequestBuilder_preview= myCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                            
            //设置关闭闪光灯
            captureRequestBuilder_preview.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
                            
            captureRequestBuilder_preview.addTarget(surface_surfaceView);
            //通道设置新循环捕获请求
            myCameraCaptureSession.setRepeatingRequest(captureRequestBuilder_preview.build(),null,null);
            i=0;
        }
    }
});

11. 常见报错

(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就会被底层调用,从而导致相机无法打开。

12. 请求建造者常用参数

CameraDevice.TEMPLATE_PREVIEW 模板_预览(预览

CameraDevice.TEMPLATE_STILL_CAPTURE 模板_静态_捕获(拍照

CameraDevice.TEMPLATE_RECORD 模板_录制(录像

13. 录像

录像可使用CameraDevice.TEMPLATE_RECORD参数创建请求建造者,然后使用MediaRecorder(媒体录制器)进行录制。

tag:相机 , 手电筒 , 闪光灯 , 摄像头 ,torch 

  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

在下嗷呜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值