文章目录
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, 1080
setTargetAspectRatio()
;宽高比,默认为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();