https://blog.csdn.net/j12345678901/article/details/78110640
为了统一和兼容各个平台图像数据格式的差异,和提供更丰富的相机参数设置,Android5.0之后推出了camera2 API,一般的我们会使用相机有几种需求
- 预览
- 拍照
- 录像
- 获取图像原始数据
这些需求在官方给的一系列demo中都有示例,我也对Camera2Basic写过一篇笔记android-camera2basic源码逻辑流程解析 ,今天还是一个笔记,记录下第四个需求的问题。
首先,YUV420是一系列格式,从这个名字只能确定Y:U:V是4:1:1,具体的有YUV420P、YUV420SP、NV21、YV12等等。
新API提供了ImageReader这样一个类,来帮助开发者获取每一帧的原始数据,同时提供了实例化方法
ImageReader reader = ImageReader.newInstance(int width, int height, int format, int maxImages);
1
这里第三个参数是指定从ImageReader中获取的原始数据的格式。提个醒,不要太相信这个参数,这里指示不具体是一,就算指定了,摄像头不一定支持这个参数,可能会给出相似的格式的数据也说不准。
先说一个Android里面很有趣的玩笑。跳到newInstance()源码里面可以看到这句
if (format == ImageFormat.NV21) {
throw new IllegalArgumentException(
"NV21 format is not supported");
}
1
2
3
4
Android提供另一个YUV数据的帮助类YuvImage,构造函数
public YuvImage(byte[] yuv, int format, int width, int height, int[] strides) {
if (format != ImageFormat.NV21 &&
format != ImageFormat.YUY2) {
throw new IllegalArgumentException(
"only support ImageFormat.NV21 " +
"and ImageFormat.YUY2 for now");
}
//ellipsis code
mData = yuv;
mFormat = format;
mWidth = width;
mHeight = height;
}
mageReader不支持NV21
YuvIamge只支持NV21和YUY2
[捂脸]What?这是什么意思?有了解这块为什么这样的同学,请不吝赐教。
说回正点上。
在ImageReader实例化时传入ImageFormat.YUV_420_888,得到的数据到底是什么样的?网上有很多经典的讲YUV数据的各种格式,简单来讲就是三个通道数据比例是4:1:1的,有效数据量大小为width*height×2/3,但是从Android中拿到的Plane中的byte怎么提取,却很少有人提及。
也就是说,一般我们调用其他的api,比如人脸识别、物体识别等API的时候,要求传入的yuvData都是byte[]类型的。我们需要从Image->Plane->Buffer->byte[],这样拿到最终的byte,这个过程看着简单,网上的其他解析文章也都是,简单的直接从三个Plane的Buffer中直接执行如下代码,然后将三个byte直接拼接就行了。[捂脸]
byte[] bytes = new byte[buffer.capacity()];
buffer.get(bytes);
1
2
但是我从Iamge的Plane中拿数据的时候确遇到了很多问题。直接按正确的取数据的过程说吧。
1. Image中拿到width,height,和Planes[]
2. 每一个plane中拿到pixelsStride和rowStride
3. pixelsStride:像素步长,有可能是1、有可能是2,如果是1也就是说U、V的数据是紧密排列的,如果是2,就是每隔一位是有效的,可以理解在U和V的buffer中数据是类似u0u0u0u0u0u0u0u…和v0v0v0v0v0v0v0v0…0表示那一位byte无效。理解是可以这么理解,但实际上数据的排列是uvuvuvuvuvuvu…vuvuvuvuvuvuv…,也就是说API从sensor那边取过来的时候的数据就是uv交错的,因为这个有效位置的作用,U的数据把第一位去掉,最后补上一位之后就成了V的数据。所以像YUV420sp格式本身就要求图像是UV交错,可以直接取U的数据,补上最后一位,就OK了。但官方没有指出可以这么取,所以正确性并不能保证。实际中我试着这么取过,只有图像右下角最后一个像素是有问题的。如果不介意这个,可以尝试直接取U的数据。这一点在另外一片文章里面看到的,说的比较清楚。链接在这Image类浅析(结合YUV_420_888) 这里其实也好理解,sensor在处理的时候是逐行扫描的,YUV都是连续生成的,为了节省存储,可能将uv的数据交错合并输出给Android的framework层。
4. rowStride:“每行数据”的“宽度”,注意这里也有个坑,这个rowStride不一定是和width一样,有的相机输出的比图片本身的width要大,需要“逐行截取”。
5. 还有一种情况,就是上面那个链接中提到的CropRect的问题,应该会是显示部分正确图像,鉴于我没遇见过,就不瞎猜了。
结合上面的理解,我自己画了一些图。以6*4的图片为例,bytebuffer的排列可以理解如下:
在这里width=6,height=4,rowStride=6或者8,等于8时,最后两列会由于某些原因空一些byte,如果你转成rgb图像预览发现有规律的绿色栅格,那么考虑rowStride>width这种情况。
当然这张图只是说可以这么理解,实际上拿到的一维的byte数组,是每行数据接出来的如下:
且有:
yBytes.length==w*h;
uBytes.length==w*h/4;
vBytes.length==w*h/4;
plane[0]==rowStride*h;
if(pixelsStride==2)
rowStride==w/2+temp;
plane[1].length==plane[2].length==rowStride*h/2-1
else if(pixelsStride=1)
rowStride==w/2+temp;
plane[1].length==plane[2].length==rowStride*h/2
1
2
3
4
5
6
7
8
9
10
最后附上我写的工具类:(这里为了节省内存,Y的数据直接copy到了最终的bytes里面,也可以)
/**
* yuv420p: yyyyyyyyuuvv
* yuv420sp: yyyyyyyyuvuv
* nv21: yyyyyyyyvuvu
*/
public class ImageUtil {
public static final int YUV420P = 0;
public static final int YUV420SP = 1;
public static final int NV21 = 2;
private static final String TAG = "ImageUtil";
/***
* 此方法内注释以640*480为例
* 未考虑CropRect的
*/
public static byte[] getBytesFromImageAsType(Image image, int type) {
try {
//获取源数据,如果是YUV格式的数据planes.length = 3
//plane[i]里面的实际数据可能存在byte[].length <= capacity (缓冲区总大小)
final Image.Plane[] planes = image.getPlanes();
//数据有效宽度,一般的,图片width <= rowStride,这也是导致byte[].length <= capacity的原因
// 所以我们只取width部分
int width = image.getWidth();
int height = image.getHeight();
//此处用来装填最终的YUV数据,需要1.5倍的图片大小,因为Y U V 比例为 4:1:1
byte[] yuvBytes = new byte[width * height * ImageFormat.getBitsPerPixel(ImageFormat.YUV_420_888) / 8];
//目标数组的装填到的位置
int dstIndex = 0;
//临时存储uv数据的
byte uBytes[] = new byte[width * height / 4];
byte vBytes[] = new byte[width * height / 4];
int uIndex = 0;
int vIndex = 0;
int pixelsStride, rowStride;
for (int i = 0; i < planes.length; i++) {
pixelsStride = planes[i].getPixelStride();
rowStride = planes[i].getRowStride();
ByteBuffer buffer = planes[i].getBuffer();
//如果pixelsStride==2,一般的Y的buffer长度=640*480,UV的长度=640*480/2-1
//源数据的索引,y的数据是byte中连续的,u的数据是v向左移以为生成的,两者都是偶数位为有效数据
byte[] bytes = new byte[buffer.capacity()];
buffer.get(bytes);
int srcIndex = 0;
if (i == 0) {
//直接取出来所有Y的有效区域,也可以存储成一个临时的bytes,到下一步再copy
for (int j = 0; j < height; j++) {
System.arraycopy(bytes, srcIndex, yuvBytes, dstIndex, width);
srcIndex += rowStride;
dstIndex += width;
}
} else if (i == 1) {
//根据pixelsStride取相应的数据
for (int j = 0; j < height / 2; j++) {
for (int k = 0; k < width / 2; k++) {
uBytes[uIndex++] = bytes[srcIndex];
srcIndex += pixelsStride;
}
if (pixelsStride == 2) {
srcIndex += rowStride - width;
} else if (pixelsStride == 1) {
srcIndex += rowStride - width / 2;
}
}
} else if (i == 2) {
//根据pixelsStride取相应的数据
for (int j = 0; j < height / 2; j++) {
for (int k = 0; k < width / 2; k++) {
vBytes[vIndex++] = bytes[srcIndex];
srcIndex += pixelsStride;
}
if (pixelsStride == 2) {
srcIndex += rowStride - width;
} else if (pixelsStride == 1) {
srcIndex += rowStride - width / 2;
}
}
}
}
image.close();
//根据要求的结果类型进行填充
switch (type) {
case YUV420P:
System.arraycopy(uBytes, 0, yuvBytes, dstIndex, uBytes.length);
System.arraycopy(vBytes, 0, yuvBytes, dstIndex + uBytes.length, vBytes.length);
break;
case YUV420SP:
for (int i = 0; i < vBytes.length; i++) {
yuvBytes[dstIndex++] = uBytes[i];
yuvBytes[dstIndex++] = vBytes[i];
}
break;
case NV21:
for (int i = 0; i < vBytes.length; i++) {
yuvBytes[dstIndex++] = vBytes[i];
yuvBytes[dstIndex++] = uBytes[i];
}
break;
}
return yuvBytes;
} catch (final Exception e) {
if (image != null) {
image.close();
}
Log.i(TAG, e.toString());
}
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
结论:基于一些特殊的sensor,Android API给出的YUV数据,需要根据rowStride,pixelsStride重新筛选拼接,才能得到正确的数据。
参考:
Image类浅析(结合YUV_420_888)