HEVC学习(八) —— 以SAO为例浅析跟踪代码方法

HM的代码跟踪其实在我转载的一篇博客HEVC/H.265参考代码跟踪里就已经有很不错的介绍了,因此,我就不重复里面所说的了,而是对里面一些我觉得需要稍微补充下并且以另一个具体实例SAO即Sample Adaptive Offset的跟踪过程进行说明。由于只是一个跟踪说明,代码的具体细节就不去探究了,其实这也是一个方法,有些时候,你仅仅需要知道实现某个功能的代码在哪,而不需要知道它的实现细节,那么你要做的只是确定它的位置,并对它的输入输出有所了解即可,只有对你所关心的问题才需要一句一句的去看懂它,而不是对每一句话都平均用力,这种做法看似认真,但其实效率是很低的,因为有很多东西是要结合其它部分才能真正弄明白的。当然了,这只是一家之说,对于大牛来说,看代码是小菜,不用讲究这么多,这个另当别论。好了,不废话了,开始进入今天的整体。

 

要跟踪代码,首先你得对整个框架都有个很好的了解,那首先的问题是:我刚开始拿到代码时,当然不了解它的框架啦。所以,这里有个建议:在看代码前,最好对编解码的大概流程有个比较清晰的认识,即能够预先知道编码器或者解码器它需要按照一个怎样的步骤来完成这些工作。对于HM来说,编码器的方框图就是个很好的参考,如图1所示:

这个图不是我画的,截取自"Overview of the High Efficiency Video Coding  (HEVC) Standard"。

在仔细阅读上图后,至少我们可以确定一点的是,SAO应该是在Deblocking即去方波滤波的附近完成的(如果你已经读过上面那篇overview或者从其它途径了解了滤波过程,那就应该知道SAO其实就是在Deblocking之后做的,在后面的代码中也能够获知这一点)。

 

我们从encmain.cpp中的main函数开始,首先进入cTAppEncTop.encode()函数里,它调用了一个函数xInitLibCfg(),里面有一句值得我们关注:m_cTEncTop.setUseSAO ( m_bUseSAO ); 这一句设置了SAO的可用性。还有另外几句也跟SAO有关:

m_cTEncTop.setLFCrossSliceBoundaryFlag( m_bLFCrossSliceBoundaryFlag ); m_cTEncTop.setMaxNumOffsetsPerPic (m_maxNumOffsetsPerPic);

m_cTEncTop.setSaoLcuBoundary (m_saoLcuBoundary);

m_cTEncTop.setSaoLcuBasedOptimization (m_saoLcuBasedOptimization);

由于本文重点是如何跟踪SAO,不是专门介绍SAO的,所以这几句是做什么的,留到以后有机会再说吧

encode函数还调用了另一个函数xCreateLib,让我们进入到这个函数里面来,重点关注m_cTEncTop.create()函数。再进入到这个函数中去,有一句判断m_bUseSAO的值的对不?if语句如果成立,即SAO可用,则执行花括号中的语句,m_cEncSAO.setSaoLcuBoundary(getSaoLcuBoundary());

m_cEncSAO.setSaoLcuBasedOptimization(getSaoLcuBasedOptimization());

m_cEncSAO.setMaxNumOffsetsPerPic(getMaxNumOffsetsPerPic());

m_cEncSAO.create( getSourceWidth(), getSourceHeight(), g_uiMaxCUWidth, g_uiMaxCUHeight, g_uiMaxCUDepth );

m_cEncSAO.createEncBuffer();

前三个函数好理解,无非就是对一些私有数据成员的值进行设置,第四、第五个比较重要。

第四个主要是为与SAO相关的参数分配内存,特别是建立了几张映射表并进行了初始化,具体细节留到以后继续。

第五个主要是为与SAO相关的熵编码器一些变量分配内存及初始化。

回到刚刚的那个encode中来,xCreateLib的下一个函数是xInitLib,它调用了另一个函数m_cTEncTop.init(); 进入这个函数,找到我们所关心的,m_cGOPEncoder.  init( this ); (PS: 一开始时也并不能确定我们所关心的东西在哪,但是凭着我们的先验知识,根据函数名,你可以大致把搜索范围缩小些,然后利用编译器的函数调用关系功能进一步地锁定范围,实在不行,就只能一个一个地找了,第一次可能费点功夫,但几次之后,你就有经验了,找起来就会更快了。这里只是把结果给出,省略了中间无效的查找过程,以节省篇幅。)这个函数里大概一个句话跟SAO有关:m_pcSAO                = pcTEncTop->getSAO(); 获取SAO滤波的类指针,或者说句柄。

