Sign Hiding

在HEVC中,对于每个变换单元,量化的变换系数以16个系数的组进行熵编码。在sign-hiding 的情况下,对于满足一定条件的系数组,沿扫描路径的第一非零系数的符号不在比特流中显式传输,而是从解码器处该系数组中的所有非零系数之和的奇偶性来推断。为了保证隐藏符号与所有非零系数之和的奇偶性匹配,编码器采用了基于率失真优化或失真最小化的奇偶性调整方法。

1 隐藏符号位的基本原理

在视频编码中,数据隐藏是一种已知的技术,通常用于将一个语法元素隐藏到部分比特流中以提供更好的编码效率。例如,用于帧内预测的MPM索引嵌入量化系数之和的奇偶性中;使用量化系数的奇偶性来传送变换模式的信息。

在将语法元素隐藏到其他语法元素的特定特征中时,必须考虑要隐藏哪个语法元素(源信号)以及要将其隐藏到哪里(目标信号)。为了产生率失真增益,必须考虑几个标准。

首先,从因果关系的角度来看,隐藏必须是可行的。在编码期间,当编码目标信号时源信号必须可用,在解码期间,必须在实际使用源信号之前解码目标信号。

其次,源信号必须代表压缩比特流中不可忽略的部分。从先前关于压缩数据隐藏的研究可知,隐藏数据时可以获得净增益,然而,以速率增益度量,它将仅表示隐藏信号的总速率的一部分。因此,如果数据隐藏要产生任何有价值的增益,则源信号应代表压缩比特流的相当大的比例。

此外,如果用于嵌入信息的目标和源信号特征不相关,则为了从隐藏源信号中获得最大增益,必须考虑其分布条件。当将二进制元素隐藏到目标信号的二进制函数中(例如将位隐藏到奇偶校验中)时,在大约50%的情况下,目标信号将需要改变。因此,由这种变化引起的率失真损失是近似恒定的,并且与源信号的分布无关:例如,隐藏始终等于0的二进制语法元素会导致相同的损失,因为奇偶校验仍然必须改变一半的时间。另一方面,当源信号在比特流中编码时达到其每符号1比特(在二进制元素的情况下)的编码上限时,通过省略源信号的传输而获得的增益是最大的。因此,必须为源信号选择一个等概率语法元素。

目标信号的一个重要条件在于能够找到对率失真性能影响最小的变化,因为在大约一半的情况下需要进行变化。因此,最好使用较大的目标信号,从而增加发现这种变化的机会。例如,将二元元素隐藏到单个量化系数的奇偶性中将在需要改变该系数时产生高率失真损失,当将其隐藏在较大的量化系数集合的和的奇偶性中时,增加了找到非常接近其量化区间边界并且其中量化值的改变将导致中等率失真损失的量化系数的机会。­

最后,希望源信号和目标信号大致成比例:当有更多的数据要隐藏时,应该有更多的目标信号嵌入其中。

量化系数可以做sign-hiding的原因:

  • 在编码器中,当要隐藏的符号已知时,可以修改量化系数的电平,并且在解码器中,在符号比特实际用于解码信号之前,可以使用这些电平。
  • 符号位代表压缩比特流的很大一部分(在HEVC通用测试条件下为15%到20%)。
  • 符号位是等概率的。
  • 在一组量化系数中嵌入符号位可以提供足够的机会找到一个量化系数,该量化系数在修改时会导致中等的率失真损失。
  • 当有更多的符号位要隐藏(源信号)时,有更多的剩余数据(目标信号)要嵌入它们。

2 量化和系数编码

可参考文章:

率失真优化量化 (Rate-Distortion Optimized Quantization RDOQ) – 我受到了惊吓 (mmedia-t.cn)

变换系数编码 – 我受到了惊吓 (mmedia-t.cn)

3 Sign Hiding

3.1 在解码器处推断隐藏符号位

对于每个CG,解码器统计沿扫描路径的第一个非零系数和最后一个非零系数之间的系数数目,并将其与阈值进行比较。如果超过阈值,则从所有非零系数之和的奇偶性推断第一个非零系数的符号。特别地,如果和为偶数,则该符号被推断为正;如果和为奇数,则该符号被推断为负。在这种情况下,不需要为该系数显式传输符号位。否则,以常规方式(每个符号1位)从比特流解码符号位。稍后在讨论编码器侧奇偶校验调整方案时,将详细说明应用这种标准以排除某些CG应用符号隐藏技术的原因。

上述多个符号位隐藏的解码处理针对一个TU内的每个CG重复。由于每个CG具有16个系数,因此对于一个4×4、8×8、16×16或32×32 TU,可分别隐藏多达1、4、16或32个符号位。­

3.2 编码器中隐藏符号位

为了避免失配,编码器执行与解码器相同的检查,以比较具有相同阈值的每个CG的第一非零系数和沿扫描过程的最后非零系数之间的系数数目,以确定是否要编码第一非零系数的符号。如果该数字不大于预定义阈值,则以常规方式编码符号;否则,不显式传输第一个非零系数的符号,因此每CG节省一位。注意,所有非零系数之和的奇偶性并不总是与第一个非零系数的符号匹配。当发生这种情况时,调整量化系数的幅度,以便奇偶性匹配符号。

将一个系数组中的原始变换系数表示为\(c={c_0, c_1, … , c_{N-1}}\),其中N是系数组中的系数总数。将量化和去量化函数分别表示为\(Q(·)\)和\(Q^{-1}(·)\)。量化输出或量化变换系数表示为\(u=Q(c)={u_0, u_1, … , u_{N-1}}\)。设k是沿扫描路径的第一个非零系数的索引,即\(u_k≠0\)且\(u_i=0, 0≤i<k\)。为了优化奇偶调整后的率失真性能,可将多符号位隐藏方案表述为以下优化问题:­­

\(D(c, \hat{c})\)是c与其重构\(\hat{c}\)之间的均方失真,\(R(u)\)表示编码系数向量u所需的速率,sign(x)是符号函数,λ是拉格朗日乘子,表征了距离和编码速率之间的折衷,以及

等式(3)中的问题本质上是矢量量化问题,对于该问题,需要设计矢量量化器以使给定速率约束以及和约束的奇偶性的失真最小化。因此可以应用一般的矢量量化器设计方法,然而通常需要迭代方法并且具有高设计复杂度。

在HEVC参考编码器中,当分别使用RDOQ或HDQ时,采用两种非迭代奇偶调整方案来解决等式(3)中的问题。首先通过RDOQ或HDQ对变换系数进行量化。在和的奇偶性与要隐藏的符号不匹配的情况下,编码器执行线性搜索,并将量化系数之一的幅度调整+1或-1,以使调整后的系数之和的奇偶校验与符号匹配。

第一个奇偶校验调整方案遵循与RDOQ类似的原则,即选择系数和调整方向以最小化调整后的总体率失真代价。第二种方案在不需要准确率信息的情况下,仅使失真最小化,适用于HDQ,且不太复杂。­

Transform Skip Residual Coding (TSRC)

VVC 中的残差编码,分为regular residual coding (RRC)和transform skip residual coding (TSRC)。 TSRC 和 RRC 都建立在 HEVC 中针对可变变换块大小 引入的 4 × 4 子块 (SB) 的概念之上,通常也称为系数组 (CG)。 图 1 说明了将大于 4 × 4 的块分解为 4 × 4 CG 以及 TSRC 的相关对角线扫描模式。 由于 TSRC 设计源自 RRC 设计,因此以下段落简要概述了 RRC 设计以及导致 TSRC 设计的修改。 请注意,由于变换块支持非正方形矩形,因此 VVC 支持非 4×4 子形状,这与仅支持 4×4 SB 的 HEVC 形成对比。 然而,在不失一般性的情况下,以下描述使用术语 CG 在解析过程的变换块内也包括非 4 × 4 子形状

RRC 设计使用多级显著性指示,即嵌入到 4 × 4 CG 概念中的非零系数的存在,以便为变换和量化的残差信号提供有效的信号。 在处理变换块的变换系数之前,语法元素\( s_{cbf}\) 出现在比特流语法中,表示对整个变换块的显著性,\( s_{cbf}=0\) 暗示变换块内的所有变换系数都等于零。 当 \( s_{cbf}=1\)指示存在至少一个不等于零的变换系数时,则处理使用变量\( v^X_{last}\)和 \( v^Y_{last}\)继续最后有效扫描位置。 正如它们的名称所示,变量分别根据列和行位置指定变换块内最后一个变换系数的位置,当沿对角线扫描模式扫描时,每个相对于变换块的左上角,如下图 1所示。这意味着当沿着对角线扫描模式向块的末端扫描超过最后一个有效扫描位置时,块中没有进一步的有效变换系数。 请注意,两个变量 \( v^X_{last}\)和\( v^Y_{last}\)中的每一个的值都是从两个语法元素重建的,这两个语法元素指定了最后一个变换系数的列或行位置的前缀和(可能为空)后缀部分。

RRC中的变换系数解析过程从最后一个有效扫描位置开始,并使用反向对角线扫描模式来处理位于扫描路径上的所有频率位置朝向DC频率位置。 对于变换块的每个 4 × 4 CG,语法元素 \( s_{csb}\) 指示当前 CG 的显著性。 有两个例外,第一个是覆盖最后一个有效扫描位置的CG,第二个是覆盖 DC 频率位置的 CG,其中符合标准的解码器推断 \( s_{csb}\)。由于最后一个有效扫描位置已经在 CG,解码器可以为相应的 CG 推断 \( s_{csb}\)。 由于 CG 传输覆盖 DC 频率位置的显着水平的可能性很高,由于变换的能量压缩特性,符合标准的解码器总是推断左上角 CG 的\( s_{csb}\)。 然而,对于左上 CG的语法元素\( s_{sig}\)(指定扫描位置的显著性),与其他CG一样,没有推理规则,即,当在前15个扫描位置内没有出现显著水平时,由于 \( s_{csb}=1\),CG的最后一个扫描位置必须是显著的。在4×4变换块的情况下,CG满足上述两个特殊条件,解码器推断\( s_{csb}\)等于1。

1 Processing Order

RRC 和 TSRC 的一个主要区别是处理扫描模式的顺序相反,即使用常规的正向扫描顺序而不是 TSRC 沿对角线扫描模式的反向扫描顺序,如图 1 所示。 因为通常帧内预测对于远离所使用的参考样本(位于当前块的上方和左侧)的样本位置变得效率较低。 这也意味着空间域中的局部残差信号方差随着与参考样本的距离增加而变大,从而导致块右下角的电平值变大。 通过反转反向扫描顺序,即通过使用常规的前向对角线扫描顺序,当在频域中沿反向扫描顺序操作时,显著级别的概率在扫描顺序中增加,类似于 RRC 中的情况。 虽然帧间预测块的好处并不显着,但改变扫描模式方向不会对这种情况下的性能产生不利影响。 请注意,HEVC RExt 中使用的 180° 剩余旋转利用了相同的现象,但由于 SB-wise 处理,它实际上与改变扫描方向不同。

第二个主要区别是消除了 TSRC 中最后一个有效扫描位置的信号通知,导致处理块内的所有扫描位置,如图 1 所示。 然而,多级显著性指示在 \( s_{csb}\) 出现在变换块的所有 CG 的意义上仍然有效,其中变换块的最后一个(右下)CG构成一个例外。对于该特殊CG,一致解码器可以推断\( s_{csb}=1\) ,但仅当\( s_{csb}=0\) 用于所有先前处理的SB时。

2 Level Coding

正如在 RRC 设计中一样,TSRC 的量化索引(级别)的编码和解码在CG内的多个编码通道中进行,即,从解码器的角度来看,解析过程在每个通道上迭代多次,沿着 CG内的扫描模式扫描位置,直到解码器可以重建完整的级别信息。 对于 TSRC,量化索引在3个pass中编码(和解码),前提是不超过上下文编码 bin 的限制。

pass-1以交错方式为每个量化索引包含最多 4 个上下文编码语法元素: \( s_{sig}\)、 \( s_{sign}\)、 \( s_{gt1}\)和 \( s_{par}\),分别是显著性指示、符号、大于 1 标志以及奇偶校验标志。 值得注意的是,RRC 中的 \( s_{sig}\)编码使用了 CABAC 引擎的旁路模式,并作为最终通道出现,而 TSRC 中的 \( s_{sign}\) 编码是采用自适应上下文模型的pass-1编码的一部分。 虽然在 TSRC 中\( s_{sign}\) 的总体概率仍然大致等于 0.5,但对于空间域中变换跳过的残差信号,局部通常会偏向某个方向。 使用利用该统计异常的自适应上下文模型可以提高 TSRC 的编码效率。 请注意,pass-1还包括为网格编码量化 (TCQ)引入的 \( s_{par}\)语法元素(指定级别的奇偶性)。 尽管 TSRC 的 TCQ 不活跃,但 TSRC 的二值化过程保持\( s_{par}\),以便为两个残差编码路径的pass-1编码保持相似的语法元素列表。

TSRC 中的pass-2由最多 4 个额外的 bin 组成,这些 bin 是上下文编码的,指示每个绝对量化索引的greater-than-x (gtx) 属性相对于阈值 x ∈ {3, 5, 7, 9} 与 相关语法元素 sgt3、sgt5、sgt7 和 sgt9。

pass-3最终指定了在前面的编码过程中未完全传输的那些绝对量化索引的剩余部分,并且所有此类相关的 \( s_{rem}\) 语法元素都通过截断 Rice (TR) 和 Exp-Golomb (EG) bin 字符串的串联进行二值化,每个 bin 都像在 RRC 路径中一样进行旁路编码。

从概念上讲,二值化和绝对量化索引编码过程之间存在直接关系。 图 2 说明了 HEVC 和 VVC 中的二值化过程,包括 3 个具有不同参数化的不同前缀代码的串联。 原则上,与截断一元 (TU) bin 字符串相关的所有 bin 使用自适应上下文模型进行上下文编码,而与 TR 和 EG bin 字符串相关的 bin 使用 CABAC 引擎的旁路模式进行编码。 HEVC 和 VVC 二值化过程之间最显着的区别是 TCQ的奇偶\( s_{par}\)语法元素,对于 RRC,当启用 TCQ 时,奇偶标志驱动 TCQ 状态机,其在第一个编码通道中的信号避免了 在知道下一个扫描位置的 TCQ 状态之前,需要重建每个扫描位置的完整级别信息。 请注意,TCQ 状态是 RRC 中 \( s_{sign}\) bin 上下文建模的重要元素。 从二值化的角度来看,每对具有不同奇偶校验的两个连续绝对级别 {(2n, 2n + 1)|n ∈ N+} 共享相同的二进制码字表示。 因此,对于所有 c ≥ 2,必须对图 2 中表示绝对水平值的 c 轴进行二次采样,从而将二值化过程应用于绝对水平集 c ∈ {0, 1}∪{2n|n ∈ N+} 而不是 HEVC 的 c ∈ N0。 另一个结果是,只要 c ≥ 2,就需要通过在适当的 bin 索引处添加表示 \( s_{par}\) 的奇偶校验 bin 来完成生成的 bin 字符串。在 RRC 和 TSRC 中,关联的 TU bin 字符串的索引顺序是这样的奇偶校验 bin 始终放在表示 \( s_{gt1}\) 的大于 1 的 bin 之后。 请注意,对于 TSRC,必须在所有 c ≥ 1 的 \( s_{sign}\) bin 之后添加表示语法元素 \( s_{sign}\) 的另一个 bin,如上文已经讨论的那样。 对于余数编码通道,TSRC 对 TR 和 EG 二值化采用相同的第二阈值 b1 和固定 Rice 参数 k = 1。

