首页 > 系统相关 >x264 亚像素插值及其内存结构

x264 亚像素插值及其内存结构

时间:2024-12-20 12:09:40浏览次数:3  
标签:插值 frame 像素 stride x264 ref

参考:
https://blog.csdn.net/leixiaohua1020/article/details/45936267

1. 亚像素插值原理

先简单介绍一下亚像素插值是如何进行的,基本来自这篇博客 https://blog.csdn.net/leixiaohua1020/article/details/45936267
h264 中像素可以分为整像素、半像素、1/4像素,其中半像素和1/4像素都是通过对整像素插值的来的,类似图像缩放算法中的插值。
具体为什么要插值出亚像素,为什么亚像素比整像素对帧间运动搜索效果更好等算法原理,这里不做讨论。
借用 https://blog.csdn.net/leixiaohua1020/article/details/45936267 的图,画的很清晰:

如上图所示,亚像素插值分为两步:

  • 第一步是先插值出半像素:b, h, j, m, s
  • 第二步是再插值出剩下的1/4像素:a, c, d, e, f, g, i, k, n, p, q, r

那么整像素 G 加上上述的亚像素,加起来一共 4x4=16 个像素。可以这么理解,通过插值,将一个整像素扩展为一个 4x4 矩形,矩形左上角第一个像素是整像素,其余都是插值出来的亚像素。
当然,亚像素不是单个整像素生成的,而是多个整像素生成的。

另外半像素还有一个概念对后面理解存储结构很重要,这里说明一下:

  • 我们上面说的 4x4 矩形中,半像素在矩形中实际上只有 b, h, j 3 个,另外两个半像素 m, s 可以看作属于其它整像素代表的 4x4 矩形
  • 像素 b 被称为 H 像素(水平半像素), 像素 h 被称为 V 像素(垂直半像素),像素 j 被称为 V 像素(对角线半像素)

2. 运动矢量 mv 如何表示亚像素

运动矢量 mv 表示当前宏块的像素级偏移量,坐标原点是当前宏块的左上角,数值可正可负数,也可以为 0。如果宏块是 16x16 分割,那么只会有一个运动矢量。
在 h264 中,码流中的运动矢量都是 1/4 像素精度:

  • 逻辑上 mv 可以看作为小数,例如 1.25,即表示移动 1 个整像素,然后移动 1 个 1/4 像素。小数部分可以取值 0, 0.25, 0.5, 0.75 即 4 种
  • 码流中如何表达这种小数呢,答案是码流中的数值是小数 mv 乘以 4 得来的,例如 1.25 * 4 = 6。乘以 4 的原因是,4 种小数乘以 4 后都可以转为整数

以一个真实的 pskip 宏块为例,其运动矢量为 (-36, -39),换算为小数形式就是 (-9.0, -9.75),即实际上参考的宏块相对于当前宏块的偏移是 (-9.0, -9.75) 个像素

结合上面说的 4x4 矩阵,我们可以进一步推测运动矢量小数部分分别表示哪些位置:

  • 运动矢量 0 即表示 G 整像素
  • 运动矢量 0.25 即表示向右或向下走第一个亚像素
  • 运动矢量 0.5 即表示向右或向下走第二个亚像素
  • 运动矢量 0.75 即表示向右或向下走第三个亚像素
  • 再往右或向下走就到了其它 4x4 矩阵的整像素了,所以这里我们就可以通过 4 个小数索引 4x4 矩阵的所有亚像素

3. 插值是必须的吗?什么时候进行

插值不是必须的,例如 x264 中有一个可设置的参数 i_subpel_refine,如果将其设置为 0,表示不会开启亚像素运动搜索,那么就不需要再进行插值了。我们一般设置 preset 为 ultrafast 后,i_subpel_refine 就会被设置为 0.
插值在什么时候进行的呢,这要分情况:

  • 半像素插值是在编码完一行宏块,进行滤波的时候,进行插值的。继续参考:https://blog.csdn.net/leixiaohua1020/article/details/45936267,里面有写明
  • 1/4 像素不会提前插值,而是在需要的时候才会插值出来,例如在运动搜索的时候,通过调用 get_ref() 函数,来进行临时插值。每调用一次,都需要插值一次

4. 亚像素的内存存储结构

