图片纵横比

本主题描述了两个相关概念,图片纵横比和像素纵横比。 然后介绍如何在 Microsoft 媒体基础中使用媒体类型来表达这些概念。

图片纵横比

图片纵横比定义了所显示视频图像的形状。 图片纵横比用 X:Y 表示,其中 X:Y 是图片宽度与图片高度的比率。 大多数视频标准使用 4:3 或 16:9 的图片纵横比。 16:9 的纵横比通常被称为宽屏。 电影胶片通常使用 1:85:1 或 1:66:1 的纵横比。 图片纵横比也称为显示纵横比 (DAR)。

diagram showing 4:3 and 16:9 aspect ratios

有时视频图像不具有与显示区域相同的形状。 例如,4:3 的视频可以在宽屏 (16×9) 电视上播放。 在计算机视频中,视频可能显示在任意大小的窗口内。 在这种情况下,有三种方式可以使图像适合显示区域:

  • 沿一个轴拉伸图像以适应显示区域。
  • 缩放图像以适应显示区域,同时保持原始图片的纵横比。
  • 裁剪图像。

拉伸图像以适应显示区域几乎总是错误的,因为它不能保持正确的图片纵横比。

信箱

缩放宽屏图像以适应 4:3 屏幕的过程称为信箱,如下图所示。 图像顶部和底部的矩形区域通常填充黑色,但也可以使用其他颜色。

diagram showing the correct way to letterbox

相反的情况,缩放 4:3 的图像以适应宽屏屏幕,有时称为邮筒。 然而,术语“信箱”也以一般意义使用,意思是缩放视频图像以适应任何给定的显示区域。

diagram showing pillarboxing

平移和扫描

平移和扫描是一种将宽屏图像裁剪为 4×3 形区域的技术,用于在 4:3 显示器上显示。 生成的图像填充了整个屏幕,而不需要黑色信箱区域,但会从图片中裁剪掉原始图像的一部分。 随着兴趣区域的移动,裁剪的区域可以从一帧移动到另一帧。 平移和扫描中的术语“平移”是指移动平移和扫描区域所产生的平移效果。

diagram showing pan-and-scan

像素纵横比

像素纵横比(PAR)测量像素的形状。

捕获数字图像时,对图像进行垂直和水平采样,产生量化样本的矩形阵列,称为像素pel。 采样网格的形状决定了数字化图像中像素的形状。

下方是使用小数字来保持数学简单的例子。 假设原始图像为正方形(即图片纵横比为 1:1);假设采样网格包含 12 个元素,排列在 4×3 的网格中。 每个生成像素的形状的高度将大于宽度。 具体来说,每个像素的形状将是 3×4。 不是正方形的像素称为非正方形像素

diagram showing a non-square sampling grid

像素纵横比也适用于显示器。 显示器的物理形状和物理像素分辨率(横向和向下)决定了显示器的 PAR。 计算机显示器通常使用正方形像素。 如果图像 PAR 和显示 PAR 不匹配,则必须在一个维度上垂直或水平缩放图像,以便正确显示。 以下公式涉及 PAR、显示纵横比 (DAR) 和以像素为单位的图像大小:

DAR = (图像宽度(以像素为单位) / 图像高度(以像素为单位)) × PAR

请注意,此公式中的图像宽度和图像高度指的图像内存,而不是显示的图像。

此为真实示例:NTSC-M 模拟视频在活动图像区域中包含 480 条扫描线。 ITU-R Rec。BT.601 指定每行 704 个可见像素的水平采样率,生成 704 x 480 像素的数字图像。 预期的画面纵横比为 4:3,产生 10:11 的 PAR。

  • DAR:4:3
  • 宽度(以像素为单位):704
  • 高度(以像素为单位):480
  • PAR:10/11

4/3 = (704/480) x (10/11)

要在具有正方形像素的显示器上正确显示此图像,则必须将宽度缩放 10/11 或将高度缩放 11/10。

使用纵横比

