今天介绍一下帧间预测技术中的MMVD技术(Merge mode with MVD),也称带有运动矢量差的融合技术,MMVD也属于基于Merge的技术中的一种,在解码端的语法元素中也属于Merge分支。不过我觉着按严格来讲,MMVD并不属于Merge系列,而更像是inter系列,因为MMVD是要传送MVD到解码端,就这一点来看,MMVD就已经不在Merge范围之内了。下面我来讲解一下MMVD的技术细节吧。

MMVD技术应用于帧间预测的Skip和Merge模式中,是一种运动矢量的特殊表达形式。在目前最新的VVC版本——VTM6.0中,MMVD技术的流程大致如下:
(1)首先重复利用普通Merge模式的Merge候选,在这些候选中选择位与列表的前两个候选作为初始的运动矢量基。
(2)对该初始MV进行扩展,主要在运动幅度和运动方向上进行扩展,来得到最终的扩展后的运动矢量,以形成新的运动矢量。

具体的详细步骤如下:
(1)首先利用VVC中普通Merge候选列表构建的过程,得到当前CU的候选列表,检查邻近块的顺序如图所示:
需要注意的是,候选列表的长度为6,通过上述顺序的方法检查块的运动信息是否有效。如果有效,将该块的运动信息添加到候选列表中去;如果没有填满,则在候选列表中填充0矢量。

(2)然后,对于列表中前两个候选,最为初始的MV,以该候选MV在参考图像(分前向或者后向)中所指向的位置为起点,在上下左右四个方向上。进行8种步长的搜索,如下图所示:

其中步长和方向的索引具体如下表所示:
步长索引:

方向索引:

那么每个初始MV在每个方向上的每种步长都会形成一个新的MV,也即是细化过的MV(该扩展MV中包括三个信息:起始点、搜索方向、搜索步长),因此一个初始MV可以扩展出32个新的MV,两个初始MV总共可以扩展形成总共64个新MV,这些新的MV也会通过一次运动补偿得到当前CU的预测值。

  1. 在Merge情况下,将所有得到的64个新的预测值之间进行率失真代价的比较,最终选出最优的一种组合作为MMVD的最优的Merge候选。我们需要储存该MV的三个信息:①其初始MV在Merge列表中的索引;②移动的方向;③搜索的步长这三个语法元素。
  2. 在Skip的情况下同Merge情况一样。

那么MMVD在代码中具体是怎么实现的呢?

首先要对扩展的64种MV进行循环遍历,进行率失真代价比较,具体的代码段如下:我有详细的注释,以及起始点、搜索方向、搜索步长是如何表示的,我都有详细的注释

 if ( pu.cs->sps->getUseMMVD() )//使用MMVD模式,这里选出最优的MMVD的Merge候选(初始MV+搜索方向+搜索步长){cu.mmvdSkip = true;
#if JVET_O0249_MERGE_SYNTAXpu.regularMergeFlag = true;//普通Merge列表依旧可用,因为初始的MV还是要从普通Merge中获取
#endifconst 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; //计算RDcostinsertPos = -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列表中}}}

当然,这里关于MMVD新的扩展MV的生成有一个专门的函数,setMmvdMergeCandiInfo()函数中设置MMVD候选的信息,得到每个扩展MV具体的搜索初始点以及每个方向以及对应的步长。该函数代码具体如下,想要看的细一点的同学可以研究一下我加的注释

//设置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,4,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和后向MVtempIdx = candIdx;//组合新的MV索引fPosGroup = tempIdx / (MMVD_BASE_MV_NUM * MMVD_MAX_REFINE_NUM);//整个候选列表的初始位置0tempIdx = tempIdx - fPosGroup * (MMVD_BASE_MV_NUM * MMVD_MAX_REFINE_NUM); //0-63,传进来的扩展MV的索引fPosBaseIdx = tempIdx / MMVD_MAX_REFINE_NUM;//第一个初始向量的索引0或者第二个初始向量的索引1tempIdx = 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->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); //前向参考帧的POCconst int poc1 = slice.getRefPOC(REF_PIC_LIST_1, refList1);//后向参考帧的POCconst 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)//若前后向参考帧都来自当前帧的时域的同一侧,后向MMVDoffet与前向的相等{tempMv[1] = tempMv[0];}else//若不来自同一侧,前向和后向的MMVDOffset相对称{tempMv[1].set(-1 * tempMv[0].getHor(), -1 * tempMv[0].getVer());}}elsetempMv[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的前向MVpu.refIdx[REF_PIC_LIST_0] = refList0;pu.mv[REF_PIC_LIST_1] = mmvdBaseMv[fPosBaseIdx][1].mv + tempMv[1];//MMVD的后向MVpu.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设置为1pu.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设置为2pu.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;
#if JVET_O0249_MERGE_SYNTAXpu.regularMergeFlag = true;
#elsepu.regularMergeFlag = false;
#endifpu.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;
#if JVET_O0057_ALTHPELIFpu.cu->imv = mmvdUseAltHpelIf[fPosBaseIdx] ? IMV_HPEL : 0;//MV使用半像素精度
#endifpu.cu->GBiIdx = (interDirNeighbours[fPosBaseIdx] == 3) ? GBiIdx[fPosBaseIdx] : GBI_DEFAULT;//双向参考索引for (int refList = 0; refList < 2; refList++){if (pu.refIdx[refList] >= 0){pu.mv[refList].clipToStorageBitDepth();}}PU::restrictBiPredMergeCandsOne(pu);
}