下面进入重点,亚像素的内存存储结构是怎么样的呢,解答这个问题需要先了解 x264 内部的几个变量/数据类型:

  • x264_frame_t,帧数据类型,里面存储了所有像素相关的数据,所有整像素,亚像素都存储在这里
  • pixel *buffer[4],此 pixel 指针型变量是 x264_frame_t 内部的一个变量,注意到维度为 4,实际上我们只关注索引 0 即可,即对应 yuv420p 中的 luma 分量
  • pixel *filtered[3][4], 此 pixel 指针型变量也是 x264_frame_t 内部的一个变量,注意到维度为 [3][4],实际上第一个维度我们只关注索引 0 即可,即对应 yuv420p 中的 luma 分量;第二个维度 4 分别为 整像素,H 半像素,V 半像素,C 半像素

frame.c::frame_new() 函数中,会新建一个 x264_frame_t 帧数据对象,此函数的调用时机是在 x264 所有编码逻辑之前。此函数调用会给 pixel *buffer[4] 申请内存空间:

static x264_frame_t *frame_new( x264_t *h, int b_fdec )
{
...
    // 可以认为 luma_plane_count = 1, 即 YUV420P 中的 luma
    for( int p = 0; p < luma_plane_count; p++ )
    {
        // luma_plane_size 的大小可以简单看作是输入图像的 width * height
        int64_t luma_plane_size = align_plane_size( frame->i_stride[p] * (frame->i_lines[p] + 2*i_padv), disalign );
        // 如果开启了 i_subpel_refine,那么 luma_plane_size 需要扩展 4 倍
        if( h->param.analyse.i_subpel_refine && b_fdec )
            luma_plane_size *= 4;

        // 给 frame->buffer[0] 分配内存空间
        PREALLOC( frame->buffer[p], luma_plane_size * SIZEOF_PIXEL );
    }
}
...

如上可以看到,如果没有开启 i_subpel_refine,那么基本上 frame->buffer[0] 的大小就是 widht * height 大小。
注意到代码中判断如果开启了 i_subpel_refine,luma_plane_size 需要扩展 4 倍大小,这是干嘛用的呢,我们接着看 pixel *filtered[3][4] 的赋值,同样也在 frame.c::frame_new() 函数中,在给 frame->buffer[0] 申请空间之后:

static x264_frame_t *frame_new( x264_t *h, int b_fdec )
{
...
    // 可以认为 luma_plane_count = 1, 即 YUV420P 中的 luma
    for( int p = 0; p < luma_plane_count; p++ )
    {
        // luma_plane_size 的大小可以简单看作是输入图像的 width * height
        int64_t luma_plane_size = align_plane_size( frame->i_stride[p] * (frame->i_lines[p] + 2*i_padv), disalign );
        if( h->param.analyse.i_subpel_refine && b_fdec )     // 如果开启了 i_subpel_refine 走这里
        {
            // 注意到这里有个 4,即对应 filtered[3][4] 的第二个维度,代表 整像素,H 半像素,V 半像素,C 半像素
            for( int i = 0; i < 4; i++ )
            {
                // 这里是关键,每轮循环,`+ i*luma_plane_size` 表示每种像素占的空间是 luma_plane_size 大小
                // 后面 `frame->i_stride[p] * i_padv + PADH_ALIGN` 一串可以忽略
                frame->filtered[p][i] = frame->buffer[p] + i*luma_plane_size + frame->i_stride[p] * i_padv + PADH_ALIGN;
            }
        }
        else                                                 // 如果没有开启 i_subpel_refine 走这里
        {
            frame->filtered[p][0] = frame->plane[p] = frame->buffer[p] + frame->i_stride[p] * i_padv + PADH_ALIGN;
        }
    }
}
...

如上可以看到,frame->filtered[0][4] 的内存空间实际上就取自 frame->buffer[0]。
当开启 i_subpel_refine 后,frame->filtered[0][4] 将 frame->buffer[0] 平分为 4 段,每段长度差不多是 width * height 大小整像素在第一段,H 半像素在第二段,V 半像素在第三段,C 半像素在第四段,且他们都是 plane 存储,而不是交织存储
对应到前面说的 4x4 矩阵:

  • 整像素 G 及其代表的整像素按顺序存储到 frame->filtered[0][0] 中
  • 半像素 b 及其代表的 H 半像素按顺序存储到 frame->filtered[0][1] 中
  • 半像素 h 及其代表的 V 半像素按顺序存储到 frame->filtered[0][2] 中
  • 半像素 j 及其代表的 C 半像素按顺序存储到 frame->filtered[0][3] 中

