【Android音视频学习之路(八)】YUV格式初探

2,214 阅读8分钟

【Android音视频学习之路(一)】如何在 Android 平台绘制一张图片

【Android音视频学习之路(二)】AudioRecord API详解及应用

【Android音视频学习之路(三)】AudioTrack 使用与详解

【Android音视频学习之路(四)】Camera 的使用

【Android音视频学习之路(五)】MediaExtractor和MediaMuxer讲解

【Android音视频学习之路(六)】 MediaCodec 简单讲解

【Android音视频学习之路(七)】音视频录制流程实践

“这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

一、YUV起源

常见的颜色模型中,RGB主要用于电子系统里表达和显示颜色,CMYK印刷四色模式用于彩色印刷,而YUV是被欧洲电视系统所采用的一种颜色编码方法。

使用YUV的优点有两个:

  1. YUV主要用于优化彩色视频信号的传输,使其向后兼容老式黑白电视,这一特性用在于电视信号上。
  2. YUV是数据总尺寸小于RGB格式(但用YUV444的话,和RGB888一样都是24bits)

YUV,分为三个分量,“Y”表示的是明亮度(Luminance或Luma),也就是灰度值;而“U”和“V” 表示的则是色度(Chrominance或Chroma),作用是用于指定像素的颜色。

UV 即CbCr(C代表颜色,b代表蓝色,r代表红色)

image.png

分类

YUV格式有两大类:==平面(planar)和紧凑(packed==)。

对于planar的YUV格式,先连续存储所有像素点的Y,紧接着存储所有像素点的U,随后是存储所有像素点的V,或者是先v后u

对于packed的YUV格式,每个像素点的Y,U,V是连续交替存储的

采样

主流的采样方式有三种,YUV4:4:4,YUV4:2:2,YUV4:2:0

YUV 4:4:4采样,每一个Y对应一组UV分量,一个YUV占8+8+8 = 24bits 3个字节。

YUV 4:2:2采样,每两个Y共用一组UV分量,一个YUV占8+4+4 = 16bits 2个字节。

YUV 4:2:0采样,每四个Y共用一组UV分量,一个YUV占8+2+2 = 12bits 1.5个字节。

最常见的YUV420P和YUV420SP都是基于4:2:0采样的,所以如果图片的宽为width,高为heigth,在内存中占的空间为width * height * 3 / 2,其中前width * height的空间存放Y分量,接着width * height / 4存放U分量,最后width * height / 4存放V分量

二、YUV不同格式

YUV420根据颜色数据的存储顺序不同,又分为了多种不同的格式,这些格式实际存储的信息还是完全一致的。举 例来说,对于4x4的图片,在YUV420下,任何格式都有16个Y值,4个U值和4个V值,不同格式只是Y、U和V的排 列顺序变化。I420 为 YYYYYYYYYYYYYYYYUUUUVVVV ,YUV420 是一类格式的集合,YUV420并不能完全确定颜色数据的存储顺序。

PlaneProxy/Plane

Y、U和V三个分量的数据分别保存在三个Plane类中,即通过 getPlanes()得到的数组。 Plane 实际是对ByteBuffer的封装。

Image保证了planes[0]一定是Y,planes[1]一定是U,planes[2]一定是V。且对于plane [0],Y分量数据一定是连续存储的,中间不会有U或V数据穿插,也就是说我们一定能够一次性得到所有Y分量的值

但是对于UV数据,可能存在以下两种情况:

1. planes[1] = {UUUU...},planes[2] = {VVVV...};  //I420
2. planes[1] = {UVUV...},planes[2] = {VUVU...}。

PixelStride

所以在我么取数据时需要在根据Plane中的另一个信息来确定如何取对应的U或者V数据。

// 行内数据值间隔
// 1:表示无间隔取值,即为上面的第一种情况
// 2: 表示需要间隔一个数据取值,即为上面的第二种情况
 var pixelStride = plane.pixelStride

根据这个属性,我们将确定数据如何存储,因此如果需要取出代表I420格式的byte[],则为:YUV420中,Y数据长度为: width*height , 而U、V都为:width / 2 * height / 2。

val planes = image.planes
//y数据的这个值只能是:1
val yPixelStride = planes[0].pixelStride
val uPixelStride = planes[1].pixelStride
Log.e("xiao", "yPixelStride: $yPixelStride uPixelStride: $uPixelStride")

image.png

yPixelStride: 1 uPixelStride: 2

小米MIX2 planes[0] 的PixelStride 为1 planes[1]的 PixelStride为2

说明是UVUV交叉存储

