YUV格式浅析

882 阅读9分钟

YUV格式浅析

YUV由来

YUV 是一种色彩编码模型,其中 “Y” 表示明亮度(Luminance),“U” 和 “V” 分别表示色度(Chrominance)和浓度(Chroma)。——模拟信号

也叫做 YCbCr,Cb指蓝色色度分量,而Cr指红色色度分量。——数字信号

YUV 色彩编码模型,其设计初衷为了解决彩色电视机与黑白电视的兼容问题,利用了人类眼睛的生理特性(对亮度敏感,对色度不敏感),允许降低色度的带宽,降低了传输带宽。

在计算机系统中应用尤为广泛 ,利用 YUV 色彩编码模型可以降低图片数据的内存占用,提高数据处理效率。

YUV分类

image.png

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