最近在学习有关人脸识别的闸机系统,对用到的知识点进行总结.
Android5.0后就弃用了Camera1,而Camera1和Camera2的差别还是很大的,Camera2提供了很多新特性.本文使用Camera2实现了相机预览,拍照等功能.
开发工具:AS 3.4.1 真机:荣耀10
以下是部分重要代码,文章最后附有完整的Demo地址.
主要的api:
1.CameraManager:摄像头管理者,用于检测、描述和连接到照相机设备;
2.CameraCharacteristics:摄像头的属性信息,可以获取摄像头的FPS,支持尺寸等属性;
3.CameraDevice:表示摄像头设备;
4.CameraCaptureSession:相机捕捉会话,通过setRepeatingRequest不断请求捕捉图像;
5.ImageReader.OnImageAvailableListener中的onImageAvailable:获取预览的图像数据,可进行转化,保存图像等操作.
主要流程:
1.使用TextureView作为载体展示预览内容,
2.动态申请权限,权限通过后进行视图的初始化,遍历摄像头,创建工作线程,以及监听SurfaceTexture的状态;
这里的匿名内部类用到了jdk8的新特性Lambda表达式(后面写个关于Lambda的博客)
权限模板可参考我另一边博客:https://blog.csdn.net/sunyFS/article/details/98175130
//java 1.8的Lambda表达式
setPermissions(mPermissions, () -> {
Toast.makeText(MainActivity.this, "权限已全部允许,可进行初始化操作", Toast.LENGTH_SHORT).show();
initView();
initLooper();
});
}
private void initView() {
btn_switch = findViewById(R.id.btn_switch);
ttvPreview = findViewById(R.id.ttvPreview);
btn_switch.setOnClickListener(this);
//遍历摄像头,检查摄像头是否可用,以及获取摄像头支持的尺寸,FPS等属性
cameraList();
//监听SurfaceTexture状态,SurfaceTexture可用时回调onSurfaceTextureAvailable方法
ttvPreview.setSurfaceTextureListener(this);
}
private void initLooper() {
//创建HandlerThread,用"CAMERA2"标记
mThreadHandler = new HandlerThread("CAMERA2");
//启动线程
mThreadHandler.start();
//创建工作线程Handler
mHandler = new Handler(mThreadHandler.getLooper());
}
3.遍历摄像头cameraList(),这里我默认使用后置摄像头mCameraId = mBackCameraId;
通过getSystemService(Context.CAMERA_SERVICE);获取摄像头的管理者cameraManager,
foreach遍历所有的摄像头:
CameraMetadata.LENS_FACING_BACK为后置摄像头,也可以用字符串"0"表示;
CameraMetadata.LENS_FACING_FRONT为前置摄像头"1";
cameraManager.getCameraCharacteristics(cameraId);传入指定id获取该摄像头的characteristics,
通过CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES字段获取支持的FPS(前后摄像头一样);
通过CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP字段获取设备支持的预览尺寸;
预览尺寸指相机把画面输出到手机屏幕上供用户预览的尺寸,一般手机都会支持多个不同尺寸供用户选择,通常在不超过手机分辨率下,选择越大的尺寸,预览画面越清晰.
(荣耀10 分辨率为2280x1080,我选择map.getOutputSizes(SurfaceTexture.class)[2],即2160x1064)
/**
* 遍历所有摄像头,检查是否可用
*/
private void cameraList() {
//获得所有摄像头的管理者CameraManager
cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
CameraCharacteristics characteristics = null;
try {
for (String cameraId : cameraManager.getCameraIdList()) {
characteristics = cameraManager.getCameraCharacteristics(cameraId);
//后置摄像头"0"
if (characteristics.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_BACK) {
mBackCameraId = cameraId;
//摄像头支持的FPS范围,前后摄像头一样
FpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
//cameraList: backFpsRanges[[12, 15], [15, 15], [14, 20], [20, 20], [14, 25], [25, 25], [14, 30], [30, 30]]
Log.i(TAG, "cameraList: backFpsRanges" + Arrays.toString(FpsRanges));
}
//前置摄像头"1"
if (characteristics.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT) {
mFontCameraId = cameraId;
}
}
if (mBackCameraId != null) {
mCameraId = mBackCameraId;//默认打开后置摄像头
} else if (mFontCameraId != null) {
mCameraId = mFontCameraId;
Toast.makeText(this, "后置摄像头不可用,已切换前置摄像头", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "摄像头不可用,请检查设备", Toast.LENGTH_SHORT).show();
}
} catch (CameraAccessException e) {
e.printStackTrace();
}
//获取摄像头支持的所有输出格式和尺寸的管理者StreamConfigurationMap
StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
//取摄像头支持的预览尺寸
mPreviewSize = map.getOutputSizes(SurfaceTexture.class)[2];
//cameraList: mPreviewSize [0->]2816x2112 (预览清晰) [2]->2160x1064 [15]->208x144(预览模糊)
Log.i(TAG, "cameraList: mPreviewSize " + mPreviewSize.toString());
}
4.我们用 ttvPreview.setSurfaceTextureListener(this);对SurfaceTexture进行监听;
当SurfaceTexture可用时回调onSurfaceTextureAvailable方法
在该方法中做打开相机:openCamera(String cameraId, CameraDevice.StateCallback callback, Handler handler);
第一个参数是传入指定的摄像头Id,一般的"0"是后置摄像头,"1"是前置摄像头;
第二个参数是相机状态监听的回调;
第三个参数是接收消息使用的mHandler,表示在哪个线程调用,如果为null,就在当前线程使用.
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) {
if (mCameraId != null) {
openCamera(mCameraId);
}
}
private void openCamera(String mCameraId) {
cameraManager.openCamera(mCameraId, mCameraDeviceStateCallback, mHandler);
}
5.接着我们要创建CameraDevice.StateCallback对象,这个抽象类是对相机设备状态的监听;
当相机成功打开时回调onOpened方法;
我们在该方法中做开始预览的操作startPreview(mCameraDevice);
若相机链接失败或者报错时关闭设备mCameraDevice.close();
private CameraDevice.StateCallback mCameraDeviceStateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(CameraDevice cameraDevice) {
mCameraDevice = cameraDevice;
startPreview(mCameraDevice);
}
@Override
public void onDisconnected(CameraDevice cameraDevice) {
if (mCameraDevice != null) {
mCameraDevice.close();
mCameraDevice = null;
}
}
@Override
public void onError(CameraDevice cameraDevice, int i) {
if (mCameraDevice != null) {
mCameraDevice.close();
mCameraDevice = null;
}
}
};
6.开始预览startPreview(CameraDevice cameraDevice),
通过 getSurfaceTexture()获取SurfaceTexture:
两者关系可以理解为TextureView是一幅画,SurfaceTexture是画布,真正渲染的载体是SurfaceTexture.
根据我们在cameraList获取支持的预览尺寸设置预览尺寸:
设置捕获请求模式为预览模式:cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
也有其他模式:TEMPLATE_STILL_CAPTURE(拍照) TEMPLATE_RECORD(录像)
这里我们还可以相机进行曝光,对焦,FPS等设置
为了得到预览数据,我们需要ImageReader对象,
ImageReader.newInstance(mImageWidth, mImageHeight, ImageFormat.JPEG, 2);
第一个和第二个参数是指定图片的大小,
第三个是指定图片的格式,
第四个参数是ImageReader获取到的图片数量(2+1张)
有了它我们就可以做预览数据的回调:
mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mHandler);
创建ImageReader.OnImageAvailableListener对象
private ImageReader.OnImageAvailableListener mOnImageAvailableListener = (ImageReader imageReader) -> {
//从ImageReader队列获取下一个图像
Image img = imageReader.acquireNextImage();
//转格式,保存等操作
//注意不用要关闭,否则会报错
img.close();
};
7.现在还不能捕捉到图像,还需要建立和相机的捕捉会话
首先要添加两个surface,一个TextureView的,用于页面显示;另一个ImageReader的,用于预览数据回调.
mPreviewBuilder.addTarget(surface);
mPreviewBuilder.addTarget(mImageReader.getSurface());
然后创建捕捉会话:cameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()), mSessionStateCallback, mHandler);
以下是开始预览操作startPreview全部代码:
private void startPreview(CameraDevice cameraDevice) {
SurfaceTexture texture = ttvPreview.getSurfaceTexture();
//设置的就是预览大小
texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
Surface surface = new Surface(texture);
try {
//设置捕获请求模式为预览
mPreviewBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
} catch (CameraAccessException e) {
e.printStackTrace();
}
//对相机参数进行设置
setCamera();
//图片的大小,格式以及捕捉数量(2+1)
mImageReader = ImageReader.newInstance(mImageWidth, mImageHeight, ImageFormat.JPEG, 2);
mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mHandler);
//添加两个surface,一个TextureView的,另一个ImageReader的,用于页面显示和预览数据回调
mPreviewBuilder.addTarget(surface);
mPreviewBuilder.addTarget(mImageReader.getSurface());
try {
cameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()), mSessionStateCallback, mHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
private void setCamera() {
//曝光
mPreviewBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
//FPS
mPreviewBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, FpsRanges[5]);
}
8.创建CameraCaptureSession.StateCallback对象,在回调方法onConfigured中进行请求捕捉图像;
cameraCaptureSession.setRepeatingRequest(mPreviewBuilder.build(), null, mHandler);
第一个参数是CaptureRequest.Builder的build()方法得到CaptureRequest对象;
第二个参数是监听预览过程的回调,不需要特殊处理的时候,可以传null;
第三个参数上面有说明了.
该方法会不断的重复捕捉图像,就会不断回调onImageAvailable方法.
而capture(CaptureRequest request,CaptureCallback listener, Handler handler)方法只会请求捕捉一次,可实现拍照功能.
private CameraCaptureSession.StateCallback mSessionStateCallback = new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession cameraCaptureSession) {
mCaptureSession = cameraCaptureSession;
try {
cameraCaptureSession.setRepeatingRequest(mPreviewBuilder.build(), null, mHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {
}
};
9.结束后要释放资源:
@Override
protected void onDestroy() {
closeCamera();
super.onDestroy();
}
private void closeCamera() {
if (mCaptureSession != null) {
mCaptureSession.close();
mCaptureSession = null;
}
if (mCameraDevice != null) {
mCameraDevice.close();
mCameraDevice = null;
}
if (mImageReader != null) {
mImageReader.close();
mImageReader = null;
}
}
前后置摄像头切换功能:先关闭当前摄像头,判断当前摄像头是前还是后,然后设置mCameraId为另一个,接着判断TextureView是否可用,可用直接openCamera(mCameraId);不可用则重新获取TextureView;
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_switch:
closeCamera();
if (mCameraId != null) {
if (mCameraId.equals(mBackCameraId) && mFontCameraId != null) {//后置切换前置
mCameraId = mFontCameraId;
} else if (mCameraId.equals(mFontCameraId) && mBackCameraId != null) {//前置切换成后置
mCameraId = mBackCameraId;
} else {
Toast.makeText(this, "摄像头不可用,请检查设备", Toast.LENGTH_SHORT).show();
break;
}
if (ttvPreview.isAvailable()) {
openCamera(mCameraId);
} else {
//重新获取textureView
ttvPreview.setSurfaceTextureListener(this);
}
Toast.makeText(this, "切换成功", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "摄像头不可用,请检查设备", Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
}
后续还会完善功能!
参考链接:https://blog.csdn.net/matrix_laboratory/article/details/80693537
camera2中文文档链接:https://blog.csdn.net/zhangbijun1230/article/details/80556903
这个链接非常有用,不懂的字段,api可以自行查看,里面都有说明.
github:https://github.com/sunfusong/Camera2Demo.git