// Y数据 pixelStride一定为1
val pixelStride = planes[0].pixelStride
val y = planes[0].buffer // Y数据
val u = ByteArray(image.width / 2 * image.height / 2)
val pixelStride = planes[1].pixelStride
if (pixelStride == 1) {
    planes[1].buffer // U数据
} else if (pixelStride == 2) {
    val uBuffer = planes[1].buffer
    for (i in 0 until uBuffer.remaining() step 2) {
        u[i] = uBuffer.get();
        //丢弃一个数据,这个数据其实是V数据,但是我们还是到planes[2]中获取V数据
        uBuffer.get()
    }
}

但是如果使用上面的代码去获取YUV数据,可能你会惊奇的发现,并不是在所有你设置的Width与 Height(分辨率)下都能够正常运行。我们忽略了什么,为什么会出现问题呢?

在Plane中 我们已经使用了 getBuffer 与 getPixelStride 两个方法,但是还有一个 getRowStride没有用到.

RowStride

RowStride表示行步长,Y数据对应的行步长可能为:

  • 等于Width;
  • 大于Width;

以4x4的I420为例,其数据可以看为

image.png

如果RowStride等于Width,那么我们直接通过 planes[0].getBuffer() 获得Y数据没有问题。

但是如果RowStride大于Width,比如对于4x4的I420,如果每行需要以8字节对齐,那么可能得到的RowStride不等于4(Width),而是得到8。那么此时会在每行数据末尾补充占位的无效数据:

image.png

获取Y数据
val planes = image.planes
val size = image.width * image.height * 3 / 2
val  yuv420 = ByteBuffer.allocate(size)
/**
 * Y数据
 */
val plane = planes[0] //y数据

//pixelStride = 1 : 取值无间隔
//pixelStride = 2 : 间隔1个字节取值
// y的此数据应该都是1
val pixelStride = plane.pixelStride //Y的肯定为1

//大于等于宽, 表示连续的两行数据的间隔
//  如:640x480的数据,
//  可能得到640
//  可能得到650,表示每行最后10个字节为补位的数据
val rowStride = plane.rowStride //rowStride 可能末尾有填充

val buffer = plane.buffer
val row = ByteArray(image.width)
// 每行要排除的无效数据,但是需要注意:实际测试中 最后一行没有这个补位数据
val skipRow = ByteArray(rowStride - image.width)
for (i in 0 until image.height) {
    buffer[row]
    yuv420.put(row)
    // 不是最后一行
    if (i < image.height - 1) {
        buffer[skipRow] //最后一行因为后面跟着U 数据,没有无效占位数据,不需要丢弃
    }
}

而对于U与V数据,对应的行步长可能为:

  1. 等于Width;
  2. 大于Width;
  3. 等于Width/2;
  4. 大于Width/2

等于Width

这表示,我们获得planes[1]中不仅包含U数据,还会包含V的数据,此时pixelStride==2。

UVUV
UVUV

那么V数据:planes[2],则为:

VUVU
VUVU

大于Width

与Y数据一样,可能由于字节对齐,出现RowStride大于Width的情况,与等于Width一样,planes[1]中不仅包含U 数据,还会包含V的数据,此pixelStride==2。

UVUV0000
UVUV最后一行没有站位

planes[2],则为:

VUVU0000
VUVU最后一行没有站位

等于Width/2

u 当获取的U数据对应的RowStride等于Width/2,表示我们得到的planes[1]只包含U数据。此时pixelStride==1。 那么planes[1]+planes[2]为:

UU
UU
VV
VV

这种情况,所有的U数据是连在一起的,即 planes[1].getBuffer 可以直接获得完整的U数据。

大于Width/2

同样我们得到的planes[1]只包含U数据,但是与Y数据一样,可能存在占位数据。此时pixelStride==1。 planes[1]+planes[2]为:

UU000000
UU最后一行没有站位
VV000000
VV最后一行没有站位
for (i in 1..2) {
    //planes[1] | planes[2] uv数据处理
    plane = planes.get(i)
    pixelStride = plane.getPixelStride() //1 I420 2 交错packed UVUV
    // uv数据的rowStride可能是
    // 如:640的宽
    // 可能得到320, pixelStride 为1
    // 可能大于320同时小于640,有为了补位的无效数据  pixelStride 为1
    // 可能得到640 uv数据在一起,pixelStride为2
    // 可能大于640,有为了补位的无效数据 pixelStride为2
    rowStride = plane.getRowStride()
    buffer = plane.getBuffer()
    val uvWidth = image.width / 2
    val uvHeight = image.height / 2
    for (j in 0 until uvHeight) {
        for (k in 0 until rowStride) {
            // 最后一行,是没有补位数据的
            if (j == uvHeight - 1) {
                //只有自己(U/V)的数据
                if (pixelStride == 1) {
                    // 结合外层if 则表示:
                    //  如果是最后一行,我们就不管结尾的占位数据了
                    if (k >= uvWidth) {
                        break
                    }
                } else if (pixelStride == 2) {
                    //与同级if相同意思
                    // todo uv混合,
                    //  planes[2]:uvu
                    //  planes[3]:vuv
                    if (k >= image.width - 1) {
                        break
                    }
                }
            }
            val b: Byte = buffer.get()
            if (pixelStride == 2) {
                //打包格式 uv在一起,偶数位取出来是U数据: 0 2 4 6
                if (k < image.width && k % 2 == 0) {
                    yuv420.put(b)
                }
            } else if (pixelStride == 1) {
                if (k < uvWidth) {
                    yuv420.put(b)
                }
            }
        }
    }
}