最后,关于MMVD熵编码的部分我在后续打算专门写一篇博客讲一下,顺便将所有Merge的熵编码部分都放在一起讲,比较统一也方便大家理解~

H.266/VVC相关技术学习笔记20:帧间预测技术中的MMVD技术(Merge mode with MVD)相关推荐

  1. 【十六】 H.266/VVC | VVC中帧间预测技术详细总结 | 所有帧间预测技术代码汇总

    前言 ​ 帧间预测是影响视频编码性能的关键环节之一,H.266/VVC帧间预测在传统只能应对简单的平移运动的基础上,采用了仿射运动模型,可以描述更加复杂的缩放.旋转等运动.为了更好的发挥合并模式(Me ...

  2. H.266/VVC相关技术学习笔记4:HEVC和VVC中块划分的差别

    关于H.265/HEVC和H.266/VVC中的块划分的区别: 一.HEVC中首先需要将一个图像固定划分为多个CTU. ① CTU的尺寸固定划分为64×64,一个CTU由一个亮度CTB和两个色度CTB ...

  3. VVC帧间预测(一)扩展的merge模式

    VVC在HEVC的基础上增加了很多新的帧间预测工具,在VTM5里新增的帧间预测工具如下: Extended merge prediction Merge mode with MVD (MMVD) AM ...

  4. H.266/VVC相关技术学习笔记16:VTM6.0中的CIIP技术(帧内帧间联合预测)

    今天讲一下目前VTM6.0版本中的CIIP技术,CIIP即为帧内帧间联合预测技术,这属于Merge系列的一个分支. 该技术需要先计算当前预测块的帧内预测值,即用Planar.DC.角度预测等传统的帧内 ...

  5. H.264学习笔记2——帧内预测

    帧内预测:根据经过反量化和反变换(没有进行去块效应)之后的同一条带内的块进行预测. A.4x4亮度块预测: 用到的像素和预测方向如图: a~f是4x4块中要预测的像素值,A~Q是临块中解码后的参考值. ...

  6. H.266/VVC相关技术学习笔记18:帧间预测中的AMVR技术(自适应运动适量精度)

    AMVR技术也称为自适应运动适量精度技术,就是在以前的HEVC中,MVD的精度只有一个默认的1/4像素精度,但是由于要适应不同分辨率的图像,仅仅使用一个精度去表示MVD是远远不够的,因此在VTM6.0 ...

  7. H.266/VVC相关技术学习笔记21:帧间预测中五种Merge模式的熵编码方式

    今天主要详细讲一下帧间预测中五种Merge模式的熵编码方式,以及对应的VTM的代码中的编码方式的实现.现阶段VTM6.0中Merge模式大致上分为五种,分别是Subblock_Merge.MMVD_M ...

  8. python库学习笔记——分组计算利器:pandas中的groupby技术

    最近处理数据需要分组计算,又用到了groupby函数,温故而知新. 分组运算的第一阶段,pandas 对象(无论是 Series.DataFrame 还是其他的)中的数据会根据你所提供的一个或多个键被 ...

  9. 【Codecs系列】H.266/VVC视频编码标准 技术系列汇总

    DATE: 2020.9.30 文章目录 1.H.266/VCC标准专栏 2.H.266/VCC视频编码标准 技术汇总 1.H.266/VCC标准专栏       视音频技术之H.266/VVC 2. ...

最新文章

  1. 5300亿参数的「威震天-图灵」,微软、英伟达合力造出超大语言模型
  2. Myeclipse加载php插件
  3. 快手公司厕所装坑位计时器,网友:再也不能带薪拉屎了!
  4. win10系统更新在哪_一键关闭win10系统更新,一款不错的小工具
  5. mysql 多个实例 备份_数据库Mysql备份多个数据库代码实例
  6. pytorch中的卷积操作详解
  7. h5首页加载慢_Webview加载H5优化小记
  8. (12)数据结构-二叉树基本操作
  9. PACKAGE-INFO.JAVA 作用及用法详解
  10. android基础--PreferenceActivity
  11. 设有单总线结构计算机的cpu数据通路,中央处理器——数据通路之单总线结构
  12. 识别图片验证码内容 -- ddddocr识别
  13. 模拟器xposed框架安装7.1-64教程
  14. 上海野生动物园一日游
  15. SCI检索号识别一法:UT-WOS与UT-ISI
  16. 搭建oa服务器后怎么接入到微信,微信oa是什么?微信如何与OA进行结合?
  17. 同学早已年薪百万 为何你却还在朋友圈集赞?
  18. mysql下出现Unknown column ‘id‘ in ‘on clause‘的解决方法
  19. 推荐一些免费的网盘给你
  20. 使用pre-signed URLs通过浏览器上传 无中间件前端直传 minio

热门文章

  1. WEB漏洞扫描器 – 北极熊扫描器
  2. 与你有关!高铁站采用的室内定位技术让你更有安全感
  3. bancor算法的全面讲解
  4. 日本电子业民族情结发酵 日企抱团欲购瑞萨
  5. android简单用户调查问卷,用户调查问卷结果及分析
  6. 实体店如何做社群裂变?社群裂变软件如何拉人进群?
  7. 好家伙!Java程序员开发了一套系统,卖了800万,成都买了6套房!属实是走上人生巅峰了!
  8. 钛白粉上市公司有哪些?钛白粉十大品牌排行榜
  9. vue forEach和map的区别
  10. 42、传输层习题及参考答案