5. me.c::get_ref() 调用如何取出亚像素

me.c::get_ref() 函数实际上用于通过给定的运动矢量 mv,从 frame->filtered[3][4] 中取出参考帧的参考像素。这个函数比较复杂,下面进行拆解分析。

首先是运动矢量 mv 的表示和一些运算,通过前文我们知道,运动矢量是整数表示的,通过乘以 4 将小数表示为整数,x264 内部也是整数表示,但是有时候需要取出整像素和小数部分:

  • get_ref() 函数中,有个 &3 的计算,3 的二进制即 0b11,所以这里实际上就是取运动矢量的小数部分
  • get_ref() 函数中,有个 qpel_idx = ((mvy&3)<<2) + (mvx&3) 的计算,这里 &3 即取小数部分,我们知道了,加法前面还有一个 <<2 的操作,即将 y 轴运动矢量的小数部分先扩大,然后加上 x 轴运动矢量的小数部分,这样可以组成一个唯一的数
  • get_ref() 函数中,有个 if( qpel_idx & 5 ) 的判断,5 的二进制即 0b0101,结合前面说的 y 轴运动矢量小数部分 <<2 的操作,我们能够知道,这里实际上是判断 x 轴和 y 轴运动矢量的小数部分,是否有 0.250.75 这两个值存在

其次,在 get_ref() 函数内部有一个重要的变量 qpel_idx,这个变量是什么意思呢,下面进行说明:

  • 结合上面,qpel_idx 通过取运动矢量的小数部分,组合成一个唯一的数,这个数的取值大小实际上我们可以计算出来,即 0 到 15 共 16 种
  • 同时结合前面第 2 节说明的运动矢量小数部分对 4x4 矩阵索引的概念,qpel_idx 的含义就能明白了:其代表了 4x4 矩阵的索引
  • 即 4x4 矩阵左上角第一个整像素代表 qpel_idx = 0 位置;4x4 矩阵右下角最后一个亚像素代表 qpel_idx = 15 的位置,其它类推

其次,在 get_ref() 函数内部有两个重要常量数组 x264_hpel_ref0x264_hpel_ref1,这两个数组是干嘛的呢:

  • x264_hpel_ref0x264_hpel_ref1 数组是结合 qpel_idx 使用的,qpel_idx 是其数组索引
  • x264_hpel_ref0x264_hpel_ref1 数组内部的取值只有 4 种,即 0, 1, 2, 3,这 4 种数值即对应了 整像素,H 半像素,V 半像素,C 半像素
  • 结合上面对 qpel_idx 的说明,我们能够推测出这两个数组的作用:即要得到 4x4 矩阵中某个像素,我们需要 整像素,H 半像素,V 半像素,C 半像素 这几种中的哪几种
  • 例如我们要得到 e 像素,首先根据 x264_hpel_ref0 得到 b 像素,然后通过 x264_hpel_ref1 得到 h 像素
  • 例如我们要得到 a 像素,首先根据 x264_hpel_ref0 得到 b 像素,然后通过 x264_hpel_ref1 得到 G 像素

经过上面的前置分析,我们可以来正式分析 me.c::get_ref() 函数了(以取 16x16 分割大小的像素为例):

