Jetpack系列CameraX使用手册

CameraX 是一个 Jetpack 支持库,旨在帮助您简化相机应用的开发工作。它提供一致且易用的 API 接口,适用于大多数 Android 设备,并可向后兼容至 Android 5.0(API 级别 21)。

主要有下面四种功能:

  1. 预览:接受用于显示预览的 Surface,例如 PreviewView
  2. 图片拍摄:拍摄并保存照片。
  3. 图片分析:为分析(例如机器学习)提供 CPU 可访问的缓冲区。
  4. 视频拍摄:录制视频。

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可用性。

  1. 创建 preview
  2. 设置前置或者后置摄像头
  3. previewPreviewView进行连接
  4. 将所选相机绑定到生命周期上。
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,其中有个很重的细节就是,在切换摄像头之前,一定要把ProcessCameraProviderunbindAll(),否则切换回黑屏,完整代码如下:

/**  
 ** @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_8888OUTPUT_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风量

该种格式又有两种存储方式,一种YYYYYYYYYYYYYYYYUUUUVVVVU,V分量连续存放),还有一种YYYYYYYYYYYYYYYYUVUVUVUVUV分量交叉存放)。mPlanes[0]一定是Y分量。

如何判断是第一种格式还是第二种格式呢?根据PixelStride来判定,它一般有两个值:

1:表示无间隔取值;

2:表示间隔一个数据取值。

在华为机型上,获取 mY,mU,mVPixelStride分别为1,2,2,说明UV存储方式是交叉的。并且mPlanes[0]PixelStride一定是1mPlanes[1]mPlanes[2]PixelStride一定是相同的。

在实际的过程中,你可能发现拿到这个数据合成图像时仍有问题,这个就需要判断RowStride,为什么需要判断呢?RowStridewidth可能不一样,一样的话不存在问题,不一样的话,需要进行补位。

具体的可以参考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,ContentResolverMetadata,然后调用开始录制接口

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,ContentResolverMetadata几种方式和基本使用。

  • 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();

参考

  1. CameraX 概览
  2. CameraX 视频捕获架构

版权声明:本文为c_he_n原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。