您的位置:首页 > 移动开发 > Android开发

Android使用Camera2打造自定义相机

2017-05-27 23:29 357 查看
从5.0开始(API Level 21),可以完全控制Android设备相机的新api Camera2(android.hardware.Camera2)被引入了进来。在以前的Camera api(android.hardware.Camera)中,对相机的手动控制需要更改系统才能实现,而且api也不友好。不过老的Camera API在5.0上已经过时,在未来的app开发中推荐的是Camera2 API。

1、Camera2介绍

在Camera类中我们多是使用这个类的对象去调用方法,而Camera2则是使用多个类去设置,功能更加强大。

Camera2包架构:



Google采用了pipeline(管道)的概念,将Camera Device相机设备和Android Device安卓设备连接起来, Android Device通过管道发送CaptureRequest拍照请求给Camera Device,Camera Device通过管道返回CameraMetadata数据给Android Device,这一切建立在一个叫作CameraCaptureSession的会话中。



基本上我们需要使用的就是这些类啦。其中CameraManager是所有相机设备(CameraDevice)的管理者,要枚举,查询和打开可用的相机设备,就获取CameraManager实例。

单个CameraDevices提供一组静态属性信息,描述硬件设备以及设备的可用设置和输出参数。该信息通过CameraCharacteristics对象提供,可通过getCameraCharacteristics(String)获得。

CameraCharacteristics是CameraDevice的属性描述类,在CameraCharacteristics中可以进行相机设备功能的详细设定(当然了,首先你得确定你的相机设备支持这些功能才行)。

要从相机设备捕获或流式传输图像,应用程序必须首先使用createCaptureSession(List,CameraCaptureSession.StateCallback,Handler)与相机设备一起使用一组输出Surfaces创建摄像机捕获会话。每个Surface必须预先配置适当的大小和格式(如果适用)以匹配相机设备可用的大小和格式。目标Surface可以从各种类获得。

CameraCaptureSession:这是一个非常重要的API,当程序需要预览、拍照时,都需要先通过该类的实例创建Session。而且不管预览还是拍照,也都是由该对象的方法进行控制的,其中控制预览的方法为setRepeatingRequest();控制拍照的方法为capture()。

通常,相机预览图像将发送到SurfaceView或TextureView(通过其SurfaceTexture)。

然后,应用程序需要构建一个CaptureRequest,它定义了相机设备捕获单个映像所需的所有捕获参数。该请求还列出了哪些配置的输出表面应该用作此捕获的目标。 CameraDevice具有用于为给定用例创建请求构建器的工厂方法,针对应用程序正在运行的Android设备进行了优化。

CameraRequest和CameraRequest.Builder:当程序调用setRepeatingRequest()方法进行预览时,或调用capture()方法进行拍照时,都需要传入CameraRequest参数。CameraRequest代表了一次捕获请求,用于描述捕获图片的各种参数设置,比如对焦模式、曝光模式……总之,程序需要对照片所做的各种控制,都通过CameraRequest参数进行设置。CameraRequest.Builder则负责生成CameraRequest对象。

一旦请求被建立,它可以交给主动捕获会话进行单次捕获或无休止地重复使用。处理请求后,相机设备将产生一个TotalCaptureResult对象,该对象包含有关拍摄时相机设备状态的信息以及使用的最终设置。如果需要舍入或解决矛盾的参数,这些请求可能会有所不同。相机设备还会将图像数据帧发送到请求中包括的每个输出表面。这些相对于输出CaptureResult是异步产生的,有时候稍后会产生。

类图中有着三个重要的callback,其中CameraCaptureSession.CaptureCallback将处理预览和拍照图片的工作,需要重点对待。





这两幅对Camera2接口使用的流程介绍我们综合起来看会有更深的理解。

可以看出调用openCamera方法后会回调CameraDevice.StateCallback这个方法,在该方法里重写onOpened函数。

在onOpened方法中调用createCaptureSession,该方法又回调CameraCaptureSession.StateCallback方法。

