Android PAI 对 YUV420_888的介绍 ,大致意思如下:
它是YCbCr的泛化格式,能够表示任何4:2:0的平面和半平面格式,每个分量用8 bits 表示。带有这种格式的图像使用3个独立的Buffer表示,每一个Buffer表示一个颜色平面(Plane),除了Buffer外,它还提供rowStride、pixelStride来描述对应的Plane。
使用Image的getPlanes()获取plane数组:
Image.Plane[] planes = image.getPlanes();
它保证planes[0]
总是Y ,planes[1]
总是U(Cb),planes[2]
总是V(Cr)。并保证Y-Plane永远不会和U/V交叉(yPlane.getPixelStride()
总是返回 1 )。U/V-Plane总是有相同的rowStride
和pixelStride()
(即有:uPlane.getRowStride() == vPlane.getRowStride() 和 uPlane.getPixelStride() == vPlane.getPixelStride();
)。
我测试几个设备没有找到存储格式是Planar的设备,这里使用参考2的例子简单说一下:
Log.i(TAG,"image format: " +image.getFormat());
// 从image里获取三个plane
Image.Plane[] planes = image.getPlanes();
for (int i = 0; i < planes.length; i++) {
ByteBuffer iBuffer = planes[i].getBuffer();
int iSize = iBuffer.remaining();
Log.i(TAG, "pixelStride " + planes[i].getPixelStride());
Log.i(TAG, "rowStride " + planes[i].getRowStride());
Log.i(TAG, "width " + image.getWidth());
Log.i(TAG, "height " + image.getHeight());
Log.i(TAG, "Finished reading data from plane " + i);
}
getPixelStride()
获取行内连续两个颜色值之间的距离(步长)。
getRowStride()
获取行间像素之间的距离。
输出如下:
image format: 35
pixelStride 1
rowStride 1920
width 1920
height 1080
buffer size 2073600
Finished reading data from plane 0
pixelStride 1
rowStride 960
width 1920
height 1080
buffer size 518400
Finished reading data from plane 1
pixelStride 1
rowStride 960
width 1920
height 1080
buffer size 518400
Finished reading data from plane 2
在ImageFormat中,YUV_420_888
格式的数值是35,如上所示,可知当前Preview格式是YUV_420_888
,根据image的分辨率是 1920 x 1080 ,像素点个数是2073600 。下面分别对plane[0]、plane[1]、plane[2]作分析。
rowStride
是1920 ,其pixelStride
是1 ,说明Y存储时中间无间隔,每行1920个像素全是Y值,buffer size 是 plane[0]的1/4 ,buffer size / rowStride= 1080
可知Y有1080行。rowStride
是960 ,其pixelStride
也是1,说明连续的U之间没有间隔,每行只存储了960个数据,buffer size 是 plane[0]的1/4 ,buffer size / rowStride = 540
可知U有540行,对于U来说横纵都是1/2采样。此时,YUV三个量分离,每一块数据单独存储在独立的plane里。此时的YUV420叫做YUV420P或I420,以分辨率8 x 4 为例其存储结构:
Nexus 6P和东东那部三星机都属于此类,采用相同的代码输出如下:
image format: 35
pixelStride 1
rowStride 1920
width 1920
height 1080
buffer size 2073600
Finished reading data from plane 0
pixelStride 2
rowStride 1920
width 1920
height 1080
buffer size 1036800
Finished reading data from plane 1
pixelStride 2
rowStride 1920
width 1920
height 1080
buffer size 1036800
Finished reading data from plane 2
image格式依然是YUV_420_888
,分辨率是1920 x 1080 。
buffer size / rowStride = 1080
Y数据有1080行。buffer size / rowStride = 540
只有540行,说明纵向采样也是1/2 ,但buffer size 是 plane[0]的 1/2而不是1/4, 连续的U之间到底存储了什么数据,才使得buffer size 变为plane[0]的1/2了?通过如下方法分别打印Y、U、V三个buffer 到文件中(十六进制格式),来看一下plane[1]和plane[2]中存储数据的特点:
// Y-buffer
ByteBuffer yBuffer = planes[0].getBuffer();
int ySize = yBuffer.remaining();
byte[] yBytes = new byte[ySize];
yBuffer.get(yBytes);
// U-buffer
ByteBuffer uBuffer = planes[1].getBuffer();
int uSize = uBuffer.remaining();
byte[] uBytes = new byte[uSize];
uBuffer.get(uBytes);
// V-buffer
ByteBuffer vBuffer = planes[2].getBuffer();
int vSize = vBuffer.remaining();
byte[] vBytes = new byte[vSize];
vBuffer.get(vBytes);
String yFileName = "Y";
String uFileName = "U";
String vFileName = "V";
// 保存目录
File dir = new File(mRootDir + File.separator + "YUVV");
if (!dir.exists()) {
dir.mkdir();
}
// 文件名
File yFile = new File(dir.getAbsolutePath() + File.separator + yFileName + ".yuv");
File uFile = new File(dir.getAbsolutePath() + File.separator + uFileName + ".yuv");
File vFile = new File(dir.getAbsolutePath() + File.separator + vFileName + ".yuv");
try {
// 以字符方式书写
Writer yW = new FileWriter(yFile);
Writer uW = new FileWriter(uFile);
Writer vW = new FileWriter(vFile);
for (int i = 0; i < ySize; i++) {
String preValue = Integer.toHexString(yBytes[i]); // 转为16进制
// 因为byte[] 元素是一个字节,这里只取16进制的最后一个字节
String lastValue = preValue.length() > 2 ? preValue.substring(preValue.length() - 2) : preValue;
yW.write(" " + lastValue + " "); // 写入文件
if ((i + 1) % 20 == 0) { // 每行20个
yW.write("\n");
}
}
yW.close();
for (int i = 0; i < uSize; i++) {
String preValue = Integer.toHexString(uBytes[i]);
String lastValue = preValue.length() > 2 ? preValue.substring(preValue.length() - 2) : preValue;
uW.write(" " + lastValue + " ");
if ((i + 1) % 20 == 0) {
uW.write("\n");
}
}
uW.close();
for (int i = 0; i < vSize; i++) {
String preValue = Integer.toHexString(vBytes[i]);
String lastValue = preValue.length() > 2 ? preValue.substring(preValue.length() - 2) : preValue;
vW.write(" " + lastValue + " ");
if ((i + 1) % 20 == 0) {
vW.write("\n");
}
}
vW.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
打开U.yuv和V.yuv :
U.yuv文件 :
80 7b 80 7b 80 7b 80 7b 80 7b 80 7b 80 7b 80 7c 80 7c 80 7c
...
V.yuv文件:
7b 80 7b 80 7b 80 7b 80 7b 80 7b 80 7b 80 7b 80 7c 80 7c 80
...
将V.yuv错开一位 :
U : 80 7b 80 7b 80 7b 80 7b 80 7b 80 7b 80 7b 80 7c 80 7c 80 7c ...
V : 7b 80 7b 80 7b 80 7b 80 7b 80 7b 80 7b 80 7b 80 7c 80 7c 80 ...
可以发现U和V错开一位后,对应位相等,实际上:
plane[1] : UVUVUVUVUVUVUVUV...
plane[2] : VUVUVUVUVUVUVUVU...
这就是为什么plane[1]和plane[2]的buffer size 是plane[0]的1/2而不是1/4的原因。
看8 x 4的NV12存储结构(NV21只是UV交错顺序相反):
plane[0] + plane[1] 可得NV12
plane[0] + plane[2] 可得NV21
参考3中获取I420和NV21的方法是:先从plane[0]中提取出Y数据,然后在plane[1]中提取U数据,最后在plane[2]中提取V数据。
两种方法通过Shader解码后都已得到正确的预览图像。
如上两个工具不能打开以上所得的NV12和NV21图像,但若是以plane[0] + plane[1] + plane[2] 顺序写入文件,在上述工具中选择NV12格式可以打开。
参考1 : ImageFormat#YUV_420_888
参考2 : Android: Image类浅析(结合YUV_420_888)
参考3 : Android: YUV_420_888编码Image转换为I420和NV21格式byte数组