HM编码器代码阅读(21)——熵编码的概念以及在HEVC中应用

熵编码

一、熵编码介绍

熵编码把一系列用于表示视频序列的元素符号转变为一个用来传输或存储的压缩码流。

信息的多少用信息量来度量,显然,信息量与不确定性的消除程度有关,消除的不确定性越大,信息量就越大。不确定性的大小与事件发生的概率相关,因此不确定性可以度量,更进一步信息量也可以度量。

假设某一个信源(就是产生信息的地方)的概率空间是:
X(表示信源产生的符号的集合),P(x)表示符号产生的概率的集合。

那么,某一个信源符号Xi的信息量就定义为:I(Xi)=log(1/P(Xi))。
那么这个信源的信息熵(即这个信源的包含的信息量)是各个信源符号的信息量的数学期望:E[I(Xi)]。

 即熵可以表示信息量的大小。

对信源输出的信息采用某一种方法进行编码就是熵编码。通常的,有下面两种编码方法:
(1)变长编码(哈夫曼编码就是一种变长编码)是一种前缀编码(即任意一个码字都不是另一个码字的前缀)。其中应用比较广泛的是指数哥伦布编码。变长编码为信源的每一个信息都指定一个不同长度的码字。
(2)算术编码。算术编码的本质是为整个输入序列分配一个码字。因此平均意义上可以为单个字符分配长度小于1的码字。

指数哥伦布编码由前缀和后缀两部分构成,前缀和后缀都依赖于指数哥伦布码的阶数k。假设指数哥伦布码是N,阶数为k,下面是它的编码步骤:
(1)把N转换为二进制数,去掉最低的k个比特位,然后加上1
(2)计算留下的比特数,把这个数减去1,这就是需要增加的前缀0的个数
(3)把步骤(1)中去掉的k个比特位补回比特串的尾部。
假如N=4,k=1,那么:
(1)把N转成二进制即100,去掉后面的1个比特位后变成10,加上1就变成11
(2)11的比特位数是2,因此需要在前面加上(2-1)个0。变成011
(3)把去掉的比特位补上,即0110.

算数编码的步骤:
(1)起始的时候把当前区间[L,H)设置为[0,1)
(2)对每一个事件,编码器按照步骤a和b进行处理:
     a)编码器把当前区间分成子区间,每个事件一个
     b)一个子区间的大小与下一个将要出现的事件的概率成正比,编码器选择的子区间与下一个将要发生的事件相对应,并把它当作新的区间
(3)最后输出的当前区间的下边界就是该给定序列的的算数编码。
下面是一个例子:
假设信源符号是{A,B,C,D},这些符号发生的概率是{0.1,0.4,0.2,0.3},因此把区间[0,1)分成4个子区间:[0,0.1),[0.1,0.5),[0.5,0.7),[0.7,1)

在HEVC中,零阶指数哥伦布编码用于视频参数集(VPS)、序列参数集(SPS)、图像参数集(PPS)和片头信息的参数的编码。而CABAC(上下文自适应的二进制算术编码)则用于其他参数以及残差系数的编码。

零阶指数哥伦布编码:
(1)由前缀和后缀组成,前缀的长度和后缀的长度满足:Lprefix - Lsuffix = 1.
(2)前缀具有一元码的形式,即:000...001,其中零的个数M等于:
M=log2(CodeNum + 1);
CodeNum由待编码的符号V算出:如果V大于0,那么CodeNum = 2V-1,否则CodeNum=-2V。
(3)后缀部分就是下面数值的二进制:CodeNum+1-2^M
 
CABAC的三个基本的步骤是:
(1)二进制化。一般采用截断莱斯二进制化(TR)、定长码(FL)、指数哥伦布码(EGK)(注意它也可以用来进行二进制化)等方法;下面介绍两个方法。
     ①截断莱斯二进制化。已知门限值cMax,莱斯参数R,语法元素的值V。截断莱斯码由前缀和后缀拼接而成。
     前缀值P=V>>R。如果P小于cMax>>R,那么前缀由P个1和一个0组成。如果P大于等于cMax>>R,则前缀由cMax>>R个1组成。
     当语法元素V大于等于cMax的时候没有后缀,当V小于cMax的时候,后缀值S=V-(P<<R),后缀码是S的二元化串,长度是R。
     ②定长二进制化。直接转换成定长的二进制符号串。
