h264具体解析过程
残差块解析过程分析:
前注:代码是我自己实现的,类似但不是标准中的,写代码不要参照这个。
残差块解码调用条件:
if(CodedBlockPatternLuma > 0 || CodedBlockPatternChroma > 0 ||\
Get_MbPartPredMode(mb_type, 0) == Intra_16x16){
mb_qp_delta = parser_g->read_ae(24);
residual(0, 15);
}
也就是解码模式(亮度、色度)中任一大于0,或者是帧内16x16时调用residual(0, 15);
同时还读取了一个宏块的量化参数偏移。
从之前的PPS的 pic_init_qp 到Slice头部的 slice_qp_delta 再到这里的 mb_qp_delta 则完全确定了这个宏块的量化参数QP。
指定解码函数指针
这个比较简单,就是用之前的解码参数entropy_coding_mode_flag来确定函数指针是CABAC还是CAVLC
if(!parser_g->pS->pps->entropy_coding_mode_flag) residual_block = ¯oblock::residual_block_cavlc;
else residual_block = ¯oblock::residual_block_cabac;
而至于这两个函数指针的具体实现不用太关心,
他的参数表为:(int32_t* coeffLevel, uint8_t startIdx, uint8_t endIdx, uint8_t length)
就简单理解为从 startIdx 到 endIdx 读入 length 个数据组成一个矩阵(或者说数组),放到第一个参数中。
亮度残差块解码
亮度块解码紧跟在函数指针的下一步。一共6个参数:AC系数返回值、DC系数返回值、4x4块系数、8x8块系数、开始索引、结束索引。
在宏块中residual(0, 15);
所以开始索引是0,结束索引是15,一共16是个,也就是。然后进入亮度块的残差解码函数中。
亮度块的残差解码函数:
大体上是两个部分:DC系数、两级for循环读入其他系数。
DC系数就只有两句话,基本就是为了帧内16x16准备的,这也是毕老师书上提到的非帧内16x16宏块不单独编码DC系数。
if(startIdx == 0 && Get_MbPartPredMode(mb_type, 0) == Intra_16x16)
(this->*residual_block)(i16x16DClevel, 0, 15, 16);
然后是一个下面所示的两级for循环,两级循环索引分别是8x8索引i8x8和4x4块索引i4x4。这样就是4个8x8块和每一个8x8里面的4个4x4块。
//不完整的代码
for(uint8_t i8x8 = 0; i8x8 < 4; i8x8++)
for(uint8_t i4x4 = 0; i4x4 < 4; i4x4++)
第一个for下面的三个条件分别是:
if(!transform_size_8x8_flag || !parser_g->pS->pps->entropy_coding_mode_flag)
else if(CodedBlockPatternLuma & (1 << i8x8))
else
1、标准句法表第一句里面很绕,可以反过来想: 非( transform_size_8x8_flag 且 entropy_coding_mode_flag )
也就是二者不能同时为真。同时为真是什么意思:是算术熵编码CABAC 并且 是8x8变换解码 ,
再继续说就是:如果这个if 不成立,应该用 CABAC 去解8x8块残差得到总的残差。
2、CodedBlockPatternLuma从最低位也就是2^0位上开始,按照4个8x8块索引依次往高位,移位求并得到CodedBlockPatternLuma当前位上的值。四个值从低到高分别是四个8x8块(0 1 2 3)的系数。也就是8x8块系数。
也即:如果编码模式为1,那么解8x8块。
3、其他:除了上面两种情况之外的其他情况,当前的8x8块的所有系数为0。
第一种情况的反面:(transform_size_8x8_flag && entropy_coding_mode_flag)同时为真,也就是:是算术熵编码CABAC并且是8x8变换解码,那么就是用8x8块的系数来决定整个宏块的亮度残差。
第二种情况的反面:该8x8块上的亮度编码模式为0,也就是第三种情况里面的全部为0不编码。
全代码如下:
void macroblock::residual_luma(int32_t i16x16DClevel[16], int32_t i16x16AClevel[16][15], int32_t level4x4[16][16], int level8x8[4][64], uint8_t startIdx, uint8_t endIdx)
{
uint8_t i = 0;
if(startIdx == 0 && Get_MbPartPredMode(mb_type, 0) == Intra_16x16)
(this->*residual_block)(i16x16DClevel, 0, 15, 16);
for(uint8_t i8x8 = 0; i8x8 < 4; i8x8++)
if(!transform_size_8x8_flag || !parser_g->pS->pps->entropy_coding_mode_flag)
for(uint8_t i4x4 = 0; i4x4 < 4; i4x4++)
{
if(CodedBlockPatternLuma & (1 << i8x8))
if(Get_MbPartPredMode(mb_type, 0) == Intra_16x16)
(this->*residual_block)(i16x16AClevel[i8x8*4+ i4x4], (0 > startIdx - 1)?0: startIdx - 1, endIdx - 1, 15);
else
(this->*residual_block)(level4x4[i8x8 * 4 + i4x4], startIdx, endIdx, 16);
else if(Get_MbPartPredMode(mb_type, 0) == Intra_16x16)
for(i = 0; i < 15; i++)
i16x16AClevel[i8x8 * 4 + i4x4][i] = 0;
else
for(i = 0; i < 16; i++)
level4x4[i8x8 * 4 + i4x4][i] = 0;
if(!parser_g->pS->pps->entropy_coding_mode_flag && transform_size_8x8_flag)
for(i = 0; i < 16; i++)
level8x8[i8x8][4 * i + i4x4] = level4x4[i8x8 * 4 + i4x4][i];
}
else if(CodedBlockPatternLuma & (1 << i8x8))
(this->*residual_block)(level8x8[i8x8], 4 * startIdx, 4 * endIdx + 3, 64);
else
for(i = 0; i < 64; i++)
level8x8[i8x8][i] = 0;
}
OK,搞清楚之后可以进入第一种情况的详细解释了:
for(uint8_t i4x4 = 0; i4x4 < 4; i4x4++)
{
if(CodedBlockPatternLuma & (1 << i8x8))
if(Get_MbPartPredMode(mb_type, 0) == Intra_16x16)
(this->*residual_block)(i16x16AClevel[i8x8*4+ i4x4], (0 > startIdx - 1)?0: startIdx - 1, endIdx - 1, 15);
else
(this->*residual_block)(level4x4[i8x8 * 4 + i4x4], startIdx, endIdx, 16);
else if(Get_MbPartPredMode(mb_type, 0) == Intra_16x16)
for(i = 0; i < 15; i++)
i16x16AClevel[i8x8 * 4 + i4x4][i] = 0;
else
for(i = 0; i < 16; i++)
level4x4[i8x8 * 4 + i4x4][i] = 0;
if(!parser_g->pS->pps->entropy_coding_mode_flag && transform_size_8x8_flag)
for(i = 0; i < 16; i++)
level8x8[i8x8][4 * i + i4x4] = level4x4[i8x8 * 4 + i4x4][i];
}
这里有两套逻辑:
第一套if:
1、如果当前8x8块的编码模式为1:是帧内16x16就解AC系数,否则就解4x4系数
2、如果当前8x8块的编码模式为0 并且 是帧内16x16,那么所有的AC系数为0
3、否则:所有的4x4系数为0。用上面的事件取逻辑反就得当前条件:当前8x8块编码模式为0 并且 不是帧内16x16宏块
这三个也是很绕的三个逻辑,主要抓住两个判断条件:当前8x8块编码模式是不是1,当前宏块是不是帧内16x16。
总结:帧内16x16宏块,那么用AC系数;不是,那么用4x4系数
第二套if:
这个比较简单只有一个条件:如果不是熵编码 并且 是8x8变换解码。那么所有的4x4系数给到相应的8x8系数矩阵里面去。
二次总结:
单独的一个是DC系数,只有16个,如果不是帧内16x16宏块,那么没有编码DC系数。
其他三个分别是AC系数、8x8系数、4x4系数
AC系数还是给帧内16x16用的,
8x8系数是给8x8变换解码用的,
4x4是给除了帧内16x16之外的其他解码(包括8x8变换解码)用的。
在这里还要注意到子宏块等等的分法的不同而不同。
其中:AC系数只有16x15个,另外两个一共都是16x16个。
维度上面的分法有所不同:
8x8:[ 4][64]
4x4:[16][16]
AC :[16][15]
色度残差块解码
if(parser_g->pV->ChromaArrayType == 1 || parser_g->pV->ChromaArrayType == 2)
{
uint8_t iCbCr = 0;
NumC8x8 = 4 / (parser_g->pV->SubWidthC * parser_g->pV->SubHeightC);
for(iCbCr = 0; iCbCr < 2; iCbCr++) // read chroma DC level
if((CodedBlockPatternChroma & 3) && startIdx == 0) (this->*residual_block)(ChromaDCLevel[iCbCr], 0, 4 * NumC8x8 - 1, 4 * NumC8x8);
else for(uint8_t i = 0; i < 4 * NumC8x8; i++) ChromaDCLevel[iCbCr][i] = 0;
for(iCbCr = 0; iCbCr < 2; iCbCr++) // read chroma AC level
for(uint8_t i8x8 = 0; i8x8 < NumC8x8; i8x8++)
for(uint8_t i4x4 = 0; i4x4 < 4; i4x4++)
if(CodedBlockPatternChroma & 2) (this->*residual_block)(ChromaACLevel[iCbCr][i8x8*4+i4x4], startIdx - 1 >= 0 ? (startIdx - 1) : 0 , endIdx - 1, 15);
else for(uint8_t i = 0; i < 15; i++) ChromaACLevel[iCbCr][i8x8*4+i4x4][i] = 0;
}
色度块就简单一些,注意其中的条件 ChromaArrayType:
上篇文章提到 ChromaArrayType 指示了亮度和色度的分组情况,这里的1和2分别就是4:2:0 4:2:2
注意到是先读完所有的(Cb Cr)DC系数,然后再读所有的AC系数。
然后也就是 CodedBlockPatternChroma 这个变量对是否编码系数的影响,从if-else就可以看出来。
后面还有 ChromaArrayType == 3 的解码,因为就是用的亮度的解码方法,这里也不多说了。
2020年05月26日16:49:31 残差块认知结束。(未完)
CABAC在解残差块的流程
void residual::residual_block_cabac(block* bl, int requestVlaue, uint8_t startIdx, uint8_t endIdx, uint8_t length)
{
int16_t i = 0, numCoeff = 0;
int32_t* coeffLevel = new int32_t[endIdx];
uint8_t* significant_coeff_flag = new uint8_t[endIdx];
uint8_t* last_significant_coeff_flag = new uint8_t[endIdx];
int16_t a= 0 ,b = 0;
if(length != 64 || parser->pV->ChromaArrayType == 3)
bl->coded_block_flag = parser->read_ae(0x61000 + requestVlaue);
for(i = 0; i < length; i++)
coeffLevel[i] = 0;
if(bl->coded_block_flag)
{
int32_t* coeff_abs_level_minus1 = new int32_t[endIdx];
int32_t* coeff_sign_flag = new int32_t[endIdx];
numCoeff = endIdx + 1 ;
i = startIdx ;
while(i < numCoeff - 1)
{
significant_coeff_flag[i] = parser->read_ae(0x62000 + requestVlaue + (((int32_t)i) << 20));
if(significant_coeff_flag[i]) {
last_significant_coeff_flag[i] = parser->read_ae(0x63000);
if(last_significant_coeff_flag[i])
numCoeff = i + 1;
}
i++;
}
coeff_abs_level_minus1[numCoeff - 1] = parser->read_ae(0x64000 + requestVlaue + (((int)a) << 24) + (((int)b )<< 20));
coeff_sign_flag[numCoeff - 1] = parser->read_ae(0);//使用的是旁路编码模式
coeffLevel[numCoeff - 1] = (coeff_abs_level_minus1[numCoeff - 1] + 1) * (1 - 2 * coeff_sign_flag[numCoeff - 1]) ;
for(i = numCoeff - 2; i >= startIdx; i--){
if(significant_coeff_flag[i])
{
if(coeffLevel[i] == 1 || coeffLevel[i] == -1) a++;
if(coeffLevel[i] > 1 || coeffLevel[i] < -1) b++;
coeff_abs_level_minus1[i] = parser->read_ae(0x64000 + (((int)a) << 24) + (((int)b )<< 20));
coeff_sign_flag[i] = parser->read_ae(0);
coeffLevel[i] = (coeff_abs_level_minus1[i] + 1) * (1 - 2 * coeff_sign_flag[i]);
}
}
delete[] coeff_abs_level_minus1;
delete[] coeff_sign_flag ;
}
for (uint16_t i = 0; i < length; i++) {bl->set_blockValue(i, coeffLevel[i]);}
printf(">>residu:result of cabac is: \n");
for (uint16_t i = 0; i < length; i++) {printf(" index %2d, value : %d\n", i, coeffLevel[i]);}
delete[] coeffLevel ;
delete[] significant_coeff_flag ;
delete[] last_significant_coeff_flag;
}
首先我还是贴一下我写的代码,这里就简单略过解析器指针parser的read方法的实现,单单就这个过程做一些学习。
首先是这个块是不是被编码,coded_block_flag
这个句法元素指明了,如果说 coded_block_flag == 0,那么后面读取的操作都不会发生。所有系数都是0.
然后我们需要分别new 4个数组出来用来存储中间计算和读取的数据。
首先是初始化两个变量,一个是系数的总数量,另一个是系数的读取索引。
numCoeff = endIdx + 1 ;
i = startIdx ;
然后看第一个while
while(i < numCoeff - 1)
{
significant_coeff_flag[i] = parser->read_ae(0x62000 + requestVlaue + (((int32_t)i) << 20));
if(significant_coeff_flag[i]) {
last_significant_coeff_flag[i] = parser->read_ae(0x63000);
if(last_significant_coeff_flag[i])
numCoeff = i + 1;
}
i++;
}
因为i是从0开始的,所以判断条件的右边是系数减一。
在中间的一段先不看,直接到结尾的i++,所以这个循环可以说是针对于整个块的。
然后里面第一个句法元素,significant_coeff_flag
,这是一个数组,对于每一个i,记录这个位置上的数据是不是0,如果是significant_coeff_flag[i] == 0,那么这个数据位上是0,否则是非零的数。
如果当前位置上不是0,那么指定 last_significant_coeff_flag
来说明这是不是在扫描顺序中最后一个非零的数。
last_significant_coeff_flag == 1,接下来从i+1到结尾扫描顺序之后全是都是0。为0说明后面还有非零系数。
如果这个位置是最后一个非零系数的话,那么之后的数据也不用读了。直接全部为0,而之前已经完成这个工作了,所以就直接退出循环。
接下来的三句把退出的时候最后一个非零系数位置上的数据读出来。coeff_abs_level_minus1
指系数绝对值减去1。coeff_sign_flag
指明符号,0为正数,1为负数。
coeffLevel 就是这个系数
coeff_abs_level_minus1[numCoeff - 1] = parser->read_ae(0x64000 + requestVlaue + (((int)a) << 24) + (((int)b )<< 20));
coeff_sign_flag[numCoeff - 1] = parser->read_ae(0);//使用的是旁路编码模式
coeffLevel[numCoeff - 1] = (coeff_abs_level_minus1[numCoeff - 1] + 1) * (1 - 2 * coeff_sign_flag[numCoeff - 1]) ;
第二个循环
for(i = numCoeff - 2; i >= startIdx; i--)
{
if(significant_coeff_flag[i])
{
if(coeffLevel[i] == 1 || coeffLevel[i] == -1) a++;
if(coeffLevel[i] > 1 || coeffLevel[i] < -1) b++;
coeff_abs_level_minus1[i] = parser->read_ae(0x64000 + (((int)a) << 24) + (((int)b )<< 20));
coeff_sign_flag[i] = parser->read_ae(0);
coeffLevel[i] = (coeff_abs_level_minus1[i] + 1) * (1 - 2 * coeff_sign_flag[i]);
}
}
总结:先从0遍历到最后一个非零的系数,然后再从最后一个非零系数往前依次读取数值。