图像步幅

当视频图像存储在内存中时,内存缓冲区可能包含每行像素之后的额外填充字节。 填充字节会影响图像存储在内存中的方式,但不会影响图像的显示方式。

该步幅是内存中一行像素到内存中下一行像素的字节数。 步幅也称为 音调。 如果存在填充字节,则步幅比图像的宽度宽,如下图所示。

diagram showing an image plus padding.

包含相同尺寸的视频帧的两个缓冲区可以有两个不同的步幅。 如果处理视频图像,则必须考虑到这一步。

此外,可通过两种方式在内存中排列图像。 在 从上到下 的图像中,图像中的上一行像素首先显示在内存中。 在 从下到下 的图像中,最后一行像素首先显示在内存中。 下图显示了从上到下的图像与从下到下的图像之间的差异。

diagram showing top-down and bottom-up images.

从下到下的图像具有负步幅,因为步幅定义为相对于显示的图像,需要向下移动一行像素的字节数。 YUV 图像应始终从上到下,Direct3D 图面中包含的任何图像都必须从上到下。 系统内存中的 RGB 图像通常是自下而上。

视频转换尤其需要处理具有不匹配步幅的缓冲区,因为输入缓冲区可能与输出缓冲区不匹配。 例如,假设你想要转换源映像并将结果写入目标映像。 假设这两个图像的宽度和高度相同,但可能没有相同的像素格式或相同的图像步幅。

以下示例代码演示了编写此类函数的通用方法。 这不是一个完整的工作示例,因为它抽象化了许多特定详细信息。

void ProcessVideoImage(
    BYTE*       pDestScanLine0,     
    LONG        lDestStride,        
    const BYTE* pSrcScanLine0,      
    LONG        lSrcStride,         
    DWORD       dwWidthInPixels,     
    DWORD       dwHeightInPixels
    )
{
    for (DWORD y = 0; y < dwHeightInPixels; y++)
    {
        SOURCE_PIXEL_TYPE *pSrcPixel = (SOURCE_PIXEL_TYPE*)pDestScanLine0;
        DEST_PIXEL_TYPE *pDestPixel = (DEST_PIXEL_TYPE*)pSrcScanLine0;

        for (DWORD x = 0; x < dwWidthInPixels; x +=2)
        {
            pDestPixel[x] = TransformPixelValue(pSrcPixel[x]);
        }
        pDestScanLine0 += lDestStride;
        pSrcScanLine0 += lSrcStride;
    }
}

此函数采用六个参数:

  • 指向目标图像中扫描行 0 开始的指针。
  • 目标图像的步幅。
  • 指向源图像中扫描行 0 开始的指针。
  • 源图像的步幅。
  • 图像的宽度(以像素为单位)。
  • 图像的高度(以像素为单位)。

一般思路是一次处理一行,循环访问行中的每个像素。 假设SOURCE_PIXEL_TYPE和DEST_PIXEL_TYPE是分别表示源图像和目标图像的像素布局的结构。 例如, (32 位 RGB 使用 RGBQUAD 结构。并非每个像素格式都有预定义的结构。) 将数组指针强制转换为结构类型,使你能够访问每个像素的 RGB 或 YUV 组件。 在每个行的开头,函数存储指向该行的指针。 在行末尾,它将指针递增图像步幅的宽度,从而将指针推进到下一行。

此示例为每个像素调用名为 TransformPixelValue 的假设函数。 这可能是从源像素计算目标像素的任何函数。 当然,确切的详细信息将取决于特定任务。 例如,如果你有平面 YUV 格式,则必须独立于 luma 平面访问色度平面;使用交错视频时,可能需要单独处理字段;等等。

为了提供更具体的示例,以下代码将 32 位 RGB 图像转换为 AYUV 图像。 RGB 像素是使用 RGBQUAD 结构访问的,AYUV 像素是使用 DXVA2_AYUVSample8 结构结构访问的。