3 Context Modeling

对于上下文建模,TSRC 采用类似于 RRC 路径但相邻位置较少的局部模板 T 进行评估。 图3左侧为RRC路径的本地模板和已经处理的相邻频率位置,右侧为TSRC对应的本地模板。 请注意,由于反向对角线扫描模式,局部模板在 RRC 中跨越到右侧和底部,而在 TSRC 中,局部模板是反转的,即它跨越到空间中当前扫描位置的左侧和顶部。 TSRC 中的上下文建模不是评估五个邻居,而是只评估当前扫描位置上方和左侧的空间邻居。

令 χ ∈ X 表示上下文模型集 X 内的上下文记忆偏移量,其中每个上下文模型集 X 具有固定偏移量,因此所有上下文模型集都是不相交的。 进一步,设 TA(s) 和 TL (s) 分别为上邻居和左邻居的评价结果。 然后,语法元素 \( s_{sign}\) 的上下文模型索引 χsig ∈ Xsig 与 |Xsig| = 3 等于

相同的规则适用于 \( s_{gt1}\),但触发单个专用上下文模型的 BDPCM 模式除外 (|Xgt1| = 4)

\( s_{sign}\) 语法元素的上下文建模比较本地模板覆盖的相邻样本值,并选择符号上下文模型集合 Xsign 的第一个上下文模型(χsign = 0)与 |Xsign| = 6 当两个邻居都不重要时 (TA(ssig ) = 0∧TL(ssig ) = 0) 或两个符号值不同 (TA(ssign) ≠TL(ssign))。 请注意,仅当两个相邻位置都包含显着水平时,第二个条件才成立。 当两个相邻位置中的一个由正水平组成时 (TA(ssign) = 1∨ TL(ssign) = 1),上下文建模选择集合的第二个上下文模型 (χsign = 1)。 所有其他情况都会导致使用第三个上下文模型 (χsign = 2)。 请注意,对于 BDPCM 模式,适用于 \( s_{sign}\) 语法元素的相同上下文建模,但固定偏移量等于 3,这意味着对于 BDPCM,使用一组单独的上下文模型。 属于第一个编码通道的 spar 语法元素和第二个编码通道的所有语法元素,即 sgt3、sgt5、sgt7 和 sgt9,使用单个专用上下文模型。

4 Limit on Context-Coded Bins

为了实现具有成本效益和可行的硬件实现,在 RRC 和 TSRC 的设计中纳入了变换块中每个系数的上下文编码箱 (ccb) 的最坏情况限制。 当 N 表示(变换)块中变换系数或样本的数量时,上下文编码箱的最大预算在级别编码开始时设置为 Bccb = 1.75× N。 对于每个上下文编码的编码通道和每个扫描位置,必须在实际执行编码通道之前检查条件 Bccb ≥ 4。 每当扫描位置发生 Bccb < 4 时,剩余电平信息的处理将回退到纯旁路编码模式,仅使用由 TR 和 EG bin 字符串组成的二值化,即图 2 所示的第一变量阈值 b0 是 设置等于 −1 的值.

5 Level Prediction

当当前扫描位置具有在pass-1通道中编码的电平信息时,处理在余数编码通道结束时应用电平预测技术。 从解码器的角度来看,上述和左邻域的绝对水平 (TA(c) 和 TL (c)) 用作预测变量 p = max{T A(c), T L(c)},并且以下三种情况之一适用。 如果当前绝对水平 c 等于 1 且预测变量大于 0 (c = 1 ∧ p > 0),则最终绝对水平等于预测变量值 (c = p)。 最终绝对水平小于或等于预测变量值时减一,否则不变,总结如下:

请注意,级别预测技术等效于二值化过程的修改,即级别预测将 bin 字符串交换为 c = 1 和 c = p。 此外,当 BDPCM 对当前块有效时,不应用电平预测。

WAIP: Wide Angular Intra Prediction

在HEVC中,使用35个帧内预测模式,如图1(a)所示,其中帧内预测模式0和1分别表示Planar预测模式和DC预测模式,模式2到34表示具有不同方向性的方向预测模式。注意,由于预测方向是所有方向预测角度中使用最频繁的方向,因此在水平和垂直帧内预测方向周围被分配为更密集的预测方向。

在VVC中,进一步采用基于65个角方向的帧内预测方案,以更细粒度地捕获边缘方向。图1(b)示出了在VVC中为方形块定义的帧内预测方向。尽管VVC中的角度帧内预测方向的数目已经增加到65个,但是方向的范围最初保持不变,并且在顺时针方向上从45度到-135度开始。如图1所示,模式34和2在HEVC中表示45度和-135度,而模式66和2在VVC中表示相同的方向。

在VVC中,通过使用多类型树(MTT)块划分结构实现矩形块形状的帧内预测,该结构利用二元和三元分割。在编码树结构中,CU可以是方形或矩形。对于W×H块,可用的顶部和左侧参考样本阵列大小相等,包括(W+H+1)个样本,如图2的左侧部分所示,这需要覆盖当前块中要预测的所有像素的上述预测角度范围。

VTM中预测角度的定义是为了与HEVC指定的方向兼容而设计的,不考虑MTT块划分结构中支持的各种块形状。例如,当块为水平形状(即W>H)时,待预测样本比左侧引用数组更靠近顶部引用数组。类似地,当块为垂直形状(即H>W)时,要预测的样本比顶部参考阵列更靠近左侧参考阵列,如图2所示。

因此,对于某些预测模式,目标像素是从较远的参考阵列预测的,而较近的参考阵列将由于定义的角度而被排除。如图3所示,在左侧,目标像素A在具有水平预测方向的左侧参考阵列上具有预测器样本L;虽然顶部参考阵列上的样本T更接近,但VTM-1.0不允许垂直预测方向,因此T可以作为A的预测样本。右图显示了高块中目标像素的类似情况。

为了进一步说明矩形块的帧内预测问题,收集并显示了帧内预测模式的模式分布的统计信息。如图4所示,计算QP等于22的C类BasketballDrill序列的每个帧内预测模式的出现次数,其中y轴上的值表示给定帧内预测模式的出现次数,x轴表示关联的帧内预测模式的出现次数。在35个帧内预测模式的情况下,预测模式2~7显著小于其它模式。

针对矩形块的帧内预测问题,提出了一种超出传统帧内预测所覆盖的预测方向范围的广角模式。这些新模式被称为广角帧内预测模式,并且仅适用于非正方形块,如下所示:

  • 如果块宽度大于块高度,则在右上方向超过45度的角度
    • 如果块高度大于块宽度,则左下方向超过45度的角度

每个广角帧内预测方向与一个常规帧内预测方向相关联,该帧内预测方向捕获相同的预测方向,但使用参考样本的相反侧(左列或顶行)。例如,在图5中,在HEVC中的35个帧内预测模式的情况下,广角模式35和36分别与常规帧内预测模式3和4相关联。

对于WAIP方向的帧内模式编码,由于与用于若干传统角度方向的参考样本相比,用于广角方向的参考采样更接近当前块,因此建议用广角模式来替换若干传统角度帧内预测模式。替换的模式使用原始方法发出信号,并在解析后重新映射到广角模式的索引。帧内预测模式的总数保持不变,并且帧内模式编码也保持不变。图5显示了水平矩形块如何用广角模式替换角内模式的示例。在该示例中,模式2和模式3被广角模式35和模式36替代。

在 VVC 中,一些传统的角度帧内预测模式被自适应地替换为非方形块的广角帧内预测模式。替换的模式使用原始模式索引传输,在解析后重新映射到宽角模式的索引。帧内预测模式总数不变,即67,帧内模式编码方法不变。为了支持这些预测方向,定义了长度为 2W+1 的顶部参考样本和长度为 2H+1 的左侧参考样本,如图 所示。

广角方向模式中替换模式的数量取决于块的纵横比。替换后的帧内预测模式如表所示

如下图所示,在广角帧内预测的情况下,两个垂直相邻的预测样本可以使用两个不相邻的参考样本。因此,低通参考样本滤波和侧平滑应用于广角预测,以减少增加的间隙 \(Δp_α\)的负面影响。广角模式中有8种模式满足这个条件,分别是[-14, -12, -10, -6, 72, 76, 78, 80]。当通过这些模式预测块时,直接复制参考缓冲区中的样本而不应用任何插值。通过这种修改,减少了需要平滑的样本数量。此外,它对齐了传统预测模式和广角模式中的非分数模式的设计。

样本自适应偏移 Sample Adaptive Offset (SAO)

样本自适应偏移的关键功能是衰减振铃效应,当使用较大的变换大小时,振铃效应更可能出现。SAO通过首先使用所选分类器将区域中的样本分类为多个类别并根据其类别向每个样本添加特定偏移量来减少样本失真。在比特流中用信号通知每个区域的分类器索引和偏移。

HEVC使用两种SAO类型:边界偏移(Edge Offset, EO)和边带偏移(Band Offset, BO)。在EO中,样本的分类基于其邻域,即当前样本与其相邻样本之间的比较。在BO中,分类基于样本值。

边界偏移 Edge Offset

图1显示了吉布斯现象,可用于解释图像和视频编码中振铃效应的出现。横轴表示沿一维线的采样位置,纵轴表示采样值。虚线曲线表示原始样本,而实线曲线表示当由于变换系数的量化而丢弃信号中的最高频率时的重构样本。

局部峰、凸边/角、凹边/角和局部谷用实心圆标记,从图中可以观察到,通过对局部峰值和凸角应用负偏移,对凹角和凹谷应用正偏移,可以减少失真。

EO使用四种单向模式进行样本分类:水平、垂直、135°对角线和45°对角线,如图2所示,其中标签“c”表示当前样本,标签“a”和“b”表示两个相邻样本。这四个样本模式形成四个EO类别。每个启用EO的CTB只能选择一个EO类。基于率失真优化,选择一个EO类,并且在比特流中用信号通知指示选择哪个EO类的索引。

对于具有特定方向的给定EO类别,CTB内的每个样本被分为五类之一。标记为“c”的当前采样值将沿选定的一维模式与其两个相邻值进行比较。下表总结了每个样本的类别分类规则。类别1和4分别与所选一维模式的局部谷和局部峰相关联。类别2和3分别与凹角和凸角相关联。如果当前样本不属于EO类别1至4中的任何一类,则将其分配至类别0,且不应用SAO。请注意,类别是互斥的,一个样本只能属于一个类别。

正负边界偏移的影响如图3所示,解释如下。类别1和类别2的正偏移会导致平滑,因为局部凹谷和凹角会变得更平滑,而这些类别的负偏移会导致锐化。相反,对于类别3和4,负偏移导致平滑,正偏移导致锐化。在HEVC中,不允许在EO中锐化。因此,编码器针对每个EO类别用一个信号通知四个特定偏移的绝对值,偏移的符号从相应的EO类别中隐式导出。EO和BO都使用四个偏移量,这限制了偏移量的数量,以减少对行缓冲器的要求。

边带偏移 Band Offset

HEVC SAO工具使用的另一个偏移是边带偏移(BO)。将向值属于同一边带(值范围)的所有采样添加一个偏移。样本值范围分为32个相等的边带。对于0到255范围内的8位样本,边带的宽度为8。因此,8k到8k+7的样本值属于边带k,其中k的范围为0到31。可以向解码器发送边带中原始样本和重构样本之间的差异(即,边带的偏移)。BO的偏移符号没有限制。

图4展示了BO如何补偿区域的样本强度偏移。横轴表示样本位置,纵轴表示样本值。虚线曲线表示原始样本,而实线曲线表示重建样本,受预测残差的量化误差和相位偏移的影响,因为编码运动矢量偏离真实运动。

如图4所示,如果重建的运动向量和“真实”运动向量之间存在相移(差异),则与原始信号相比,具有梯度的平滑区域可能会偏移一定值。在该示例中,与原始样本相比,重构样本被向左移动,这导致可以通过BO针对边带k、k+1、k+2和k+3校正的系统负误差,其中范围从k*8到((k+1)*8)-1的样本被分类为属于边带k,并且可以通过使用相应的偏移值来修改。

在HEVC中,只有四个连续边带的偏移和当前区域的起始(或最小)边带位置被发信号给解码器。在BO中用信号通知四个偏移量,这等于EO中用信号发送的偏移量的数量(偏移量的数目被限制以减少线缓冲器的需求)。仅发出四个边带的信号的原因是CTB形成的区域中的样本值范围可能非常有限。

因此,通过发信号通知当前区域的起始边带位置,BO可以识别当前区域中要补偿的最小采样值,以便解码器可以恢复它,如图5中的示例所示。这对于色度CTB尤其如此。在自然图像中,色度分量通常由窄带信号表示,这意味着通过几个边带偏移,编码器可以恢复该区域中的大多数样本。

SAO Parameters Signaling

序列参数集(SPS)中用信号通知的语法元素sample_adaptive_offset_enabled_flag指示在当前视频序列中是否启用了SAO。在Slice header中,两个语法元素slice_sao_luma_flag和slice_sio_chroma_flag指示当前Slice中是否分别为亮度和色度启用了SAO。

低延迟应用可以使用基于编码树单元(CTU)的SAO编码算法。如图6所示,CTU包括其对应的luma CTB、Cb CTB和Cr CTB。语法方面,SAO参数自适应的基本单元始终是一个CTU。如果在当前Slice中启用SAO,则将每个CTU的SAO参数交织到Slice数据中。比特流中的SAO数据在每个CTU的开头用信号通知。CTU级SAO参数包括SAO merge信息、类型信息和偏移信息。

A. SAO Parameters Merging

CTU可以使用三个选项来发送SAO参数:重用left CTU的SAO参数(通过将语法元素SAO_merge_left_flag设置为1),重用top CTU的SAO参数(设置语法元素
sao_merge_up_flag设置为1)或通过发送新的SAO参数。SAO Parameters Merging 由所有三个颜色分量共享。当SAO merge_left或SAO merge_up模式被指示时,来自左侧或上方CTU的所有SAO参数都将被复制,并且当前CTU没有更多信息。这种基于CTU的SAO Parameters Merging 有效地减少了需要用信号通知的SAO消息。

