在VVC的扩展merge模式当中,当前CU生成的merge list中选择一个率失真代价值最小的候选项直接作为自己的运动信息,而MMVD是将候选项的运动信息作为自己MV的预测值,最终编码对象为实际获得的MV与该预测值之间的差值(MVD)
MMVD技术起源于之前提案中的ultimate motion vector expression(UMVE)技术,该技术是一种新的运动向量表示方法,在skip和merge模式当中使用起始点、运动步长、运动方向三个量来表示运动向量
MMVD技术应用与帧间预测的Skip和Merge模式当中,是一种运动矢量的特殊表达形式,在VTM中,MMVD技术流程大致如下所示:
具体实现步骤如下:
(1)、首先利用VVC中普通merge候选列表构建的过程,得到当前CU的merge list,检查邻块的顺序如下图所示
(2)、对于第一步得到的merge list中的前两个候选,把它们作为初始的MV,以该候选MV在参考图像中所指向的位置作为起始点,在上下左右四个方向上,进行8中步长的搜索,如下图所示
步长索引表:
方向索引表:
这样每一个初始MV在每个方向上的每种步长都会形成一个新的MV,即细化过的MV,该MV包含三个信息,分别是起始点、搜索方向、搜索步长,因此一个初始的MV可以扩展出32个新的MV,这些新的MV也会通过一次运动补偿得到当前CU的预测值。
(3)、在Skip和merge模式下,将所有得到的64个新的预测值之间进行率失真代价比较,最终选择出最优的一种组合作为MMVD的最优的merge候选,我们需要存储该MV的三个信息:分别是其初始MV在merge list中的索引值、移动的方向和搜索步长三个语法元素
1.在merge list中选择前两个运动向量作为MMVD的初始向量代码示例
static const int MMVD_BASE_MV_NUM = 2; //一个全局静态常量,基于MMVD初始向量的个数
//从merge list当中选取两个初始MV
void PU::getInterMMVDMergeCandidates(const PredictionUnit &pu, MergeCtx& mrgCtx, const int& mrgCandIdx)
{
int refIdxList0, refIdxList1;
int k;
int currBaseNum = 0;
const uint16_t maxNumMergeCand = mrgCtx.numValidMergeCand;
for (k = 0; k < maxNumMergeCand; k++)
{
//只取MRG_TYPE_DEFAULT_N类型候选项
if (mrgCtx.mrgTypeNeighbours[k] == MRG_TYPE_DEFAULT_N)
{
refIdxList0 = mrgCtx.mvFieldNeighbours[(k << 1)].refIdx;
refIdxList1 = mrgCtx.mvFieldNeighbours[(k << 1) + 1].refIdx;
if ((refIdxList0 >= 0) && (refIdxList1 >= 0))
{
mrgCtx.mmvdBaseMv[currBaseNum][0] = mrgCtx.mvFieldNeighbours[(k << 1)];
mrgCtx.mmvdBaseMv[currBaseNum][1] = mrgCtx.mvFieldNeighbours[(k << 1) + 1];
}
else if (refIdxList0 >= 0)
{
mrgCtx.mmvdBaseMv[currBaseNum][0] = mrgCtx.mvFieldNeighbours[(k << 1)];
mrgCtx.mmvdBaseMv[currBaseNum][1] = MvField(Mv(0, 0), -1);
}
else if (refIdxList1 >= 0)
{
mrgCtx.mmvdBaseMv[currBaseNum][0] = MvField(Mv(0, 0), -1);
mrgCtx.mmvdBaseMv[currBaseNum][1] = mrgCtx.mvFieldNeighbours[(k << 1) + 1];
}
mrgCtx.mmvdUseAltHpelIf[currBaseNum] = mrgCtx.useAltHpelIf[k];
currBaseNum++;
//只取两个base MV
if (currBaseNum == MMVD_BASE_MV_NUM)
break;
}
}
}
2.对选择的初始运动矢量进行步长和方向的扩展
//设置MMVD的候选信息,candIdx就是32中组合中的一种模式的索引
void MergeCtx::setMmvdMergeCandiInfo(PredictionUnit& pu, int candIdx)
{
const Slice &slice = *pu.cs->slice;
const int mvShift = MV_FRACTIONAL_BITS_DIFF;
//参考MVD的候选,2,8,16,32,64,128,256
//这里实际就是8中补偿的偏置量
const int refMvdCands[8] = { 1 << mvShift , 2 << mvShift , 4 << mvShift , 8 << mvShift , 16 << mvShift , 32 << mvShift, 64 << mvShift , 128 << mvShift };
int fPosGroup = 0;
int fPosBaseIdx = 0; //起始MV的索引
int fPosStep = 0; //搜索步长
int tempIdx = 0;
int fPosPosition = 0; //搜索的方向
Mv tempMv[2]; //里面存储前向和后向MV
tempIdx = candIdx; //组合新的MV索引
fPosGroup = tempIdx / (MMVD_BASE_MV_NUM * MMVD_MAX_REFINE_NUM); //整个候选列表的初始位置为0
tempIdx = tempIdx - fPosGroup * (MMVD_BASE_MV_NUM * MMVD_MAX_REFINE_NUM); //传进来扩展MV的索引
fPosBaseIdx = tempIdx / MMVD_MAX_REFINE_NUM;//第一个初始向量的索引0或者第二个初始向量的索引1
tempIdx = tempIdx - fPosBaseIdx * (MMVD_MAX_REFINE_NUM);//0-31(各自初始向量的32中扩展MV)
fPosStep = tempIdx / 4; //每个初始MV对应对应的8种搜索步长的索引
fPosPosition = tempIdx - fPosStep * (4); //该值总为0-3,对应四个搜索方向
int offset = refMvdCands[fPosStep]; //8中步长对应的偏置
if ( pu.cu->slice->getPicHeader()->getDisFracMMVD() )
{
offset <<= 2;
}
const int refList0 = mmvdBaseMv[fPosBaseIdx][0].refIdx; //每个初始MV的前向参考列表
const int refList1 = mmvdBaseMv[fPosBaseIdx][1].refIdx; //每个初始MV的候选项参考列表
if ((refList0 != -1) && (refList1 != -1)) //双向参考列表存在
{
const int poc0 = slice.getRefPOC(REF_PIC_LIST_0, refList0); //前向参考帧的POC
const int poc1 = slice.getRefPOC(REF_PIC_LIST_1, refList1); //后向参考帧的POC
const int currPoc = slice.getPOC(); //当前参考帧的POC
//每个MV的结构体里包含器搜索方向、以及搜索步长、然后赋值给tempMv数组
if (fPosPosition == 0)
{
tempMv[0] = Mv(offset, 0); //右
}
else if (fPosPosition == 1)
{
tempMv[0] = Mv(-offset, 0); //左
}
else if (fPosPosition == 2)
{
tempMv[0] = Mv(0, offset); //上
}
else
{
tempMv[0] = Mv(0, -offset); //下
}
//前后参考帧属于同一帧
if ((poc0 - currPoc) == (poc1 - currPoc))
{
tempMv[1] = tempMv[0]; //双向的MVD是一样的
}
else if (abs(poc1 - currPoc) > abs(poc0 - currPoc)) //后向参考帧的POC大于前向参考帧的POC
{
const int scale = PU::getDistScaleFactor(currPoc, poc0, currPoc, poc1);
tempMv[1] = tempMv[0];
const bool isL0RefLongTerm = slice.getRefPic(REF_PIC_LIST_0, refList0)->longTerm;
const bool isL1RefLongTerm = slice.getRefPic(REF_PIC_LIST_1, refList1)->longTerm;
if (isL0RefLongTerm || isL1RefLongTerm)//有其中一个是长期参考帧
{
if ((poc1 - currPoc)*(poc0 - currPoc) > 0)
{
tempMv[0] = tempMv[1];
}
else
{
tempMv[0].set(-1 * tempMv[1].getHor(), -1 * tempMv[1].getVer());
}
}
else //都不是长期参考帧
tempMv[0] = tempMv[1].scaleMv(scale);
}
else
{
const int scale = PU::getDistScaleFactor(currPoc, poc1, currPoc, poc0);
const bool isL0RefLongTerm = slice.getRefPic(REF_PIC_LIST_0, refList0)->longTerm;
const bool isL1RefLongTerm = slice.getRefPic(REF_PIC_LIST_1, refList1)->longTerm;
if (isL0RefLongTerm || isL1RefLongTerm)
{
if ((poc1 - currPoc)*(poc0 - currPoc) > 0)//若前后参考帧都来自当前帧的时域的同一侧,后向MMVDoffset与前向相等
{
tempMv[1] = tempMv[0];
}
else //若前后参考帧来自不同侧,前向和后向的MMVDoffset相对称
{
tempMv[1].set(-1 * tempMv[0].getHor(), -1 * tempMv[0].getVer());
}
}
else
tempMv[1] = tempMv[0].scaleMv(scale);
}
pu.interDir = 3; //双向列表的DIr设置为3
//这里为每个选中的MV选择起始MV然后加上然后加上步长和方向
pu.mv[REF_PIC_LIST_0] = mmvdBaseMv[fPosBaseIdx][0].mv + tempMv[0]; //MMVD的前向MV
pu.refIdx[REF_PIC_LIST_0] = refList0;
pu.mv[REF_PIC_LIST_1] = mmvdBaseMv[fPosBaseIdx][1].mv + tempMv[1]; //MMVD的后向MV
pu.refIdx[REF_PIC_LIST_1] = refList1;
}
else if (refList0 != -1) //如果只有前向列表存在
{
if (fPosPosition == 0) //右
{
tempMv[0] = Mv(offset, 0);
}
else if (fPosPosition == 1) //左
{
tempMv[0] = Mv(-offset, 0);
}
else if (fPosPosition == 2) //上
{
tempMv[0] = Mv(0, offset);
}
else //下
{
tempMv[0] = Mv(0, -offset);
}
pu.interDir = 1; //前向列表的DIr设置为1
pu.mv[REF_PIC_LIST_0] = mmvdBaseMv[fPosBaseIdx][0].mv + tempMv[0];
pu.refIdx[REF_PIC_LIST_0] = refList0;
pu.mv[REF_PIC_LIST_1] = Mv(0, 0);
pu.refIdx[REF_PIC_LIST_1] = -1;
}
else if (refList1 != -1)
{
if (fPosPosition == 0)
{
tempMv[1] = Mv(offset, 0);
}
else if (fPosPosition == 1)
{
tempMv[1] = Mv(-offset, 0);
}
else if (fPosPosition == 2)
{
tempMv[1] = Mv(0, offset);
}
else
{
tempMv[1] = Mv(0, -offset);
}
pu.interDir = 2; //后向类表的DIr设置为2
pu.mv[REF_PIC_LIST_0] = Mv(0, 0);
pu.refIdx[REF_PIC_LIST_0] = -1;
pu.mv[REF_PIC_LIST_1] = mmvdBaseMv[fPosBaseIdx][1].mv + tempMv[1];
pu.refIdx[REF_PIC_LIST_1] = refList1;
}
pu.mmvdMergeFlag = true;
pu.mmvdMergeIdx = candIdx;
pu.mergeFlag = true;
pu.regularMergeFlag = true;
pu.mergeIdx = candIdx;
pu.mergeType = MRG_TYPE_DEFAULT_N;
pu.mvd[REF_PIC_LIST_0] = Mv();
pu.mvd[REF_PIC_LIST_1] = Mv();
pu.mvpIdx[REF_PIC_LIST_0] = NOT_VALID;
pu.mvpIdx[REF_PIC_LIST_1] = NOT_VALID;
pu.mvpNum[REF_PIC_LIST_0] = NOT_VALID;
pu.mvpNum[REF_PIC_LIST_1] = NOT_VALID;
pu.cu->imv = mmvdUseAltHpelIf[fPosBaseIdx] ? IMV_HPEL : 0; //MV使用半像素精度
pu.cu->BcwIdx = (interDirNeighbours[fPosBaseIdx] == 3) ? BcwIdx[fPosBaseIdx] : BCW_DEFAULT; //双向参考索引
for (int refList = 0; refList < 2; refList++)
{
if (pu.refIdx[refList] >= 0)
{
pu.mv[refList].clipToStorageBitDepth();
}
}
PU::restrictBiPredMergeCandsOne(pu);
}
3.MMVD技术的实现代码
if ( pu.cs->sps->getUseMMVD() )//使用MMVD模式,这里选出最优的MMVD的Merge候选(初始MV+搜索方向+搜索步长)
{
cu.mmvdSkip = true;
#if JVET_O0249_MERGE_SYNTAX
pu.regularMergeFlag = true;//普通Merge列表依旧可用,因为初始的MV还是要从普通Merge中获取
#endif
const int tempNum = (mergeCtx.numValidMergeCand > 1) ? MMVD_ADD_NUM : MMVD_ADD_NUM >> 1;
//对MMVD候选循环遍历
for (int mmvdMergeCand = 0; mmvdMergeCand < tempNum; mmvdMergeCand++)//循环64遍,前32种得到起始点1以及对应的8个步长
//因为对于每种MV扩展都可以得到4*8种组合,因此两个起始点总共是64种组合
{
int baseIdx = mmvdMergeCand / MMVD_MAX_REFINE_NUM;//候选基MV(即初始MV)索引,要么0 要么1,(这里就是两个初始MV的起点,每个初始MV有32种4*8的步长+方向的组合)
int refineStep = (mmvdMergeCand - (baseIdx * MMVD_MAX_REFINE_NUM)) / 4;//表示8种步长
if (refineStep >= m_pcEncCfg->getMmvdDisNum())
continue;
//设置MMVD候选的信息,得到每个扩展MV具体的搜索初始点以及每个方向以及对应的步长
mergeCtx.setMmvdMergeCandiInfo(pu, mmvdMergeCand);
PU::spanMotionInfo(pu, mergeCtx);
pu.mvRefine = true;
distParam.cur = singleMergeTempBuffer->Y();
pu.mmvdEncOptMode = (refineStep > 2 ? 2 : 1);
CHECK(!pu.mmvdMergeFlag, "MMVD merge should be set");
// Don't do chroma MC here
//MMVD的Merge候选运动补偿计算预测值
m_pcInterSearch->motionCompensation(pu, *singleMergeTempBuffer, REF_PIC_LIST_X, true, false);
pu.mmvdEncOptMode = 0;
pu.mvRefine = false;
Distortion uiSad = distParam.distFunc(distParam);//失真函数
m_CABACEstimator->getCtx() = ctxStart;
uint64_t fracBits = m_pcInterSearch->xCalcPuMeBits(pu);//计算码率
double cost = (double)uiSad + (double)fracBits * sqrtLambdaForFirstPassIntra; //计算RDcost
insertPos = -1;
//更新候选列表
updateCandList(ModeInfo(mmvdMergeCand, false, true, false), 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]);//将代价更小的那个MV插入到Merge列表中
}
}
}
更多关于视频编码知识和资源的分享,更精致的文章排版,欢迎关注博主微信公众号,一起交流、学习、进步!!!