YUV格式

常见的YUV格式有YUY2、YUYV、YVYU、UYVY、AYUV、Y41P、Y411、Y211、IF09、IYUV、YV12、YVU9、YUV411、YUV420等,Android中比较常见是YUV420分为两种:YUV420PYUV420SP。所以就先了解下YUV420PYUV420SP

libX264中对YUV各种格式的定义

#define X264_CSP_I400           0x0001  /* monochrome 4:0:0 */
#define X264_CSP_I420           0x0002  /* yuv 4:2:0 planar */
#define X264_CSP_YV12           0x0003  /* yvu 4:2:0 planar */
#define X264_CSP_NV12           0x0004  /* yuv 4:2:0, with one y plane and one packed u+v */
#define X264_CSP_NV21           0x0005  /* yuv 4:2:0, with one y plane and one packed v+u */
#define X264_CSP_I422           0x0006  /* yuv 4:2:2 planar */
#define X264_CSP_YV16           0x0007  /* yvu 4:2:2 planar */
#define X264_CSP_NV16           0x0008  /* yuv 4:2:2, with one y plane and one packed u+v */
#define X264_CSP_YUYV           0x0009  /* yuyv 4:2:2 packed */
#define X264_CSP_UYVY           0x000a  /* uyvy 4:2:2 packed */
#define X264_CSP_V210           0x000b  /* 10-bit yuv 4:2:2 packed in 32 */
#define X264_CSP_I444           0x000c  /* yuv 4:4:4 planar */
#define X264_CSP_YV24           0x000d  /* yvu 4:4:4 planar */

YUV420P

YUV420P是平面模式,Y , U , V分别在不同平面,也就是有三个平面,它是YUV标准格式4:2:0

image.png

那么真实的在字节流中就是按照行从左到右一行一行的拼起来的:

img

==YUV420P分为:YU12和YV12==

YU12格式

在Android中也叫作I420格式,首先是所有Y值,然后是所有U值,最后是所有V值。比如6x6的图片,内存大小就是6x6x3/2=54个字节。为了更清晰的查看,我们换行看,真实的是一行byte[]数据流。

YYYYYY
YYYYYY
YYYYYY
YYYYYY
UUUUUU
VVVVVV
YV12格式

YV12格式与YU12基本相同,首先是所有Y值,然后是所有V值,最后是所有U值。比如6x6的图片,内存大小就是6x6x3/2=54个字节

YYYYYY
YYYYYY
YYYYYY
YYYYYY
VVVVVV
UUUUUU

YUV420SP

YUV420SP 也是是平面模式分为NV21和NV12两种格式。Y是一个平面,UV是一个平面,UV/VU为交替存储,而不是分为三个平面

在Android Camera中文档中强烈推荐使用NV21YV12,因为这两种格式支持所有的相机设备。Camera默认输出YUV的数据格式为NV21。但是在Camera2中,推荐使用的格式则是YUV_420_888

NV21

NV21格式

在Android Camera中手机从摄像头采集的预览数据默认值是NV21

NV21存储顺序是先存Y值,再VU交替存储:YYYYVUVUVU,比如6x6的图片,内存大小就是6x6x3/2=54个字节

YYYYYY
YYYYYY
YYYYYY
YYYYYY
VUVUVU
VUVUVU
NV12格式

NV12存储顺序是先存Y值,再UV交替存储:YYYYUVUVUV,比如6x6的图片,内存大小就是6x6x3/2=54个字节

YYYYYY
YYYYYY
YYYYYY
YYYYYY
UVUVUV
UVUVUV

这里先熟悉下Android中常见的YUV420PYUV420SP。一般我们在使用yuv数据的时候,会对yuv数据进行变换,比如:摄像头数据旋转,从一种格式转为另一种数据等。