B. SAO Type and Offsets Signaling

如果不使用SAO Parameters Merging,则如图7所示发送当前CTU的信息。首先发送亮度分量的语法元素,然后发送Cb语法元素,再发送Cr语法元素。对于每个颜色分量,发送SAO类型(SAO_type_idx_luma或SAO_type_idx_chrma),表示EO、BO或未应用(SAO已关闭)。如果选择了BO或EO,则传输四个偏移。如果选择了BO,则发信号通知起始边带位置(sao_band_position)。否则,如果选择EO,则用信号通知EO类(sao_EO_class_luma或sao_EO_class_chrma)。

Cb和Cr共享SAO类型(SAO_type_idx_chroma)和EO类(SAO_EO_class_chrma)语法元素,以通过在某些平台上实现更高效的内存访问来减少辅助信息并加快SAO处理。因此,这些语法元素仅针对Cb进行编码。码字(包括“关闭”、“EO类别选择索引”和“BO频带位置索引”)的设计基于概率分布,以减少信息。

C. CABAC Contexts and Bypass Coding

使用基于上下文的自适应二进制算术编码(CABAC)对包括SAO merge信息、SAO类型信息和偏移信息的所有CTU级SAO语法元素进行编码。只有SAO类型的第一个bin(指定在当前CTU中SAO是打开还是关闭)以及SAO merge_left和merge-up标志使用CABAC上下文。所有其他箱都在旁路模式下进行编码,这显著增加了CABAC中的SAO解析吞吐量,而没有很大的编码效率损失。

变换系数编码

与HEVC类似,VVC采用基于上下文的自适应二进制算术编码(CABAC)对所有底层语法元素进行熵编码。非二进制语法元素映射到二进制码字。符号和码字之间的双射映射(通常使用简单的结构化码字)称为二值化。使用二进制算术编码对二进制语法元素和非二进制数据的码字的二进制符号(也称为bin)进行编码。

CABAC支持两种操作模式:规则模式(使用自适应概率模型对bin进行编码)和不太复杂的旁路模式(使用1/2的固定概率)。自适应概率模型也称为上下文,将概率模型分配给单个bin称为上下文建模。注意,所使用的二值化和上下文建模对编码效率都有显著影响。

所需的编码器和解码器的复杂性主要随着上下文编码的bin(即在常规模式下编码的bin)的数量而增加。但是,它们还受到其他方面的影响,例如连续bin之间的依赖程度、所使用的上下文建模的复杂性,或者算术编码引擎的规则模式和旁路模式之间发生切换的频率。

变换块量化索引的熵编码通常称为变换系数编码。由于在典型的视频比特率下,变换系数level消耗了总比特率的主要部分,因此在编码效率和实现复杂度之间找到合理的折衷是重要的。VVC中变换系数编码的基本概念类似于HEVC中规定的系数编码:

  1. coded block flag(CBF)指示变换块是否包括任何非零level
  2. 对于CBF等于1的块,传输前向扫描顺序中最后一个非零level的x和y坐标;
  3. 从指示的最后位置开始,以反向扫描顺序传输level,并将其组织为所谓的系数组(CG)。CG的bin在multiple passes中编码,其中所有旁路编码的bins被分组在一起以实现高效实现

由于VVC比HEVC支持更大范围的变换大小,因此对变换系数编码的一些方面进行了推广。与HEVC相反,扫描顺序不依赖于帧内预测模式,因为发现这种依赖于模式的扫描仅提供可忽略的改进,并且将不必要地使设计复杂化。

此外,表示level的bin的上下文建模与块大小无关。但是,在HEVC中发现的上下文依赖限制被放宽,并且利用level之间的局部统计依赖来提高编码效率。为了能够利用某些TCQ属性,level的二值化包括一个奇偶校验单元,CG的所有上下文编码单元在一个过程中编码。VVC使用基于变换块的上下文编码单元数量限制,以保持与HEVC类似的worst-case复杂度。

A. Coded Block Flag

编码块标志(CBF)以编码引擎的常规模式进行编码。总共使用了9个上下文(4个用于luma,2个用于Cb,3个用于Cr)。每个上下文为BDPCM模式编码的块保留一个上下文。对于luma,两个上下文仅用于在帧内子分区模式中编码的变换块(参见[32]);这里,所选择的上下文取决于同一编码单元内的前一luma变换块的CBF。为了利用色度分量的CBF之间的统计相关性,根据同位Cb块的CBF来选择未在BDPCM模式中编码的Cr块的上下文。

B. Coefficient Groups and Scan Order

W×H变换块的变换系数level{q}分布在W×H矩阵中。为了实现跨越所有块大小的协调处理,而且为了提高变换块的编码效率,其中信号能量被集中到对应于低水平或低垂直频率的变换系数中,变换块被划分为系数组(CG)。使用多个扫描过程以统一的方式对每个CG的level进行编码。由于VVC还支持宽度和高度小于4的块大小,因此CG的形状取决于变换块大小,如表II所示。对于具有至少16个系数的变换块,CG总是包括16个level;对于较小的块,使用2×2级的CG。CG的编码顺序由图3所示的反向对角线扫描给出。与CG大小无关,CG对角线从变换块的右下角到左上角进行处理,其中每条对角线沿左下方向进行扫描。CGs内部level的编码顺序由相同的反向对角扫描指定。

为了限制大变换大小的worst-case解码器复杂性,高频位置的变换系数被迫等于零。非零量化索引只能出现在变换块左上角的min(W,Wn)×min(H,Hn)区域中,其中Wn×Hn表示可以在解码器侧推断的非零输出区域的大小。该区域外的CG未编码,因此被排除在扫描之外,如图3d。在大多数情况下,Wn×Hn等于32×32,这是非零输出区域的最大支持大小。尽管VVC为DCT-II以外的变换指定了较小的非零输出区域,但这通常不会影响变换系数编码,因为指定所用变换的语法元素是在level之后编码的,并且它们取决于某些区域中非零level的存在。唯一的例外是亮度块,最大值为(W,H)≤32,以特殊子块变换模式编码。

如果启用了非DCT-II变换(在序列级别上),则始终使用非DCT-II变换对这些块进行编码,因此,推断出non-zero-out区域的大小等于16×16

C. Last Significant Coefficient Position

与在HEVC中类似,通过以前向扫描顺序(即编码顺序中的第一个非零level)发送最后一个非零level的位置来消除与高频分量相关的系数的零量化索引的显式编码。这不仅提高了编码效率,而且减少了上下文编码的bin的数量。

分别对应于系数level矩阵中的列号和行号的x和y坐标彼此独立地传输。如表III所示,每个component由前缀码字和(可能为空的)后缀码字的组合表示。前缀部分指定值的间隔。使用截断一元(TU)二值化对其进行二值化,并以常规模式对bin进行编码。表示变换块的non-zero-out区域的最后一个间隔的前缀部分被截断。也就是说,如果x坐标的min(W,Wn)或y坐标的min(H,Hn)等于表中最后一列中的数字,则不对表III中括号内的零位进行编码。特别是,如果相应的块宽度或高度等于1,则完全跳过坐标的编码。后缀部分表示前缀部分指示的间隔内的偏移量。它是二值化使用固定长度(FL)二值化和编码在旁路模式。只有值大于3的x和y坐标具有后缀部分。

在解码器侧,导出最后有效level的x和y坐标的值如下。设\(v_{pre}\)是前缀码字中等于1的bin数。然后,要解码的后缀bin的数量\(n_{suf}\)由以下公式导出

\(v_{suf}\)是后缀码字(二进制表示)指定的值,解码后的坐标值last根据

先signal x坐标的前缀部分,然后signal y坐标的前缀部分。对于分组旁路编码bins,后缀部分在前缀码字之后进行编码。x和y坐标的前缀bins使用单独的上下文模型集进行编码。表IV列出了表示集合内使用的概率模型的上下文偏移量。选择的模型取决于是否对亮度块或色度块进行编码、变换块的宽度或高度以及前缀码字内的bin编号。请注意,对于存在zero-out的大型变换块,变换维度(而不是非零输出区域的维度)用于导出上下文偏移。总共有46个上下文(40个用于亮度,6个用于色度)用于编码最后的系数位置。

D. Binarization and Coding Order

从包含最后一个非零level(如x和y坐标所示)的CG开始,CG以编码顺序传输(通过反向对角线扫描给出)。为CG编码的第一个语法元素是sb_coded_flag。如果此标志等于0,则表示CG仅包含零个级别。对于第一个CG(包含最后一个非零level)和最后一个CG(包含DClevel),不编码sb_coded_flag,但推断为等于1。sb_coded_flag以常规模式编码。

选择的上下文取决于右边的CG或下面的CG是否包含任何非零级别,其中为亮度和色度指定了单独的上下文集。总共使用了4个上下文(2个用于亮度,2个用于色度)。对于sb_coded_flag等于1的CGs,按照以下说明对电平值进行编码。

选择系数level的二值化和编码顺序来支持TCQ和常规量化的有效熵编码。由于TCQ中使用的两个标量量化器Q0和Q1的结构不同,电平等于0的概率在很大程度上取决于所使用的量化器。为了在上下文建模中利用此效应,同时分组上下文编码和旁路编码的bin,二值化包括专用奇偶校验标志,用于在熵编码期间确定TCQ状态。

通过另外考虑实现良好编码效率所需的上下文编码bins的数量以及连续bins之间的相关性,选择了表V中所示的二值化。量化索引的绝对值|q|被映射到bin sig(大于0)、gt1(大于1)、par(奇偶性)、gt3(大于3)和非二进制余数rem

CG的语法元素在扫描位置上进行多次编码。与HEVC不同,在HEVC中,每个系数的单个语法元素在每个扫描过程中进行编码,VVC在单个过程中对每个系数最多编码4个语法元素。

pass 1中,上下文编码的存储单元siggt1pargt3以交错方式进行编码(即,在继续下一个扫描位置之前对一个扫描位置的所有存储单元进行编码)。注意,驱动TCQ状态机的奇偶校验bin包括在第一个过程中,以便为TCQ情况启用sig bin的有效编码。对于可以推断sig bin等于1的扫描位置(例如,对于最后一个有效位置),不发送信号。gt1pargt3 bin的存在按照表V中的规定进行控制。非二进制余数rempass 2中进行编码。使用与HEVC中类似的参数码对其进行二值化,并在旁路模式下对产生的bin进行编码。

为了增加worst-case下的吞吐量,可在第一次过程中编码的上下文编码bin的数量受到限制。为了允许上下文编码的bin在cg之间的适当分布,在变换块的基础上指定限制。N是变换块非零输出区域中变换系数的数量,上下文编码bin的最大允许数量设置为1.75 N。如果bin预算在CG之间平均分配,则这将对应于每个CG 28个bin,这仅略高于HEVC中规定的限制(25个bin)。

对上下文编码的bins的限制如下所示。如果在扫描位置的开始处,用于变换块的已编码siggt1pargt3 bin的总数超过1.75 ×N – 4,即预算中剩余的bin不到4个,则终止第一个编码过程。在这种情况下,剩余扫描位置的绝对值|q|pass 3中被编码,它们由语法元素decAbsLevel表示,这些元素在旁路模式下被完全编码。

最后,在pass 4中,CG的所有非零level的符号以旁路模式进行编码。如果启用SDH且CG内最后一个和第一个非零level的扫描索引之间的差值大于3,则不发送最后一个非零level的符号。图4示出了将level数据组织到不同扫描过程中。

E. Context Modeling

为了有效地利用条件统计进行算术编码,VVC使用了一组相当大的上下文模型对bin的siggt1pargt3进行编码。除了TCQ状态,上下文建模还利用了空间相邻量化索引之间的统计相关性。

sig的上下文取决于相关的TCQ state \(s_k\)、变换块内系数的对角位置d=x+y,以及图5a所示的局部模板T内部分重建的level的绝对q*的总和。部分重建的绝对level由相邻扫描位置的已编码的bin给出,并且可以根据

对于亮度块,指示所使用的自适应概率模型的上下文索引\(c_{lum}^{sig}\)根据

作为局部模板T内部分重建level q*的函数。对于色度块,仅使用两类对角位置(d<2和d≥2)。上下文索引\(c_{lum}^{chr}\)由

当TCQ未启用时,TCQ state \(s_k\)的值设置为0。总共使用60个上下文模型对sig bin进行编码(36表示luma,24表示色度)。

gt1pargt3选择的概率模型不依赖于TCQ状态,因为发现它只提供很小的好处。计算单个共享上下文偏移量以选择这些语法元素的概率模型。根据系数的对角线位置d(4类luma,2类色度)和max(0,q*-1)之和在本地模板T内选择,使用

作为局部模板T内部分重建level q*的另一个函数,亮度块和色度块的上下文索引\(c_{lum}\)和\(c_{chr}\)分别由下式给出

此外,对于最后一个系数位置,使用单独的上下文(由\(c_{lum}=0\)和\(c_{chr}=0\)给出)。对于每个gt1pargt3 bins,使用32个概率模型(21个用于亮度,11个用于色度)。

F. Binarization of Bypass-Coded Level Data

第二遍中编码的语法元素rem表示绝对level的余数。如果相关的gt3 bin等于1,则仅针对扫描位置发送它们。q*是根据(8)部分重建的level,level的绝对值|q|由下式给出

与HEVC中的余数值类似,使用截短的Rice(TR)和Exp-Golomb(EG)码的组合对表示在第三遍中编码的绝对级别的余数rem和语法元素decAbsLevel进行二进制化。在编码引擎的旁路模式中对生成的bin进行编码。与HEVC不同,TR码的Rice参数是基于本地模板T中的绝对level值|q|的和导出的。使用的本地模板T与第一次编码过程中用于上下文索引导出的模板相同。Rice参数m

其中,\(z_0\)设置为等于4,用于对余数rem进行编码,\(z_0\)设置为等于0,用于对decAbsLevel进行编码。产生这种差异的原因是decAbsLevel的值指定了完整的绝对级别,而余数rem表示差异rem =(|q| – q∗)/2,具有较小的值。

对于每个Rice参数m,小于\(v_{max}=2^m·6\)的值仅使用m(TRm)阶的TR码进行编码;这对应于长度为6的一元前缀的码。对于大于或等于\(v_{max}\)的值,TRm码与顺序为m+1(EGm+1)的Exp-Golomb码相关联。表VI显示了Rice参数m=0和m=3的二值化,以及TRm和EGm+1码的级联。表中粗体的bin对应于二值化的TRm部分。当组合的码长度将超过32个存储单元时,二值化将略微修改。在这种情况下,Exp-Golomb前缀的长度限制为11个bin(参见表VI中带下划线的m=0条目),32位预算的其余15个bin用于表示后缀部分。

为了提高完全旁路编码电平的编码效率,decAbsLevel的值不直接表示绝对电平值|q|,而是导出为