在CameraCaptureSession.StateCallback中重写onConfigured方法,设置setRepeatingRequest方法(也就是开启预览)。

setRepeatingRequest又会回调 CameraCaptureSession.CaptureCallback方法。

重写CameraCaptureSession.CaptureCallback中的onCaptureCompleted方法,result就是未经过处理的元数据了。

顺便提一下CameraCaptureSession.CaptureCallback中的onCaptureProgressed方法很明显是在Capture过程中的,也就是在onCaptureCompleted之前,所以,在这之前想对图像干什么就看你的了,像美颜等操作就可以在这个方法中实现了。

可以看出Camera2相机使用的逻辑还是比较简单的,其实就是3个Callback函数的回调,先说一下:setRepeatingRequest和capture方法其实都是向相机设备发送获取图像的请求,但是capture就获取那么一次,而setRepeatingRequest就是不停的获取图像数据,所以呢,使用capture就想拍照一样,图像就停在那里了,但是setRepeatingRequest一直在发送和获取,所以需要连拍的时候就调用它,然后在onCaptureCompleted中保存图像就行了。(注意了,图像的预览也是用的setRepeatingRequest,只是你不处理数据就行了)。

通过上面对Camera2的API的分析,我们可以知道控制拍照的大致步骤为:

调用CameraManager的openCamera(String cameraId, CameraDevice.StateCallback callback, Handler handler)方法打开指定摄像头。该方法的第一个参数代表要打开的摄像头ID;第二个参数用于监听摄像头的状态;第三个参数代表执行callback的Handler,如果程序希望直接在当前线程中执行callback,则可将handler参数设为null。

当摄像头被打开之后,程序即可获取CameraDevice—即根据摄像头ID获取了指定摄像头设备,然后调用CameraDevice的createCaptureSession(List outputs, CameraCaptureSession. StateCallback callback,Handler handler)方法来创建CameraCaptureSession。该方法的第一个参数是一个List集合,封装了所有需要从该摄像头获取图片的Surface,第二个参数用于监听CameraCaptureSession的创建过程;第三个参数代表执行callback的Handler,如果程序希望直接在当前线程中执行callback,则可将handler参数设为null。

不管预览还是拍照,程序都调用CameraDevice的createCaptureRequest(int templateType)方法创建CaptureRequest.Builder,该方法支持TEMPLATE_PREVIEW(预览)、TEMPLATE_RECORD(拍摄视频)、TEMPLATE_STILL_CAPTURE(拍照)等参数。

通过第3步所调用方法返回的CaptureRequest.Builder设置拍照的各种参数,比如对焦模式、曝光模式等。

调用CaptureRequest.Builder的build()方法即可得到CaptureRequest对象,接下来程序可通过CameraCaptureSession的setRepeatingRequest()方法开始预览,或调用capture()方法拍照。

2、自定义相机

经过上面的说明,相信大家对Camera2的接口已经有了一定的了解,不是很清楚不要紧,实践出真知,我们就开始上代码啦。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextureView
android:id="@+id/textureView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true" />

<FrameLayout
android:id="@+id/control"
android:layout_width="match_parent"
android:layout_height="112dp"
android:layout_alignParentBottom="true"
android:layout_alignParentStart="true"
android:background="@color/control_background">

<Button
android:id="@+id/picture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/picture" />

</FrameLayout>

</RelativeLayout>


既然只是示例,我们的布局就简单一些就好,就下来我们先为TextureView设置好它的回调:

@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
setupCamera();
openCamera();
}

@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {

}

@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
return false;
}

@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {

}


我们这个案例主要是为了介绍如何用Camera2实现拍照,所以关于尺寸大小适配的处理就不多做了,所以我们就在onSurfaceTextureSizeChanged()中设置并打开Camera。

