前序
以下的文字只是本人在学习H.264代码过程中的一些心得体会,限于本人水平有限,所以有错误的地方请阅读者谅解,并提出,大家共同讨论学习。
在这里,特别感谢H264乐园版主天之骄子及群里兄弟姐妹们的帮助!
说明:
红色表示函数
绿色表示函数中的参数
褐色表示函数内部的代码
正文
本文主要讲述了一些H.264中后处理错误隐藏的知识,而且也集中在解码端实现,根据空域和时域不同的掩盖方法,文章分为两大部分,第一部分主要讲帧内掩盖,相对应的c文件是erc_do_i.c;第二部分主要讲帧间掩盖,相对应的c文件是erc_do_p.c,这一部分相对于帧内掩盖要复杂得多,也是本文的重点。下面,我们就从简到难,细细讲述其中的原理。
第一部分:帧内误码掩盖(erc_do_i.c)
大家先对整个帧内误码掩盖有个大体的框架,请看下图,其中ercConcealIntraFrame ()是帧内的入口函数。
帧内的掩盖方式为像素平均权值,方法相对来说比较简单。下面,我们通过一个一个函数分析来了解帧内掩盖算法。
int ercConcealIntraFrame( frame *recfr, int32 picSizeX, int32 picSizeY, ercVariables_t *errorVar )
函数功能简述:这是帧内掩盖的入口函数,被image.c中的exit_picture()函数所调用。这里,
没有太多的代码,只是做了坐标级之间的转换,并调用了concealBlocks()。
函数参数解释:frame *recfr 表示当前帧指针结构体,这个结构体包含了指向当前帧Y,U和V块的指针。
int32 picSizeX 表示一帧的宽度,当QCIF图像时,该值为176,当CIF图像时,该值为352。
int32 picSizeY表示一帧的高度,当QCIF图像时,该值为144,当CIF图像时,该值为288。
ercVariables_t *errorVar 表示包含了一些掩盖状态和信息的结构体。
注意点:1. ercVariables_t结构体中变量的含义(详看我的erc_api.c初探)。
2.不同基本单位之间的转换,我的意思是指以宏块为单位的横纵坐标转换成以8x8
块为单位的坐标之类的问题。
函数详述:
函数内部的动作比较简单,但是照顾到刚刚接触误码掩盖的朋友,在介绍第一个函数的时候,我更多的会讲一些变量的含义以及代码的操作习惯。这边我主要现讲两方面,一个就是errorVar->yCondition、errorVar->uCondition和errorVar->vCondition指的是什么?另外一个就是不同基本单位之间的转换。
在接收端,有一个宏块状态图的概念,它的作用就是记录一帧图像所有宏块的接收状态。如下图:
标记为ERC_BLOCK_OK的宏块表示正确接收,标记为ERC_BLOCK_CONCEALED的宏块表示错误的块但是已经被掩盖过,标记为ERC_BLOCK_CORRUPTED或ERC_BLOCK_EMPTY就表示错误接收或已经丢失的块。这样,我们在程序中就可以通过这张表来知道哪些块是需要进行误码掩盖的,而errorVar->yCondition、errorVar->uCondition和errorVar->vCondition这三个数组就是分别存放了YUV块的这种状态标志,但是这边需要注意的是,在程序中存放的时候,并不是以宏块为单位的,而是进一步将一个宏块分割成4个8x8的子块,存放的是这些子块的状态信息。当然,对于UV来说,不用分割,基本单位已经是8x8了。由此我们知道,接下去误码掩盖处理的过程中,并不是以整像素单位进行的,而是以8x8块为单位的。
进一步,我们现在应该明白为什么程序一开始对于YUV的lastRow和lastColumn所作的赋值。
//Y
lastRow = (int) (picSizeY>>3);
lastColumn = (int) (picSizeX>>3);
//UV
lastRow = (int) (picSizeY>>4);
lastColumn = (int) (picSizeX>>4);
假设对于一个DCIF图像来说,picSizeX=176 picSizeY=144,作了处理后,对于Y块就被分割成22x18个8x8块,而对于UV块就被分割成11x9个8x8块。其实这个就是我所说的不同基本单位之间的转换。
明白了上面这两点,这个函数就理解了,下面就是调用了concealBlocks()。
static void concealBlocks( int lastColumn, int lastRow, int comp, frame *recfr, int32 picSizeX, int *condition )
函数功能简述:函数根据亮度和色度分别以不同的基本单位扫描当前帧,先定列,然后在该
列内逐行扫描,确定受损区域的范围。但是这里扫描列的顺序跟帧间的不同,是从从左往右逐步扫描。在确定受损区域后,就调用相应函数进行掩盖。
函数参数解释:int lastColumn 表示以8x8块为单位的一帧最大列数。
int lastRow 表示以8x8块为单位的一帧最大行数。
int comp 用来判断亮度块还是色度块的标志,其中0表示Y块、1表示U块、2表示V块。
frame *recfr 表示当前帧指针结构体,这个结构体包含了指向当前帧Y,U和V块的指针。
int32 picSizeX 表示一帧的宽度,当QCIF图像时,该值为176,当CIF图像时,该值为352。
int *condition 表示当前块的状态标志。
注意点:1.亮度和色度分别以不同的基本单位扫描当前帧。
2.跟帧间的扫描方式加以区别。
函数详述:
函数一开始便根据亮度和色度而分别设定了相应的步长。
if ( comp == 0 )
step = 2;
else
step = 1;
由此可见,亮度块是以宏块单位来扫描,而色度块是以8x8块单位来扫描。确定基本单位后,下面就可以扫描一帧图像来确定误码区域,相应的代码以及图片如下。
for ( column = 0; column < lastColumn; column += step )
{
for ( row = 0; row < lastRow; row += step )
{
[查找误码区域]
}
}
接下去,根据一列内受损区域的不同(共3种),采取相应不同的掩盖顺序,判断代码如下:
if ( lastCorruptedRow >= lastRow )
{
[从上往下进行掩盖]
}
else if ( firstCorruptedRow == 0 )
{
[从下往上进行掩盖]
}
else
{
[从上下一起向中间进行掩盖]
}
相应的具体步骤我不详说,这一部分帧内和帧间是相同的,请看下图,加深印象。
接下去就是进行真正的掩盖工作,这边一共会调用到两个函数,分别是ercCollect8PredBlocks(),ercPixConcealIMB(),其中ercCollect8PredBlocks()用来检测当前丢失块四周块的正确接收程度,ercPixConcealIMB()会进一步调用pixMeanInterpolateBlock()来进行最后的误码掩盖。
最后,进行完误码掩盖后,就会将相应块的状态标志置为已经被掩盖,即ERC_BLOCK_CONCEALED,相关代码如下。
if ( comp == 0 )
{
condition[ currRow*lastColumn+column] = ERC_BLOCK_CONCEALED;
condition[ currRow*lastColumn+column + 1] = ERC_BLOCK_CONCEALED;
condition[ currRow*lastColumn+column + lastColumn] = ERC_BLOCK_CONCEALED;
condition[ currRow*lastColumn+column + lastColumn + 1] = ERC_B