第一个编码过程。Rice参数m由这些值给出,使用与余数rem相同的二值化进行编码。注意,参数\(pos_0\)基本上指定了重新排序的码字表中|q| = 0的码字位置。根据Rice参数m和TCQ状态\(s_k\),根据

G. Transform Skip Residual Coding

除了上述用于变换系数的规则残差编码(RRC)之外,VVC还包括用于Transform Skip模式中的量化索引的专用熵编码,其被称为Transform Skip残差编码(TSRC)。它主要是为了提高屏幕内容的编码效率而设计的,可以在slice级别启用。启用时,TSRC方案用于对TS块的量化索引进行编码;未启用时,使用规则残差编码对TS块的量化索引进行编码。­

与常规残差编码相比,最后一个非零电平的位置不被传输,并且按照前向扫描顺序编码,即从左上角系数开始,然后到右下角系数。与RRC类似,CG的语法元素在扫描位置的多个过程中进行编码,并且对上下文编码的bins的数量应用相同的限制。只要未达到此限制,就使用三个过程对level进行编码,如图6所示。

在第一个过程中,使用自适应概率模型对sigsigngt1par的bin进行交织和上下文编码。如图5b所示,局部模板也应用于TSRC中,用于导出上下文索引,但它仅包括两个相邻的系数位置。因为,在TS块中,连续符号通常具有相似性的值,符号标志包含在第一次传递中,并在编码引擎的常规模式下进行编码。如果在pass 1后仍未达到常规编码bin的限制,则在pass 2时,每个系数最多有四个大于x的标志(gt3gt5gt7gt9)被编码。这些bins也是上下文编码的。最后,在pass 3中,绝对level(rem)的余数以旁路模式编码。注意,根据第二次扫描期间扫描位置是否达到bins限制(因此,无法对gt3 bin进行编码),余数可以具有不同的含义。对于第一次扫描中未传输数据的所有扫描位置,在pass 4中以旁路模式对完全绝对值(decAbsLevel)以及相关符号标志进行编码。rem和decAbsLevel的Rice参数m始终设置为1。

MMVD(Merge mode with MVD)

针对未来的视频编码标准,提出了一种新的运动矢量表达方法,即Merge mode with MVD(MMVD)。在先前的标准中,两种方法通常用于运动矢量表示。在第一种方法中,运动矢量从相邻块中导出,并直接用于运动补偿(HEVC中的Merge模式),在另一种方法中用MVP和MVD(自适应运动矢量预测;HEVC中为AMVP)表示运动矢量。Merge模式通过节省用于表示运动信息的比特而受益。AMVP表示更精确的运动信息,但它需要用信号发送MVD,这会消耗额外的比特。MMVD为运动矢量精度及其开销之间的权衡提供了折衷的解决方案。MMVD可以通过引入简化的运动向量表示来提高运动向量精度。结果表明,该方法提高了VVC的编码效率,平均节省了0.51%的BD码率。

1. INTRODUCTION

与先前的压缩标准H.264/AVC(高级视频编码)[1]相比,最新的视频压缩标准高效视频编码(HEVC)显示出编码效率的显著提高。得益于HEVC,从全高清(FHD)到超高清(UHD)的视频分辨率广播服务的新市场可能会成为下一代广播标准(如ATSC3.0[2])的主要特征。然而,随着先进显示技术的出现,对各种大型视频内容的需求不断增加,如8K UHD、360度虚拟现实、高动态范围(HDR)和宽色域(WCG)。为了满足这些需求,自2014年以来,已经讨论了支持多种视频内容的新视频编码标准[3]。

最终,ITU-T SG 16 Q.6(VCEG)和ISO/IEC JTC 1/SC 29/WG 11(MPEG)的联合视频专家组(JVET)于2018年开始开发新的视频编码标准,即通用视频编码(VVC)[4]。

大约在同一时间,中国AVS(音频视频标准)工作组启动了另一种新的视频编码标准AVS3[5]的开发,作为先前视频标准AVS2的继任者,AVS2在中国广泛用于4K广播[6]。最后,自2019年1月以来,MPEG正在开发MPEG-5基本视频编码(EVC),以更明确地解决许可问题[7]。

在上述三种标准中,与先前的编码标准(如HEVC和AVS2)相比,已观察到显著的编码性能改进。为了提高编码性能,三种标准化中的一种常见方法是减少帧间冗余及其在信令中的表示。仿射运动模型和几何分割的应用可以是减少基于块的平移运动无法覆盖的冗余的新方法的示例。为了改进运动表示,MMVD模式和扩展的Merge模式可以被认为是已经研究过的节省比特的新工具。

在本文中,我们介绍了一种具有简化表达式的运动矢量编码的新方法,该方法已被上述三种正在开发的视频编码标准所采用。

2.  MOTIVATION

2.1 Merge and AMVP modes

在HEVC中,Merge和AMVP模式可以被视为典型的运动向量预测工具,其利用相邻编码块的空间运动相似性来进行运动向量预测。通常,单预测帧的运动信息由运动向量(水平和垂直坐标信息)、参考帧索引和预测方向组成。在双向预测的情况下,要编码的运动信息量加倍。

HEVC中新引入了Merge模式。Merge模式生成由从空间相邻块和时间相邻块收集的运动信息组成的候选列表。仅用信号通知解码器应该用于预测的来自列表的候选的索引。

AMVP模式是为HEVC中的非Merge块设计的。AMVP模式使用更精确的运动向量执行运动补偿预测,这有利于减少残差。然而,由于运动差信息是编码器侧搜索处理的结果,并且不能在解码器侧导出,因此需要用信号通知MVD信息。尽管AMVP模式也使用预测器进行运动矢量编码,但要编码的相应侧运动信息仍然很大,包括预测方向、参考帧索引和运动矢量差(MVD)。

2.2 Motion Vector Inaccuracy of Merge mode

Merge模式的运动信息可能是不准确的,因为它直接从相邻块获得运动信息。相邻块的运动向量可能不是真实的运动,但它可以为速率失真优化提供最佳成本。

在运动参数相似性方面,存在针对HEVC的一系列序列和编码条件的观察结果。根据结果,与具有完全相同运动的先前编码块相邻的块平均占所有像素的40%[8]。其余块具有与相邻块不同的运动参数集。图1显示了2K序列的MVD分布。x轴和y轴的坐标表示AMVP模式下运动向量相对于其运动向量预测器的偏移。大小表示命中率,可以将其视为在相应运动向量偏移处发生的概率。这些MVD应以AMVP模式发出信号。MVD是当前块和相邻块之间的运动差,并且暗示预测精度的不足。这种分布类似于二维高斯分布。大多数MVD位于坐标平面的x轴和y轴上。此分布形状为摄像机拍摄的自然视频序列的特征。此外,更靠近中心的MVD的命中率大于更远离中心的MVDs的命中率。

8K序列的这些差异更明显,因为大分辨率序列的运动通常比小分辨率序列的大得多。因此,运动向量与其对应的预测器之间的差异也变得更大。HEVC中的MVD是用指数Golomb码编码的,占据了比特率的很大一部分。

对于典型运动向量的表示,在下一节中提出了一种新的运动向量表达方法。此表达式为更频繁出现的运动向量生成更短的码字。

3 MERGE MODE WITH MOTION VECTOR DIFFERENCE

在HEVC中,Skip模式也与Merge模式一起定义。Skip模式只是没有任何残差编码的Merge模式,即在Skip模式编码块的运动补偿之后没有重建阶段。以类似的方式,MMVD也可以是Skip模式编码或Merge模式编码。MMVD将额外的新运动向量视为Skip模式或Merge模式的候选。

3.1 Proposed Motion Vector Expression Method

所提出的运动矢量表达方法包括三个部分,即起点、距离和方向。由于运动矢量差也是运动矢量的一种形式,因此它也可以通过所提出的新的运动矢量表达方法来表示。

第一个分量是起点,它被用作进一步细化的绝对运动矢量位置。在MMVD中,用信号通知基本候选索引以指示来自候选列表的Merge候选中的哪一个被用作起点。候选索引的定义如表1所示。

第二个分量是运动向量的大小。该大小是运动向量沿坐标平面的x轴和y轴与起点的距离。预定义的对数缩放距离用于构建MMVD距离列表。如表2所定义的距离索引使用截断一元编码方案进行编码,以缩短频繁出现的距离的码字。

第三个分量是运动向量的方向。如图1所示,大多数MVD沿x轴和y轴分布。因此,仅允许沿x轴和y轴的四个方向。方向索引(如表3所示)将符号分配给相对于起点的MVD的每个分量。它采用固定长度编码方案进行编码。

3.2 MMVD for VVC

本节介绍在VVC的Skip和Merge模式中使用新的运动矢量表达方法。如果用Skip或Merge模式对当前块进行编码,并且如果在当前片中启用了MMVD,则发信号通知MMVD标志以指示MMVD模式是否应用于当前块。运动向量的推导过程基于如下所述使用的基础候选的预测方向而变化。

3.2.1 Uni-prediction

当基本候选是单预测运动向量时,用于MMVD推导的中心位置是基本候选的MV,与基本候选的距离如表2所示,图2显示了可以表达出的各种可能的MV。

3.2.2 Bi-prediction

当基本候选是双预测运动向量时,MMVD应用于具有相同信息的两个参考帧,如图3所示。当当前图片位于时间轴上的两个基准图片帧之间时,可以通过镜像另一个参考帧上MVD的方向来导出一个参考框上的MVD

3.3 MMVD for EVC and AVS3

虽然VVC中的MMVD从Merge候选列表中的基础候选继承预测方向,但EVC中的MMVD另外用信号通知预测方向。每个基础候选有三个可能的预测方向。如果基础候选是单预测的,则通过使用镜像方法导出相反的预测方向。结合两个单预测因子,合成双预测因子作为预测方向的第三候选。如果基础候选是双预测的,则可以将其分为两个单预测因子,用于每个单预测。

AVS3中的MMVD生成其自己的基本候选列表,该列表由相邻块的运动信息组成。其余过程类似于VVC中的MMVD。

4 EXPERIMENTAL RESULTS

低频不可分离变换 (Low Frequency Non-Separable Transform LFNST)

对于所有现代基于块的混合视频编码,变换是去除预测残差块中空间冗余的重要部分。此外,现有的方向性帧内预测导致预测残差中的方向性模式,并且导致变换系数的可预测模式。变换系数中的可预测模式主要在低频分量中观察到。因此,在本文中,引⼊了⼀种称为低频不可分离变换(LFNST)的不可分离变换,以进⼀步压缩低频主变换系数之间的冗余,低频主变换系数是传统方向帧内预测的变换系数.

提议的 LFNST 是直接矩阵乘法,因此它不需要多次迭代,而多次迭代会导致变换处理中出现不希望的延迟。提议的 LFNST 包含在称为通⽤视频编码 (VVC)的下⼀代视频编码标准中。

I. INTRODUCTION

在 HEVC 的第一个版本完成后,已经研究了具有超越高效视频编码标准 (HEVC) 能力的高级视频压缩技术。有关未来视频编码的活动是在ITU-T 视频编码专家组 (VCEG) 和ISO/IEC 运动图像专家组 (MPEG)的联合视频探索小组 (JVET) 内进行的,该专家组开发了联合探索模型(JEM) ),它现在正积极致力于开发称为通用视频编码 (VVC) 的下⼀个视频编码标准。 VVC 的主要⽬标是指定⼀种视频编码技术,其压缩能力大大超过此类标准的前几代,并且还支持在更广泛的应用中使用视频编解码器,包括超高-清晰度视频(例如,具有 3840×2160 或7620×4320 图像分辨率和 10 或 12 位的位深度,如 ITU-R BT.2100建议书所规定),具有高动态范围和宽色域的视频,以及用于身临其境的媒体应⽤,例如 360° 全向视频,以及先前视频编码标准 JVET-O2001[1] 通常解决的应⽤。 VVC中加⼊了许多先进的压缩技术,它包含了更多高级变换技术,例如多变换集 (MTS) 和低频不可分离变换 (LFNST)。本文重点介绍 LFNST 视频编码技术。

变换一直是去除预测残差块中空间冗余的重要编码工具。预测残差的空间冗余特性取决于预测模式。特别是,在帧内编码模式中,当使用仍在VVC 中使用的方向性帧内预测时,已经观察到方向性预测残差模式。方向预测残差产生变换系数,其中⼀些低频分量可能是可预测的模式,具体取决于帧内预测方向。因此,引⼊称为低频不可分离变换(LFNST)的不可分离变换以进⼀步压缩低频主变换系数之间的冗余,低频主变换系数是来自传统定向帧内预测的变换系数。提议的 LFNST 是直接矩阵乘法,因此它不需要多次迭代,而多次迭代会导致不希望的延迟并阻止变换处理中的并行执行。

II. BACKGROUND

在 JEM 中,二次变换应用于前向初级变换和量化(在编码器)之间以及去量化和反向初级变换(在解码器端)之间。 如图 1 所示,执行 4×4(或 8×8)二次变换取决于块大小。 例如,4×4 二次变换适用于小块(即 min (width, height) < 8),8×8 二次变换适用于每个 8×8 块的较大块(即 min (width, height) > 4)。

下面以输入为例描述不可分离变换的应用。 为了应用不可分离变换,4×4 输入块 \(X\)

\(\vec{X} =\begin{Bmatrix}
X_{00} & X_{01} & X_{02} & X_{03}\\
X_{10} & X_{11} & X_{12} & X_{13}\\
X_{20} & X_{21} & X_{22} & X_{23}\\
X_{30} & X_{31} & X_{32} & X_{33}
\end{Bmatrix}\)

首先表示为一维向量\(\vec{X}\)。

不可分离变换计算为\(\vec{F} =T\cdot \vec{X}\),其中\(\vec{F}\)表示变换系数向量,\(T\)是一个 16×16 的变换矩阵。随后使用该块的扫描顺序(水平、垂直或对角线)将 16×1 系数向量\(\vec{F}\)重组为 4×4 块。具有较小索引的系数将与较小的扫描索引一起放置在 4×4 系数块中。总共有 35 个变换集,每个变换集使用 3 个不可分离的变换矩阵(内核)。从帧内预测模式到变换集的映射是预先定义的。对于每个变换集,选择的不可分离次级变换候选者进一步由显式发送的次级变换索引指定。在变换系数之后,每个 Intra CU 在比特流中用信号通知索引一次。为了降低二次变换的复杂度,JVET-B0059 [2] 在二次变换的计算中提出了一种新的超立方给定变换(HyGT)。这种正交变换的基本元素是吉文斯旋转,它由旋转正交矩阵定义。图 2 描绘了二级变换结构。

HyGT 的乘法复杂度很高。 变换的分层结构也给硬件设计带来了问题,因为相邻阶段之间的数据依赖性限制了并行性。

