文章目录
CameraX是一个Jetpack支持库,旨在帮助您简化相机应用的开发工作。它提供一致且易用的API接口,适用于大多数Android设备,并可向后兼容至Android 5.0(API 级别 21)。
主要有下面四种功能:
- 预览:接受用于显示预览的
Surface,例如PreviewView。 - 图片拍摄:拍摄并保存照片。
- 图片分析:为分析(例如机器学习)提供 CPU 可访问的缓冲区。
- 视频拍摄:录制视频。
1 在build.gradle添加CameraX依赖
def camerax_version = '1.1.0-alpha10'
implementation "androidx.camera:camera-core:$camerax_version"
// CameraX Camera2 extensions[可选]拓展库可实现人像、HDR、夜间和美颜、滤镜但依赖于OEM
implementation "androidx.camera:camera-camera2:$camerax_version"
// CameraX Lifecycle library[可选]避免手动在生命周期释放和销毁数据
implementation "androidx.camera:camera-lifecycle:$camerax_version"
// CameraX View class[可选]最佳实践,最好用里面的PreviewView,它会自行判断用SurfaceView还是TextureView来实现
implementation 'androidx.camera:camera-view:1.0.0-alpha23'
如果遇到NDK at …… is not supported (pre-r11)这种问题,在build.gradle中指定ndk版本。
android{
android {
ndkVersion '21.3.6528147' //本地可用的ndk版本
}
}
2 在application设置CameraXConfig
在Application中设置CameraConfig。
public class CameraApp extends Application implements CameraXConfig.Provider {
@NonNull
@Override
public CameraXConfig getCameraXConfig() {
return Camera2Config.defaultConfig();
}
}
3 在布局文件中添加PreviewView
<androidx.constraintlayout.widget.ConstraintLayout
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">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="拍照"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<androidx.camera.view.PreviewView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
4 在主Activity中获取camera权限
在AndroidManifest中添加权限:
<uses-permission android:name="android.permission.CAMERA" />
需要在 Activity 中动态申请该权限
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();
}
5 视频预览
5.1 配置CameraXConfig.Provider
private PreviewView mPreviewView;
private ListenableFuture<ProcessCameraProvider> mProcessCameraProviderListenableFuture;
private ProcessCameraProvider mProcessCameraProvider;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mPreviewView = findViewById(R.id.preview);
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();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
startCamera();
}
5.2 获取 CameraProvider
private void startCamera() {
mProcessCameraProviderListenableFuture = ProcessCameraProvider.getInstance(this);
mProcessCameraProviderListenableFuture.addListener(new Runnable() {
@Override
public void run() {
try {
mProcessCameraProvider = mProcessCameraProviderListenableFuture.get();
bindPreview(mProcessCameraProvider);
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, ContextCompat.getMainExecutor(this));
}
5.3 检查CameraProvider可用性。
- 创建
preview - 设置前置或者后置摄像头
- 将
preview和PreviewView进行连接 - 将所选相机绑定到生命周期上。
public void bindPreview(ProcessCameraProvider processCameraProvider) {
//获得预览配置
Preview mPreview = new Preview.Builder().build();
//设置camera配置
CameraSelector mCameraSelector = new CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build();
//将preview和previewView进行绑定
mPreview.setSurfaceProvider(mPreviewView.getSurfaceProvider());
//将camera和previe进行绑定
Camera mCamera = processCameraProvider.bindToLifecycle((LifecycleOwner) this, mCameraSelector, mPreview);
}
到此就可以实现预览了。
5.4 切换摄像头
其实很简单,只需要在CameraSelector中设置是CameraSelector.LENS_FACING_BACK还是CameraSelector.LENS_FACING_FRONT,其中有个很重的细节就是,在切换摄像头之前,一定要把ProcessCameraProvider给unbindAll(),否则切换回黑屏,完整代码如下:
/**
** @param processCameraProvider
* @param isBack 是否是后置摄像头。
*/
public void bindPreview(ProcessCameraProvider processCameraProvider, boolean isBack) {
Preview mPreview = new Preview.Builder().build();
CameraSelector mCameraSelector = isBack ? new CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build() : new CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_FRONT).build();
mPreview.setSurfaceProvider(mPreviewView.getSurfaceProvider());
mImageCapture = new ImageCapture.Builder().setTargetRotation(mPreviewView.getDisplay().getRotation()).build();//用于拍照
processCameraProvider.unbindAll();//一定要调否则,在切换摄像头时报错
Camera mCamera = processCameraProvider.bindToLifecycle(this, mCameraSelector, mImageCapture, mPreview);
}
6 拍照ImageCapture
6.1 绑定ImageCapture
无非在预览的基础上绑定个ImageCapture,在原来的bindPreview函数基础上进行修改。
public void bindPreview(ProcessCameraProvider processCameraProvider) {
Preview mPreview = new Preview.Builder().build();
CameraSelector mCameraSelector = new CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build();
mPreview.setSurfaceProvider(mPreviewView.getSurfaceProvider());
// Camera mCamera = processCameraProvider.bindToLifecycle((LifecycleOwner) this, mCameraSelector, mPreview);预览的代码
mImageCapture = new ImageCapture.Builder().setTargetRotation(mPreviewView.getDisplay().getRotation()).build();
//将ImageCapture也和生命周期进行绑定。
Camera mCamera = processCameraProvider.bindToLifecycle(this, mCameraSelector, mImageCapture, mPreview);
}
最常用的几种配置:
setTargetRotation:设置旋转, 以五种模式,Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180, Surface.ROTATION_270,加上自动旋转,默认是不旋转就是Surface.ROTATION_0。setFlashMode:是否开启闪光灯,有四种模式,FLASH_MODE_UNKNOWN(未知),FLASH_MODE_AUTO(根据环境光感自动开启闪光灯),FLASH_MODE_ON(开启闪光灯),FLASH_MODE_OFF(关闭闪光灯)。setCaptureMode:有两种模式:CaptureMode.CAPTURE_MODE_MINIMIZE_LATENCY最小延迟;CaptureMode.CAPTURE_MODE_MAXIMIZE_QUALITY以图片质量为先。setTargetResolution():参数为size(width,height),拍出来的照片尺寸就width,height。
6.2 设置照片存放位置
配置保存的File或者ContentProvider
6.2.1 保存到文件
保存本地根目录中,文件名为test.jpeg
ImageCapture.OutputFileOptions outputFileOptions =
new ImageCapture.OutputFileOptions.Builder(new File(Environment.getExternalStorageDirectory().getPath() + "/test.jpeg")).build();
6.2.1 保存到媒体库
保存到系统相册中
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, "NEW_IMAGE");
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
ImageCapture.OutputFileOptions options = new ImageCapture.OutputFileOptions.Builder(
getContentResolver(),
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues).build();
6.3调取拍照接口
findViewById(R.id.take_picture).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getApplicationContext(), "拍照", Toast.LENGTH_LONG).show();
ImageCapture.OutputFileOptions outputFileOptions =
new ImageCapture.OutputFileOptions.Builder(new File(Environment.getExternalStorageDirectory().getPath() + "/test.jpeg")).build();
mImageCapture.takePicture(outputFileOptions, ContextCompat.getMainExecutor(getApplicationContext()), new ImageCapture.OnImageSavedCallback() {
@Override
public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
Toast.makeText(getApplicationContext(), "拍照成功" + outputFileResults, Toast.LENGTH_LONG).show();
}
@Override
public void onError(@NonNull ImageCaptureException exception) {
Toast.makeText(getApplicationContext(), "拍照error" + exception.toString(), Toast.LENGTH_LONG).show();
}
});
}
});
ImageCapture有个弱点就是无法获取原始图像信息,只能将拍照的图片保存到文件或许相册中。
8 图像分析ImageAnalysis
8.1 绑定生命周期
只需要在bindPreview函数中增加ImageAnalysis绑定即可,具体代码如下:
public void bindPreview(ProcessCameraProvider processCameraProvider) {
Preview mPreview = new Preview.Builder().build();
CameraSelector mCameraSelector = new CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build();
mPreview.setSurfaceProvider(mPreviewView.getSurfaceProvider());
mImageCapture = new ImageCapture.Builder().setTargetRotation(mPreviewView.getDisplay().getRotation()).build();//用于拍照
mImageAnalysis = new ImageAnalysis.Builder().setTargetRotation(Surface.ROTATION_0) .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888)).build();//用户图像分析
mImageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(getApplicationContext()), new ImageAnalysis.Analyzer() {
@Override
public void analyze(@NonNull ImageProxy image) {
Log.e("test", "image");
image.close();
}
});
Camera mCamera = processCameraProvider.bindToLifecycle(this, mCameraSelector, mImageCapture, mImageAnalysis, mPreview);
}
运行上面代码,你就发现在setAnalyzer有远远不断的图形信息Image输出来,格式为ImageProxy,可以通过getImage() 获取Image信息。到此,你会发现只要你会预览了,你就拍照和图形分析接口了,相比Camera2,在使用流程上确实有所简化,使用起来更顺手。
到这个层面,我已经可以做很多东西了,比如录制视频,做特效处理等等。
8.2 ImageProxy参数说明
目前这个版本中,ImageProxy支持两种输出格式:OUTPUT_IMAGE_FORMAT_RGBA_8888和OUTPUT_IMAGE_FORMAT_YUV_420_888。
根据设置不同格式,对不同格式的Image进行不同解析,如下:
mImageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(getApplication()), new ImageAnalysis.Analyzer() {
@Override
public void analyze(@NonNull ImageProxy image) {
Log.e(TAG, "format:" + image.getFormat());
if (image.getFormat() == ImageFormat.YUV_420_888) {
ImageProxy.PlaneProxy[] mPlanes = image.getPlanes();
ImageProxy.PlaneProxy mY = mPlanes[0];//Y分量
ImageProxy.PlaneProxy mU = mPlanes[1];//U分量
ImageProxy.PlaneProxy mV = mPlanes[2];//V风量
Log.e(TAG, "planes0:" + mY.getPixelStride() + " planes1:" + mU.getPixelStride() + " planes2:" + mV.getPixelStride());
} else if (ImageFormat.FLEX_RGBA_8888 == image.getFormat()) {
ImageProxy.PlaneProxy[] mPlanes = image.getPlanes();
image.getPlanes()[0].getBuffer().get(0); //alpha透明度
image.getPlanes()[0].getBuffer().get(1); //red红色
image.getPlanes()[0].getBuffer().get(2); //green绿色
image.getPlanes()[0].getBuffer().get(3); //blue蓝色
}
image.close();
}
});
8.2.1 RGBA_8888格式解析
ImageProxy.PlaneProxy[] mPlanes = image.getPlanes();
image.getPlanes()[0].getBuffer().get(0); //alpha透明度
image.getPlanes()[0].getBuffer().get(1); //red红色
image.getPlanes()[0].getBuffer().get(2); //green绿色
image.getPlanes()[0].getBuffer().get(3); //blue蓝色
拿到R、G、B、A之后可以做任意的转换操作和处理。
8.2.2 YUV_420_888格式解析
YUV_420_888,从名字上可以看出Y占4,UV占2,总共8+8+8位,也就是说Y占16位,UV共占8位。从Image解析获取YUV数据的如下:
ImageProxy.PlaneProxy[] mPlanes = image.getPlanes();
ImageProxy.PlaneProxy mY = mPlanes[0];//Y分量
ImageProxy.PlaneProxy mU = mPlanes[1];//U,V分量
ImageProxy.PlaneProxy mV = mPlanes[2];//U,V风量
该种格式又有两种存储方式,一种YYYYYYYYYYYYYYYYUUUUVVVV(U,V分量连续存放),还有一种YYYYYYYYYYYYYYYYUVUVUVUV(U,V分量交叉存放)。mPlanes[0]一定是Y分量。
如何判断是第一种格式还是第二种格式呢?根据PixelStride来判定,它一般有两个值:
1:表示无间隔取值;
2:表示间隔一个数据取值。
在华为机型上,获取 mY,mU,mV的PixelStride分别为1,2,2,说明UV存储方式是交叉的。并且mPlanes[0]的PixelStride一定是1,mPlanes[1]和mPlanes[2]的PixelStride一定是相同的。
在实际的过程中,你可能发现拿到这个数据合成图像时仍有问题,这个就需要判断RowStride,为什么需要判断呢?RowStride和width可能不一样,一样的话不存在问题,不一样的话,需要进行补位。
具体的可以参考Android CameraX 摄像头数据ImageProxy数据分析,讲的特别详细。
9 录制视频
9.1 申请权限
除了在AndroidManifest中申请android.permission.RECORD_AUDIO权限,还需要Activity中动态申请权限。
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
return;
}
9.2 绑定VideoCapture
VideoCapture是录制视频的设置,有如下设置:
setVideoFrameRate():帧率,默认为30;setBitRate():比特率,默认为8 * 1024 * 1024;setIFrameInterval():帧间隔,默认1;setAudioBitRate():音频比特率,默认为64000;setAudioSampleRate():音频采集频率,默认8000;setAudioChannelCount():音频通道数,默认1;setAudioMinBufferSize():音频最小缓存大小,默认为1024;setMaxResolution():最大分辨率,默认为1920, 1080setTargetAspectRatio();宽高比,默认为16:9;
Preview mPreview = new Preview.Builder().setTargetAspectRatio(AspectRatio.RATIO_16_9).build();
CameraSelector mCameraSelector = isBack ? new CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build() : new CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_FRONT).build();
mPreview.setSurfaceProvider(mPreviewView.getSurfaceProvider());
VideoCapture mVideoCapture = new VideoCapture.Builder().build();//用于录制视频
processCameraProvider.bindToLifecycle(this, mCameraSelector, mVideoCapture, mPreview);
9.3 开始录制
通过VideoCapture.OutputFileOptions设置录制视频保存方式,如File,FileDescriptor,ContentResolver和Metadata,然后调用开始录制接口
VideoCapture.OutputFileOptions mOutputFileOptions = new VideoCapture.OutputFileOptions.Builder(new File(Environment.getExternalStorageDirectory().getPath() + "/test.mp4")).build();
mVideoCapture.startRecording(mOutputFileOptions, getMainExecutor(), new VideoCapture.OnVideoSavedCallback() {
@Override
public void onVideoSaved(@NonNull VideoCapture.OutputFileResults outputFileResults) {
}
@Override
public void onError(int videoCaptureError, @NonNull String message, @Nullable Throwable cause) {
}
});
简单介绍File,FileDescriptor,ContentResolver和Metadata几种方式和基本使用。
- File
将录制的文件写到本地
VideoCapture.OutputFileOptions mOutputFileOptions = new VideoCapture.OutputFileOptions.Builder(new File(Environment.getExternalStorageDirectory().getPath() + "/test.mp4")).build();
- FileDescriptor
存放到文件描述符中,便于其他用户使用。
VideoCapture.OutputFileOptions mOutputFileOptions = new VideoCapture.OutputFileOptions.Builder(new FileDescriptor()).build();
- ContentResolver
用于存放到媒体库中
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, "NEW_VIDEO");
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
OutputFileOptions options = new OutputFileOptions.Builder(
getContentResolver(),
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
contentValues).build();
9.4 结束录制
mVideoCapture.stopRecording();