//-------------------------------------------------------------------
// Name: RGB32_To_AYUV
// Description: Converts an image from RGB32 to AYUV
//-------------------------------------------------------------------
void RGB32_To_AYUV(
    BYTE*       pDest,
    LONG        lDestStride,
    const BYTE* pSrc,
    LONG        lSrcStride,
    DWORD       dwWidthInPixels,
    DWORD       dwHeightInPixels
    )
{
    for (DWORD y = 0; y < dwHeightInPixels; y++)
    {
        RGBQUAD             *pSrcPixel = (RGBQUAD*)pSrc;
        DXVA2_AYUVSample8   *pDestPixel = (DXVA2_AYUVSample8*)pDest;
        
        for (DWORD x = 0; x < dwWidthInPixels; x++)
        {
            pDestPixel[x].Alpha = 0x80;
            pDestPixel[x].Y = RGBtoY(pSrcPixel[x]);   
            pDestPixel[x].Cb = RGBtoU(pSrcPixel[x]);   
            pDestPixel[x].Cr = RGBtoV(pSrcPixel[x]);   
        }
        pDest += lDestStride;
        pSrc += lSrcStride;
    }
}

下一个示例将 32 位 RGB 图像转换为 YV12 图像。 此示例演示如何处理平面 YUV 格式。 (YV12 是一个平面 4:2:0 format.) 在此示例中,函数维护目标图像中三个平面的三个单独的指针。 但是,基本方法与前面的示例相同。

void RGB32_To_YV12(
    BYTE*       pDest,
    LONG        lDestStride,
    const BYTE* pSrc,
    LONG        lSrcStride,
    DWORD       dwWidthInPixels,
    DWORD       dwHeightInPixels
    )
{
    assert(dwWidthInPixels % 2 == 0);
    assert(dwHeightInPixels % 2 == 0);

    const BYTE *pSrcRow = pSrc;
    
    BYTE *pDestY = pDest;

    // Calculate the offsets for the V and U planes.

    // In YV12, each chroma plane has half the stride and half the height  
    // as the Y plane.
    BYTE *pDestV = pDest + (lDestStride * dwHeightInPixels);
    BYTE *pDestU = pDest + 
                   (lDestStride * dwHeightInPixels) + 
                   ((lDestStride * dwHeightInPixels) / 4);

    // Convert the Y plane.
    for (DWORD y = 0; y < dwHeightInPixels; y++)
    {
        RGBQUAD *pSrcPixel = (RGBQUAD*)pSrcRow;
        
        for (DWORD x = 0; x < dwWidthInPixels; x++)
        {
            pDestY[x] = RGBtoY(pSrcPixel[x]);    // Y0
        }
        pDestY += lDestStride;
        pSrcRow += lSrcStride;
    }

    // Convert the V and U planes.

    // YV12 is a 4:2:0 format, so each chroma sample is derived from four 
    // RGB pixels.
    pSrcRow = pSrc;
    for (DWORD y = 0; y < dwHeightInPixels; y += 2)
    {
        RGBQUAD *pSrcPixel = (RGBQUAD*)pSrcRow;
        RGBQUAD *pNextSrcRow = (RGBQUAD*)(pSrcRow + lSrcStride);

        BYTE *pbV = pDestV;
        BYTE *pbU = pDestU;

        for (DWORD x = 0; x < dwWidthInPixels; x += 2)
        {
            // Use a simple average to downsample the chroma.

            *pbV++ = ( RGBtoV(pSrcPixel[x]) +
                       RGBtoV(pSrcPixel[x + 1]) +       
                       RGBtoV(pNextSrcRow[x]) +         
                       RGBtoV(pNextSrcRow[x + 1]) ) / 4;        

            *pbU++ = ( RGBtoU(pSrcPixel[x]) +
                       RGBtoU(pSrcPixel[x + 1]) +       
                       RGBtoU(pNextSrcRow[x]) +         
                       RGBtoU(pNextSrcRow[x + 1]) ) / 4;    
        }
        pDestV += lDestStride / 2;
        pDestU += lDestStride / 2;
        
        // Skip two lines on the source image.
        pSrcRow += (lSrcStride * 2);
    }
}

在所有这些示例中,假定应用程序已确定图像步幅。 有时可以从媒体缓冲区获取此信息。 否则,必须基于视频格式计算它。 有关计算图像步幅和使用视频媒体缓冲区的详细信息,请参阅 未压缩的视频缓冲区

视频媒体类型

媒体类型