III. ALGORITHM DESCRIPTION OF LFNST

LFNST 最初是在 JVET-K0099 [3](以前称为缩减二次变换或 RST)中引入的。 LFNST 和 HyGT 的原始版本有 35 个变换集,每个变换集有 3 个不可分离的变换矩阵(内核),这需要大量的内存使用,不适合硬件设计。 JVET-L0133 中引入了 4 个变换集(而不是 35 个变换集)映射,最后在 VTM5 中采用的 JVET-N0193 [4] 中引入了每组 2 个内核的 4 个变换集。 在本文中,16×48 和 16×16 矩阵分别用于 8×8 和 4×4 块。 为方便起见,16×48 变换表示为 LFNST8x8,16×16 变换表示为 LFNST4x4。 图 3 表示正向和反向 LFNST。

A. LFNST computation

LFNST或缩减变换(RT)的主要思想是将一个N维向量映射到不同空间中的一个R维向量,其中R/N(R < N)是缩减因子。 RT矩阵是一个R×N矩阵,如下:

其中变换的 R 行是 N 维空间的 R 个基。 RT 的逆变换矩阵是其正向变换的转置。 正向和反向 LFNST 如图 4 所示。

在本文中,应用了缩减因子为 3(1/3 大小)的 LFNST8x8。 因此使用 16×48 直接矩阵,每个矩阵从左上角 8×8 块中的三个 4×4 块中获取 48 个输入数据,不包括右下角 4×4 块(图 5)。我们将此区域称为 LFNST8x8 的感兴趣区域或 ROI8x8 .

换句话说,在解码器端使用 48×16 逆 LFNST 矩阵来生成 ROI8x8 中的核心(主)变换系数。 前向 LFNST8x8 使用 16×48(或 8×48 用于 8×8 块)矩阵,因此它仅在给定 ROI8x8 内的左上 4×4 区域中产生非零系数。 换句话说,如果应用 LFNST,那么除了左上角 4×4 区域之外的 ROI8x8 将只有零系数。 对于 LFNST4x4,应用 16×16(或 8×16 用于 4×4 块)直接矩阵乘法。

当满足以下两个条件时,有条件地应用逆 LFNST:

  • 块大小大于等于给定阈值(W>=4 && H>=4)
  • Transform skip 模式标志等于零.

如果变换系数块的宽度 (W) 和高度 (H) 均大于 4,则将 LFNST8x8 应用于变换系数块的 ROI8x8。 否则,LFNST4x4 可以应用于变换系数块的左上 min(8, W) × min(8, H) 区域。

如果 LFNST 索引等于 0,则不应用 LFNST。 否则,应用 LFNST,使用 LFNST 索引选择内核。 LFNST 选择方法和 LFNST 索引的编码将在以下部分中进行说明。

此外,LFNST 应用于帧内和帧间Slice中的帧内 CU,以及亮度和色度。 如果启用dual tree,Luma 和 Chroma 的 LFNST 索引将分别发出信号。 如果dual tree被禁用,单个 LFNST 索引被发送并用于 Luma 和 Chroma。

在第 13 次 JVET 会议上,采用了帧内子分区 (ISP) 作为一种新的帧内预测模式。 选择 ISP 模式时,禁用 LFNST 并且不发出 LFNST 索引信号,因为即使将 LFNST 应用于每个可行的分区块,性能改进也是微不足道的。 此外,对 ISP 预测的残差禁用 LFNST 可以降低编码复杂度。

B. LFNST selection

LFNST 矩阵是从四个变换集中选择的,每个变换集由两个变换组成。 应用哪个变换集由帧内预测模式确定,如下所示:

  • 如果指示了三种 CCLM 模式之一,则选择变换集 0。
  • 否则,按照表1进行变换集选择:

访问表 1 的索引 IntraPredMode 的范围为 [-14, 83],这是用于广角帧内预测的变换模式索引。

C. LFNST Signaling

R = 16 的前向 LFNST8x8 使用 16×48 矩阵,因此它仅在给定 ROI8x8 内的左上角 4×4 区域中产生非零系数。 换句话说,如果应用了 LFNST,那么除了左上角的 4×4 区域之外的 ROI8x8 只生成零系数。 因此,当在 ROI8x8 中检测到除了左上 4×4(如图 6 所示)之外的任何非零元素时,LFNST 索引不会被编码,因为这意味着没有应用 LFNST。 在这种情况下,LFNST 索引被推断为零。

D. Worse case multiplication handling of LFNST

根据硬件专家的意见,LFNST 需要将每个样本的最坏情况乘法次数限制为小于或等于 8。

如果我们使用 LFNST8x8 和 LFNST4x4,当所有 TU 都由 4×4 TU 或 8×8 TU 组成时,就会出现乘法计数的worst-case情况。 因此,前 8×48 和 8×16 矩阵(换句话说,每个矩阵中从顶部开始的前 8 个变换基向量)分别应用于 8×8 TU 和 4×4 TU。 在块大于 8×8 TU 的情况下,不会发生worst-case的情况,因此 LFNST8x8(即 16×48 矩阵)应用于左上角的 8×8 区域。 对于 8×4 TU 或 4×8 TU,LFNST4x4(即 16×16 矩阵)仅应用于不包括其他 4×4 区域的左上角 4×4 区域,以避免worst-case的情况发生。 在 4xN 或 Nx4 TU (N ≥ 16) 的情况下,LFNST4x4 分别应用于两个相邻的左上角 4×4 块。 通过上述简化,worst-case下的乘法次数变为每个样本 8 次。

IV. EXPERIMENTAL RESULTS

根据 JVET-M1010 [5] 中定义的通用测试条件和 JVET-M1026 [6] 中的核心实验描述,基于 VTM-4.0 参考软件实现了所提出的方法。 执行所有All Intra (AI) 和随机访问 (RA) 测试。
在 A 部分,报告了inter MTS 关闭的 AI、RA 和 LD 结果,其中与 VTM anchor相比,编码时间为 126% (AI)、111% (RA) 和 108% (LD),BD 速率变化为 -1.34% (AI)、-0.69% (RA) 和 -0.22% (LD)。
在 B 部分中,报告了启用 inter MTS 的 RA 和 LD 结果,其中与 VTM anchor相比,编码时间分别为 109% (RA) 和 105% (LD),BD 速率变化分别为 -0.69% (RA) 和 -0.18% (LD)。
最后,在 C 部分报告了 AI 案例的低 qp 结果,其中与 VTM anchor相比,编码时间为 137%,BD 速率变化为 -0.63%。

REFERENCES 

[1] B. Bross, J. Chen, S. Liu, “Versatile Video Coding (Draft 6)”, document JVET-O2001, 15th Meeting: Gothenburg, SE, 3–12 July 2019. 

[2] X. Zhao, A. Said, V. Seregin, M. Karczewicz, J. Chen, R. Joshi, “TU-level non-separable secondary transform”, Joint Video Exploration Team (JVET) of ITU-T SG 16 WP 3 and ISO/IEC JTC 1/SC 29/WG 11, Doc. JVET-B0059, 3rd Meeting 

[3] M. Salehifar, M. Koo, J. Lim, S. Kim, “CE 6.2.6: Reduced Secondary Transform (RST)”, document JVET-K0099, Ljubljana, SI, Jul. 2018. 

[4] M. Koo, M. Salehifar, J. Lim, S. Kim, “CE6: Reduced Secondary Transform (RST) (CE6-3.1)”, Joint Video Exploration Team (JVET) of ITU-T SG 16 WP 3 and ISO/IEC JTC1/SC 29/WG 11 JVET-N0193, 14th meeting, Geneva, CH, 19–27 March 2019. 

[5] F. Bossen, J. Boyce, K. Suehring, X. Li and V. Seregin, “JVET common test conditions and software reference configurations for SDR video”, JVET-M1010, 13th JVET meeting, Marrakech, MA, 9–18 Jan. 2019. 

[6] X. Zhao et al., “Description of Core Experiment 6 (CE6): Transforms and transform signalling”, JVET-M1026, 13th JVET meeting, Marrakech, MA, 9–18 Jan. 2019. 

Combined inter and intra prediction (CIIP)

Megre模式是HEVC编码标准引入的一项帧间预测编码技术,在VVC编码标准中对Merge模式进行了扩展。包括对候选列表的扩展,增加了HMVP和 Pair-wise average candidates。编码工具新增CIIP (combined inter-intra prediction)、MMVD(merge mode with MV difference)、GEO(geometric partitioning mode)。VVC新提出的affine也存在merge模式,不过affine merge的候选推导和上面几个编码工具是不一样的。本文主要讨论CIIP技术。

1 原理

在VVC中,当一个CU在merge模式下编码时,如果CU包含至少64个亮度样本(即CU宽度乘以CU高度等于或大于64),并且如果CU宽度和CU高度都小于 128 ,可以选择使用CIIP模式,当前CU是否使用CIIP会被写入码流中。

图1

顾名思义,CIIP 就是将将帧间预测信号与帧内预测信号相结合。 CIIP模式下的帧间预测信号\(P_{inter}\)是使用与Nomal Megre模式(不是MMVD和Affine merge)相同的帧间预测过程导出的;并且帧内预测信号\(P_{intra}\)是按照Planar模式的常规帧内预测过程导出的。然后,使用加权平均来组合帧内和帧间预测信号,其中根据上方和左侧相邻块的编码模式(如图1所示)计算权重值,如下所示:

  • 如果上方相邻CU可用且预测模式为帧内预测,则将 isIntraTop 设置为 1,否则将 isIntraTop 设置为 0;
  • 如果左侧相邻CU可用且预测模式为帧内预测,则将 isIntraLeft 设置为 1,否则将 isIntraLeft 设置为 0;
  • 如果 (isIntraLeft + isIntraTop) = 2,则 wt = 3;
  • 否则,如果 (isIntraLeft + isIntraTop) = 1,则 wt = 2;
  • 否则,将 wt = 1。

CIIP加权预测公式如下:

\(P_{CIIP}=((4-wt)*P_{inter}+wt*P_{intra}+2)>>2\)

一般来说,Nomal merge有最多有6个候选,CIIP也最多有6个候选,MMVD最多有64个候选,Affine merge最多有5个候选,如果这些merge模式每个都做RDO的话,时间复杂度肯定极高,所以一般编码器的解决方案是和Intra预测类似,先使用SATD cost进行一遍粗选,选择几个最优的候选最后才做RDO,这样可以节省很多的时间。CIIP的粗选过程可以放在Nomal merge粗选的后面,只选择其中几个比较好的候选来check CIIP的候选,也可以节省一定的时间。

值得注意的是,在编码标准中,CIIP的merge是不可以判断为skip模式的,也就是说选择CIIP模式,残差系数一定不为0,遇到CIIP模式且cbf = 0可以根据需求决定是否要丢掉这个CIIP候选。

2 VTM代码

2.1 CIIP粗选

if (isIntrainterEnabled)
{
  // prepare for Intra bits calculation
  pu.ciipFlag = true;  // 先将ciip flage设置成true,用来计算bits

  // save the to-be-tested merge candidates
  uint32_t CiipMergeCand[NUM_MRG_SATD_CAND];
  for (uint32_t mergeCnt = 0; mergeCnt < std::min(NUM_MRG_SATD_CAND, (const int)mergeCtx.numValidMergeCand); mergeCnt++)
  {
    CiipMergeCand[mergeCnt] = RdModeList[mergeCnt].mergeCand;
  }
  for (uint32_t mergeCnt = 0; mergeCnt < std::min(std::min(NUM_MRG_SATD_CAND, (const int)mergeCtx.numValidMergeCand), 4); mergeCnt++)
  {
    uint32_t mergeCand = CiipMergeCand[mergeCnt];
    acMergeTmpBuffer[mergeCand] = m_acMergeTmpBuffer[mergeCand].getBuf(localUnitArea); // 复用nomal merge的插值buffer

    // estimate merge bits
    mergeCtx.setMergeInfo(pu, mergeCand); 

    // first round
    pu.intraDir[0] = PLANAR_IDX;
    uint32_t intraCnt = 0;
    // generate intrainter Y prediction
    if (mergeCnt == 0)
    {
      m_pcIntraSearch->initIntraPatternChType(*pu.cu, pu.Y()); // 获取参考像素
      m_pcIntraSearch->predIntraAng(COMPONENT_Y, pu.cs->getPredBuf(pu).Y(), pu); // Planar预测
      m_pcIntraSearch->switchBuffer(pu, COMPONENT_Y, pu.cs->getPredBuf(pu).Y(), m_pcIntraSearch->getPredictorPtr2(COMPONENT_Y, intraCnt));
    }
    pu.cs->getPredBuf(pu).copyFrom(acMergeTmpBuffer[mergeCand]);
    if (pu.cs->slice->getLmcsEnabledFlag() && m_pcReshape->getCTUFlag())
    {
      pu.cs->getPredBuf(pu).Y().rspSignal(m_pcReshape->getFwdLUT());
    }
    m_pcIntraSearch->geneWeightedPred(COMPONENT_Y, pu.cs->getPredBuf(pu).Y(), pu, m_pcIntraSearch->getPredictorPtr2(COMPONENT_Y, intraCnt)); //加权组合

    // calculate cost
    if (pu.cs->slice->getLmcsEnabledFlag() && m_pcReshape->getCTUFlag())
    {
      pu.cs->getPredBuf(pu).Y().rspSignal(m_pcReshape->getInvLUT());
    }
    distParam.cur = pu.cs->getPredBuf(pu).Y();
    Distortion sadValue = distParam.distFunc(distParam); // 计算 dist
    if (pu.cs->slice->getLmcsEnabledFlag() && m_pcReshape->getCTUFlag())
    {
      pu.cs->getPredBuf(pu).Y().rspSignal(m_pcReshape->getFwdLUT());
    }
    m_CABACEstimator->getCtx() = ctxStart;
    pu.regularMergeFlag = false;
    uint64_t fracBits = m_pcInterSearch->xCalcPuMeBits(pu); //计算 bits
    double cost = (double)sadValue + (double)fracBits * sqrtLambdaForFirstPassIntra; // 计算 cost

    insertPos = -1;
    updateCandList(ModeInfo(mergeCand, false, false, true), cost, RdModeList, candCostList, uiNumMrgSATDCand, &insertPos); // 更新候选排序列表
    if (insertPos != -1)
    {
      for (int i = int(RdModeList.size()) - 1; i > insertPos; i--)
      {
        swap(acMergeTempBuffer[i - 1], acMergeTempBuffer[i]);
      }
      swap(singleMergeTempBuffer, acMergeTempBuffer[insertPos]);
    }
  }
  pu.ciipFlag = false;
}

2.2 组合预测

这一块的代码涉及到像素的加权组合,因此可以使用SIMD来实现,VTM这里是使用C++实现的。

