解析完成条带头之后下一步的工作是解析条带数据slice_segment_data。slice_segment_data数据主要由一个个的Coding_Tree_Unit(CTU)组成。每一个CTU的结构如下所示:
可以看出,每一个CTU的coding_quadtree部分之前是语法结构sao。那么现在来研究一下如何从码流中解码出sao,这里着重讨论cabac而非sao的意义。
对sao的解析部分在TDecSlice::decompressSlice()中。在真正开始解码之前,TDecSlice::decompressSlice()调用了m_pcEntropyDecoderIf->resetEntropy(p)函数进行熵解码器的一些初始化操作,如为不同语法元素进行initBuffer。在最后,调用了m_pcTDecBinIf->start()函数。该函数读出了码流中的前两个字节组成一个UInt16的数字赋给m_uiValue。
以下是TDecSlice::decompressSlice()中sao的解析部分:
具体的解析过程在pcSbacDecoder->parseSaoOneLcuInterleaving()中实现。实现如下:
在调试过程中,对于第一个ctu来讲,rx和ry均为0,因此不会解析MergeUp和MergeLeft语法元素。实际上执行的部分是parseSaoOffset(&(pSaoParam->saoLcuParam[iCompIdx][iAddr]), iCompIdx);
在该函数中执行parseSaoTypeIdx(uiSymbol);
该函数调用了上篇文章中解释过的decodeBin函数实现解析。回顾上篇文章中对该函数的解释,可以看出该函数的输出值实际就是valMps或者1-valMps,组成一串二进制码流,也就是算数编码前的二值化过的语法元素。通过这个函数输出了一个二进制字符,但是却并没有从原始码流中读取数据,这种现象开始时相当让我感到费解,不过想了一下便明白了这是很正常的,因为这正是算数编码过程的逆过程。在编码的时候,编码器“吸收”了一个、两个甚至更多个字符,多次进行MPS/LPS判断和区间划分,直到满足一定条件后才会输出一个0或1。那么在解码的时候,也会可能会出现输出了多个字符才“消耗”了1bit码流的情况。从上述函数的实现中可以得知,parseSaoTypeIdx函数返回给uiSymbol的值为0,那么parseSaoOffset(SaoLcuParam*
psSaoLcuParam, UInt compIdx)函数便不会做后续操作,将psSaoLcuParam->length置0后直接返回。
从parseSaoOneLcuInterleaving函数中可以看出,parseSaoOffset将会循环调用3次。在第二次调用中,我们的demo程序显示,parseSaoTypeIdx中uiCode返回值为1,因此,下面继续调用了decodeBinEP函数:
该函数向实参uiCode输出值为0,根据条件,parseSaoTypeIdx的输出值为5。并且将执行4次parseSaoMaxUvlc,将解码的结果赋予psSaoLcuParam->offset[i]。
在执行第一次parseSaoMaxUvlc中,只执行了函数①,code等于0因此返回val=0;执行第二次parseSaoMaxUvlc,①返回code=1,因此将会执行下面的while循环,共执行三次,返回val=3;值得注意的是,在第三次循环中,decodeBinEP函数读取了码流中的第三个字符加到了m_uiValue上;执行第三次parseSaoMaxUvlc,①返回code=1,下面的while循环只会执行一次,返回val=1;第四次执行parseSaoMaxUvlc,①返回code=1,while循环执行一次,返回val=1。
接下来,对第2~4个offset值,将各调用一次decodeBinEP,为1则取其负值。
调用parseSaoUflc(5, uiSymbol ),该函数调用了decodeBinsEP,该函数可以认为一次处理多位。实现如下:
在该函数中读取了码流中的第四个字节(107),在for循环中比较m_uiValue和scaledRange的值并决定bins加1或倍增,最终返回值为13。该值赋予psSaoLcuParam->subTypeIdx。
自此,parseSaoOffset的任务便已完成。而类似的工作将循环三次以完成pcSbacDecoder->parseSaoOneLcuInterleaving的功能。
PS:跟踪了这么久的代码,但是现在的问题依然是知其然不知其所以然……看来接下来需要在标准文档和其他一些资料上面下些功夫了。