什么是Android CameraX
CameraX 是一个 Jetpack 支持库,旨在帮助您简化相机应用的开发工作。它提供一致且易于使用的 API Surface,适用于大多数 Android 设备,并可向后兼容至 Android 5.0(API 级别 21)。
虽然它利用的是 camera2 的功能,但使用的是更为简单且基于用例的方法,该方法具有生命周期感知能力。它还解决了设备兼容性问题,因此您无需在代码库中添加设备专属代码。这些功能减少了将相机功能添加到应用时需要编写的代码量。
官方教程也很详细,如下: 官方教程
Add the Gradle dependencies
- Open the
build.gradle(Module: app)
file and add the CameraX dependencies to our app Gradle file, inside the dependencies section:
def camerax_version = "1.0.0-alpha05"
// CameraX core library using camera2 implementation
implementation "androidx.camera:camera-camera2:$camerax_version"
// CameraX Lifecycle Library
- CameraX needs some methods that are part of Java 8, so we need to set our compile options accordingly. At the end of the
android
block, right afterbuildTypes
, add the following:
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
3、Request camera permissions
public boolean checkPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission(
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.CAMERA
}, 1);
}
return false;
}
4、Implement Preview use case
创建ImageCaptureConfig和ImageCapture这两个对象,用imageCapture.takePicture方法传入相片保存地址就行了。当然在生命周期绑定中也加上imageCapture。
ImageCaptureConfig可以定制相片尺寸和长宽比例,这里的尺寸和比例跟相机预览的尺寸比例无关,我测试传入任何比例都能得到图片。
// 2. capture
ImageCaptureConfig imageCaptureConfig = new ImageCaptureConfig.Builder()
.setTargetAspectRatio(new Rational(1,1))
.setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY)
.build();
final ImageCapture imageCapture = new ImageCapture(imageCaptureConfig);
viewFinder.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
File photo = new File(getExternalCacheDir() + "/" + System.currentTimeMillis() + ".jpg");
imageCapture.takePicture(photo, new ImageCapture.OnImageSavedListener() {
@Override
public void onImageSaved(@NonNull File file) {
showToast("saved " + file.getAbsolutePath());
}
@Override
public void onError(@NonNull ImageCapture.UseCaseError useCaseError, @NonNull String message, @Nullable Throwable cause) {
showToast("error " + message);
cause.printStackTrace();
}
});
return true;
}
});
CameraX.bindToLifecycle(this, preview, imageCapture);
- Implement ImageCapture use case
给TextureView设置布局变化的监听,用updateTransform()更新相机预览,然后startCamera()启动相机
TextureView viewFinder = findViewById(R.id.view_finder);
viewFinder.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View view, int i, int i1, int i2, int i3, int i4, int i5, int i6, int i7) {
updateTransform();
}
});
viewFinder.post(new Runnable() {
@Override
public void run() {
startCamera();
}
});
更新相机预览:主要是给TextureView设置一个旋转的矩阵变化,防止预览方向不对
private void updateTransform() {
Matrix matrix = new Matrix();
// Compute the center of the view finder
float centerX = viewFinder.getWidth() / 2f;
float centerY = viewFinder.getHeight() / 2f;
float[] rotations = {0,90,180,270};
// Correct preview output to account for display rotation
float rotationDegrees = rotations[viewFinder.getDisplay().getRotation()];
matrix.postRotate(-rotationDegrees, centerX, centerY);
// Finally, apply transformations to our TextureView
viewFinder.setTransform(matrix);
}
启动相机:创建PreviewConfig和Preview这两个对象,可以设置预览图像的尺寸和比例,在OnPreviewOutputUpdateListener回调中用setSurfaceTexture方法,将相机图像输出到TextureView。最后用CameraX.bindToLifecycle方法将相机与当前页面的生命周期绑定。
private void startCamera() {
// 1. preview
PreviewConfig previewConfig = new PreviewConfig.Builder()
.setTargetAspectRatio(new Rational(1, 1))
.setTargetResolution(new Size(640,640))
.build();
Preview preview = new Preview(previewConfig);
preview.setOnPreviewOutputUpdateListener(new Preview.OnPreviewOutputUpdateListener() {
@Override
public void onUpdated(Preview.PreviewOutput output) {
ViewGroup parent = (ViewGroup) viewFinder.getParent();
parent.removeView(viewFinder);
parent.addView(viewFinder, 0);
viewFinder.setSurfaceTexture(output.getSurfaceTexture());
updateTransform();
}
});
CameraX.bindToLifecycle(this, preview);
这样就实现了基本的相机预览功能。这几个方法都很简单明了,对外只依赖一个TextureView。生命周期自动绑定,这意味着代码可以写在一块,在一处调用。不像以前这里插一段代码,那里插一段代码。
还有最大的好处,就是可扩展性。相机预览使用了PreviewConfig和Preview两个对象,加入新的相机功能同样是加两个对象XXXConfig和XXX,其他地方都不同改!
加入拍照功能就加入ImageCaptureConfig和ImageCapture,加入图像分析功能就加入ImageAnalysisConfig和ImageAnalysis,非常方便统一。
6、Implement ImageAnalysis use case
图片分析名字很高大上,实际上就是图像数据回调,实时获取相机的图像数据,可以自己处理这些图像。
创建ImageAnalysisConfig和ImageAnalysis这两个对象,创建一个HandlerThread用于在子线程中处理数据,创建一个ImageAnalysis.Analyzer接口实现类,在analyze(ImageProxy imageProxy, int rotationDegrees)回调方法中就能拿到图像数据了。当然ImageAnalysis对象也要绑定生命周期。
我这里分析图像数据用了之前写的一个工具YUVDetectView,来分析图像属于哪种YUV420格式。
// 3. analyze
HandlerThread handlerThread = new HandlerThread("Analyze-thread");
handlerThread.start();
ImageAnalysisConfig imageAnalysisConfig = new ImageAnalysisConfig.Builder()
.setCallbackHandler(new Handler(handlerThread.getLooper()))
.setImageReaderMode(ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
.setTargetAspectRatio(new Rational(2, 3))
// .setTargetResolution(new Size(600, 600))
.build();
ImageAnalysis imageAnalysis = new ImageAnalysis(imageAnalysisConfig);
imageAnalysis.setAnalyzer(new MyAnalyzer());
CameraX.bindToLifecycle(this, preview, imageCapture, imageAnalysis);
private class MyAnalyzer implements ImageAnalysis.Analyzer {
@Override
public void analyze(ImageProxy imageProxy, int rotationDegrees) {
final Image image = imageProxy.getImage();
if(image != null) {
Log.d("chao", image.getWidth() + "," + image.getHeight());
imageView.input(image);
}
}
}
摄像头数据处理
就图像而言,首先需要获得摄像头采集的数据,然后得到这个byte[] 进行编码,再进行后续的封包与发送。我们通 过CameraX图像分析接口得到的数据为ImageProxy(Image的代理类)。那么怎么从ImageProxy/Image 中获取 我们需要的数据呢,这个数据格式是什么?
@Override
public void analyze(ImageProxy image, int rotationDegrees) {
Log.i(TAG, "analyze: " + image.getWidth() + " height " + image.getHeight() + " rotationDegrees:" + rotationDegrees);
//图像格式
int format = image.getFormat();
if (format != ImageFormat.YUV_420_888) {
Log.i(TAG, "analyze: format:" + format);
}
lock.lock();
ImageProxy.PlaneProxy[] planes = image.getPlanes();
lock.unlock();
}
可以通过 getPlanes方法得到PlaneProxy数组。PlaneProxy为Image.Plane代理,同ImagePrxoy与Image的关系 一样。
/** A plane proxy which has an analogous interface as {@link android.media.Image.Plane}. */
interface PlaneProxy {
/**
* Returns the row stride.
*
* <p>@see {@link android.media.Image.Plane#getRowStride()}.
*/
int getRowStride();
/**
* Returns the pixel stride.
*
* <p>@see {@link android.media.Image.Plane#getPixelStride()}.
*/
int getPixelStride();
/**
* Returns the pixels buffer.
*
* <p>@see {@link android.media.Image.Plane#getBuffer()}.
*/
ByteBuffer getBuffer();
}
YUV420根据颜色数据的存储顺序不同,又分为了多种不同的格式,这些格式实际存储的信息还是完全一致的。举 例来说,对于4x4的图片,在YUV420下,任何格式都有16个Y值,4个U值和4个V值,不同格式只是Y、U和V的排 列顺序变化。I420 为 YYYYYYYYYYYYYYYYUUUUVVVV ,YUV420 是一类格式的集合,YUV420并不能完全确定颜色数据的存储顺序。
PlaneProxy/Plane
Y、U和V三个分量的数据分别保存在三个Plane类中,即通过 getPlanes()得到的数组。 Plane 实际是对ByteBuffer的封装。
Image保证了planes[0]一定是Y,planes[1]一定是U,planes[2]一定是V。且对于plane [0],Y分量数据一定是连续存储的,中间不会有U或V数据穿插,也就是说我们一定能够一次性得到所有Y分量的值。
但是对于UV数据,可能存在以下两种情况:
1. planes[1] = {UUUU...},planes[2] = {VVVV...}; //I420
2. planes[1] = {UVUV...},planes[2] = {VUVU...}。
PixelStride
所以在我么取数据时需要在根据Plane中的另一个信息来确定如何取对应的U或者V数据。
// 行内数据值间隔
// 1:表示无间隔取值,即为上面的第一种情况
// 2: 表示需要间隔一个数据取值,即为上面的第二种情况
int pixelStride = plane.getPixelStride();
根据这个属性,我们将确定数据如何存储,因此如果需要取出代表I420格式的byte[],则为:YUV420中,Y数据长度为: width*height , 而U、V都为:width / 2 * height / 2。
ImageProxy.PlaneProxy[] planes = image.getPlanes();
//y数据的这个值只能是:1
int pixelStride = planes[0].getPixelStride();
int pixelStride2 = planes[1].getPixelStride();
Log.i(TAG, "pixelStride: " + pixelStride+" pixelStride2: "+ pixelStride2 );
pixelStride: 1 pixelStride2: 2
小米手机运行 planes[0] 的PixelStride 为1 planes[1]的 PixelStride为2
说明是UVUV交叉存储。
// Y数据 pixelStride一定为1
int pixelStride = planes[0].getPixelStride();
planes[0].getBuffer() // Y数据
byte[] u = new byte[image.getWidth() / 2 * image.getHeight() / 2];
int pixelStride = planes[1].getPixelStride();
if (pixelStide == 1) {
planes[1].getBuffer() // U数据
} else if (pixelStide == 2) {
ByteBuffer uBuffer = planes[1].getBuffer()
for (int i = 0; i < uBuffer.remaining(); i+=2) {
u[i] = uBuffer.get(); //丢弃一个数据,这个数据其实是V数据,但是我们还是到planes[2]中获取V数据
uBuffer.get();
}
}
但是如果使用上面的代码去获取YUV数据,可能你会惊奇的发现,并不是在所有你设置的Width与 Height(分辨率)下都能够正常运行。我们忽略了什么,为什么会出现问题呢?
在Plane中 我们已经使用了 getBuffer
与 getPixelStride
两个方法,但是还有一个 getRowStride没有用到.
RowStride
RowStride表示行步长,Y数据对应的行步长可能为:
- 等于Width;
- 大于Width;
以4x4的I420为例,其数据可以看为
如果RowStride等于Width,那么我们直接通过 planes[0].getBuffer() 获得Y数据没有问题。
但是如果RowStride大于Width,比如对于4x4的I420,如果每行需要以8字节对齐,那么可能得到的RowStride不等于4(Width),而是得到8。那么此时会在每行数据末尾补充占位的无效数据:
获取Y数据
ImageProxy.PlaneProxy[] planes = image.getPlanes();
// todo 避免内存抖动.
int size = image.getWidth() * image.getHeight() * 3 / 2;
if (yuv420 == null || yuv420.capacity() < size) {
yuv420 = ByteBuffer.allocate(size);
}
yuv420.position(0);
/**
* Y数据
*/
ImageProxy.PlaneProxy plane = planes[0];//y数据
//pixelStride = 1 : 取值无间隔
//pixelStride = 2 : 间隔1个字节取值
// y的此数据应该都是1
int pixelStride = plane.getPixelStride();//Y的肯定为1
//大于等于宽, 表示连续的两行数据的间隔
// 如:640x480的数据,
// 可能得到640
// 可能得到650,表示每行最后10个字节为补位的数据
int rowStride = plane.getRowStride();//rowStride 可能末尾有填充
ByteBuffer buffer = plane.getBuffer();
byte[] row = new byte[image.getWidth()];
// 每行要排除的无效数据,但是需要注意:实际测试中 最后一行没有这个补位数据
byte[] skipRow = new byte[rowStride - image.getWidth()];
for (int i = 0; i < image.getHeight(); i++) {
buffer.get(row);
yuv420.put(row);
// 不是最后一行
if (i < image.getHeight() - 1) {
buffer.get(skipRow);//最后一行因为后面跟着U 数据,没有无效占位数据,不需要丢弃
}
}
而对于U与V数据,对应的行步长可能为:
- 等于Width;
- 大于Width;
- 等于Width/2;
- 大于Width/2
等于Width
这表示,我们获得planes[1]中不仅包含U数据,还会包含V的数据,此时pixelStride==2。
U | V | U | V |
---|---|---|---|
U | V | U | V |
那么V数据:planes[2],则为:
V | U | V | U |
---|---|---|---|
V | U | V | U |
大于Width
与Y数据一样,可能由于字节对齐,出现RowStride大于Width的情况,与等于Width一样,planes[1]中不仅包含U 数据,还会包含V的数据,此pixelStride==2。
U | V | U | V | 0 | 0 | 0 | 0 |
---|---|---|---|---|---|---|---|
U | V | U | V | 最后一行没有站位 |
---|---|---|---|---|
planes[2],则为:
V | U | V | U | 0 | 0 | 0 | 0 |
---|---|---|---|---|---|---|---|
V | U | V | U | 最后一行没有站位 |
---|---|---|---|---|
等于Width/2
当获取的U数据对应的RowStride等于Width/2,表示我们得到的planes[1]只包含U数据。此时pixelStride==1。 那么planes[1]+planes[2]为:
U | U | ||
---|---|---|---|
U | U | ||
V | V | ||
V | V |
这种情况,所有的U数据是连在一起的,即 planes[1].getBuffer 可以直接获得完整的U数据。
大于Width/2
同样我们得到的planes[1]只包含U数据,但是与Y数据一样,可能存在占位数据。此时pixelStride==1。 planes[1]+planes[2]为:
U | U | 0 | 0 | 0 | 0 | 0 | 0 |
---|---|---|---|---|---|---|---|
U | U | 最后一行没有站位 |
---|---|---|
V | V | 0 | 0 | 0 | 0 | 0 | 0 |
---|---|---|---|---|---|---|---|
V | V | 最后一行没有站位 |
---|---|---|
获取UV数据
/**
* U V 数据
*/
for (int i = 1; i < 3; i++) {//planes[1] | planes[2] uv数据处理
plane = planes[i];
pixelStride = plane.getPixelStride();//1 I420 2 交错packed UVUV
// uv数据的rowStride可能是
// 如:640的宽
// 可能得到320, pixelStride 为1
// 可能大于320同时小于640,有为了补位的无效数据 pixelStride 为1
// 可能得到640 uv数据在一起,pixelStride为2
// 可能大于640,有为了补位的无效数据 pixelStride为2
rowStride = plane.getRowStride();
buffer = plane.getBuffer();
int uvWidth = image.getWidth() / 2;
int uvHeight = image.getHeight() / 2;
for (int j = 0; j < uvHeight; j++) {
for (int k = 0; k < rowStride; k++) {
// 最后一行,是没有补位数据的
if (j == uvHeight - 1) {
//只有自己(U/V)的数据
if (pixelStride == 1) {
// 结合外层if 则表示:
// 如果是最后一行,我们就不管结尾的占位数据了
if (k >= uvWidth) {
break;
}
} else if (pixelStride == 2) {
//与同级if相同意思
// todo uv混合,
// planes[2]:uvu
// planes[3]:vuv
if (k >= image.getWidth() - 1) {
break;
}
}
}
byte b = buffer.get();
if (pixelStride == 2) {
//打包格式 uv在一起,偶数位取出来是U数据: 0 2 4 6
if (k < image.getWidth() && k % 2 == 0) {
yuv420.put(b);
}
} else if (pixelStride == 1) {
if (k < uvWidth) {
yuv420.put(b);
}
}
}
}
}
YUV简介
与RGB类似,YUV也是一种颜色编码方法,主要用于视频领域,它将亮度信息(Y)与色彩信息(UV)分离,没有UV信息一样可以显示完整的图像,只不过是黑白的,比如这样的设计解决了彩色电视机与黑白电视的兼容问题。
YUV,分为三个分量,“Y”表示的是明亮度(Luminance或Luma),也就是灰度值;而“U”和“V” 表示的则是色度(Chrominance或Chroma),作用是用于指定像素的颜色。
UV 即CbCr(C代表颜色,b代表蓝色,r代表红色)
分类
YUV格式有两大类:==平面(planar)和紧凑(packed==)。
对于planar的YUV格式,先连续存储所有像素点的Y,紧接着存储所有像素点的U,随后是存储所有像素点的V,或者是先v后u
对于packed的YUV格式,每个像素点的Y,U,V是连续交替存储的。
采样
主流的采样方式有三种,YUV4:4:4,YUV4:2:2,YUV4:2:0
YUV 4:4:4采样,每一个Y对应一组UV分量,一个YUV占8+8+8 = 24bits 3个字节。
YUV 4:2:2采样,每两个Y共用一组UV分量,一个YUV占8+4+4 = 16bits 2个字节。
YUV 4:2:0采样,每四个Y共用一组UV分量,一个YUV占8+2+2 = 12bits 1.5个字节。
最常见的YUV420P和YUV420SP都是基于4:2:0采样的,所以如果图片的宽为width,高为heigth,在内存中占的空间为width * height * 3 / 2,其中前width * height的空间存放Y分量,接着width * height / 4存放U分量,最后width * height / 4存放V分量。
YUV格式
常见的YUV格式有YUY2、YUYV、YVYU、UYVY、AYUV、Y41P、Y411、Y211、IF09、IYUV、YV12、YVU9、YUV411、YUV420等,Android中比较常见是YUV420
分为两种:YUV420P
和YUV420SP
。所以就先了解下YUV420P
和YUV420SP
.
libX264中对YUV各种格式的定义。
#define X264_CSP_I400 0x0001 /* monochrome 4:0:0 */
#define X264_CSP_I420 0x0002 /* yuv 4:2:0 planar */
#define X264_CSP_YV12 0x0003 /* yvu 4:2:0 planar */
#define X264_CSP_NV12 0x0004 /* yuv 4:2:0, with one y plane and one packed u+v */
#define X264_CSP_NV21 0x0005 /* yuv 4:2:0, with one y plane and one packed v+u */
#define X264_CSP_I422 0x0006 /* yuv 4:2:2 planar */
#define X264_CSP_YV16 0x0007 /* yvu 4:2:2 planar */
#define X264_CSP_NV16 0x0008 /* yuv 4:2:2, with one y plane and one packed u+v */
#define X264_CSP_YUYV 0x0009 /* yuyv 4:2:2 packed */
#define X264_CSP_UYVY 0x000a /* uyvy 4:2:2 packed */
#define X264_CSP_V210 0x000b /* 10-bit yuv 4:2:2 packed in 32 */
#define X264_CSP_I444 0x000c /* yuv 4:4:4 planar */
#define X264_CSP_YV24 0x000d /* yvu 4:4:4 planar */
YUV420P
YUV420P是平面模式,Y , U , V分别在不同平面,也就是有三个平面,它是YUV标准格式4:2:0
那么真实的在字节流中就是按照行从左到右一行一行的拼起来的:
==YUV420P分为:YU12和YV12==
YU12格式
在Android中也叫作I420格式,首先是所有Y值,然后是所有U值,最后是所有V值。比如6x6的图片,内存大小就是6x6x3/2=54个字节。为了更清晰的查看,我们换行看,真实的是一行byte[]数据流。
YYYYYY
YYYYYY
YYYYYY
YYYYYY
UUUUUU
VVVVVV
YV12格式
YV12格式与YU12基本相同,首先是所有Y值,然后是所有V值,最后是所有U值。比如6x6的图片,内存大小就是6x6x3/2=54个字节
YYYYYY
YYYYYY
YYYYYY
YYYYYY
VVVVVV
UUUUUU
YUV420SP
YUV420SP 也是是平面模式。分为NV21和NV12两种格式。Y是一个平面,UV是一个平面,UV/VU为交替存储,而不是分为三个平面。
在Android Camera中文档中强烈推荐使用NV21
和YV12
,因为这两种格式支持所有的相机设备。Camera默认输出YUV的数据格式为NV21。但是在Camera2中,推荐使用的格式则是YUV_420_888
。
NV21格式
在Android Camera中手机从摄像头采集的预览数据默认值是NV21。
NV21存储顺序是先存Y值,再VU交替存储:YYYYVUVUVU,比如6x6的图片,内存大小就是6x6x3/2=54个字节
YYYYYY
YYYYYY
YYYYYY
YYYYYY
VUVUVU
VUVUVU
NV12格式
NV12存储顺序是先存Y值,再UV交替存储:YYYYUVUVUV,比如6x6的图片,内存大小就是6x6x3/2=54个字节
YYYYYY
YYYYYY
YYYYYY
YYYYYY
UVUVUV
UVUVUV
这里先熟悉下Android中常见的YUV420P
和YUV420SP
。一般我们在使用yuv数据的时候,会对yuv数据进行变换,比如:摄像头数据旋转,从一种格式转为另一种数据等。