void IntraPrediction::geneWeightedPred(const ComponentID compId, PelBuf &pred, const PredictionUnit &pu, Pel *srcBuf)
{
  const int width = pred.width;
  CHECK(width == 2, "Width of 2 is not supported");
  const int height    = pred.height;
  const int srcStride = width;
  const int dstStride = pred.stride;

  Pel *dstBuf = pred.buf;
  int wIntra, wMerge;

  const Position posBL = pu.Y().bottomLeft();
  const Position posTR = pu.Y().topRight();
  const PredictionUnit *neigh0 = pu.cs->getPURestricted(posBL.offset(-1, 0), pu, CHANNEL_TYPE_LUMA);
  const PredictionUnit *neigh1 = pu.cs->getPURestricted(posTR.offset(0, -1), pu, CHANNEL_TYPE_LUMA);
  bool isNeigh0Intra = neigh0 && (CU::isIntra(*neigh0->cu));
  bool isNeigh1Intra = neigh1 && (CU::isIntra(*neigh1->cu));

  if (isNeigh0Intra && isNeigh1Intra)
  {
    wIntra = 3; wMerge = 1;
  }
  else
  {
    if (!isNeigh0Intra && !isNeigh1Intra)
    {
      wIntra = 1; wMerge = 3;
    }
    else
    {
      wIntra = 2; wMerge = 2;
    }
  }

  for (int y = 0; y < height; y++)
  {
    for (int x = 0; x < width; x++)
    {
      dstBuf[y*dstStride + x] = (wMerge * dstBuf[y*dstStride + x] + wIntra * srcBuf[y*srcStride + x] + 2) >> 2;
    }
  }
}

2.3 encoder

首选判断是否ciipAvailable,CIIP和GEO都不是regular merge,如果有一个是true,则需要编是否为regular merge,如果不是,则还需要编一个flag来判断是CIIP还是GEO。

const bool ciipAvailable = pu.cs->sps->getUseCiip() && !pu.cu->skip && pu.cu->lwidth() < MAX_CU_SIZE && pu.cu->lheight() < MAX_CU_SIZE && pu.cu->lwidth() * pu.cu->lheight() >= 64;
  const bool geoAvailable = pu.cu->cs->slice->getSPS()->getUseGeo() && pu.cu->cs->slice->isInterB() &&
    pu.cs->sps->getMaxNumGeoCand() > 1
                                                                    && pu.cu->lwidth() >= GEO_MIN_CU_SIZE && pu.cu->lheight() >= GEO_MIN_CU_SIZE
                                                                    && pu.cu->lwidth() <= GEO_MAX_CU_SIZE && pu.cu->lheight() <= GEO_MAX_CU_SIZE
                                                                    && pu.cu->lwidth() < 8 * pu.cu->lheight() && pu.cu->lheight() < 8 * pu.cu->lwidth();
  if (geoAvailable || ciipAvailable)
  {
    m_BinEncoder.encodeBin(pu.regularMergeFlag, Ctx::RegularMergeFlag(pu.cu->skip ? 0 : 1));
  }
  if (pu.regularMergeFlag)
  {
    if (pu.cs->sps->getUseMMVD())
    {
      m_BinEncoder.encodeBin(pu.mmvdMergeFlag, Ctx::MmvdFlag(0));
      DTRACE(g_trace_ctx, D_SYNTAX, "mmvd_merge_flag() mmvd_merge=%d pos=(%d,%d) size=%dx%d\n", pu.mmvdMergeFlag ? 1 : 0, pu.lumaPos().x, pu.lumaPos().y, pu.lumaSize().width, pu.lumaSize().height);
    }
    if (pu.mmvdMergeFlag || pu.cu->mmvdSkip)
    {
      mmvd_merge_idx(pu);
    }
    else
    {
      merge_idx(pu);
    }
  }
  else
  {
    if (geoAvailable && ciipAvailable)
    {
      Ciip_flag(pu);
    }
    merge_idx(pu);
  }

Merge模式候选列表构建

Megre模式是HEVC编码标准引入的一项帧间预测编码技术,在VVC编码标准中对Merge模式进行了扩展。包括对候选列表的扩展,增加了HMVP和 Pair-wise average candidates。编码工具新增CIIP (combined inter-intra prediction)、MMVD(merge mode with MV difference)、GEO(geometric partitioning mode)。VVC新提出的affine也存在merge模式,不过affine merge的候选推导和上面几个编码工具是不一样的。本文主要讨论Merge模式候选列表构建。

1 HEVC Merge候选列表构建

HEVC的Merge候选个数最大为5个,构建过程如图1。

图1

为了构建空域Merge候选,在位于图2所示位置的候选中选择最多四个Merge候选。

图2

构建的顺序是A1→B1→B0→A0→B2。仅当位置 A1、B1、B0 和 A0 的任何 PU 不可用(例如它属于另一个Slice或Tile)或者不是inter mode时才考虑位置 B2。

在A1位置的候选加入后,剩余候选的加入会进行冗余校验,确保将具有相同运动信息的候选排除在列表之外,以提高编码效率。为了降低计算复杂度,仅比较下图3的箭头链接的对,并且仅当候选通过冗余检查时才将其添加到列表中。

图3

在 HEVC 中,一个 CU 可能会被划分为多个 PU,这可能会对Merge模式带来冗余。图4描绘了从 CU 中分别按 N × 2N 和 2N × N 模式划分的“第二个 PU”。

图4

当第二个 PU 从一个 CU 划分 N × 2N 时,位置 A1 的候选者不考虑用于列表构建。事实上,通过选择这个候选者,两个 PU 将共享相同的运动信息,这对于 CU 中只有一个 PU 的情况是多余的。 类似地,当第二个 PU 从一个 CU 被分割为 2N × N 时,不考虑位置 B1。

在时域Merge候选的推导中,TMVP 候选是从存储在位置 H 或 C 的 MV 推导出来的,如图 1 所示的并置图片,类似于 AMVP 模式的 TMVP 候选。 对于Merge候选列表中的 TMVP 候选,MV 将被缩放到相应参考帧列表中具有参考索引 0 的参考帧。

除了时空Merge候选之外,还有两种附加类型的Merge候选:组合的双向预测Merge候选和具有 (0, 0) 运动向量的零运动候选。 组合的双向预测Merge候选是通过仅利用 B Slice的时空Merge候选来生成的。 通过组合具有参考列表0的第一Merge候选和具有参考列表1的第二Merge候选来生成组合双向预测候选,其中第一和第二Merge候选根据预定义顺序从Merge候选列表中的可用Merge候选中选择。这两个MV将形成新的双向预测候选。如果未满足Merge候选列表,则将向列表添加零运动候选以填充列表。

2 VVC Merge候选列表构建

2.1 空域候选推导

VVC中空域Merge候选的推导与HEVC相同,只是前两个Merge候选的位置交换了。在位于图 5 所示位置的候选中,最多选择四个Merge候选。推导顺序为 B1、A1、B0、A0 和 B2。仅当位置 B0、A0、B1、A1 的一个或多个 CU 不可用(例如它属于另一个Slice或Tile)或者不是inter mode时,才考虑位置 B2。在A1位置的候选加入后,剩余候选的加入进行冗余校验,保证将具有相同运动信息的候选排除在列表之外,从而提高编码效率。为了降低计算复杂度,在提到的冗余校验中并未考虑所有可能的候选对,与HEVC一样仅考虑与图 3 中的箭头链接的对,并且仅当用于冗余校验的相应候选具有不同的运动信息时,才将候选添加到列表中。

图5

2.2 时域候选推导(TMVP)

在此步骤中,仅将一个候选者添加到列表中。 特别地,在该时域Merge候选的推导中,基于属于并置参考图片的 co-located CU来缩放运动矢量。 用于推导位于同一位置的 CU 的参考帧列表和参考索引会被写入slice header中。 时域Merge候选的缩放运动向量如图 6中的虚线所示,它是从 co-located CU 的运动向量缩放的。

图6

如图 7 所示,在候选 C0 和 C1 之间选择时域候选的位置。如果位置 C0 处的 CU 不可用、被帧内编码或位于当前 CTU 行之外,则使用位置 C1。 否则,在时域Merge候选的推导中使用位置 C0。

图7

2. 3 HMVP候选

在 HEVC 中,有两种类型的 MVP,即时域 MVP 和空域 MVP,它们利用来自空间相邻或时间块的运动信息。在 VVC 中,引入了一种新型 MVP,即 基于历史的 MVP(HMVP)。 HMVP 的基本思想是进一步使用先前编码的 MV 作为 MVP,这些 MV 与相对于当前块的相邻或不相邻块相关联。为了跟踪可用的 HMVP 候选者,在编码器和解码器处都维护了一个 HMVP 候选者表,并动态更新。每当新的 CTU 行开始时,表就会重置以简化并行编码。

HMVP 表中最多有5个候选者。在对一个不处于子块模式(包括仿射模式)或 GPM 的帧间预测块进行编码之后,通过将关联的运动信息附加到表的末尾作为新的 HMVP 候选者来选择性地更新表。应用受限的先进先出 (FIFO) 规则来管理表,其中首先应用冗余检查以查找表中是否存在相同的HMVP。 如果找到,则从表中删除相同的 HMVP,然后将所有 HMVP 候选向前移动,并将相同的 HMVP 插入到表的最后一个条目中。使用 HMVP,即使编码块在空间上不与当前块相邻,先前编码块的运动信息也可以用于更有效的运动矢量预测。

为了减少冗余校验操作的数量,引入了以下简化:

  1. 表中的最后两个条目分别对 A1 和 B1 空域候选进行冗余检查。
  2. 一旦可用Merge候选的总数达到最大允许Merge候选减1,则终止来自HMVP的Merge候选列表构建过程。

2.4 Pair-wise average merge candidates derivation

VVC 中的Pair-wise average Merge候选取代了 HEVC 中的组合双预测Merge候选。Pair-wise average 候选是通过使用前两个Merge候选对现有Merge候选列表中预定义的候选对进行平均来生成的。 第一个Merge候选定义为 p0Cand,第二个Merge候选定义为 p1Cand。 根据 p0Cand 和 p1Cand 的运动向量的可用性分别计算每个参考列表的平均运动向量。 如果两个运动矢量都在一个列表中,则即使它们指向不同的参考帧,这两个运动矢量也会被平均,并将其参考图片设置为p0Cand之一; 如果只有一个运动矢量可用,则直接使用一个; 如果没有可用的运动矢量,则保持此列表无效。 此外,如果 p0Cand 和 p1Cand 的半像素插值滤波器索引不同,则将其设置为 0。

当添加Pair-wise average Merge候选后Merge列表未满时,则将向列表添加零运动候选以填充列表。

2.5 VTM代码分析

删去了代码中和GDR相关的和一些不重要的内容。

空域:代码里的符号和上面图中的符号不一致,但是仔细看顺序还是一样的。

// above
  const PredictionUnit *puAbove = cs.getPURestricted(posRT.offset(0, -1), pu, pu.chType);

  bool isAvailableB1 = puAbove && isDiffMER(pu.lumaPos(), posRT.offset(0, -1), plevel) && pu.cu != puAbove->cu && CU::isInter(*puAbove->cu);

  if (isAvailableB1)
  {
    miAbove = puAbove->getMotionInfo(posRT.offset(0, -1));

    // get Inter Dir
    mrgCtx.interDirNeighbours[cnt] = miAbove.interDir;
    mrgCtx.useAltHpelIf[cnt] = miAbove.useAltHpelIf;
    // get Mv from Above
    mrgCtx.bcwIdx[cnt] = (mrgCtx.interDirNeighbours[cnt] == 3) ? puAbove->cu->bcwIdx : BCW_DEFAULT;
    mrgCtx.mvFieldNeighbours[cnt << 1].setMvField(miAbove.mv[0], miAbove.refIdx[0]);

    if (slice.isInterB())
    {
      mrgCtx.mvFieldNeighbours[(cnt << 1) + 1].setMvField(miAbove.mv[1], miAbove.refIdx[1]);
    }
    cnt++;
  }

  //left
  const PredictionUnit* puLeft = cs.getPURestricted(posLB.offset(-1, 0), pu, pu.chType);

  const bool isAvailableA1 = puLeft && isDiffMER(pu.lumaPos(), posLB.offset(-1, 0), plevel) && pu.cu != puLeft->cu && CU::isInter(*puLeft->cu);

  if (isAvailableA1)
  {
    miLeft = puLeft->getMotionInfo(posLB.offset(-1, 0));

    if (!isAvailableB1 || (miAbove != miLeft))
    {
      // get Inter Dir
      mrgCtx.interDirNeighbours[cnt] = miLeft.interDir;
      mrgCtx.useAltHpelIf[cnt]       = miLeft.useAltHpelIf;
      mrgCtx.bcwIdx[cnt]             = (mrgCtx.interDirNeighbours[cnt] == 3) ? puLeft->cu->bcwIdx : BCW_DEFAULT;
      // get Mv from Left
      mrgCtx.mvFieldNeighbours[cnt << 1].setMvField(miLeft.mv[0], miLeft.refIdx[0]);

      if (slice.isInterB())
      {
        mrgCtx.mvFieldNeighbours[(cnt << 1) + 1].setMvField(miLeft.mv[1], miLeft.refIdx[1]);
      }
      cnt++;
    }
  }

  // above right
  const PredictionUnit *puAboveRight = cs.getPURestricted( posRT.offset( 1, -1 ), pu, pu.chType );

  bool isAvailableB0 = puAboveRight && isDiffMER( pu.lumaPos(), posRT.offset(1, -1), plevel) && CU::isInter( *puAboveRight->cu );

  if( isAvailableB0 )
  {
    miAboveRight = puAboveRight->getMotionInfo( posRT.offset( 1, -1 ) );

    if( !isAvailableB1 || ( miAbove != miAboveRight ) )
    {

      // get Inter Dir
      mrgCtx.interDirNeighbours[cnt] = miAboveRight.interDir;
      mrgCtx.useAltHpelIf[cnt] = miAboveRight.useAltHpelIf;
      // get Mv from Above-right
      mrgCtx.bcwIdx[cnt] = (mrgCtx.interDirNeighbours[cnt] == 3) ? puAboveRight->cu->bcwIdx : BCW_DEFAULT;
      mrgCtx.mvFieldNeighbours[cnt << 1].setMvField( miAboveRight.mv[0], miAboveRight.refIdx[0] );

      if( slice.isInterB() )
      {
        mrgCtx.mvFieldNeighbours[( cnt << 1 ) + 1].setMvField( miAboveRight.mv[1], miAboveRight.refIdx[1] );
      }
      cnt++;
    }
  }

  //left bottom
  const PredictionUnit *puLeftBottom = cs.getPURestricted( posLB.offset( -1, 1 ), pu, pu.chType );

  bool isAvailableA0 = puLeftBottom && isDiffMER( pu.lumaPos(), posLB.offset(-1, 1), plevel) && CU::isInter( *puLeftBottom->cu );

  if( isAvailableA0 )
  {
    miBelowLeft = puLeftBottom->getMotionInfo( posLB.offset( -1, 1 ) );

    if( !isAvailableA1 || ( miBelowLeft != miLeft ) )
    {
      // get Inter Dir
      mrgCtx.interDirNeighbours[cnt] = miBelowLeft.interDir;
      mrgCtx.useAltHpelIf[cnt]       = miBelowLeft.useAltHpelIf;
      mrgCtx.bcwIdx[cnt]             = (mrgCtx.interDirNeighbours[cnt] == 3) ? puLeftBottom->cu->bcwIdx : BCW_DEFAULT;
      // get Mv from Bottom-Left
      mrgCtx.mvFieldNeighbours[cnt << 1].setMvField( miBelowLeft.mv[0], miBelowLeft.refIdx[0] );

      if( slice.isInterB() )
      {
        mrgCtx.mvFieldNeighbours[( cnt << 1 ) + 1].setMvField( miBelowLeft.mv[1], miBelowLeft.refIdx[1] );
      }
      cnt++;
    }
  }

  // above left
  if ( cnt < 4 )
  {
    const PredictionUnit *puAboveLeft = cs.getPURestricted( posLT.offset( -1, -1 ), pu, pu.chType );

    bool isAvailableB2 = puAboveLeft && isDiffMER( pu.lumaPos(), posLT.offset(-1, -1), plevel ) && CU::isInter( *puAboveLeft->cu );

    if( isAvailableB2 )
    {
      miAboveLeft = puAboveLeft->getMotionInfo( posLT.offset( -1, -1 ) );

      if( ( !isAvailableA1 || ( miLeft != miAboveLeft ) ) && ( !isAvailableB1 || ( miAbove != miAboveLeft ) ) )
      {
        // get Inter Dir
        mrgCtx.interDirNeighbours[cnt] = miAboveLeft.interDir;
        mrgCtx.useAltHpelIf[cnt]       = miAboveLeft.useAltHpelIf;
        mrgCtx.bcwIdx[cnt]             = (mrgCtx.interDirNeighbours[cnt] == 3) ? puAboveLeft->cu->bcwIdx : BCW_DEFAULT;
        // get Mv from Above-Left
        mrgCtx.mvFieldNeighbours[cnt << 1].setMvField( miAboveLeft.mv[0], miAboveLeft.refIdx[0] );

        if( slice.isInterB() )
        {
          mrgCtx.mvFieldNeighbours[( cnt << 1 ) + 1].setMvField( miAboveLeft.mv[1], miAboveLeft.refIdx[1] );
        }
        cnt++;
      }
    }
  }

