前序
以下的文字只是本人在学习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_BLOCK_CONCEALED;
}
else
{
condition[ currRow*lastColumn+column ] = ERC_BLOCK_CONCEALED;
}
int ercCollect8PredBlocks( int predBlocks[], int currRow, int currColumn, int *condition,
int maxRow, int maxColumn, int step, byte fNoCornerNeigh )
函数功能简述:函数用来收集当前丢失块四周8块的状态信息(是否可用),但程序实际上只
收集了上下左右四块的信息。
函数参数解释:int predBlocks[] 表示用来收集当前丢失块四周8块的状态信息的数组。
int currRow 表示扫描当前丢失块的行数(以8x8块为单位)。
int currColumn表示扫描当前丢失块的列数(以8x8块为单位)。
int *condition 表示当前块的状态标志。
int maxRow 表示以8x8块为单位的一帧最大行数。
int maxColumn 表示以8x8块为单位的一帧最大列数。
int step 根据亮度块和色度块而设定的步长,亮度块为2,色度块1。
byte fNoCornerNeigh 用来判断是否要收集斜角的四个块的状态信息。
函数详述:
整个函数的目的非常明确,就是要收集到当前丢失块四周块的状态信息,并将信息保存到predBlock[8]数组中。这边要明白数组中不同索引所代表周围块的位置,并且根据亮度块和色度块不同所收集周围块的区别,这边比较简单,我不多说了,大家请看下图,再对照代码,我想很快就能够明白。
void ercPixConcealIMB(byte *currFrame, int row, int column, int predBlocks[], int frameWidth,
int mbWidthInBlocks)
函数功能简述:函数根据ercCollect8PredBlocks()所得到的predBlocks数组,即周围块的状
态信息,来决定是否得到该8x8块所在宏块左上顶点的指针,并且还会取得当前丢失宏块左上顶点的指针。
函数参数解释:byte *currFrame 表示当前帧的启示指针地址。
int row表示扫描当前丢失块的行数(以8x8块为单位)。
int column表示扫描当前丢失块的列数(以8x8块为单位)。
int predBlocks[] 表示用来收集当前丢失块四周8块的状态信息的数组。
int frameWidth 表示一帧的宽度,当QCIF图像时,该值为176,当CIF图像时,该值为352。
int mbWidthInBlocks根据亮度块和色度块而设定的步长,亮度块为2,色度块1。
函数详述:
这部分代码也比较简单,大家请看下图,结合了图我讲下函数中一些变量所代表的含义。
A点表示src[4]取得的指针地址。
B点表示src[5]取得的指针地址。
C点表示src[6]取得的指针地址。
D点表示src[7]取得的指针地址。
E点表示currBlock取得的指针地址。
static void pixMeanInterpolateBlock( byte *src[], byte *block, int blockSize, int frameWidth )
函数功能简述:这是整个帧内部分进行掩盖的核心函数,具体方法为平均像素插值。
函数参数解释:byte *src[] 表示存放当前块四周8块宏块左上顶点的指针地址。
byte *block表示当前丢失块左上顶点的指针地址。
int blockSize表示块的大小,亮度块为16,色度块为8。
int frameWidth 表示一帧的宽度,当QCIF图像时,该值为176,当CIF图像时,该值为352。
函数详述:
这个函数就是围绕一个平均像素插值公式来展开的,所以我在这边主要是给大家解释这个公式,具体的代码留给阅读者自己去理解,应该不会有太大的困难。
Y=(Y1xD2+Y2xD1+Y3xD4+Y4xD3)/(D1+D2+D3+D4)
公式中Y1,Y2,Y3,Y4分别是当前丢失像素与上下左右四块相邻宏块的临近像素,D1,D2,D3,D4很明显,就是之间的像素具体,图中都标出了。
第二部分:帧间误码掩盖(erc_do_p.c)
大家先对整个帧间误码掩盖有个大体的框架,请看下图,其中ercConcealIntraFrame()是帧间的入口函数。
根据整帧图像的运动剧烈程度大小,帧间的掩盖分为两种方式。如果程度比较小,那么采用比较简单的方法,就直接把前一帧的相同位置的块拷贝到当前受损块(concealByCopy());否则通过预测运动矢量把预测帧中对应位置的块拷贝到当前受损块(concealByTrial())。下面,我们通过一个一个函数分析来了解帧间掩盖算法。
int ercConcealInterFrame(frame *recfr, objectBuffer_t *object_list, /*erc_object_list*/
int32 picSizeX, int32 picSizeY, ercVariables_t *errorVar )
函数功能简述:这是帧间掩盖的入口函数,被image.c中的exit_picture()函数所调用。这里,
我主要讲下在进行掩盖前的前置动作,这里跟帧内的差不多,以宏块为基本单位扫描当前帧,先定列,然后在该列内逐行扫描,确定受损区域的范围。但是这里扫描列的顺序跟帧内的不同,是从两边开始扫描逐步逼近内部。再确定受损区域后,就调用相应函数进行掩盖。
函数参数解释:frame *recfr 表示当前帧指针结构体,这个结构体包含了指向当前帧Y,U和V块的指针。
objectBuffer_t *object_list 表示了一个8x8块的一些状态信息的结构体,包含了块的状态、以像素为单位的左上顶点横纵坐标和运动矢量。
int32 picSizeX 表示一帧的宽度,当QCIF图像时,该值为176,当CIF图像时,该值为352。
int32 picSizeY表示一帧的高度,当QCIF图像时,该值为144,当CIF图像时,该值为288。
ercVariables_t *errorVar 表示包含了一些掩盖状态和信息的结构体。
注意点:1.细心的朋友肯定发现帧内程序会判断yCondition、uCondition和vCondition,但在
帧间只判断yCondition,这是因为帧间主要目的就是查找MV,而一个宏块内亮度与色度的MV是相同的,所以只要查找Y块的就可以。
2.predMB所代表的含义。
3.以宏块为基本单位扫描列时顺序。
4.确定某列的受损区域后,程序根据区域的不同位置会进行相应的掩盖顺序。
5.不同基本单位之间的转换,我的意思是指以宏块为单位的横纵坐标转换成以8x8
块为单位的坐标之类的问题。
函数详述:
函数的一开始也定义了一些变量,这些变量命名规范,都比较好理解,在这里,我只想解释下predMB,程序给它分配了384 byte空间大小,这个384所代表的含义就是一个宏块所需存储YUV块的大小,384=16x16+8x8+8x8。接下去,程序就开始以宏块为基本单位开始扫描P帧,详细步骤见下图,产生下图的关键语句是column = ((columnInd%2) ? (lastColumn - columnInd/2 -1) : (columnInd/2))阅读者自己可以分析下,应该不难。
确定列后就要开始在该列上逐行扫描查找受损区域,并确定从第几行到第几行,相应程序的代码如下:
for ( row = 0; row < lastRow; row++)
{
int part1 = MBxy2YBlock(column, row, 0, picSizeX); //自己添加变量
if(errorVar->yCondition[part1]<=ERC_BLOCK_CORRUPTED)
{
firstCorruptedRow = row;
for(lastCorruptedRow=row+1;lastCorruptedRow<lastRow; lastCorruptedRow++)
{
int part2 = MBxy2YBlock(column, lastCorruptedRow, 0, picSizeX);
//自己添加变量
if (errorVar->yCondition[part2] > ERC_BLOCK_CORRUPTED)
{
lastCorruptedRow --;
break;
}
}
这边我之所以自己单独建立了两个变量part1和part2,目的就是提醒大家,MBxy2YBlock()输入的参数column和row都是以宏块为单位的,而得到的结果part1和part2都是以8x8块为单位的当前块相对于帧头的偏移位置,因为yCondition存储的就是Y块中每个8x8块的标志状态(正确接受?已经掩盖?还没有掩盖?空?)。这边,我们也看到,程序正是通过这个yCondition来确定firstCorruptedRow和lastCorruptedRow。接下去,根据一列内受损区域的不同(共3种),采取相应不同的掩盖顺序,判断代码如下:
if ( lastCorruptedRow >= lastRow )
{
[从上往下进行掩盖]
}
else if ( firstCorruptedRow == 0 )
{
[从下往上进行掩盖]
}
else
{
[从上下一起向中间进行掩盖]
}
相应的具体步骤我不详说,请看下图,加深印象。
接下去就是进行真正的掩盖工作,这边一共会调用到四个函数,分别是ercCollect8PredBlocks(),concealByTrial(),concealByCopy()和ercMarkCurrMBConcealed(),其中ercCollect8PredBlocks()已经在帧内掩盖的时候详细说明,concealByTrial()和concealByCopy()会根据帧平均运动矢量的大小相应的被调用,最后就是调用ercMarkCurrMBConcealed()进行8x8块标志状态的改写,表明已被掩盖。
concealByTrial()比concealByCopy()掩盖过程复杂的多,所以在这里我先介绍concealByCopy()和相关的一个函数copyBetweenFrames(),之后,深入帧间运动矢量拷贝。
static int concealByCopy(frame *recfr, int currMBNum,objectBuffer_t *object_list, int32
picSizeX)
函数功能简述:函数其实并没有什么动作,就调用了copyBetweenFrames(),一切实质性的
掩盖动作全在这个函数里面。
函数参数解释:frame *recfr 表示当前帧指针结构体,这个结构体包含了指向当前帧Y,U和V块的指针。
int currMBNum 从传参我们就可以知道这个参数代表含义currRow*lastColumn+column,即当前宏块相对于帧头偏移的宏块数。
objectBuffer_t *object_list表示了一个8x8块的一些状态信息的结构体,包含了块的状态、以像素为单位的左上顶点横纵坐标和运动矢量。
int32 picSizeX 表示一帧的宽度,当QCIF图像时,该值为176,当CIF图像时,该值为352。
函数详述:
函数体本身非常简单,也就调用了copyBetweenFrames(),所以具体的动作我准备在介绍copyBetweenFrames()的时候再详细说明。这里,我只是提下函数中对结构体objectBuffer_t的赋值,我想这边定义currRegion这个指针变量作用仅仅想和concealByTrial()在代码结构上保持一致,两者不同仅在于前者currRegion只被赋值了但没有调用,后者被赋值也被调用了。当然,这只代表我个人的看法。
static void copyBetweenFrames (frame *recfr, int currYBlockNum, int32 picSizeX, int32
regionSize)
函数功能简述:函数的作用就是从前一帧与当前受损块(16x16)的相同位置宏块拷贝到当前
帧。
函数参数解释:frame *recfr 表示当前帧指针结构体,这个结构体包含了指向当前帧Y,U和V块的指针。
int currYBlockNum 当前Y块相对于帧头偏移的8x8块数。
int32 picSizeX 表示一帧的宽度,当QCIF图像时,该值为176,当CIF图像时,该值为352。
int32 regionSize 表示掩盖区域的大小,这边取的值为16。
函数详述:
帧间拷贝函数相对来说很是比较简单的,代码也较短,我们先看下图,基本描述了整个函数的动作。
函数的一开始就将前一帧的首地址listX[0][0]赋给了refPic指针,后面就通过该指针直接寻找YUV像素值,接着就是定义了xmin和ymin,两个变量的含义我已经在下图中写明。最后的两块for循环:
for (j = ymin; j < ymin + regionSize; j++)
for (k = xmin; k < xmin + regionSize; k++)
和
for (j = ymin / 2; j < (ymin + regionSize) / 2; j++)
for (k = xmin / 2; k < (xmin + regionSize) / 2; k++)
分别开始在16x16范围内拷贝Y块和在8x8范围拷贝UV块。
到这里,帧间直接拷贝的函数都已经介绍完毕,接下去,我们就开始研究帧间运动矢量拷贝,这部分是相当复杂,为了让大家更加清晰地看到里面的具体动作,我也尽量多用图来说明。这边一共涉及到5个函数,分别是concealByTrial(),buildPredRegionYUV(),copyPredMB(),edgeDistortion()和get_block()。
static int concealByTrial(frame *recfr, byte *predMB, int currMBNum, objectBuffer_t *object_list, int predBlocks[], int32 picSizeX, int32 picSizeY, int *yCondition)
函数功能简述:函数通过遍历当前受损宏块上下左右的宏块,试图在这四个相邻宏块中搜索
出最合适运动矢量,并以此作为当前受损宏块的预测运动矢量,通过它从参考帧中取得相应的宏块来取代当前受损块。
函数参数解释:frame *recfr 表示当前帧指针结构体,这个结构体包含了指向当前帧Y,U和V块的指针。
byte *predMB表示用来存储一个宏块YUV像素值的内存空间(predMB=16*16+8*8+8*8)。
int currMBNum当前Y块相对于帧头偏移的宏块数。
objectBuffer_t *object_list表示了一个8x8块的一些状态信息的结构体,包含了块的状态、以像素为单位的左上顶点横纵坐标和运动矢量。
int predBlocks[] 存放当前受损宏块四周相邻8块的状态标志。
int32 picSizeX 表示一帧的宽度,当QCIF图像时,该值为176,当CIF图像时,该值为352。
int32 picSizeY表示一帧的高度,当QCIF图像时,该值为144,当CIF图像时,该值为288。
int *yCondition 表示包含Y块每个8x8块的状态标志的数组。
注意点:1.整个函数结构较为复杂,我已经将结构图写在附录二中。
2. compSplit1和compSplit2两个变量的含义。
函数详述:
函数的一开始定义了很多变量,这些变量命名都很规范,理解起来都不是很困难。整个函数就是在一个个循环中进行,只要了解了这些循环所代表的动作,对这个函数的理解也就迎刃而解了,这边我打算拆开来一个一个循环来谈下我的理解。
第一层do-while循环:
do
{
……
compLeft--;
} while (compLeft);
这是最外面的一个while循环,个人感觉这个循环在这边没有太多的含义,因为compLeft初始化值为1,也就是这个循环只遍历一次,去掉这个循环也没关系。这里面包含了第二层do-while循环,另外就是对currRegion指向objectBuffer_t结构体的指针变量的赋值,这跟concealByCopy()类似。
第二层do-while循环:
do
{
……
threshold--;
} while ((threshold >= ERC_BLOCK_CONCEALED) && (fInterNeighborExists == 0));
程序的目的就是要查找当前受损宏块的预测运动矢量,而只有帧间拷贝的相邻块才存在运动矢量,所以这边while循环的条件我们可以这么理解:我们优先考虑正确接收的相邻块,如果相邻块中存在帧间拷贝块(此时fInterNeighborExists=1),那么我们就不去考虑虽然错误接收但已经被掩盖的相邻块,但是如果不存在帧间拷贝块,即还没有找到运动矢量,那么退求次,在已经掩盖过的相邻块中再查找一遍,如果还是没有,退出循环,直接采用concealByCopy()的方法。
第三层for循环:
for (i = 4; i < 8; i++)
{
if (predBlocks[i] >= threshold)
{
……
}
}
这个for循环比较好理解的,请看下图,其实就是在遍历当前受损宏块的上下左右宏块。predBlocks[4]、predBlocks[5]、predBlocks[6]和predBlocks[7]分别代表了上方、左方、下方和右方宏块的状态标志。
第四层switch循环:
switch (i)
{
case 4:
predMBNum
compSplit1
compSplit2
case 5:
case 6:
case 7:
}
这边就是为不同相邻宏块确定相应的自己左上顶点相对于帧头的宏块偏移量(predMBNum)和最临近当前受损块的两个8x8块相对于自己左上顶点的8x8块偏移量(compSplit1和compSplit2)。
第四层for循环:
帧间情况
for (predSplitted = isSplitted(object_list, predMBNum), compPred = compSplit1;
predSplitted >= 0;
compPred = compSplit2,predSplitted -= ((compSplit1 == compSplit2) ? 2 : 1))
改写成:
帧间情况
//如果相邻宏块被分割
for(predSplitted=1, compPred = compSplit1; predSplitted >= 0; compPred = compSplit2, predSplitted -= 1)
//如果相邻宏块没有被分割
for(predSplitted=0, compPred = compSplit1; predSplitted >= 0; compPred = compSplit2, predSplitted -= 2)
改写后的代码就比较好理解了,相邻宏块没有被分割的话,for循环仅仅遍历一次,当然只需要把整个宏块的运动矢量找出,如果相邻宏块被分割,那么for循环要遍历两次,也就是遍历当前相邻宏块中两个最临近受损宏块的8x8块。
到次,大概的结构只有介绍完毕,剩下的就是判断相邻块的regionMode,由此确定运动矢量并调用相应函数。
static void buildPredRegionYUV(struct img_par *img, int32 *mv, int x, int y, byte *predMB)
函数功能简述:将受损宏块(corrupted MB)四周的某一个块的运动矢量(MV)作为参数传入函数内,通过这个MV先建立预测块(prediction region),到参考帧(reference frame)中利用这个MV找到最合适的宏块,并将得到的宏块作为临时宏块复制到当前受损宏块(这边所谓的临时,因为还要通过edgeDistortion()函数的检验才能判断是否是最合适的宏块)。
函数参数解释:struct img_par *img 表示指向当前帧的img_par结构体,这个结构体非常复杂,本人能力有限,也未完全理解,请谅解,望高手能补充。
int32 *mv 表示当前受损宏块的预测运动矢量,是一个有3个元素的一维数组:mv[0]指向x轴偏移,mv[1]指向y轴偏移,mv[2]指向所参考的帧。
int x 表示当前8*8块左上角的横坐标(整像素单位)。
int y 表示当前8*8块左上角的纵坐标(整像素单位)。
byte *predMB 表示用来存储一个宏块YUV像素值的内存空间(predMB=16*16+8*8+8*8)。
注意点:1.函数内部的很多变量定义都很潦草,所以给代码阅读带来了很大的不便。
2.函数通过get_block()函数对亮度信号做了1/4插值。
3.函数在进行亮度和色度4x4块整像素扫描时顺序不同,亮度是先定列,后在该列
上逐行扫描;而色度是相反的。
函数详述:
函数一开始定义了很多非常潦草的变量,反正我当时自己是看晕了,如果你是第一次看先可以跳过这些变量往下看(到int ref_frame = mv[2]结束),而且等一下我也会告诉你这些变量到底代表了什么。
这边的ref_frame很明显就是指向了当前受损宏块参考宏块所在帧,接下去函数一口气给img->mb_x,img->mb_y,img->block_x ,img->block_y,img->pix_c_x,img->pix_c_y赋值,很明显,单从这6个变量命名上就能看出他们之间是存在联系的。
img->mb_x = x/MB_BLOCK_SIZE 表示当前受损宏块在帧中的横坐标(以宏块为单位)。
img->mb_y = y/MB_BLOCK_SIZE 表示当前受损宏块在帧中的纵坐标(以宏块为单位)。
img->block_x = img->mb_x * BLOCK_SIZE 表示当前受损宏块左上角4x4亮度块在帧中的横坐标(以4x4块为单位)。
img->block_y = img->mb_y * BLOCK_SIZE 表示当前受损宏块左上角4x4亮度块在帧中的纵坐标(以4x4块为单位)。
img->pix_c_x = img->mb_x * MB_BLOCK_SIZE/2 表示当前受损宏块左上角4*4色度块在帧中的横坐标(以整像素为单位)。
img->pix_c_y = img->mb_y * MB_BLOCK_SIZE/2 表示当前受损宏块左上角4*4色度块在帧中的纵坐标(以整像素为单位)。
定义完了这些变量(对mv_mul,f1,f2,f3和f4的赋值后面会讲到,这边先跳过),下面就是分为两大块:一块是对受损宏块的亮度信号进行处理,代码从for(j=0;j<MB_BLOCK_SIZE/BLOCK_SIZE;j++)到pMB += 256结束,另一块当然就是对色度信号的处理,代码从pMB += 256往下到函数结束都是。那么接下去,我们就分块来详细分析代码,我并不准备一句一句代码解释下去,你看着不爽,我写得也累,而主要以图为主。
一.受损宏块的亮度信号处理
get_block()函数在这里我先不做详细介绍,它的功能是以6抽头滤波进行1/4插值。
接下去便是将得到的预测4x4块像素值赋给img->mpr,整个功能块核心部分已经结束,但是在接下去的一小部分代码中,有一个值得我们注意的地方:
for (i = 0; i < 16; i++)
{
for (j = 0; j < 16; j++)
{
pMB[i*16+j] = img->mpr[j][i];
}
}
pMB += 256;
根据i*16+j,我们可以推断这边的i指的是宏块中的行,而j指的是列,这与前面是相反的,所以我们看到img->mpr[j][i],而不是img->mpr[i][j],请阅读者注意这种细节的地方。最后一句使pMB指向U块的地址,这就很简单了。
一.受损宏块的色度信号处理
再讲对色度信号进行处理过程之前,阅读者身边最好有H.264的标准文档或者毕厚杰的《新一代视频压缩编码准备-H.264/AVC》,264标准文档查看8.4.2.2.2章节,毕书查看第224页,这边有一个重要的公式就是预测色度像素值的推导式:
predPartLXc[Xc,Yc]=((8-xFracc)*(8-yFracc)*A+xFracc*(8-yFracc)*B+
(8-xFracc)*yFracc*C+xFracc*yFracc*D+32)>>6
这就是这部分核心,我们不难发现在代码中有这么一段代码:
img->mpr[ii+ioff][jj+joff]=(if0*jf0*listX[0][ref_frame]->imgUV[uv][jj0][ii0]+ if1*jf0*listX[0][ref_frame]->imgUV[uv][jj0][ii1]+
if0*jf1*listX[0][ref_frame]->imgUV[uv][jj1][ii0]+ if1*jf1*listX[0][ref_frame]->imgUV[uv][jj1][ii1]+f4)/f3;
其中f4=32,f3=64,这在前面已经定义,这下你至少应该知道f3和f4这两个变量的用处了吧(其实不定义,直接代入觉得更好),我们跟上面的推导式比较,很明显,这段代码就是描述上面预测色度像素值公式,有了这些前提,我们再去一一对应,我想里面那些一开始就定义的潦草变量你就知道他代表是什么啦,下面我们反过来从头开始分析这段代码。
这段代码在for(uv=0;uv<2;uv++)循环里面完成,uv相当于一个索引值,第一次循环处理U分量,第二次循环处理V分量,每次循环进去后先以4x4为单位扫描,到达每个4x4块后,再以整像素为单位扫描,随后就是按照上面的预测色度像素值推导式进行运算,不过在这段代码中,有这么3句,我个人感觉会使阅读者摸不着头脑(我的理解是没有理由,可能是写代码的人喜欢这样写吧),所以我在这边事先提下,也请阅读者注意。分别是(实际代码中这3句不是在一起的,我只是单独挑了出来):
for (j=4;j<6;j++) //为什么j选择从4开始进行循环?
jf=(j4+jj)/2; //if变量接下去就会被重新赋值,这边赋值的用意是什么?
if1=(i4+ii)/2; // if1变量接下去就会被重新赋值,这边赋值的用意是什么?
好了,下面以图描述具体过程,让阅读者更加清楚里面的动作。
接下去,代码中给ii0,jj0,ii1,jj1这四个变量赋值,这四个变量到底代表了什么,请看图buildPredRegionYUV_4。这边我们看到了在计算这四个变量过程都用到了这么一个式子max (0, min (参数, dec_picture->size_x_cr-1)),首先我要告诉大家的是dec_picture->size_x_cr和dec_picture->size_y_cr分别是指一个宏块内色度块以像素为单位的宽度和高度,即88和72(我采用的是QCIF图像)。这样就很明白了,上面的式子的作用就是使1/8像素单位转换到整像素单位时的坐标限制在(0,87)和(0,71)的色度块有效范围内。
代码马上就要结束,下面四个变量if1,jf1,if0,jf0的赋值我们也应该换个角度来看,当然大家要对照着图buildPredRegionYUV_2,我挑里面的一个语句给大家解释,相信其余的3句也就不难理解了。
if1=(i1 & f2);//f2=7
我们知道&操作只有两个操作数的对应位都是1时结果才为1,而把f2转换成二进制就是00000111,那么我们可以知道if1的结果肯定在0-7(十进制)之内,更进一步换个角度,可以写成if1=(i1%(f2+1)),也就是图中的xFracc,呵呵,我想现在阅读者肯定是豁然开朗。
代码最后一句就是套公式拉,我也不多说啦。
static void copyPredMB (int currYBlockNum, byte *predMB, frame *recfr,
int32 picSizeX, int32 regionSize)
函数功能简述:函数将暂存在predMB中的YUV的像素值拷贝到当前帧中。
函数参数解释:int currYBlockNum 当前Y块相对于帧头偏移的8x8块数。
byte *predMB表示用来存储一个宏块YUV像素值的内存空间(predMB=16*16+8*8+8*8)。
frame *recfr 表示当前帧指针结构体,这个结构体包含了指向当前帧Y,U和V块的指针。
int32 picSizeX 表示一帧的宽度,当QCIF图像时,该值为176,当CIF图像时,该值为352。
int32 regionSize表示掩盖区域的大小,这边取的值为16。
函数详述:
函数本身比较简单,一开始定义的4个变量xmin、ymin、xmax和ymax所代表的含义见下图。
接下去就是分别复制Y块和UV块,相对应了两个for循环。
for (j = ymin; j <= ymax; j++)
for (k = xmin; k <= xmax; k++)
和
for (j = (ymin>>1); j <= (ymax>>1); j++)
for (k = (xmin>>1); k <= (xmax>>1); k++)
最后,只是提下locationTmp = (j-ymin) * 16 + (k-xmin)因为在predMB中,Y块的像素值存在predMB[0]到predMB[255]之间,所以才有这边locationTmp的计算。
static int edgeDistortion (int predBlocks[], int currYBlockNum, byte *predMB,
byte *recY, int32 picSizeX, int32 regionSize)
函数功能简述:当buildPredRegionYUV()从参考帧中找到合适的MB后,此函数就会从空间
的角度来验证找到的MB是不是最好的选择,叫做DSM(Side Match Distortion)。
函数参数解释:int predBlocks[] 存放当前受损宏块四周相邻8块的状态标志。
int currYBlockNum当前Y块相对于帧头偏移的8x8块数。
byte *predMB表示用来存储一个宏块YUV像素值的内存空间(predMB=16*16+8*8+8*8)。
frame *recfr 表示当前帧指针结构体,这个结构体包含了指向当前帧Y,U和V块的指针。
int32 picSizeX 表示一帧的宽度,当QCIF图像时,该值为176,当CIF图像时,该值为352。
int32 regionSize表示掩盖区域的大小,这边取的值为16。
函数详述:
这个函数就是围绕一个smoothness条件判别式来展开的,所以我在这边主要是给大家解释这个判别式,具体的代码留给阅读者自己去理解,应该不会有太大的困难。