// dst 是取出的 16x16 个像素目标地址
// i_dst_stride 是 dst 目标地址的 stride,这个 stride 与 i_width 有关
// src[4] 即源像素地址,维度 4 即分别存储了 整像素,H 半像素,V 半像素,C 半像素,注意每种像素在整个图像中都是连续存储的
// i_src_stride 即源像素的 stride,这个 stride 可以简单认为就是源图像的 width
// mvx, mvy 即相对于当前宏块的运动矢量
// i_width, i_height 在 16x16 分割中即为 16
// weight 是加权参数
static pixel *get_ref( pixel *dst,   intptr_t *i_dst_stride,
                       pixel *src[4], intptr_t i_src_stride,
                       int mvx, int mvy,
                       int i_width, int i_height, const x264_weight_t *weight )
{
    // 上面已经分析了
    int qpel_idx = ((mvy&3)<<2) + (mvx&3);
    // 结合上面的分析,容易知道这里是组合运动矢量整像素,乘以 i_src_stride 是因为一个 mvy 跨越一整行
    int offset = (mvy>>2)*i_src_stride + (mvx>>2);
    // x264_hpel_ref0[qpel_idx] 即当前运动矢量是依赖哪种像素类型
    // src[x264_hpel_ref0[qpel_idx]] 即去到像素类型所在的维度
    // + offset 即从位置 0 去到目标位置
    // (((mvy&3) == 3) * i_src_stride 的意思是,如果当前是取 n, p, q, r 这几种 1/4 像素,那么还需要走到下一行,因为 1/4 像素插值需要用到下一行的半像素
    pixel *src1 = src[x264_hpel_ref0[qpel_idx]] + offset + ((mvy&3) == 3) * i_src_stride;

    // 上面已经分析了,即是否是取 1/4 像素
    if( qpel_idx & 5 ) /* qpel interpolation needed */
    {
        // 取 1/4 像素插值需要的两一个半像素
        pixel *src2 = src[x264_hpel_ref1[qpel_idx]] + offset + ((mvx&3) == 3);
        // 所有的 1/4 像素都能通过两个已经提前计算并存储好的半像素插值得到
        pixel_avg( dst, *i_dst_stride, src1, i_src_stride,
                   src2, i_src_stride, i_width, i_height );
        if( weight->weightfn )
            mc_weight( dst, *i_dst_stride, dst, *i_dst_stride, weight, i_width, i_height );
        return dst;
    }
    else if( weight->weightfn )
    {
        mc_weight( dst, *i_dst_stride, src1, i_src_stride, weight, i_width, i_height );
        return dst;
    }
    else
    {
        // 如果是取整像素或者半像素,那么这里直接返回 src1 即可,不需要再做其它逻辑
        // 注意这里 i_dst_stride 赋值为了 i_src_stride,因为实际上没有对输入的 dst 做任何修改,而是直接返回的 src,所以外面要正确取值,需要正确的 stride
        *i_dst_stride = i_src_stride;
        return src1;
    }
}

需要注意的是,我们获取的 16x16 的亚像素大小(16x16 分割),结合上述的内存连续的特征,其中每个像素要么都是整像素,要么都是 H 半像素,要么都是 1/4 像素等,不存在混合。

7. 亚像素如何使用

我们获取亚像素一般用来在模式选择中计算 cost,注意一下当前待编码宏块没有亚像素的概念。
例如我们现在有一个给定的运动矢量,如何计算其 cost 呢,我们可以这么写代码(一个 16x16 分割的宏块):

x264_mb_analysis_t a;
// 初始化 a
mb_analyse_load_costs(h, a);
// 新建运动搜索对象
x264_me_t m;
m.i_pixel = PIXEL_16x16;
// 载入当前待编码帧的宏块整像素
LOAD_FENC(&m, h->mb.pic.p_fenc, 0, 0);
// 参考帧序号
int i_ref = 0;
// 参考帧开销
m.i_ref_cost = REF_COST(0, i_ref);
// 加载参考帧像素指针
LOAD_HPELS(&m, h->mb.pic.p_fref[0][i_ref], 0, i_ref, 0, 0);
LOAD_WPELS(&m, h->mb.pic.p_fref_w[i_ref], 0, i_ref, 0, 0);
// 预测运动矢量
x264_mb_predict_mv_16x16(h, 0, i_ref, m.mvp);

// 给定的运动矢量
int mx = 0;
int my = 0;
// 16x16 中,bw, bh = 16
int bw = x264_pixel_size[m.i_pixel].w;
int bh = x264_pixel_size[m.i_pixel].h;
// get_ref() 输入参数中的 i_src_stride
int stride = m.i_stride[0];
// get_ref() 输入参数中的 dst
ALIGNED_ARRAY_32(pixel, pix, [16 * 16]);
// get_ref() 输入参数中的 i_dst_stride
intptr_t stride2 = 16;
// 调用 get_ref()
pixel* src = h->mc.get_ref(pix, &stride2, m.p_fref, stride, mx, my, bw, bh, &m.weight[0]);
// 待编码宏块像素
pixel* p_fenc = m.p_fenc[0];
// 计算残差 cost
m.cost = h->pixf.fpelcmp[m.i_pixel](p_fenc, FENC_STRIDE, src, stride2);
// 累加参考帧 cost
m.cost += m.i_ref_cost;
const uint16_t* p_cost_mvx = m.p_cost_mv - m.mvp[0];
const uint16_t* p_cost_mvy = m.p_cost_mv - m.mvp[1];
// 累加运动矢量 cost
m.cost += p_cost_mvx[mx] + p_cost_mvy[my];