TMVP

  if (slice.getPicHeader()->getEnableTMVPFlag() && (pu.lumaSize().width + pu.lumaSize().height > 12))
  {
    //>> MTK colocated-RightBottom
    // offset the pos to be sure to "point" to the same position the uiAbsPartIdx would've pointed to
    Position posRB = pu.Y().bottomRight().offset( -3, -3 );
    const PreCalcValues& pcv = *cs.pcv;

    Position posC0;
    Position posC1 = pu.Y().center();
    bool C0Avail = false;
    bool boundaryCond = ((posRB.x + pcv.minCUWidth) < pcv.lumaWidth) && ((posRB.y + pcv.minCUHeight) < pcv.lumaHeight);
    const SubPic& curSubPic = pu.cs->slice->getPPS()->getSubPicFromPos(pu.lumaPos());
    if (curSubPic.getTreatedAsPicFlag())
    {
      boundaryCond = ((posRB.x + pcv.minCUWidth) <= curSubPic.getSubPicRight() &&
                      (posRB.y + pcv.minCUHeight) <= curSubPic.getSubPicBottom());
    }
    if (boundaryCond)
    {
      int posYInCtu = posRB.y & pcv.maxCUHeightMask;
      if (posYInCtu + 4 < pcv.maxCUHeight)
      {
        posC0 = posRB.offset(4, 4);
        C0Avail = true;
      }
    }

    Mv        cColMv;
    int       refIdx      = 0;
    int       dir         = 0;
    unsigned  arrayAddr   = cnt;
    bool      existMV     = (C0Avail && getColocatedMVP(pu, REF_PIC_LIST_0, posC0, cColMv, refIdx, false))
                   || getColocatedMVP(pu, REF_PIC_LIST_0, posC1, cColMv, refIdx, false);
    if (existMV)
    {
      dir     |= 1;
      mrgCtx.mvFieldNeighbours[2 * arrayAddr].setMvField(cColMv, refIdx);
    }

    if (slice.isInterB())
    {
      existMV = (C0Avail && getColocatedMVP(pu, REF_PIC_LIST_1, posC0, cColMv, refIdx, false))
                || getColocatedMVP(pu, REF_PIC_LIST_1, posC1, cColMv, refIdx, false);
      if (existMV)
      {
        dir     |= 2;
        mrgCtx.mvFieldNeighbours[2 * arrayAddr + 1].setMvField(cColMv, refIdx);
      }
    }

    if( dir != 0 )
    {
      bool addTMvp = true;
      if( addTMvp )
      {
        mrgCtx.interDirNeighbours[arrayAddr] = dir;
        mrgCtx.bcwIdx[arrayAddr]             = BCW_DEFAULT;
        mrgCtx.useAltHpelIf[arrayAddr]       = false;
        if (mrgCandIdx == cnt)
        {
          return;
        }

        cnt++;
      }
    }
  }

HMVP

bool PU::addMergeHMVPCand(const CodingStructure &cs, MergeCtx &mrgCtx, const int &mrgCandIdx,
                          const uint32_t maxNumMergeCandMin1, int &cnt, const bool isAvailableA1,
                          const MotionInfo miLeft, const bool isAvailableB1, const MotionInfo miAbove,
                          const bool ibcFlag, const bool isGt4x4

)
{
  const Slice& slice = *cs.slice;
  MotionInfo miNeighbor;

  auto &lut = ibcFlag ? cs.motionLut.lutIbc : cs.motionLut.lut;

  const int numAvailCandInLut = (int) lut.size();

  for (int mrgIdx = 1; mrgIdx <= numAvailCandInLut; mrgIdx++)
  {
    miNeighbor = lut[numAvailCandInLut - mrgIdx];

    if ( mrgIdx > 2 || ((mrgIdx > 1 || !isGt4x4) && ibcFlag)
      || ((!isAvailableA1 || (miLeft != miNeighbor)) && (!isAvailableB1 || (miAbove != miNeighbor))) )
    {
      mrgCtx.interDirNeighbours[cnt] = miNeighbor.interDir;
      mrgCtx.useAltHpelIf      [cnt] = !ibcFlag && miNeighbor.useAltHpelIf;
      mrgCtx.bcwIdx[cnt]             = (miNeighbor.interDir == 3) ? miNeighbor.bcwIdx : BCW_DEFAULT;

      mrgCtx.mvFieldNeighbours[cnt << 1].setMvField(miNeighbor.mv[0], miNeighbor.refIdx[0]);
      if (slice.isInterB())
      {
        mrgCtx.mvFieldNeighbours[(cnt << 1) + 1].setMvField(miNeighbor.mv[1], miNeighbor.refIdx[1]);
      }

      if (mrgCandIdx == cnt)
      {
        return true;
      }
      cnt ++;

      if (cnt  == maxNumMergeCandMin1)
      {
        break;
      }
    }
  }

  if (cnt < maxNumMergeCandMin1)
  {
    mrgCtx.useAltHpelIf[cnt] = false;
  }

  return false;
}

pairwise-average candidates

if (cnt > 1 && cnt < maxNumMergeCand)
    {
      mrgCtx.mvFieldNeighbours[cnt * 2].setMvField( Mv( 0, 0 ), NOT_VALID );
      mrgCtx.mvFieldNeighbours[cnt * 2 + 1].setMvField( Mv( 0, 0 ), NOT_VALID );
      // calculate average MV for L0 and L1 seperately
      unsigned char interDir = 0;

      mrgCtx.useAltHpelIf[cnt] = (mrgCtx.useAltHpelIf[0] == mrgCtx.useAltHpelIf[1]) ? mrgCtx.useAltHpelIf[0] : false;
      for( int refListId = 0; refListId < (slice.isInterB() ? 2 : 1); refListId++ )
      {
        const short refIdxI = mrgCtx.mvFieldNeighbours[0 * 2 + refListId].refIdx;
        const short refIdxJ = mrgCtx.mvFieldNeighbours[1 * 2 + refListId].refIdx;

        // both MVs are invalid, skip
        if( (refIdxI == NOT_VALID) && (refIdxJ == NOT_VALID) )
        {
          continue;
        }

        interDir += 1 << refListId;
        // both MVs are valid, average these two MVs
        if( (refIdxI != NOT_VALID) && (refIdxJ != NOT_VALID) )
        {
          const Mv &mvI = mrgCtx.mvFieldNeighbours[0 * 2 + refListId].mv;
          const Mv &mvJ = mrgCtx.mvFieldNeighbours[1 * 2 + refListId].mv;

          // average two MVs
          Mv avgMv = mvI;
          avgMv += mvJ;
          avgMv.roundAffine(1);

          mrgCtx.mvFieldNeighbours[cnt * 2 + refListId].setMvField( avgMv, refIdxI );
        }
        // only one MV is valid, take the only one MV
        else if( refIdxI != NOT_VALID )
        {
          Mv singleMv = mrgCtx.mvFieldNeighbours[0 * 2 + refListId].mv;
          mrgCtx.mvFieldNeighbours[cnt * 2 + refListId].setMvField( singleMv, refIdxI );
        }
        else if( refIdxJ != NOT_VALID )
        {
          Mv singleMv = mrgCtx.mvFieldNeighbours[1 * 2 + refListId].mv;
          mrgCtx.mvFieldNeighbours[cnt * 2 + refListId].setMvField( singleMv, refIdxJ );
        }
      }

      mrgCtx.interDirNeighbours[cnt] = interDir;
      if( interDir > 0 )
      {
        cnt++;
      }
    }

Zero Mv

uint32_t arrayAddr = cnt;

  int numRefIdx = slice.isInterB() ? std::min(slice.getNumRefIdx(REF_PIC_LIST_0), slice.getNumRefIdx(REF_PIC_LIST_1))
                                   : slice.getNumRefIdx(REF_PIC_LIST_0);

  int r = 0;
  int refcnt = 0;
  while (arrayAddr < maxNumMergeCand)
  {
    mrgCtx.interDirNeighbours[arrayAddr] = 1;
    mrgCtx.bcwIdx[arrayAddr]             = BCW_DEFAULT;
    mrgCtx.mvFieldNeighbours[arrayAddr << 1].setMvField(Mv(0, 0), r);
    mrgCtx.useAltHpelIf[arrayAddr] = false;

    if (slice.isInterB())
    {
      mrgCtx.interDirNeighbours[arrayAddr] = 3;
      mrgCtx.mvFieldNeighbours[(arrayAddr << 1) + 1].setMvField(Mv(0, 0), r);
    }

    arrayAddr++;

    if (refcnt == numRefIdx - 1)
    {
      r = 0;
    }
    else
    {
      ++r;
      ++refcnt;
    }
  }
  mrgCtx.numValidMergeCand = arrayAddr;

代码和上文描述的是一致的,代码里包含了更多的细节,这里就不再讨论了。

Planar, DC, 角度预测模式(VVC)

VVC 中的帧内预测技术包括 Planar 和 DC 模式,以及与 HEVC 相比具有更多角度的更细粒度的角度预测模式,VVC 将原本的 33 种角度模式增加到 65 种。除此之外,VVC 的帧内编码技术中还包含了许多新的编码工具,本文将对 VVC 中的 Planar, DC 和角度预测三种模式结合 VTM-16.0 做深入分析。

1 参考像素构建与滤波

参考像素的填充主要包含两个步骤:

  • 分析当前预测块边界,判断当前预测块左上角C、上方D、右上E、左侧B、左下A重建像素是否可用,并统计可用像素的数目
  • 使用重建像素填充参考像素

这里填充参考像素时,有以下三种情况:

  • 重建像素全部不可用,则参考像素全部填充1<<(bitDepth-1)
  • 重建像素全部可用,则直接使用重建像素填充参考像素
  • 重建像素部分可用部分不可用时,则先查看最左下角的重建像素是否可用,有以下两个规则
    • 如果可用,则从下往上遍历,不可用的重建像素值用其下方最相邻的像素值填充,到达左上角后,从左到右遍历,若有某点的重建像素值不可用,则用其左边最相邻的像素填充;
    • 如果不可用,则先从下往上,从左往右遍历一次直到找到第一个可用的重建像素值,将该重建值填充到最左下角的位置,然后将其之前遍历到的不可用的重建像素都使用该重建值填充,接着按照规则1填充。