(2)上下文建模。一般情况下,不同的语法元素之间并不是完全独立的,因此可以根据已经编码的语法元素进行条件编码,这就是所谓的上下文。这些上下文信息通常做成表格。
     ①在编码过程中,语法元素使用的上下文概率模型都被唯一的上下文索引号r标识,每一个r涉及两个概率模型变量:最大概率符号MPS和概率状态索引。MPS表示待编码的Bin很有可能出现的符号(0或1);与之对应的,待编码的Bin不可能出现的符号即为最小概率符号LPS。
     ②在CABAC中,为LPS的概率设置了64个代表值,每一个都与LPS一一对应。
     ③编码器会初始化上下文模型的除号变量MPS和
     ④在获取初始的概率模型变量后,即可对当前符号(或语法元素)进行二元算数编码和概率模型参数更新,实现上下文自适应的编码。
     ⑤更新的方法:如果编码的符号等于MPS,那么通过查表更新 = transIdxMps();否则,如果为0,那么互换LPS和MPS然后更新,否则只更新=transIdxLps(
(3)二进制算术编码。有两种模式:常规模式,旁路模式。
     1、常规模式。假设当前编码器的区间长度是R,区间下限是L。
          ①计算索引值=(R>>6)&3
          ②查表得到LPS对应的子区间=rangeTabLps[][],那么=-
          ③如果当前的二进制符号Bin等于MPS,则作为下一个符号的编码区间R,下限L不变;如果Bin等于LPS,那么作为下一个符号的编码区间R,区间下限L要加上的长度。然后更加当前符号值更新上下文。
     2、旁路模式。这种模式无需对概率进行自适应更新,而是采用0和1概率各占0.5的固定概率进行编码。为了是区间划分更加简单,才用了保存编码区间长度不变,使下限L值加倍的方法来实现区间划分。


二、熵编码在HEVC中的应用

熵编码的方法有两种:
1、变长码。最常用的就是指数哥伦布编码,它可以分为无符号数的指数哥伦布编码ue(v)和有符号数的指数哥伦布编码se(v),常常用于HEVC中各种参数集以及slice头的编码。在HEVC中,变长码的具体实现是cavlc(基于上下文自适应变长编码)。
2、算数编码。最常用的就是cabac。cabac分为下面几个步骤:
(1)二进制化。这里需要注意了,二进制化有很多种方法,其中指数哥伦布编码也是一种二进制化的方法。
(2)上下文建模
(3)二进制算数编码

1、变长码(指数哥伦布编码)

以vps(视频参数集)的编码为例来说明:
// vps的格式需要参考官方文档
Void TEncCavlc::codeVPS( TComVPS* pcVPS )
{
	// 编码vps的id
	WRITE_CODE( pcVPS->getVPSId(),                    4,        "vps_video_parameter_set_id" );
	// 保留位,指定为3
	WRITE_CODE( 3,                                    2,        "vps_reserved_three_2bits" );
	// 保留的6个0比特
	WRITE_CODE( 0,                                    6,        "vps_reserved_zero_6bits" );
	// 时域层的数量
	WRITE_CODE( pcVPS->getMaxTLayers() - 1,           3,        "vps_max_sub_layers_minus1" );
	// 时域层嵌套标志
	WRITE_FLAG( pcVPS->getTemporalNestingFlag(),                "vps_temporal_id_nesting_flag" );
	assert (pcVPS->getMaxTLayers()>1||pcVPS->getTemporalNestingFlag());
	// 保留的16个比特(值都是0xffff)
	WRITE_CODE( 0xffff,                              16,        "vps_reserved_ffff_16bits" );
	// 对Profile Tier Level的信息进行编码
	codePTL( pcVPS->getPTL(), true, pcVPS->getMaxTLayers() - 1 );
	const Bool subLayerOrderingInfoPresentFlag = 1;
	// 时域层排序信息存在的标志?
	WRITE_FLAG(subLayerOrderingInfoPresentFlag,              "vps_sub_layer_ordering_info_present_flag");
	// 写入每个时域层的信息
	for(UInt i=0; i <= pcVPS->getMaxTLayers()-1; i++)
	{
		// 解码缓冲区的数量
		WRITE_UVLC( pcVPS->getMaxDecPicBuffering(i) - 1,       "vps_max_dec_pic_buffering_minus1[i]" );
		// 重排图像的数量(指定按照解码顺序在某一帧之后,而显示顺序在某一帧之前的帧的最大数量)
		WRITE_UVLC( pcVPS->getNumReorderPics(i),               "vps_num_reorder_pics[i]" );
		WRITE_UVLC( pcVPS->getMaxLatencyIncrease(i),           "vps_max_latency_increase_plus1[i]" );
		if (!subLayerOrderingInfoPresentFlag)
		{
			break;
		}
	}

	assert( pcVPS->getNumHrdParameters() <= MAX_VPS_NUM_HRD_PARAMETERS );
	assert( pcVPS->getMaxNuhReservedZeroLayerId() < MAX_VPS_NUH_RESERVED_ZERO_LAYER_ID_PLUS1 );
	WRITE_CODE( pcVPS->getMaxNuhReservedZeroLayerId(), 6,     "vps_max_nuh_reserved_zero_layer_id" );
	pcVPS->setMaxOpSets(1);
	// 操作集的数量?
	WRITE_UVLC( pcVPS->getMaxOpSets() - 1,                    "vps_max_op_sets_minus1" );
	for( UInt opsIdx = 1; opsIdx <= ( pcVPS->getMaxOpSets() - 1 ); opsIdx ++ )
	{
		// Operation point set
		for( UInt i = 0; i <= pcVPS->getMaxNuhReservedZeroLayerId(); i ++ )
		{
			// Only applicable for version 1
			pcVPS->setLayerIdIncludedFlag( true, opsIdx, i );
			WRITE_FLAG( pcVPS->getLayerIdIncludedFlag( opsIdx, i ) ? 1 : 0, "layer_id_included_flag[opsIdx][i]" );
		}
	}
	// 计时或者时域方面的信息
	TimingInfo *timingInfo = pcVPS->getTimingInfo();
	// 时域信息存在的标志
	WRITE_FLAG(timingInfo->getTimingInfoPresentFlag(),          "vps_timing_info_present_flag");
	// 写入计时或者时域信息
	if(timingInfo->getTimingInfoPresentFlag())
	{
		WRITE_CODE(timingInfo->getNumUnitsInTick(), 32,           "vps_num_units_in_tick");
		WRITE_CODE(timingInfo->getTimeScale(),      32,           "vps_time_scale");
		WRITE_FLAG(timingInfo->getPocProportionalToTimingFlag(),  "vps_poc_proportional_to_timing_flag");
		if(timingInfo->getPocProportionalToTimingFlag())
		{
			WRITE_UVLC(timingInfo->getNumTicksPocDiffOneMinus1(),   "vps_num_ticks_poc_diff_one_minus1");
		}
		pcVPS->setNumHrdParameters( 0 );
		WRITE_UVLC( pcVPS->getNumHrdParameters(),                 "vps_num_hrd_parameters" );

		if( pcVPS->getNumHrdParameters() > 0 )
		{
			pcVPS->createHrdParamBuffer();
		}
		for( UInt i = 0; i < pcVPS->getNumHrdParameters(); i ++ )
		{
			// Only applicable for version 1
			pcVPS->setHrdOpSetIdx( 0, i );
			WRITE_UVLC( pcVPS->getHrdOpSetIdx( i ),                "hrd_op_set_idx" );
			if( i > 0 )
			{
				WRITE_FLAG( pcVPS->getCprmsPresentFlag( i ) ? 1 : 0, "cprms_present_flag[i]" );
			}
			// 编码HRD(参考解码器)的参数
			codeHrdParameters(pcVPS->getHrdParameters(i), pcVPS->getCprmsPresentFlag( i ), pcVPS->getMaxTLayers() - 1);
		}
	}

	// 扩展标志
	WRITE_FLAG( 0,                     "vps_extension_flag" );

	//future extensions here..

	return;
}
指数哥伦布编码实现如下:
/*
** 有符号指数哥伦布编码
*/
Void SyntaxElementWriter::xWriteUvlc     ( UInt uiCode )
{
	UInt uiLength = 1;
	UInt uiTemp = ++uiCode;

	assert ( uiTemp );

	while( 1 != uiTemp )
	{
		uiTemp >>= 1;
		uiLength += 2;
	}
	// Take care of cases where uiLength > 32
	m_pcBitIf->write( 0, uiLength >> 1);
	m_pcBitIf->write( uiCode, (uiLength+1) >> 1);
}
/*
** 无符号哥伦布指数
*/
Void SyntaxElementWriter::xWriteSvlc     ( Int iCode )
{
	UInt uiCode;

	uiCode = xConvertToUInt( iCode );
	xWriteUvlc( uiCode );
}

2、算术编码(cabac)

2.1、二进制化
    因为经过了变换量化之后,很多语法元素的值都是0或者1,因此大部分语法元素都不需要进行二进制化,只有很少的一部分才需要进行处理,例如:MVD、DeltaQP等。HEVC中涉及二进制化的函数有三个,分别是下面的三个:
/*
** 各种符号的编码算法,比如:指数哥伦布,截断码什么的,具体什么符号用什么算法进行编码,需要参考hevc的官方文档
** 内部实际还是会调用encodeBin进行二进制的编码
** 这个函数没有被使用
*/
Void TEncSbac::xWriteUnarySymbol( UInt uiSymbol, ContextModel* pcSCModel, Int iOffset )
{
	m_pcBinIf->encodeBin( uiSymbol ? 1 : 0, pcSCModel[0] );

	if( 0 == uiSymbol)
	{
		return;
	}

	while( uiSymbol-- )
	{
		m_pcBinIf->encodeBin( uiSymbol ? 1 : 0, pcSCModel[ iOffset ] );
	}

	return;
}

/*
** 定长二元化
*/
Void TEncSbac::xWriteUnaryMaxSymbol( UInt uiSymbol, ContextModel* pcSCModel, Int iOffset, UInt uiMaxSymbol )
{
	if (uiMaxSymbol == 0)
	{
		return;
	}

	m_pcBinIf->encodeBin( uiSymbol ? 1 : 0, pcSCModel[ 0 ] );

	if ( uiSymbol == 0 )
	{
		return;
	}

	Bool bCodeLast = ( uiMaxSymbol > uiSymbol );

	while( --uiSymbol )
	{
		m_pcBinIf->encodeBin( 1, pcSCModel[ iOffset ] );
	}
	if( bCodeLast )
	{
		m_pcBinIf->encodeBin( 0, pcSCModel[ iOffset ] );
	}

	return;
}

/*
** 指数哥伦布编码
*/
Void TEncSbac::xWriteEpExGolomb( UInt uiSymbol, UInt uiCount )
{
	UInt bins = 0;
	Int numBins = 0;

	while( uiSymbol >= (UInt)(1<<uiCount) )
	{
		bins = 2 * bins + 1;
		numBins++;
		uiSymbol -= 1 << uiCount;
		uiCount  ++;
	}
	bins = 2 * bins + 0;
	numBins++;

	bins = (bins << uiCount) | uiSymbol;
	numBins += uiCount;

	assert( numBins <= 32 );
	m_pcBinIf->encodeBinsEP( bins, numBins );
}
2.2、上下文模型初始参数的选择(即初始化)
    在看HM的源码的时候我们都知道,每个语法元素都需要上下文模型参数,但是我们看到每个语法元素对应的上下文表格都有多个参数值(这里的表格就是2维数组),而且通过观察可以发现每个表格的第一个维度都是3,例如:
/* 3x1大小的表格 */
static const UChar INIT_CU_TRANSQUANT_BYPASS_FLAG[3][NUM_CU_TRANSQUANT_BYPASS_FLAG_CTX] =
{
	{ 154 }, 
	{ 154 }, 
	{ 154 }, 
};
/* 3x4大小的表格 */
static const UChar INIT_PART_SIZE[3][NUM_PART_SIZE_CTX] =  
{
	{ 154,  139,  154,  154 },
	{ 154,  139,  154,  154 },
	{ 184,  CNU,  CNU,  CNU },
};
这是因为,slice的类型有3种(I、P、B),slice类型不同的时候应该对应不同的上下文参数初始值;至于第二个维度,大小可以从1到N,这是因为有的语法元素可能会有多个不同的值(或者候选值),例如PartSize(可以选择2Nx2N、NxN、2NxN等值),因此第二个维度的值是语法元素不同的值(或者不同的模式等)对应的上下文初始参数,具体可以看下面的例子:
Void TEncSbac::codePartSize( TComDataCU* pcCU, UInt uiAbsPartIdx, UInt uiDepth )
{
	// 获取尺寸
	PartSize eSize         = pcCU->getPartitionSize( uiAbsPartIdx );

	// 是否为帧内预测模式
	if ( pcCU->isIntra( uiAbsPartIdx ) )
	{
		if( uiDepth == g_uiMaxCUDepth - g_uiAddCUDepth )
		{
			// 二值化编码
			m_pcBinIf->encodeBin( eSize == SIZE_2Nx2N? 1 : 0, m_cCUPartSizeSCModel.get( 0, 0, 0 ) );
		}
		// 如果是帧内模式,到这里就已经处理完了
		return;
	}

	switch(eSize)
	{
	case SIZE_2Nx2N:
		{
			m_pcBinIf->encodeBin( 1, m_cCUPartSizeSCModel.get( 0, 0, 0) );
			break;
		}
	case SIZE_2NxN:
	case SIZE_2NxnU:
	case SIZE_2NxnD:
		{
			m_pcBinIf->encodeBin( 0, m_cCUPartSizeSCModel.get( 0, 0, 0) ); // 注意这里!!!
			m_pcBinIf->encodeBin( 1, m_cCUPartSizeSCModel.get( 0, 0, 1) );
			if ( pcCU->getSlice()->getSPS()->getAMPAcc( uiDepth ) )
			{
				if (eSize == SIZE_2NxN)
				{
					m_pcBinIf->encodeBin(1, m_cCUPartSizeSCModel.get( 0, 0, 3 ));
				}
				else
				{
					m_pcBinIf->encodeBin(0, m_cCUPartSizeSCModel.get( 0, 0, 3 ));
					m_pcBinIf->encodeBinEP((eSize == SIZE_2NxnU? 0: 1));
				}
			}
			break;
		}
	case SIZE_Nx2N:
	case SIZE_nLx2N:
	case SIZE_nRx2N:
		{
			m_pcBinIf->encodeBin( 0, m_cCUPartSizeSCModel.get( 0, 0, 0) );
			m_pcBinIf->encodeBin( 0, m_cCUPartSizeSCModel.get( 0, 0, 1) );
			if( uiDepth == g_uiMaxCUDepth - g_uiAddCUDepth && !( pcCU->getWidth(uiAbsPartIdx) == 8 && pcCU->getHeight(uiAbsPartIdx) == 8 ) )
			{
				m_pcBinIf->encodeBin( 1, m_cCUPartSizeSCModel.get( 0, 0, 2) );
			}
			if ( pcCU->getSlice()->getSPS()->getAMPAcc( uiDepth ) )
			{
				if (eSize == SIZE_Nx2N)
				{
					m_pcBinIf->encodeBin(1, m_cCUPartSizeSCModel.get( 0, 0, 3 ));
				}
				else
				{
					m_pcBinIf->encodeBin(0, m_cCUPartSizeSCModel.get( 0, 0, 3 ));
					m_pcBinIf->encodeBinEP((eSize == SIZE_nLx2N? 0: 1));
				}
			}
			break;
		}
	case SIZE_NxN:
		{
			if( uiDepth == g_uiMaxCUDepth - g_uiAddCUDepth && !( pcCU->getWidth(uiAbsPartIdx) == 8 && pcCU->getHeight(uiAbsPartIdx) == 8 ) )
			{
				m_pcBinIf->encodeBin( 0, m_cCUPartSizeSCModel.get( 0, 0, 0) );
				m_pcBinIf->encodeBin( 0, m_cCUPartSizeSCModel.get( 0, 0, 1) );
				m_pcBinIf->encodeBin( 0, m_cCUPartSizeSCModel.get( 0, 0, 2) );
			}
			break;
		}
	default:
		{
			assert(0);
		}
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值