视频帧的正确形状由像素纵横比 (PAR) 和显示区域定义。

  • PAR 定义图像中像素的形状。 正方形像素的纵横比为 1:1。 任何其他纵横比都描述非正方形像素。 例如,NTSC 电视使用 10:11 PAR。 假设在电脑显示器上显示视频,屏幕将具有正方形像素 (1:1 PAR)。 源内容的 PAR 在媒体类型的 MF_MT_PIXEL_ASPECT_RATIO 属性中给出。
  • 显示区域是要显示的视频图像区域。 媒体类型中可能指定两个相关的显示区域:
    • 平移和扫描光圈。 平移和扫描光圈是 4×3 的视频区域,应在平移/扫描模式下显示。 它用于在 4×3 屏幕上显示宽屏内容,而不使用信箱。 平移和扫描光圈在 MF_MT_PAN_SCAN_APERTURE 属性中给出,仅当 MF_MT_PAN_SCAN_ENABLED 属性为 TRUE 时才应使用。
    • 显示光圈。 这种光圈在一些视频标准中有所定义。 显示光圈之外的任何内容都是过度扫描区域,不应显示。 例如,NTSC 电视是 720×480 像素,显示光圈为 704×480。 显示光圈在 MF_MT_MINIMUM_DISPLAY_APERTURE 属性中给定。 如果存在,应在平移和扫描模式为 FALSE 时使用。

如果平移和扫描模式为 FALSE 且未定义显示光圈,则应显示整个视频帧。 事实上,除了电视和 DVD 视频之外,大多数视频内容都是这样。 整个画面的宽高比计算为(显示区域宽度 / 显示区域高度)× PAR

代码示例

查找显示区域

以下代码显示如何从媒体类型中获取显示区域。

MFVideoArea MakeArea(float x, float y, DWORD width, DWORD height);

HRESULT GetVideoDisplayArea(IMFMediaType *pType, MFVideoArea *pArea)
{
    HRESULT hr = S_OK;
    BOOL bPanScan = FALSE;
    UINT32 width = 0, height = 0;

    bPanScan = MFGetAttributeUINT32(pType, MF_MT_PAN_SCAN_ENABLED, FALSE);

    // In pan-and-scan mode, try to get the pan-and-scan region.
    if (bPanScan)
    {
        hr = pType->GetBlob(MF_MT_PAN_SCAN_APERTURE, (UINT8*)pArea, 
            sizeof(MFVideoArea), NULL);
    }

    // If not in pan-and-scan mode, or the pan-and-scan region is not set, 
    // get the minimimum display aperture.

    if (!bPanScan || hr == MF_E_ATTRIBUTENOTFOUND)
    {
        hr = pType->GetBlob(MF_MT_MINIMUM_DISPLAY_APERTURE, (UINT8*)pArea, 
            sizeof(MFVideoArea), NULL);

        if (hr == MF_E_ATTRIBUTENOTFOUND)
        {
            // Minimum display aperture is not set.

            // For backward compatibility with some components, 
            // check for a geometric aperture. 

            hr = pType->GetBlob(MF_MT_GEOMETRIC_APERTURE, (UINT8*)pArea, 
                sizeof(MFVideoArea), NULL);
        }

        // Default: Use the entire video area.

        if (hr == MF_E_ATTRIBUTENOTFOUND)
        {
            hr = MFGetAttributeSize(pType, MF_MT_FRAME_SIZE, &width, &height);

            if (SUCCEEDED(hr))
            {
                *pArea = MakeArea(0.0, 0.0, width, height);
            }
        }
    }
    return hr;
}
MFOffset MakeOffset(float v)
{
    MFOffset offset;
    offset.value = short(v);
    offset.fract = WORD(65536 * (v-offset.value));
    return offset;
}
MFVideoArea MakeArea(float x, float y, DWORD width, DWORD height)
{
    MFVideoArea area;
    area.OffsetX = MakeOffset(x);
    area.OffsetY = MakeOffset(y);
    area.Area.cx = width;
    area.Area.cy = height;
    return area;
}

在像素纵横比之间转换