对于参考样本平滑滤波,使用有限脉冲响应滤波器 {1, 2, 1}/4 对参考样本进行滤波。对参考像素进行平滑滤波的需要同时满足以下条件:

  • 帧内预测模式是(−14、−12、−10、−6、0(planar)、2、34、66、72、76、78、80)模式之一
  • CU中包含像素数大于32(width*height > 32
  • 参考行索引为0,即使用单参考行
  • 亮度分量
  • 非ISP模式

参考像素的构建和滤波在initIntraPatternChType()函数中。

2 Planar 预测模式

2.1 原理

Planar 预测通过水平和垂直线性插值的平均作为当前像素的预测值,解决帧内预测而在块边界上没有不连续性的问题,适合纹理比较平滑的区域,尤其是变化趋势比较一致的区域。Planar 预测对亮度和色度分量均适用。

标准文档[1]里给出了计算公式:

其中 nTbW 和 nTbH 为块的宽度和高度,p[ x ][ y ]为参考样本 ,其中 x = -1,y = -1..nTbH 或 x = 0..nTbW,y = -1。

上图说明了 Planar 模式下预测样本值的推导过程。右上角的参考样本 p[N][-1] 用作所有水平线性插值的右参考。类似地,左下参考样本 p[-1][N] 被用作所有垂直线性插值的底部参考。通过平均水平和垂直预测来获得每个样本的最终预测值。

2.2 VTM 实现

void IntraPrediction::xPredIntraPlanar( const CPelBuf &pSrc, PelBuf &pDst )
{
  const uint32_t width  = pDst.width;
  const uint32_t height = pDst.height;

  const uint32_t log2W = floorLog2( width );
  const uint32_t log2H = floorLog2( height );

  int leftColumn[MAX_CU_SIZE + 1], topRow[MAX_CU_SIZE + 1], bottomRow[MAX_CU_SIZE], rightColumn[MAX_CU_SIZE];
  const uint32_t offset = 1 << (log2W + log2H);

  // Get left and above reference column and row
  CHECK(width > MAX_CU_SIZE, "width greater than limit");
  for( int k = 0; k < width + 1; k++ )
  {
    topRow[k] = pSrc.at( k + 1, 0 );
  }

  CHECK(height > MAX_CU_SIZE, "height greater than limit");
  for( int k = 0; k < height + 1; k++ )
  {
    leftColumn[k] = pSrc.at(k + 1, 1);
  }

  // Prepare intermediate variables used in interpolation
  int bottomLeft = leftColumn[height];
  int topRight = topRow[width];

  for( int k = 0; k < width; k++ )
  {
    bottomRow[k] = bottomLeft - topRow[k];
    topRow[k]    = topRow[k] << log2H;
  }

  for( int k = 0; k < height; k++ )
  {
    rightColumn[k] = topRight - leftColumn[k];
    leftColumn[k]  = leftColumn[k] << log2W;
  }

  const uint32_t finalShift = 1 + log2W + log2H;
  const uint32_t stride     = pDst.stride;
  Pel*       pred       = pDst.buf;
  for( int y = 0; y < height; y++, pred += stride )
  {
    int horPred = leftColumn[y];

    for( int x = 0; x < width; x++ )
    {
      horPred += rightColumn[y];
      topRow[x] += bottomRow[x];

      int vertPred = topRow[x];
      pred[x]      = ( ( horPred << log2H ) + ( vertPred << log2W ) + offset ) >> finalShift;
    }
  }
}

VTM 的 Planar 实现基本是按照公式来的,在计算 horPred 和 vertPred 的过程中,通过累加代替乘法。如果两个公式合在一起,也可通过一步来计算,但是要注意中间变量的数据类型,避免出现溢出的情况。

3 DC 预测模式

3.1 原理

DC 模式对当前块的所有像素使用同一个预测值,即预测参考像素的平均值。这种模式适用于图像的平坦区域。DC 预测模式对亮度和色度分量均适用。

由于 VVC 中存在矩形块,在计算平均数时会引入不是 2 的幂的除数。为了减小复杂度,VVC 仅使用沿矩形块较长边的参考样本来计算平均值,而对于方形块,则使用来自两侧的参考样本。根据 Filippov[2]这种修改不会导致压缩性能的任何下降。

标准文档里给出了计算公式:

  • 当 nTbW 和 nTbH 相等时
  • 当 nTbW 大于 nTbH 时
  • 当 nTbW 小于 nTbH 时

预测样本值为:

3.2 VTM 实现

Pel IntraPrediction::xGetPredValDc( const CPelBuf &pSrc, const Size &dstSize )
{
  CHECK( dstSize.width == 0 || dstSize.height == 0, "Empty area provided" );

  int idx, sum = 0;
  Pel dcVal;
  const int width  = dstSize.width;
  const int height = dstSize.height;
  const auto denom     = (width == height) ? (width << 1) : std::max(width,height);
  const auto divShift  = floorLog2(denom);
  const auto divOffset = (denom >> 1);

  if ( width >= height )
  {
    for( idx = 0; idx < width; idx++ )
    {
      sum += pSrc.at(m_ipaParam.multiRefIndex + 1 + idx, 0);
    }
  }
  if ( width <= height )
  {
    for( idx = 0; idx < height; idx++ )
    {
      sum += pSrc.at(m_ipaParam.multiRefIndex + 1 + idx, 1);
    }
  }

  dcVal = (sum + divOffset) >> divShift;
  return dcVal;
}

逻辑比较简单,不再赘述。

4 角度预测

VVC 的角度预测从 HEVC 的 33 种角度扩展至65种,由于矩形块的存在,VVC又增加了广角度预测。

4.1 一般角度预测

4.1.1 原理

这里一般角度是指角度 2~66 这些角度,其中把角度2~32成为水平预测角度,把33~66成为垂直预测角度。水平预测和垂直预测过程是一样的,这里重点讨论垂直预测过程。如图4.1所示,在帧内预测模式51~66的情况下,对于每个预测方向,根据比例关系,可以利用下式计算预测点\(P(x,y)\)在上方参考像素中的投影点位置,得到投影点的横坐标相对于\(P(x,y)\)横坐标的位移\(c_x\):

\(c_x/y = d/32\)

其中,\(c_x\)表示待预测点\((x,y)\)的横坐标和点\((x,y)\)沿着预测方向投影到上参考像素行的横坐标之差,也就是 VVC 标准中定义的便宜索引 iIdx;d 表示预测模式方向和垂直方向的偏移距离(格数,其中模式66为32格,每种预测角度的格数,可由表3.1查询)。由上式可以定义偏移索引iIdx和权重因子iFact:

\(iIdx = c_x=(y \cdot d)/32\)
\(iFact=w=(y \cdot d)\&31\)

其中\(iIdx\)用来确定参考像素的位置,\(iFact\)用来确定滤波参数,

VVC 中的帧内预测有两种应用于参考样本的滤波机制,即参考样本平滑和插值滤波。参考样本平滑仅应用于亮度块中的整数斜率(\(iFact=0\))模式,而插值滤波应用于分数斜率模式。

对于插值滤波,如果给定预测方向的样本投影落在参考样本之间的分数位置上,则通过对分数样本位置周围的参考样本应用插值滤波器来获得预测样本值。对于亮度块,使用 4-tap 插值滤波器,预测样本 pred(x, y) 为:

其中\(i_0=iIdx+x\); \(p=iFact\)。

VVC 中包含两个插值滤波器,分别为基于 DCT 的插值滤波器 (DCTIF) 或 4 抽头平滑插值滤波器 (SIF)。插值滤波器的类型不写入比特流中,而是基于块的大小和帧内预测模式索引\(m\)来确定。 如果 \(min(|m-50|, |m-18|) > T\) ,则使用 SIF,否则,使用 DCTIF。 这里,\(T\) 是一个取决于块大小的阈值。对于具体系数值可以参考标准文档 Table 25。

对于色度分量,在 VVC 中使用 HEVC 的线性 2 抽头插值滤波器。

4.1.2 VTM 实现
void IntraPrediction::xPredIntraAng( const CPelBuf &pSrc, PelBuf &pDst, const ChannelType channelType, const ClpRng& clpRng)
{
  int width =int(pDst.width);
  int height=int(pDst.height);

  const bool bIsModeVer     = m_ipaParam.isModeVer; // m_ipaParam.isModeVer = predMode >= DIA_IDX;
  const int  multiRefIdx    = m_ipaParam.multiRefIndex;
  const int  intraPredAngle = m_ipaParam.intraPredAngle;  // tan()值
  const int  absInvAngle    = m_ipaParam.absInvAngle; // itan()值

  Pel* refMain;
  Pel* refSide;

  Pel  refAbove[2 * MAX_CU_SIZE + 3 + 33 * MAX_REF_LINE_IDX];
  Pel  refLeft [2 * MAX_CU_SIZE + 3 + 33 * MAX_REF_LINE_IDX];

  // Initialize the Main and Left reference array.
  if (intraPredAngle < 0)  // 角度19-49,需要两个方向的参考像素
  {
    for (int x = 0; x <= width + 1 + multiRefIdx; x++)
    {
      refAbove[x + height] = pSrc.at(x, 0);
    }
    for (int y = 0; y <= height + 1 + multiRefIdx; y++)
    {
      refLeft[y + width] = pSrc.at(y, 1);
    }
    refMain = bIsModeVer ? refAbove + height : refLeft + width;
    refSide = bIsModeVer ? refLeft + width : refAbove + height;

    // Extend the Main reference to the left.
    int sizeSide = bIsModeVer ? height : width;
    for (int k = -sizeSide; k <= -1; k++)
    {
      refMain[k] = refSide[std::min((-k * absInvAngle + 256) >> 9, sizeSide)];
      // 将上方和左方的参考像素合并成一个一维的像素集
    }
  }
  else // 角度-14-18, 50-80,只需要一个方向的参考像素
  {
    for (int x = 0; x <= m_topRefLength + multiRefIdx; x++)
    {
      refAbove[x] = pSrc.at(x, 0);
    }
    for (int y = 0; y <= m_leftRefLength + multiRefIdx; y++)
    {
      refLeft[y] = pSrc.at(y, 1);
    }

    refMain = bIsModeVer ? refAbove : refLeft;
    refSide = bIsModeVer ? refLeft : refAbove;

    // Extend main reference to right using replication
    const int log2Ratio = floorLog2(width) - floorLog2(height);
    const int s         = std::max<int>(0, bIsModeVer ? log2Ratio : -log2Ratio);
    const int maxIndex  = (multiRefIdx << s) + 2;
    const int refLength = bIsModeVer ? m_topRefLength : m_leftRefLength;
    const Pel val       = refMain[refLength + multiRefIdx];
    for (int z = 1; z <= maxIndex; z++)
    {
      refMain[refLength + multiRefIdx + z] = val; // 使用最邻近像素填充参考像素
    }
  }

  // swap width/height if we are doing a horizontal mode:
  if (!bIsModeVer)
  {
    std::swap(width, height);
  }
  Pel       tempArray[MAX_CU_SIZE * MAX_CU_SIZE];
  const int dstStride = bIsModeVer ? pDst.stride : width;
  Pel *     pDstBuf   = bIsModeVer ? pDst.buf : tempArray;

  // compensate for line offset in reference line buffers
  refMain += multiRefIdx;
  refSide += multiRefIdx;

  Pel *pDsty = pDstBuf;

  if( intraPredAngle == 0 )  // pure vertical or pure horizontal
  {
    for( int y = 0; y < height; y++ )
    {
      for( int x = 0; x < width; x++ )
      {
        pDsty[x] = refMain[x + 1];
      }
      pDsty += dstStride;
    }
  }
  else
  {
    for (int y = 0, deltaPos = intraPredAngle * (1 + multiRefIdx); y<height; y++, deltaPos += intraPredAngle, pDsty += dstStride)
    {
      const int deltaInt   = deltaPos >> 5;  // 确定参考像素位置
      const int deltaFract = deltaPos & 31;  // 确定滤波器参数位置

      if ( !isIntegerSlope( abs(intraPredAngle) ) )
      {
        if( isLuma(channelType) )
        {
          const bool useCubicFilter = !m_ipaParam.interpolationFlag; // 选择滤波器种类

          const TFilterCoeff        intraSmoothingFilter[4] = {TFilterCoeff(16 - (deltaFract >> 1)), TFilterCoeff(32 - (deltaFract >> 1)), TFilterCoeff(16 + (deltaFract >> 1)), TFilterCoeff(deltaFract >> 1)};
          const TFilterCoeff* const f                       = (useCubicFilter) ? InterpolationFilter::getChromaFilterTable(deltaFract) : intraSmoothingFilter;

          for (int x = 0; x < width; x++)
          {
            Pel p[4];

            p[0] = refMain[deltaInt + x];
            p[1] = refMain[deltaInt + x + 1];
            p[2] = refMain[deltaInt + x + 2];
            p[3] = refMain[deltaInt + x + 3];

            Pel val = (f[0] * p[0] + f[1] * p[1] + f[2] * p[2] + f[3] * p[3] + 32) >> 6;

            pDsty[x] = ClipPel(val, clpRng);   // always clip even though not always needed
          }
        }
        else
        {
          // Do linear filtering
          for (int x = 0; x < width; x++)
          {
            Pel p[2];

            p[0] = refMain[deltaInt + x + 1];
            p[1] = refMain[deltaInt + x + 2];

            pDsty[x] = p[0] + ((deltaFract * (p[1] - p[0]) + 16) >> 5);
          }
        }
      }
      else
      {
        // Just copy the integer samples
        for( int x = 0; x < width; x++ ) // 该模式下参考像素在xFilterReferenceSamples()函数中已经做过平滑滤波
        {
          pDsty[x] = refMain[x + deltaInt + 1];
        }
      }
    }
  }

  // Flip the block if this is the horizontal mode
  if( !bIsModeVer )
  {
    for( int y = 0; y < height; y++ )
    {
      for( int x = 0; x < width; x++ )
      {
        pDst.at( y, x ) = pDstBuf[x];
      }
      pDstBuf += dstStride;
    }
  }
}

VTM 代码基本按照上面的原理分析来的,其中一些重要的过程加入了注释,删去了其中的PDPC部分。VTM 中将水平和垂直预测做统一处理,对角度18和50这两个方向做了单独处理。

4.2 广角度预测

4.2.1 原理

传统的角度帧内预测方向定义为顺时针方向从 45 度到 -135 度。在 VVC 中,一些传统的角度帧内预测模式被自适应地替换为非方形块的广角帧内预测模式。替换的模式使用原始模式索引传输,在解析后重新映射到宽角模式的索引。帧内预测模式总数不变,即67,帧内模式编码方法不变。为了支持这些预测方向,定义了长度为 2W+1 的顶部参考样本和长度为 2H+1 的左侧参考样本,如图 所示。

广角方向模式中替换模式的数量取决于块的纵横比。替换后的帧内预测模式如表所示

如下图所示,在广角帧内预测的情况下,两个垂直相邻的预测样本可以使用两个不相邻的参考样本。因此,低通参考样本滤波和侧平滑应用于广角预测,以减少增加的间隙 \(Δp_α\)的负面影响。广角模式中有8种模式满足这个条件,分别是[-14, -12, -10, -6, 72, 76, 78, 80]。当通过这些模式预测块时,直接复制参考缓冲区中的样本而不应用任何插值。通过这种修改,减少了需要平滑的样本数量。此外,它对齐了传统预测模式和广角模式中的非分数模式的设计。

4.2.2 VTM 实现

首先在initPredIntraParams函数中调用了getModifiedWideAngle这个函数,这个函数根据块尺寸和预测角度将一般角度扩展为广角度。预测代码和一般角度预测代码一致。

int IntraPrediction::getModifiedWideAngle( int width, int height, int predMode )
{
  //The function returns a 'modified' wide angle index, given that it is not necessary
  //in this software implementation to reserve the values 0 and 1 for Planar and DC to generate the prediction signal.
  //It should only be used to obtain the intraPredAngle parameter.
  //To simply obtain the wide angle index, the function PU::getWideAngle should be used instead.
  if ( predMode > DC_IDX && predMode <= VDIA_IDX )
  {
    int modeShift[] = { 0, 6, 10, 12, 14, 15 };
    int deltaSize = abs(floorLog2(width) - floorLog2(height));
    if (width > height && predMode < 2 + modeShift[deltaSize])
    {
      predMode += (VDIA_IDX - 1);
    }
    else if (height > width && predMode > VDIA_IDX - modeShift[deltaSize])
    {
      predMode -= (VDIA_IDX - 1);
    }
  }
  return predMode;
}

参考资料

  1. Versatile Video Coding Editorial Refinements on Draft 10: Oct.2020[2]
  2. A. Filippov, V. Rufitskiy, and J. Chen, CE3-related: Alternative techniques for DC mode without division, document JVET-K0122 of JVET: Jul.2018.