至此,关于SAO的初始化工作基本完成,可以进入到实际的编码过程中去了。我们回到一开始提到的encode函数中,找到函数m_cTEncTop.encode( bEos, pcPicYuvOrg, m_cListPicYuvRec, outputAccessUnits, iNumEncoded ); 每读入一帧YUV,就会调用该函数一次,从而完成实际的编码工作。 我们进入到这个函数中,比较关心的是这个函数中调用了

m_cGOPEncoder.compressGOP(m_iPOCLast, m_iNumPicRcvd, m_cListPic, rcListPicYuvRecOut, accessUnitsOut);

这个函数足足1000行之多,想要一口气看完估计不是那么容易的事,而且,我们暂时只是熟悉代码框架,没必要每一行这会儿都去弄懂它,只要有个大概印象它做了哪些事情(基本靠先验知识和函数名来判断)就够了,关键是要找到我们所关心的SAO。首先可以看到这么一句

pcSlice->setLFCrossSliceBoundaryFlag(  pcSlice->getPPS()->getLFCrossSliceBoundaryFlag()  );

这句是设置滤波时是否可以跨slice边界的。

再往下是m_pcSliceEncoder->compressSlice   ( pcPic ); 这句对每一个slice进行编码。不过这次我们不打算进入这个函数去看,因为我们关心的是SAO,它并没有在这个函数里去实现,事实上,SAO是对编码后经过反变换、去方波滤波后的重建图像进行滤波的,所以它的完成不在compressSlice里也是可以理解的了。

继续往下,会看到一个判断语句if( m_pcCfg->getSaoLcuBasedOptimization() && m_pcCfg->getSaoLcuBoundary() )

条件中的两个函数的返回值就是在前面初始化时对应数据成员的值,它们的含义和作用随着你对SAO了解的深入就会明白了,默认情况下,第一个的返回值是true,而第二个的返回值是false,表明SAO是基于LCU的且未进行去方波滤波的像素是不能用来进行SAO滤波的。在这种条件下,m_pcSAO->resetStats() 和

m_pcSAO->calcSaoStatsCu_BeforeDblk( pcPic ) 是不会被调用的,它们的功能有兴趣的朋友可以进去看看,这里还是继续按照我们的主线继续下去。

接下来又是一个判断if(pcSlice->getSPS()->getUseSAO())。当条件成立时,

LFCrossSliceBoundaryFlag.push_back(  ((uiNumSlices==1)?true:pcPic->getSlice(s)->getLFCrossSliceBoundaryFlag()) );

该语句根据slice的数目将跨边界滤波的标识压入栈中。

pcPic->createNonDBFilterInfo(m_storedStartCUAddrForEncodingSlice, 0, &LFCrossSliceBoundaryFlag ,pcPic->getPicSym()->getNumTiles() ,bLFCrossTileBoundary);

创建non-deblocked 滤波信息。

接下去还有一句m_pcSAO->createPicSaoInfo(pcPic, uiNumSlices);  这个函数里面主要做了一件事,

m_pcYuvTmp = pcPic->getYuvPicBufferForIndependentBoundaryProcessing();

但是呢,并不是一定会做这件事,只有当m_bUseNIF为true时才会做这件事,根据代码中的注释,它为真时表示滤波时不跨越slice的边界。

好了,我们离真正实现SAO的地方越来越近了... ...

这里怕前面讲了太多的函数,会忘记我们现在是在哪个函数里了,重申一下,是在compressGOP里,我们可以看到这么一句:

Int processingState = (pcSlice->getSPS()->getUseSAO())?(EXECUTE_INLOOPFILTER):(ENCODE_SLICE);

这里我们自然认为processingState被赋予EXECUTE_INLOOPFILTER了。紧接着,我们就能够看到swith语句了:switch(processingState)

既然是条件选择,我们可以直接跳到 case EXECUTE_INLOOPFILTER: 这一句了。

到了这里,特别是看到m_pcSAO->SAOProcess(&cSaoParam, pcPic->getSlice(0)->getLambdaLuma(), pcPic->getSlice(0)->getLambdaChroma(), pcPic->getSlice(0)->getDepth()); 这一句的时候,我们完全有理由相信我们已经成功定位到完成SAO实际工作的主要函数了。至于这个case语句中的其它函数,肯定是跟它有关的,但就不是本文所要讨论的问题了。SAO的更为具体的实现细节,则是我们定位到这些函数后,有朝一日有需求的时候所要做的工作了。

至此,本文结束。

 

(转载请注明出处。)

你可能感兴趣的:(HEVC,HEVC,HM,SAO,代码框架)