解码端运动向量修正(Decoder side motion vector refinement ,DMVR)是为了提高merge模式下双向预测MV的准确性而提出的技术。双向预测是在list0和list1中分别找一个运动向量MV0和MV1,然后将MV0和MV1所指向的预测块进行加权得到最终的预测块。而DMVR不是直接使用MV0和MV1,而是在MV0和MV1附近搜索更准确的MV0‘和MV1’。
如上图所示,在初始MV附件搜索MV‘,计算MV’指向的块(红色块)间的SAD,选择SAD值最小的MV‘作为最终修正的MV。
在VTM5中满足以下条件的CU才可以使用DMVR模式:
-
CU使用merge模式,且是双向预测。
-
两个参考帧分别位于当前帧之前和之后。
-
即两个参考帧离当前帧一样远(即两个参考帧的POC与当前帧POC的差值相等)。
-
CU至少有64个亮度像素。
-
CU的宽和高都大于等于8。
-
BCW使用相等权值。
-
当前块不使用WP模式。
DMVR生成的修正MV用于生成帧间预测值和后续图像编码的时域运动向量预测值(TMVP)。原始MV用于去方块效应过程和后续CU编码的空域运动向量预测值(SMVP)。
VTM5中DMVR的具体过程如下:
搜索
DMVR中要搜索初始MV周围的点以找到最优的修正MV。待搜索的MV满足以下方程:
MV_offset表示修正MV相对初始MV的偏移量。VTM5规定搜索范围是初始MV附近2个整数亮度像素点内,搜索过程包括两个阶段,第一阶段搜索整像素第二阶段搜索分像素。
整像素搜索:
由于搜索范围是初始MV附近2个整数亮度像素点,所以一共有25个整像素点。
Mv m_pSearchOffset[25] = { Mv(-2,-2), Mv(-1,-2), Mv(0,-2), Mv(1,-2), Mv(2,-2),Mv(-2,-1), Mv(-1,-1), Mv(0,-1), Mv(1,-1), Mv(2,-1),Mv(-2, 0), Mv(-1, 0), Mv(0, 0), Mv(1, 0), Mv(2, 0),Mv(-2, 1), Mv(-1, 1), Mv(0, 1), Mv(1, 1), Mv(2, 1),Mv(-2, 2), Mv(-1, 2), Mv(0, 2), Mv(1, 2), Mv(2, 2) };uint64_t m_SADsArray[((2 * DMVR_NUM_ITERATION) + 1) * ((2 * DMVR_NUM_ITERATION) + 1)];
首先计算初始MV0和MV1对应的块的SAD,如果该SAD小于阈值则终止整像素搜索过程。否则按扫描顺序计算剩下24个点的SAD,选择SAD最小的作为整数像素搜索阶段的输出。
分像素搜索:
为了减少计算复杂性,进行分像素搜索时用参数误差曲面方程(parametric error surface equation)代替SAD。只有当第一轮或第二轮整像素搜索阶段中心像素SAD最小时,才需要进行分像素搜索。
在计算分像素参数误差曲面时,需要使用中心位置和其周围四个位置的cost来拟合一共二维抛物线误差曲面方程:
通过使用5个点的cost值可以求解上面的方程:
x_min和y_min的值会被自动限制在-8~8间,因为所有的cost都为正且最小的cost是E(0,0)。计算得到的分像素(x_min,y_min)加上整像素MV得到最终修正MV。
双线性插值和像素填充
VVC中MV是1/16亮度像素精度。这些分像素是通过8抽头插值滤波器生成的。在DMVR中,为了减少计算复杂性在分像素搜索阶段使用双线性插值滤波器生成分像素。另外,使用双线性插值只需要访问周围2像素范围,而不用像普通运动补偿一样要参考更多像素。当获得修正MV后,在生成最终预测值时需要使用8抽头插值滤波器。
DMVR最大能处理16x16的块,如果块的宽和/或高大于16,需要将其分为宽和/或高等于16的子块。
VTM5中相关代码如下:
for (int y = puPos.y; y < (puPos.y + pu.lumaSize().height); y = y + dy, yStart = yStart + dy){for (int x = puPos.x, xStart = 0; x < (puPos.x + pu.lumaSize().width); x = x + dx, xStart = xStart + dx){uint64_t minCost = MAX_UINT64;bool notZeroCost = true;int16_t totalDeltaMV[2] = { 0,0 };int16_t deltaMV[2] = { 0, 0 };uint64_t *pSADsArray;for (int i = 0; i < (((2 * DMVR_NUM_ITERATION) + 1) * ((2 * DMVR_NUM_ITERATION) + 1)); i++){//!<25个整像素点SAD初始化m_SADsArray[i] = MAX_UINT64;}pSADsArray = &m_SADsArray[(((2 * DMVR_NUM_ITERATION) + 1) * ((2 * DMVR_NUM_ITERATION) + 1)) >> 1];Pel *addrL0Centre = biLinearPredL0 + yStart * m_biLinearBufStride + xStart;Pel *addrL1Centre = biLinearPredL1 + yStart * m_biLinearBufStride + xStart;for (int i = 0; i < iterationCount; i++){deltaMV[0] = 0;deltaMV[1] = 0;Pel *addrL0 = addrL0Centre + totalDeltaMV[0] + (totalDeltaMV[1] * m_biLinearBufStride);Pel *addrL1 = addrL1Centre - totalDeltaMV[0] - (totalDeltaMV[1] * m_biLinearBufStride);if (i == 0){//!<计算初始MV对应的costminCost = xDMVRCost(clpRngs.comp[COMPONENT_Y].bd, addrL0, m_biLinearBufStride, addrL1, m_biLinearBufStride, dx, dy);if (minCost < ((4 * dx * (dy >> 1/*for alternate line*/)))){//!<判断是否小于阈值notZeroCost = false;break;}pSADsArray[0] = minCost;}if (!minCost){notZeroCost = false;break;}//!<计算25个整像素点对应的costxBIPMVRefine(bd, addrL0, addrL1, minCost, deltaMV, pSADsArray, dx, dy);if (deltaMV[0] == 0 && deltaMV[1] == 0){break;}totalDeltaMV[0] += deltaMV[0];totalDeltaMV[1] += deltaMV[1];pSADsArray += ((deltaMV[1] * (((2 * DMVR_NUM_ITERATION) + 1))) + deltaMV[0]);}totalDeltaMV[0] = (totalDeltaMV[0] << mvShift);totalDeltaMV[1] = (totalDeltaMV[1] << mvShift);//!<计算分像素代价xDMVRSubPixelErrorSurface(notZeroCost, totalDeltaMV, deltaMV, pSADsArray);pu.mvdL0SubPu[num] = Mv(totalDeltaMV[0], totalDeltaMV[1]);num++;}}
//计算整像素点cost
void InterPrediction::xBIPMVRefine(int bd, Pel *pRefL0, Pel *pRefL1, uint64_t& minCost, int16_t *deltaMV, uint64_t *pSADsArray, int width, int height)
{const int32_t refStrideL0 = m_biLinearBufStride;const int32_t refStrideL1 = m_biLinearBufStride;Pel *pRefL0Orig = pRefL0;Pel *pRefL1Orig = pRefL1;for (int nIdx = 0; (nIdx < 25); ++nIdx){//!<计算25个整像素点对应的cost,找出代价最小的int32_t sadOffset = ((m_pSearchOffset[nIdx].getVer() * ((2 * DMVR_NUM_ITERATION) + 1)) + m_pSearchOffset[nIdx].getHor());pRefL0 = pRefL0Orig + m_pSearchOffset[nIdx].hor + (m_pSearchOffset[nIdx].ver * refStrideL0);pRefL1 = pRefL1Orig - m_pSearchOffset[nIdx].hor - (m_pSearchOffset[nIdx].ver * refStrideL1);if (*(pSADsArray + sadOffset) == MAX_UINT64){const uint64_t cost = xDMVRCost(bd, pRefL0, refStrideL0, pRefL1, refStrideL1, width, height);*(pSADsArray + sadOffset) = cost;}if (*(pSADsArray + sadOffset) < minCost){minCost = *(pSADsArray + sadOffset);deltaMV[0] = m_pSearchOffset[nIdx].getHor();deltaMV[1] = m_pSearchOffset[nIdx].getVer();}}
}
//计算分像素点cost
void xSubPelErrorSrfc(uint64_t *sadBuffer, int32_t *deltaMv)
{int64_t numerator, denominator;int32_t mvDeltaSubPel;int32_t mvSubPelLvl = 4;/*1: half pel, 2: Qpel, 3:1/8, 4: 1/16*///!<计算x_min /*horizontal*/numerator = (int64_t)((sadBuffer[1] - sadBuffer[3]) << mvSubPelLvl);denominator = (int64_t)((sadBuffer[1] + sadBuffer[3] - (sadBuffer[0] << 1)));if (0 != denominator){if ((sadBuffer[1] != sadBuffer[0]) && (sadBuffer[3] != sadBuffer[0])){mvDeltaSubPel = div_for_maxq7(numerator, denominator);deltaMv[0] = (mvDeltaSubPel);}else{if (sadBuffer[1] == sadBuffer[0]){deltaMv[0] = -8;// half pel}else{deltaMv[0] = 8;// half pel}}}/*vertical*/ //!<计算y_min numerator = (int64_t)((sadBuffer[2] - sadBuffer[4]) << mvSubPelLvl);denominator = (int64_t)((sadBuffer[2] + sadBuffer[4] - (sadBuffer[0] << 1)));if (0 != denominator){if ((sadBuffer[2] != sadBuffer[0]) && (sadBuffer[4] != sadBuffer[0])){mvDeltaSubPel = div_for_maxq7(numerator, denominator);deltaMv[1] = (mvDeltaSubPel);}else{if (sadBuffer[2] == sadBuffer[0]){deltaMv[1] = -8;// half pel}else{deltaMv[1] = 8;// half pel}}}return;
}
感兴趣的请关注微信公众号Video Coding