YUV420数据格式学习(420P和420SP)

在之前的Metal与图形渲染三:透明通道视频已经对YUV有所简介,但当时也是大概了解了YUV的格式,直接套用了YUV转RGB的公式,其实对YUV的数据格式还不是太了解,这次来深入一下对YUV420格式的学习。

一. YUV420简介

YUV使用亮度(Y)和色度(UV)来指定一个像素的颜色,由于人眼对色度不敏感,因此一个Y可以对应多个UV,以减少数据的带宽占用。如果单独只有Y,那么图像是能正常显示的,但只有黑白的颜色。下图分别代表正常图像、Y值、U值和V值。

YUV420是指YUV以4:2:0的方式进行采样,在每一行扫描时,只扫描一种色度分量(U 或者 V),和 Y 分量按照 2 : 1 的方式采样。参考文章这个例子非常清晰易懂

举个例子 :
 
假设图像像素为:
 
[Y0 U0 V0]、[Y1 U1 V1]、 [Y2 U2 V2]、 [Y3 U3 V3]
[Y5 U5 V5]、[Y6 U6 V6]、 [Y7 U7 V7] 、[Y8 U8 V8]
 
那么采样的码流为:Y0 U0 Y1 Y2 U2 Y3 Y5 V5 Y6 Y7 V7 Y8
 
其中,每采样过一个像素点,都会采样其 Y 分量,而 U、V 分量就会间隔一行按照 2 : 1 进行采样。
 
最后映射出的像素点为:

[Y0 U0 V5]、[Y1 U0 V5]、[Y2 U2 V7]、[Y3 U2 V7]
[Y5 U0 V5]、[Y6 U0 V5]、[Y7 U2 V7]、[Y8 U2 V7]

下图叉叉代表Y分量,圆圈代表UV分量,可以看到每4个Y分量共用一组UV分量,也就是420格式了。

二. YUV420采样格式

YUV的存储格式包括Planar和Packed,Planar先连续存储所有像素点的 Y 分量,然后存储 U 分量,最后是 V 分量。Packed连续交替存储每个像素点的YUV分量。Planar和Packed在代码上的区别就是,Planar会将YUV数据分别存于多个数组,而Packed会将数据存在一个数组里面。

YUV420采样格式包括YUV420P和YUV420SP,格式都是Planar的,区别在于 YUV420P (420 Planar)类型就会先存储所有的 U 分量或者 V 分量,而 YUV420SP (YUV420 Semi Planar)则是按照 UV 或者 VU 的交替顺序进行存储。

下图是YUV420P格式:

下图是YUV420SP格式:

可以看出,YUV420P和YUV420SP的区别在于:

YUV420SP的UV分量存放在一个数组里面,该数组的宽度和Y分量的宽度相等,而高度为Y分量的一半,总共用了两个数组存放YUV数据,即两平面。在iOS开发中,形成CVPixelBuffer的数据结构。我们之前根据CVPixelBuffer渲染视频RGB就是这个格式。

- (id )textureWithPixelBuffer:(CVMetalTextureRef)pixelBuffer pixelFormat:(MTLPixelFormat)pixelFormat planeIndex:(NSInteger)planeIndex {
    id  texture = nil;

    // planeIndex为0时是Y分量数据,planeIndex为1时是UV分量数据
    size_t width = CVPixelBufferGetWidthOfPlane(pixelBuffer, planeIndex);
    size_t height = CVPixelBufferGetHeightOfPlane(pixelBuffer, planeIndex);
    CVMetalTextureRef textureRef = NULL;
    CVReturn status = CVMetalTextureCacheCreateTextureFromImage(NULL, _textureCache, pixelBuffer, NULL, pixelFormat, width, height, planeIndex, &textureRef);
    if (status == kCVReturnSuccess) {
        texture = CVMetalTextureGetTexture(textureRef);
        CFRelease(textureRef);
    } else {
        texture = nil;
    }
    return texture;
}

渲染时也会将UV作为一个纹理进行转换。