private void setupCamera() {
//获取摄像头的管理者CameraManager
CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
try {
//遍历所有摄像头
for (String id : manager.getCameraIdList()) {
CameraCharacteristics characteristics = manager.getCameraCharacteristics(id);
//默认打开后置摄像头
if (characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT)
continue;
//获取StreamConfigurationMap,它是管理摄像头支持的所有输出格式和尺寸
StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);

// 对于静态图像捕获,我们使用最大的可用尺寸。
mPreviewSize = Collections.max(
Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)),
new Comparator<Size>() {
@Override
public int compare(Size lhs, Size rhs) {
return Long.signum(lhs.getWidth() * lhs.getHeight()
- rhs.getHeight() * rhs.getWidth());
}
});
mCameraId = id;
break;
}
} catch (CameraAccessException e) {
e.printStackTrace();
}
}


我们这里就启用后置摄像头,setupCamera()我们就是设置图像尺寸并获得摄像头ID,方便我们在openCamera()中使用。

private void openCamera() {
//获取摄像头的管理者CameraManager
CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
//检查权限
try {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
return;
}
//打开相机,第一个参数指示打开哪个摄像头,第二个参数stateCallback为相机的状态回调接口,第三个参数用来确定Callback在哪个线程执行,为null的话就在当前线程执行
manager.openCamera(mCameraId, stateCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}


这样我们算是完成了第一步,按照流程图接下来就是启用我们设备的回调开始预览:

private final CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(CameraDevice camera) {
mCameraDevice = camera;
//开启预览
startPreview();
}

@Override
public void onDisconnected(CameraDevice camera) {

}

@Override
public void onError(CameraDevice camera, int error) {

}
};


mCameraDevice是我设置的CameraDevice对象,现在给它初始化,我们知道CameraDevice相当于旧的Camera,所以我们就得到了这个摄像头。

private void startPreview() {
SurfaceTexture mSurfaceTexture = mPreviewView.getSurfaceTexture();

//设置TextureView的缓冲区大小
mSurfaceTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());

//获取Surface显示预览数据
Surface mSurface = new Surface(mSurfaceTexture);

setupImageReader();

//获取ImageReader的Surface
Surface imageReaderSurface = mImageReader.getSurface();

try {
//创建CaptureRequestBuilder,TEMPLATE_PREVIEW比表示预览请求
mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
//设置Surface作为预览数据的显示界面
mPreviewBuilder.addTarget(mSurface);
//创建相机捕获会话,第一个参数是捕获数据的输出Surface列表,第二个参数是CameraCaptureSession的状态回调接口,当它创建好后会回调onConfigured方法,第三个参数用来确定Callback在哪个线程执行,为null的话就在当前线程执行
mCameraDevice.createCaptureSession(Arrays.asList(mSurface, imageReaderSurface), mSessionStateCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}


这个方法就是我们实现预览的关键,我们设置好了Surface就把它与CaptureRequestBuilder对象关联,然后就是设置会话开始捕获画面。

private CameraCaptureSession.StateCallback mSessionStateCallback = new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession session) {
try {
//创建捕获请求
mCaptureRequest = mPreviewBuilder.build();
mPreviewSession = session;
//设置反复捕获数据的请求,这样预览界面就会一直有数据显示
mPreviewSession.setRepeatingRequest(mCaptureRequest, mSessionCaptureCallback, mHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}

@Override
public void onConfigureFailed(CameraCaptureSession session) {

}
};


最后的回调CameraCaptureSession.CaptureCallback就给我们设置预览完成的逻辑处理:

private CameraCaptureSession.CaptureCallback mSessionCaptureCallback = new CameraCaptureSession.CaptureCallback() {

@Override
public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
super.onCaptureCompleted(session, request, result);
//重启预览
restartPreview();
}
};