下面的代码显示如何将矩形从一个像素纵横比 (PAR) 转换为另一个,同时保留图片纵横比。

//-----------------------------------------------------------------------------
// Converts a rectangle from one pixel aspect ratio (PAR) to another PAR.
// Returns the corrected rectangle.
//
// For example, a 720 x 486 rect with a PAR of 9:10, when converted to 1x1 PAR,
// must be stretched to 720 x 540.
//-----------------------------------------------------------------------------

RECT CorrectAspectRatio(const RECT& src, const MFRatio& srcPAR, const MFRatio& destPAR)
{
    // Start with a rectangle the same size as src, but offset to (0,0).
    RECT rc = {0, 0, src.right - src.left, src.bottom - src.top};

    // If the source and destination have the same PAR, there is nothing to do.
    // Otherwise, adjust the image size, in two steps:
    //  1. Transform from source PAR to 1:1
    //  2. Transform from 1:1 to destination PAR.

    if ((srcPAR.Numerator != destPAR.Numerator) || 
        (srcPAR.Denominator != destPAR.Denominator))
    {
        // Correct for the source's PAR.

        if (srcPAR.Numerator > srcPAR.Denominator)
        {
            // The source has "wide" pixels, so stretch the width.
            rc.right = MulDiv(rc.right, srcPAR.Numerator, srcPAR.Denominator);
        }
        else if (srcPAR.Numerator < srcPAR.Denominator)
        {
            // The source has "tall" pixels, so stretch the height.
            rc.bottom = MulDiv(rc.bottom, srcPAR.Denominator, srcPAR.Numerator);
        }
        // else: PAR is 1:1, which is a no-op.

        // Next, correct for the target's PAR. This is the inverse operation of 
        // the previous.

        if (destPAR.Numerator > destPAR.Denominator)
        {
            // The destination has "wide" pixels, so stretch the height.
            rc.bottom = MulDiv(rc.bottom, destPAR.Numerator, destPAR.Denominator);
        }
        else if (destPAR.Numerator < destPAR.Denominator)
        {
            // The destination has "tall" pixels, so stretch the width.
            rc.right = MulDiv(rc.right, destPAR.Denominator, destPAR.Numerator);
        }
        // else: PAR is 1:1, which is a no-op.
    }
    return rc;
}

计算信箱区域

以下代码在给定源和目标矩形的情况下计算信箱面积。 假设两个矩形具有相同 PAR。

RECT LetterBoxRect(const RECT& rcSrc, const RECT& rcDst)
{
    // Compute source/destination ratios.
    int iSrcWidth  = rcSrc.right - rcSrc.left;
    int iSrcHeight = rcSrc.bottom - rcSrc.top;

    int iDstWidth  = rcDst.right - rcDst.left;
    int iDstHeight = rcDst.bottom - rcDst.top;

    int iDstLBWidth;
    int iDstLBHeight;

    if (MulDiv(iSrcWidth, iDstHeight, iSrcHeight) <= iDstWidth) 
    {
        // Column letterboxing ("pillar box")
        iDstLBWidth  = MulDiv(iDstHeight, iSrcWidth, iSrcHeight);
        iDstLBHeight = iDstHeight;
    }
    else 
    {
        // Row letterboxing.
        iDstLBWidth  = iDstWidth;
        iDstLBHeight = MulDiv(iDstWidth, iSrcHeight, iSrcWidth);
    }

    // Create a centered rectangle within the current destination rect

    LONG left = rcDst.left + ((iDstWidth - iDstLBWidth) / 2);
    LONG top = rcDst.top + ((iDstHeight - iDstLBHeight) / 2);

    RECT rc;
    SetRect(&rc, left, top, left + iDstLBWidth, top + iDstLBHeight);
    return rc;
}

媒体类型

视频媒体类型

MF_MT_MINIMUM_DISPLAY_APERTURE

MF_MT_PAN_SCAN_APERTURE

MF_MT_PAN_SCAN_ENABLED

MF_MT_PIXEL_ASPECT_RATIO