YUV格式浅析
YUV由来
YUV 是一种色彩编码模型,其中 “Y” 表示明亮度(Luminance),“U” 和 “V” 分别表示色度(Chrominance)和浓度(Chroma)。——模拟信号
也叫做 YCbCr,Cb指蓝色色度分量,而Cr指红色色度分量。——数字信号
YUV 色彩编码模型,其设计初衷为了解决彩色电视机与黑白电视的兼容问题,利用了人类眼睛的生理特性(对亮度敏感,对色度不敏感),允许降低色度的带宽,降低了传输带宽。
在计算机系统中应用尤为广泛 ,利用 YUV 色彩编码模型可以降低图片数据的内存占用,提高数据处理效率。
YUV分类
YUV 图像主流的采样方式有三种:
- YUV 4:4:4,每1个 Y 分量对于一对 UV 分量,每像素占用 (Y + U + V = 8 + 8 + 8 = 24bits)3 字节;
- YUV 4:2:2,每2个 Y 分量共用一对 UV 分量,每像素占用 (Y + 0.5U + 0.5V = 8 + 4 + 4 = 16bits)2 字节;
- YUV 4:2:0,每4个 Y 分量共用一对 UV 分量,每像素占用 (Y + 0.25U + 0.25V = 8 + 2 + 2 = 12bits)1.5 字节。
ImageFormat.YUV_420_888 : 对于YUV420,ImageFormat在API 21中新加入了YUV_420_888类型,其表示YUV420格式的集合,888表示Y、U、V分量中每个颜色占8bit。
按照 YUV 的排列储存顺序,将其细分为好多种格式。这里再次将 YUV 分成三个大类,Planar,Semi-Planar 和 Packed。
- Planar YUV 三个分量分开存放
- Semi-Planar Y 分量单独存放,UV 分量交错存放
- Packed YUV 三个分量全部交错存放
具体分类
YUYV 、YU12(I420)、NV21 和 NV12 最为常用。以下按4x4分辨率画示意图
YUYV属于YUV422 INter leaved,2 个Y 分量共用一对 UV 分量
YUYV
YUYV YUYV
YUYV YUYV
YUYV YUYV
YUYV YUYV
YU12/YU12(I420)属于YUV420P,Y、U、V分别存储于3个不同的平面
YV12
YYYY
YYYY
YYYY
YYYY
VVVV
UUUU
YU12(I420)
YYYY
YYYY
YYYY
YYYY
UUUU
VVVV
NV21/NV12属于YUV420SP,Y存储于一个平面,UV存储于另一个平面,且UV交错存储。这两种格式的数据中,仅U和V的排列顺序不同
NV21
YYYY
YYYY
YYYY
YYYY
VUVU
VUVU
NV12
YYYY
YYYY
YYYY
YYYY
UVUV
UVUV
I422/YV16属于YUV422P,Y、U、V分别存储于3个不同的平面
I422
YYYY
YYYY
YYYY
YYYY
UUUU
UUUU
UUUU
UUUU
VVVV
VVVV
VVVV
VVVV
YV16
YYYY
YYYY
YYYY
YYYY
VVVV
VVVV
VVVV
VVVV
UUUU
UUUU
UUUU
UUUU
YUV处理
lineSize(行宽) = stride(步长) = pitch(间距)
宏块 是H.264编码的基本单位,一个宏块由一个16×16亮度像素和附加的一个8×8 Cb和一个8×8 Cr彩色像素块组成。
Q:1440x1080size无法编码
image.getPlanes()[0].getRowStride():1472
image.getPlanes()[1].getRowStride():1472
image.getPlanes()[0].capacity() == limit() == remaining():1589728
字节对齐,把数据复制一份,不带padding的部分
/**
* 某些Size无法编码 如1440x1080
*/
public static byte[] getBytesFromImageReader(ImageReader imageReader) {
try (Image image = imageReader.acquireNextImage()) {
final Image.Plane[] planes = image.getPlanes();
ByteBuffer b0 = planes[0].getBuffer();
ByteBuffer b1 = planes[1].getBuffer();
ByteBuffer b2 = planes[2].getBuffer();
int y = b0.remaining(), u = y >> 2, v = u; //y:1589728 /4 = u:397432
byte[] bytes = new byte[y + u + v];
if(b1.remaining() > u) { // y420sp
b0.get(bytes, 0, b0.remaining());
b1.get(bytes, y, b1.remaining()); // uv
} else { // y420p
b0.get(bytes, 0, b0.remaining());
b1.get(bytes, y, b1.remaining()); // u
b2.get(bytes, y + u, b2.remaining()); // v
}
return bytes;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
Q:1440x1080有绿边
/**
* 有绿边
*/
public static byte[] convertYUV420ToNV21(ImageReader imageReader) {
Image image = imageReader.acquireNextImage();
if (image == null) {
return null;
}
int yStride;
int uvStride;
int width = image.getWidth();
int height = image.getHeight();
try {
if (image.getFormat() != ImageFormat.YUV_420_888) {
return null;
}
yStride = image.getPlanes()[0].getRowStride();//1536
uvStride = image.getPlanes()[1].getRowStride();//1536
// Converting YUV_420_888 data to NV21.
ByteBuffer buffer0 = image.getPlanes()[0].getBuffer();
ByteBuffer buffer2 = image.getPlanes()[1].getBuffer();
int buffer0Size = buffer0.limit();//1658784
int buffer2Size = buffer2.limit(); //829343
Log.v(TAG, "convertYUV420888ToNV21: size = " + width + "x" + height
+ ", yStride = " + yStride + ", uvStride = " + uvStride
+ ", buffer0Size = " + buffer0Size + ", buffer2Size = " + buffer2Size);
mData = new byte[Math.max(width * height * 3 / 2, buffer0Size + buffer2Size)];
mData[mData.length - 1] = mData[mData.length - 3];
buffer0.get(mData, 0, buffer0Size);
buffer2.get(mData, buffer0Size, buffer2Size);
} catch (Exception e) {
//Image is closed while ImageReader is closed in other thread, ignore this exception
return null;
}
image.close();
int size = width * height * 3 / 2;
int srcPos = 0;
int destPos = 0;
byte[] noStrideData = new byte[size];
/** @param src 源数组。
* @param srcPos 源数组中的起始位置。
* @param dest 目标数组。
* @param destPos 目标数据中的起始位置。
* @param length 要复制的数组元素的数量。
* */
// remove y panel data padding
for (int i = 0; i < height; i++) {
System.arraycopy(mData, srcPos, noStrideData, destPos, width);
//if (i == height - 1) {
// srcPos += width;
//} else {
srcPos += yStride;
// }
destPos += width;
}
// remove uv panel data padding
for (int j = 0; j < height / 2; j++) {
if (j == height / 2 - 1) {//倒数第一行?
// less a byte data in preview data!
System.arraycopy(mData, srcPos, noStrideData, destPos, width - 1);
} else {
System.arraycopy(mData, srcPos, noStrideData, destPos, width);
}
srcPos += uvStride;
destPos += width;
}
mData = noStrideData;
return mData;
}
public static boolean dumpYuvImage(Image image, String directory) {
uv数据少一个字节,需要手动填最后一个字节。一般补齐是对yuv数据进行补齐的,会补0,yuv全0显示出来就是绿色,所以一般出现绿边大概率是因为没有处理好间距和图像真正的宽度
NV12
W即图像的宽度,H即图像的高度,Stride表示图像行的跨度,超出W部分为填充数据,主要目的是为了字节对齐,一般以16字节或者或者32字节对齐居多。 NV21、YUYV 等格式的处理,都是 8 bit YUV 格式,即每个 Y、U、V 分量分别占用 8 个 bit (一个字节)。
从左侧数据存储结构图看出高度(H)是分层次的,YV12三层和NV12两层,这个层次结构称为 Plane,即YV12在代码中用 Plane[0]表示Y数据的起始地址,Plane[1]表示V数据的起始地址,Plane[2]表示U数据的起始地址。
而NV12的UV是在一个Plane中交错存放,因此用两个Plane表示即可。
代码
developer.android.com/reference/a…
The order of planes in the array returned by
Image#getPlanes() is guaranteed such that plane #0 is always Y, plane #1 is always U (Cb), and plane #2 is always V (Cr).
——仅针对Google指定的NV21,实际QCOM部分平台是NV12,于是U和V的plane索引需要调换
/**
* 从YUV_420_888 image 3个plane中读取nv21数据
* @param image YUV_420_888 image
* @return nv21 byte array
*/
public static byte[] getBytesFromImage(Image image) {
//获取源数据,如果是YUV格式的数据planes.length = 3
//plane[i]里面的实际数据可能存在byte[].length <= capacity (缓冲区总大小)
final Image.Plane[] planes = image.getPlanes();
//数据有效宽度,一般的,图片width <= rowStride,这也是导致byte[].length <= capacity的原因
// 所以我们只取width部分
final int width = image.getWidth();
final int height = image.getHeight();
final int yPlaneIndex = 0;
final int uPlaneIndex, vPlaneIndex;
//MTK 平台和部分QCOM是NV21,太难了。。。
if (Device.isMTKPlatform() || CameraCapabilities.XIAOMI_YUV_FORMAT_NV21 == sYuvFormat) {
uPlaneIndex = 1;
vPlaneIndex = 2;
} else {
uPlaneIndex = 2;
vPlaneIndex = 1;
}
final Image.Plane yPlane = planes[yPlaneIndex];
final Image.Plane uPlane = planes[uPlaneIndex];
final Image.Plane vPlane = planes[vPlaneIndex];
//读取y buffer
final ByteBuffer yBuffer = yPlane.getBuffer();
final byte[] yBytes = new byte[yBuffer.capacity()];
yBuffer.get(yBytes);
//读取v buffer
final ByteBuffer vBuffer = vPlane.getBuffer();
final byte[] vBytes = new byte[vBuffer.capacity()];
vBuffer.get(vBytes);
//读取u buffer
final ByteBuffer uBuffer = uPlane.getBuffer();
final byte[] uBytes = new byte[uBuffer.capacity()];
uBuffer.get(uBytes);
final int yRowStride = yPlane.getRowStride(); // y 行距,= width + padding
final int vuRowStride = uPlane.getRowStride(); // vu行距
// vu像素间距,即相邻两个u(或v)的间距
final int vuPixelsStride = uPlane.getPixelStride();
//此处用来装填最终的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;
// 如果rowStride == width,说明没有padding,直接读取全部y buffer
if (yRowStride == width) {
int size = width * height;
System.arraycopy(yBytes, 0, yuvBytes, dstIndex, size);
dstIndex = size;
} else {
//直接取出来所有Y的有效区域,也可以存储成一个临时的bytes,到下一步再copy
int srcIndex = 0;
for (int i = 0; i < height; i++) {
System.arraycopy(yBytes, srcIndex, yuvBytes, dstIndex, width);
srcIndex += yRowStride;
dstIndex += width;
}
}
//读取vu数据
int srcIndex = 0;
for (int i = 0; i < height / 2; i++) {
for (int j = 0; j < width / 2; j++) {
yuvBytes[dstIndex++] = vBytes[srcIndex];
yuvBytes[dstIndex++] = uBytes[srcIndex];
srcIndex += vuPixelsStride;
}
//换行
if (vuPixelsStride == 2) {
srcIndex += vuRowStride - width;//vuRowStride - width就是padding
} else if (vuPixelsStride == 1) {
//v、u不间隔,只读取width/2确保这一行的v或者u完整,然后就跳到下一行吧,目前没见过这种case
srcIndex += vuRowStride - width / 2;
}
}
return yuvBytes;
}
转I420:
//读取vu数据
int srcIndex = 0;
int uSize = (width * height) >> 2;
for (int i = 0; i < height / 2; i++) {
for (int j = 0; j < width / 2; j++) {
yuvBytes[dstIndex] = uBytes[srcIndex];
yuvBytes[uSize + dstIndex] = vBytes[srcIndex];
srcIndex += vuPixelsStride;
dstIndex++;
}
if (vuPixelsStride == 2) {
srcIndex += vuRowStride - width;
} else if (vuPixelsStride == 1) {
srcIndex += vuRowStride - width / 2;
}
}
YUV旋转、拼接
-
NV21或NV12顺时针旋转90度
/**
* 源数据的像素排列示例: 1234
* 5678
*
* 顺时针旋转90度示例: 51
* 62
* 73
* 84
*/
public static void rotateSP90(byte[] src, byte[] dest, int w, int h) {
int pos = 0;
int k = 0;
//旋转Y
for (int i = 0; i <= w - 1; i++) {
for (int j = h - 1; j >= 0; j--) {
dest[k++] = src[j * w + i];
}
}
//旋转U V
pos = w * h; //U V紧接着Y数据排列
for (int i = 0; i <= w - 2; i += 2) { // UV数据是一半
for (int j = h / 2 - 1; j >= 0; j--) {// UV数据是一半
dest[k++] = src[pos + j * w + i]; //U 或者 V
dest[k++] = src[pos + j * w + i + 1]; //V 或者 U
}
}
}
10bit YUV(P010)
存储结构
从8bit YUV类比而来,10bit YUV表示每个Y、U、V分量分别占用10bit。由于实际操作中,都以字节为单位进行存储和处理,所以10bit YUV占用了2个字节来存储有效数据,
即每个Y、U、V分量分别占用两个字节16bit,除10bit有效数据外,剩下6bit作为padding补0。
P010 最早是微软定义的格式,表示的是 YUV 4:2:0 的采样方式,也就是说 P010 表示的是一类 YUV 格式,它的内存排布方式可能是 NV21、NV12、YU12、YV12 。
P010处理
由于当前OpenGL等图像渲染和算法暂未支持处理10bit数据,实际操作中常见需要将P010转换为8bit YUV。
P010 -> 8bit YUV:右移8位,舍弃第6、7位有效数据
8bit YUV -> ****P010:左移8位,低8位刚好全部补0