private void restartPreview() {
try {
//执行setRepeatingRequest方法就行了,注意mCaptureRequest是之前开启预览设置的请求
mPreviewSession.setRepeatingRequest(mCaptureRequest, null, mHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}


这样就创建好了,但是要注意的是因为Camera2没有onPictureTaken()方法,所以我们不能直接获得图像数据,这里我们要用的是ImageReader:

private void setupImageReader() {

//前三个参数分别是需要的尺寸和格式,最后一个参数代表每次最多获取几帧数据,本例的2代表ImageReader中最多可以获取两帧图像流
mImageReader = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(),
ImageFormat.JPEG, 2);

//监听ImageReader的事件,当有图像流数据可用时会回调onImageAvailable方法,它的参数就是预览帧数据,可以对这帧数据进行处理
mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
mHandler.post(new ImageSaver(reader.acquireNextImage()));
}
}, mHandler);
}


在处理ImageReader我们可以用handler来做:

public class ImageSaver implements Runnable {

private Image mImage;
private File mFile;

public ImageSaver(Image image) {
this.mImage = image;
}

@Override
public void run() {
ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
FileOutputStream output = null;

SimpleDateFormat sdf = new SimpleDateFormat(
"yyyyMMdd_HHmmss",
Locale.US);

String fname = "IMG_" +
sdf.format(new Date())
+ ".jpg";
mFile = new File(getApplication().getExternalFilesDir(null), fname);

try {
output = new FileOutputStream(mFile);
output.write(bytes);
} catch (IOException e) {
e.printStackTrace();
}
finally {
mImage.close();
if (null != output) {
try {
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}


这个Run()方法里做的就是把从Image中获得的帧数据输出到指定的文件里,文件名我们用当前时间来生成。

这样我们就做好所有的拍照前的设置了,现在只要处理点击按钮时进行拍照即可。

private HandlerThread mThreadHandler;
private TextureView mPreviewView;
private Handler mHandler = new Handler();
private CaptureRequest.Builder mPreviewBuilder;
private Button mButton;
private ImageReader mImageReader;
private String mCameraId;
private Size mPreviewSize;
private CameraDevice mCameraDevice;
private CaptureRequest mCaptureRequest;
private CameraCaptureSession mPreviewSession;

private static final SparseIntArray ORIENTATION = new SparseIntArray();

static {
ORIENTATION.append(Surface.ROTATION_0, 90);
ORIENTATION.append(Surface.ROTATION_90, 0);
ORIENTATION.append(Surface.ROTATION_180, 270);
ORIENTATION.append(Surface.ROTATION_270, 180);
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
requestCameraPermission();

mThreadHandler = new HandlerThread("CAMERA2");
mThreadHandler.start();
mHandler = new Handler(mThreadHandler.getLooper());
mPreviewView = (TextureView) findViewById(textureView);
mPreviewView.setSurfaceTextureListener(this);
mButton = (Button) findViewById(R.id.picture);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

try {
//获取屏幕方向
int rotation = getWindowManager().getDefaultDisplay().getRotation();
//设置CaptureRequest输出到mImageReader
//CaptureRequest添加imageReaderSurface,不加的话就会导致ImageReader的onImageAvailable()方法不会回调
mPreviewBuilder.addTarget(mImageReader.getSurface());
//设置拍照方向
mPreviewBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATION.get(rotation));
//聚焦
mPreviewBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);

//停止预览
mPreviewSession.stopRepeating();
//开始拍照,然后回调上面的接口重启预览,因为mPreviewBuilder设置ImageReader作为target,所以会自动回调ImageReader的onImageAvailable()方法保存图片
mPreviewSession.capture(mPreviewBuilder.build(), mSessionCaptureCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
});
}


这里要注意的是给ImageReader的surface的设置必须放在拍照这里,否则再预览的时候就会不断的执行handler,将图像保存下来。



Camera2还有很多的功能,谷歌在给我们提供强大类的时候也让我们的学习量增大了,所以大家不要认为基本了解Camera2的工作流程就是掌握了Camera2,只有能将其运用到我们的开发中去才算掌握了,所以如果你是看到这篇博客才了解了Camera2,那这只是你的第一步而已,让我们彼此共勉,最后附上GitHub的开源项目

结束语:本文仅用来学习记录,参考查阅。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: