YUV基础知识
YUV 格式分为两种类型:
- Packed 格式 :类似 RGB 的存放方式。每个像素点的 Y、U、V 是连续交错存储的
- Planar 格式:Y、U、V分别放在三个数组中
- YUV444: 每一个Y对应一组U/V分量。一组YUV占3个字节(8+8+8=24bits)
- YUV422: 2:1 水平取样,垂直完全采样,一组YUV占2个字节(8+4+4=16bit)
- YUV420: 2:1 水平取样,2:1 垂直取样,一组YUV占1.5个字节(8+2+2=12bits)或2个字节(8+4+4=16bits)其中YUV420细分为YUV420Planar 包含I420,YV12;YUV420SemiPlanar包含NV12,NV21
不同格式的yuv存储顺序:
I420:YYYYYYYY UUVV
YV12:YYYYYYYY VVUU
NV12:YYYYYYYY UVUV
NV21: YYYYYYYY VUVU
android Camera
Camera2
输出的帧信息采用的是 Image
,支持全新的 YUV420Flexible 格式
,配套格式为 YUV_420_888,888
表示 Y、U、V 分量中每个颜色占8 bit。YUV420Flexible
并不是一种具体的格式,而是一类 YUV 格式,包括 I420 还有旧版 Camera 支持的 NV21 和 YV12。在 COLOR_FormatYUV420Flexible
描述中,在该颜色格式下,图像中的每个像素占用 12 bit(即 1.5 个字节),该颜色格式与 YUV_420_888
配套使用。
Camera1官方建议使用 NV21 或 YV12,编码用到的颜色格式一般也用的是 COLOR_FormatYUV420SemiPlanar
或 COLOR_FormatYUV420Planar。
获取YUV数据
通过 Image#getPlanes()
可以得到Y、U 和 V 三个分量 的数据,Y分量是连续存储的 pixelStride 一定等于1,对于U/V plane来说,u分量的RowStride等于v分量的RowStride
,u分量的PixelStride等于v分量的PixelStride。pixelStride
代表相邻像素样本之间的距离,单位是字节,1表示是连续存储,2表示间隔存储。
当 Image
中的 U/V 分量的 pixelStride
均为 2
的话,实际上三个 Plane 的数据如下:
U/V 分量仅有偶数索引值才是真实有效值,而奇数索引值是无效值。虽然奇数索引值是无效的,但是依然占用了存储空间,因此 U 和 V 分量长度是 Y 分量长度的 1/2,而不是 1/4。
从 Image 中获取图像数据
/**
* colorFormat 0为`I420(YU12)` format `YYYYYYYY UUVV` or
* 1为`NV21` format `YYYYYYYY VUVU`
**/
public byte[] getYuvDataFromImage(Image image, int colorFormat) {
Rect crop = image.getCropRect();
int format = image.getFormat();
int width = crop.width();
int height = crop.height();
Image.Plane[] planes = image.getPlanes();
byte[] data = new byte[width * height * ImageFormat.getBitsPerPixel(format) / 8];
byte[] rowData = new byte[planes[0].getRowStride()];
int channelOffset = 0;
int outputStride = 1;
for (int i = 0; i < planes.length; i++) {
switch (i) {
case 0:
channelOffset = 0;
outputStride = 1;
break;
case 1:
if (colorFormat == 0) {
channelOffset = width * height;
outputStride = 1;
}
else if (colorFormat == 1) {
channelOffset = width * height + 1;
outputStride = 2;
}
break;
case 2:
if (colorFormat == 0) {
channelOffset = (int) (width * height * 1.25);
outputStride = 1;
}
else if (colorFormat == 1) {
channelOffset = width * height;
outputStride = 2;
}
break;
}
ByteBuffer buffer = planes[i].getBuffer();
int rowStride = planes[i].getRowStride();
int pixelStride = planes[i].getPixelStride();
int shift = (i == 0) ? 0 : 1;
int w = width >> shift;
int h = height >> shift;
buffer.position(rowStride * (crop.top >> shift) + pixelStride * (crop.left >> shift));
for (int row = 0; row < h; row++) {
int length;
if (pixelStride == 1 && outputStride == 1) {
length = w;
buffer.get(data, channelOffset, length);
channelOffset += length;
} else {
length = (w - 1) * pixelStride + 1;
buffer.get(rowData, 0, length);
for (int col = 0; col < w; col++) {
data[channelOffset] = rowData[col * pixelStride];
channelOffset += outputStride;
}
}
if (row < h - 1) {
buffer.position(buffer.position() + rowStride - length);
}
}
}
return data;
}
直接获取NV21数据
public byte[] getBytesFromImage(Image image) {
Image.Plane[] planes = image.getPlanes();
int imageBytesLength = 0;
for (Image.Plane plane : planes) {
imageBytesLength += plane.getBuffer().remaining();
}
byte[] imageBytes = new byte[imageBytesLength];
int offset = 0;
for (Image.Plane plane : planes) {
ByteBuffer buffer = plane.getBuffer();
int remain = buffer.remaining();
buffer.get(imageBytes, offset, remain);
offset += remain;
}
return imageBytes;
}
关于旋转
// The input imageBytes is in YYYYYYYYUVUV(NV12)
// Return data in YYYYYYYYUVUV(NV12)
public byte[] rotateYUV420Degree90(byte[] imageBytes, int imageWidth, int imageHeight) {
int yuvSize = imageWidth * imageHeight * ImageFormat.getBitsPerPixel(ImageFormat.YUV_420_888) >> 3;
byte[] yuv = new byte[yuvSize];
int i = 0;
for (int x = 0; x < imageWidth; x++) {
for (int y = imageHeight - 1; y >= 0; y--) {
yuv[i++] = imageBytes[y * imageWidth + x];
}
}
i = yuv.length - 1;
for (int x = imageWidth - 1; x >= 0; x -= 2) {
for (int y = 0; y < (imageHeight >> 1); y++) {
yuv[i--] = imageBytes[imageWidth * imageHeight + y * imageWidth + x];
yuv[i--] = imageBytes[imageWidth * imageHeight + y * imageWidth + (x - 1)];
}
}
return yuv;
}
public byte[] rotateYUV420Degree270(byte[] imageBytes, int imageWidth, int imageHeight) {
byte[] yuv = new byte[imageWidth * imageHeight * 3 / 2];
int i = 0;
for (int x = imageWidth - 1; x >= 0; x--) {
for (int y = 0; y < imageHeight; y++) {
yuv[i] = imageBytes[y * imageWidth + x];
i++;
}
}
i = imageWidth * imageHeight;
int x = imageWidth - 1;
while (x > 0) {
for (int y = 0; y < imageHeight / 2; y++) {
yuv[i] = imageBytes[imageWidth * imageHeight + y * imageWidth + (x - 1)];
i++;
yuv[i] = imageBytes[imageWidth * imageHeight + y * imageWidth + x];
i++;
}
x -= 2;
}
return yuv;
}
public byte[] rotateYUVDegree270AndMirror(byte[] imageBytes, int imageWidth, int imageHeight) {
byte[] yuv = new byte[imageWidth * imageHeight * 3 / 2];
int i = 0;
int maxY;
for (int x = imageWidth - 1; x >= 0; x--) {
maxY = imageWidth * (imageHeight - 1) + x * 2;
for (int y = 0; y < imageHeight; y++) {
yuv[i] = imageBytes[maxY - (y * imageWidth + x)];
i++;
}
}
int uvSize = imageWidth * imageHeight;
i = uvSize;
int maxUV;
int x = imageWidth - 1;
while (x > 0) {
maxUV = imageWidth * (imageHeight / 2 - 1) + x * 2 + uvSize;
for (int y = 0; y < imageHeight / 2; y++) {
yuv[i] = imageBytes[maxUV - 2 - (y * imageWidth + x - 1)];
i++;
yuv[i] = imageBytes[maxUV - (y * imageWidth + x)];
i++;
}
x -= 2;
}
return yuv;
}
public byte[] rotateYUV420Degree180(byte[] imageBytes, int imageWidth, int imageHeight) {
byte[] yuv = new byte[imageWidth * imageHeight * 3 / 2];
int count = 0;
for (int i = imageWidth * imageHeight - 1; i >= 0; i--) {
yuv[count] = imageBytes[i];
count++;
}
for (int j = imageWidth * imageHeight * 3 / 2 - 1; j >= imageWidth * imageHeight; j -= 2) {
yuv[count++] = imageBytes[j - 1];
yuv[count++] = imageBytes[j];
}
return yuv;
}