在视频渐隐或者渐渐变亮的画面中,前后帧的关系仅仅是一钟比例变化,如果直接引用,那么压缩编码的效果并不理想,这种时候需要一个叫做Weight-P的特性来优化码流。
此feature只作用于P帧。
x264一般的帧间引用关系:
graph RL; p --> ref
优化之后的Weight-P引用关系:不再把ref直接当做P的引用帧,而是将ref的每一个像素分量先乘以w,再把结果当做P帧的引用帧。
graph RL; p --> ref[ref * w]
要实现这个特性,要解决两个问题:
- 在正式编码阶段,p帧会搜索前面一串旧的Frame,挨个挑选最优的引用帧,那么,到底哪些帧需要乘以w,哪些帧又不需要?
在x264编码中,仅仅把P紧邻的前一帧乘以系数w,其余帧并不会进行乘w处理。这其实很好理解,因为如果Weight-P能发挥作用,和它有决定性关联的就应该是紧邻的前一帧。 - 系数w是怎么算出来
比起w系数怎么得来的,或许“在哪个阶段被算出来”这个问题更重要:w的系数在lookahead阶段被算出来,它第一轮参考的数据是经过向下采样的lowres数据,得到w0,再经过第二轮更精确的计算(考虑运动预测的结果),得到w1。后面会详细解说它的计算过程。
系数w的函数定义:
\[Weighted_{Z}=k \times p_{z}+b\]
它是一个线性变换,把ref帧的每一个数据都作这个方程变换:
- k : 是系数间的AC斜率变化(代表两幅图像的差异度,也就是协方差)
- b : 代表两帧的DC差异(平均值的差异)。
系数w的计算详解:两轮计算
第一次粗略估算w0(也就是k0和b0):
在lookahead模型中,第一轮粗略估计w0的时候还没有进行运动预测,故而只用整个ref->lowres图像的AC值除以P->lowres的AC值,得到初步的k0,然后通过ref和P帧的图像均值算出b0。
\[\begin {align}&k0 = \sqrt \frac{AC(ref->lowres)}{AC(P->lowres)} \\ 根据线性方程公式,带入k0&可得到:\\ &b0 = AVG(P->lowres) - k0 \times AVG(ref->lowres) \end {align} \]
至此就算出来一个初步的k0和b0,不过x264代码中可能认为这样太粗糙了,于是加入了一个优化策略:
//利用如下数组进行优化,这个二维的数组分别代表k和b的的偏移量,通过外部参数i_subpel_refine等级来选择一组偏移范围,如果i_subpel_refine=9,那么调整k的搜索范围[k-2, k+2], 调整b的搜索范围[b-1, b+1],然后搜索遍历这个范围内的所有k和b--->计算微调之后的图像压缩率-->找出最优的k和b
static const uint8_t weight_check_distance[][2] =
{
{0,0},{0,0},{0,1},{0,1},
{0,1},{0,1},{0,1},{1,1},
{1,1},{2,1},{2,1},{4,2}
};
关于k和b的优化代码就这么多了,如果在x264代码中看到另外一串串的计算代码,其实那是为了调整k和b的数值范围,因为在网络传输的时候也要把k和b传过去,所以k和b的绝对值大小不能超过127,才有那么多的调整策略。
第二轮精细化估算出w1(k1和b1):
在搞清楚第二轮计算w1干了些什么之前,得先搞清楚第一轮算出来的w0都被x264拿去做什么了:
graph TB; A[第一轮w0] --> B[把ref->lowres中的数据全部先乘以w0得到: W_lowres]--> C[把P->lowres中的数据以8x8拆成MB, 参考W_lowres作运动预测] --> D[得到运动预测的结果MVS] --> E[用运动预测结果MVS, 重建ref->lowres, 得到Z_MVS_lowres数据] -->F[P最后用Z_MVS_lowres作参考, 再次优化一遍, 得到k1和b1]
P最后将会用Z_MVS_lowres当做参考帧,把w0中得到的k0和b0再一次进行偏移优化(不会再利用AC和DC计算出新的k和b,仅仅在k0和b0的基础上再进行一次偏移优化)此时评估图像压缩率的时候用的不是ref->lowres而是Z_MVS_lowres:
//同样利用如下数组进行优化,这个二维的数组分别代表k和b的的偏移量,通过外部参数i_subpel_refine等级来选择一组偏移范围,如果i_subpel_refine=9,那么调整k的搜索范围[k-2, k+2], 调整b的搜索范围[b-1, b+1],然后搜索遍历这个范围内的所有k和b--->计算微调之后的图像压缩率-->找出最优的k和b
static const uint8_t weight_check_distance[][2] =
{
{0,0},{0,0},{0,1},{0,1},
{0,1},{0,1},{0,1},{1,1},
{1,1},{2,1},{2,1},{4,2}
};
所以w1仅仅是在w0上,又进行了一次偏移优化。
以上是lookahead阶段的责任,计算出正确的系数w。(w保存在P->weight中,所以是P自己持有w,当处理P帧的时候,P帧就理所应当的把前一个引用帧先乘以w,再把结果当做引用帧。)
正式编码阶段,Weight-P的设计逻辑
因为在lookahead阶段已经计算出了w,所以正式编码阶段只要去用w就行了。而Weight-P主要添加的代码是在x264_reference_build_list函数中,如果当前帧编码帧是P帧,那么在编码阶段,会调用x264_mb_analyse_init函数把wieghted-ref帧数据算出来,然后就可以进行运动预测的计算了。
不谈具体的x264线程模型,一张简单的逻辑图如下:
在处理Weight-P的时候,为了精度考虑,需要在引用列表中插入一个复制帧r0', 但在BIT_DEPTH>8的时候(这时候数据本身的精度比较高),那么是不需要插入r0'的。
同时为了保持Weight-P的正确性,也要插入r0'',让P既可以引用到Weight-P的目标帧r0 * w, 同时也能参考到r0的原始的数据。