标签:插值,frame,像素,stride,x264,ref
From: https://www.cnblogs.com/moonwalk/p/18618289

相关文章

  • labelme标注后的数据只剩下面积1600像素以内的小颗粒
    点击查看代码importcv2importnumpyasnpimportjsonimportosdeflist_jsons(folder_path):forfilenameinos.listdir(folder_path):iffilename.endswith(('.json',)):yieldos.path.join(folder_path,filename)defremove_spec......
  • PbootCMS模板上传栏目缩略图时,图片宽度自动变成1000像素,如何解决?
    在使用PbootCMS模板时,如果你发现上传的栏目缩略图在保存后自动变成了1000像素的宽度,这通常是由于系统配置中的缩略图最大宽度设置所致。你可以通过修改配置文件来解决这个问题。以下是详细的步骤和实现方法:理解问题原因:PbootCMS在上传图片时会根据配置文件中的设置自动调整图......
  • 拉格朗日插值和数值微积分
    +++date='2024-11-30T15:26:27+08:00'draft=truetitle='拉格朗日插值和数值微积分'+++初次发布于我的个人文档。(每次都是个人文档优先发布哦)本文想简要介绍和推导一下拉格朗日插值和数值积分方法。什么是插值?所谓的插值就是已知几个离散点的信息视图求一个满足这些......
  • 【Unity 顶视角火焰与爆炸效果图像包】Top Down Fire and Explosion Sprites Pixel Ar
    TopDownFireandExplosionSpritesPixelArt是一款专为Unity开发者设计的像素艺术风格图像包,专注于提供顶视角(TopDown)火焰和爆炸效果的精美像素艺术图标。这个插件适用于需要火焰、爆炸和其他视觉效果的游戏场景,特别是用于RPG、射击游戏、策略游戏等类型。以下是该插......
  • html中表格的width和height的值为什么没有单位?它的的默认单位是像素吗?
    在HTML中,当你为(<td>、<th>)或其他HTML元素设置width和height属性时,确实可以不带单位。在这种情况下,浏览器默认使用像素(px)作为单位。示例<tablewidth="500"height="300"><tr><td>单元格内容</td></tr></table>在这个例子中,表格的宽度被设置为500像素,高度......
  • 克里金插值举例
    1. 采样数据收集-假设我们研究的农田是一个长方形区域,长100米,宽80米。我们在这片农田里按照一定的网格布局,选取了20个采样点。在每个采样点,我们都精确地测量了土壤中氮元素的含量(单位:mg/kg)。例如,其中5个采样点的数据如下:采样点1的氮含量为15mg/kg,采样点2的氮含量为18mg/kg,采......
  • Python OpenCV按照像素点图片切割
    图像分割是从图像处理到图像分析的关键步骤,在目标检测、特征提取、图像识别等领域具有广泛应用。OpenCV是一个强大的计算机视觉库,提供了多种图像分割方法。本文将详细介绍如何使用Python和OpenCV进行基于像素点的图像分割,包括阈值分割、自适应阈值分割、Otsu's二值化、分水岭算法......
  • 你了解什么是像素追踪吗?它是用来做什么的?它的实现原理是什么?
    像素追踪(PixelTracking)在前端开发中是一种用于收集用户行为数据的方法,它通常用于网站分析、广告转化跟踪和个性化推荐等方面。它允许网站所有者了解用户如何与他们的网站互动,例如用户点击了哪些链接、浏览了哪些页面、在每个页面停留了多长时间等等。像素追踪主要用途:网站分析......
  • p标签里面嵌套img标签会出现向上高3像素是什么原因?如何处理?
    img元素默认是inline元素,与文本的基线对齐。而p元素内部的文本也与基线对齐。img元素底部会有几像素的空白,这是由于img的默认vertical-align属性值为baseline造成的。这个空白通常表现为向上偏移3px左右,但具体数值取决于字体大小、行高以及图片的底部边缘形状。解......
  • 什么是物理像素和逻辑像素?
    在前端开发中,物理像素和逻辑像素是两个重要的概念,它们共同影响着如何在屏幕上显示内容。它们的区别在于:物理像素(PhysicalPixel):指的是显示器上可以实际控制发光的最小单位。一个物理像素就是一个屏幕上的一个物理光点。物理像素的数量是由屏幕硬件决定的,是固定的,不可改变......