float3 rgbFromYuv(float2 textureCoor,
                  texture2d  textureY,
                  texture2d  textureUV,
                  constant CCAlphaVideoMetalConvertMatrix *convertMatrix) {
    
    constexpr sampler textureSampler (mag_filter::linear,
                                      min_filter::linear);
    float3 yuv = float3(textureY.sample(textureSampler, textureCoor).r,
                        textureUV.sample(textureSampler, textureCoor).rg);
    
    return convertMatrix->matrix * (yuv + convertMatrix->offset);
}

fragment float4 movieFragment(SingleInputVertexIO input [[ stage_in ]],
                               texture2d  textureY [[ texture(0) ]],
                               texture2d  textureUV [[ texture(1) ]],
                               constant CCAlphaVideoMetalConvertMatrix *convertMatrix [[ buffer(0) ]]) {
    float3 rgb = rgbFromYuv(input.textureCoordinate, textureY, textureUV, convertMatrix);
    return float4(rgb, 1.0);
}

YUV420P的UV分量分别存放于两个数组,U分量数组、V分量数组的宽度为Y分量的一半,高度也为Y分量的一半,总共用了三个数组存放YUV数据,即三平面。渲染时需要将YUV分别作为三个纹理进行输入。

fragment float4 movieByPixelsFragment(SingleInputVertexIO input [[ stage_in ]],
                                      texture2d  textureY [[ texture(0) ]],
                                      texture2d  textureU [[ texture(1) ]],
                                      texture2d  textureV [[ texture(2) ]],
                                      constant CCAlphaVideoMetalConvertMatrix *convertMatrix [[ buffer(0) ]]) {
    float2 textureCoor = input.textureCoordinate;
    constexpr sampler textureSampler (mag_filter::linear,
                                      min_filter::linear);
    
    float y = textureY.sample(textureSampler, textureCoor).r;
    float u = textureU.sample(textureSampler, textureCoor).r;
    float v = textureV.sample(textureSampler, textureCoor).r;
    
    float3 yuv = float3(y, u, v);
    
    float3 rgb = convertMatrix->matrix * (yuv + convertMatrix->offset);

    return float4(rgb, 1.0);
}

三. YUV420的具体格式内容

YUV420P包括YU12(也称I420P)和YV12,其区别在于,YU12按Y、U、V的顺序存储,而UV12按Y、V、U的顺序存储。

YUV420SP包括NV12和NV21,同理,其区别在于,NV12按Y、UV分量的格式存储,而NV21按Y、VU分量的格式存储。值得一提的是,iOS采样格式为NV12,Android采样格式为NV21。

用参考文章的图总结下格式:

四. YUV420的数据内容

一个YUV数据并不一定全是图像数据,也有可能是一些填充的内容,需要确保是按16位对齐的,即为16的倍数。

Padding用于16位对齐的填充,Pitch(也称为Stride)为YUV数据的行字节数(BytesPerRow),因此,Width不一定等于Pitch,Pitch为16的倍数,但Width不一定为16的倍数,此外,Height维度是不会有填充的,这点在YUV数据渲染或复制时需要注意。

下面四个方法返回的内容各不相同,需要注意区分,特别是Pitch和Width的区别:

// Width
 CVPixelBufferGetWidthOfPlane(pixelBuffer, planeIndex)

// Pitch
CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, planeIndex)

// Height
CVPixelBufferGetHeightOfPlane(pixelBuffer, planeIndex)

// Base Address
CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, planeIndex)

五. 总结

YUV420采样时,四个Y分量共用1对UV分量。

YUV420P和YUV420SP的区别在于前者有三个平面(U和V各为一个平面,宽度和高度均为Y平面的1/2),后者有两个平面(U和V处于一个平面,宽度和Y平面相等,高度为Y平面的1/2)。

YU12和YV12、NV12和NV21的区别在于取样先U后V还是先V后U。

YUV420可能会有填充数据,需要注意Pitch和Width的区别。

六. 参考

一文读懂 YUV 的采样与格式

YUV数据采集存储及部分转换原理

YUV格式

你可能感兴趣的:(YUV420数据格式学习(420P和420SP))