熵编码

熵编码部分看的:http://blog.csdn.net/nb_vol_1/article/details/71374859,因博主禁止转载,请点链接去看原博主文章,写的很清晰明了,学习后整理内容如下,方便以后查看。

熵编码:

消息发生的概率越小,携带的信息量就越大。确定的消息没有信息量,因为你已经知道要发生什么了。信息量定义:

由以上性质可知,序列中依赖性越强,不确定性就越弱。

二进制编码:

变长编码给大概率分配短码字,给小概率分配长码字,目标是使平均长最小。

指数哥伦布编码:

编码流程:

解码流程:记录0个数为m,哥伦布编码阶数为k,第一个1之后的值记为value,

解码值= 2m+k次幂) - 2m次幂) + value

零阶指数哥伦布编码:
  假设输入值是N
1、计算N += 1
  2、把N转换成二进制串bins,计算二进制串的长度,假设是M,那么在bins的前面添加M-10
 假设输入值N对应的二进制串的长度是len,那么零阶哥伦布码的长度是2*len-1
哥伦布码golomb = 前缀prefix + 后缀suffix
    前缀prefix : len - 10
    后缀suffix : (N+1)对应的二进制串

HEVC中的熵编码:

HEVC的熵编码使用了两种算术编码:CABACCAVLCCAVLC主要用于编码SEI、参数集、片头等,剩下的所有数据和语法元素均使用CABAC来编码。

CAVLC中的编码方法:

1、零阶无符号哥伦布指数编码
 2、零阶有符号指数哥伦布编码
   3、不编码直接写入若干比特
   4、不编码直接写入一个比特

//无符号指数哥伦布编码
Void SyntaxElementWriter::xWriteUvlc     ( UInt uiCode )
{
  UInt uiLength = 1;//哥伦布码的长度
  UInt uiTemp = ++uiCode;//执行UITemp= UICode + 1
  
  assert ( uiTemp );
  
  while( 1 != uiTemp )//计算长度,若uiTemp长度为len,则uiLength=2*len-1
  {
    uiTemp >>= 1;
    uiLength += 2;
  }
  // Take care of cases where uiLength > 32
  m_pcBitIf->write( 0, uiLength >> 1);//写入前缀,len-1个零
  m_pcBitIf->write( uiCode, (uiLength+1) >> 1);//写入后缀,uiTemp的二进制。
}

将哥伦布码写入比特流:

写入比特流中的流程:

    1TComOutputBitstream包含两个部分:缓冲区和比特流
    2、比特流就是已经处理完成的数据,存放在一个vector
    3、缓冲区暂存还没有写入比特流中的数据,TComOutputBitstream使用一个uchar类型的数据(8 bit)作为缓存区,因为很多时候写入的数据长度只有若干比特,不能直接写入比特流中,需要等到缓冲区满(达到8bit),才写入
    3m_num_held_bits表示缓冲区中已有的比特数
    4num_total_bits表示写入数据之后,缓冲区中总的比特数(可能会溢出,后面会解决这个问题)
    5next_num_held_bits有两种意思:
        1)如果缓冲区没有溢出,那么它表示缓冲区中总的比特数(已有+新增)
        2)如果缓冲区溢出,那么它表示数据占用完缓冲区后还需要的比特数,只能存放数据的一部分,剩下那部分需要等缓冲区的数据写入比特流之后再存放
    6next_held_bits是格式化之后的数据
        1)如果缓冲区不溢出,那它表示将要写入缓冲区中的数据
        2)如果缓冲区溢出,那它表示将一部分数据写入缓冲区之后,剩下的那部分数据
    7、判断num_total_bits是否大于8,即判断新增数据之后,缓冲区是否会溢出
    8、如果缓冲区不溢出,那么把数据写入缓冲区中,然后返回
    9、如果缓冲区溢出,那么先写一部分数据到缓冲区中,然后把缓冲区写入比特流中,清空缓冲区,继续把剩余的数据写入缓冲区中

Void TComOutputBitstream::write   ( UInt uiBits, UInt uiNumberOfBits )
{
  assert( uiNumberOfBits <= 32 );
  assert( uiNumberOfBits == 32 || (uiBits & (~0 << uiNumberOfBits)) == 0 );

  /* any modulo 8 remainder of num_total_bits cannot be written this time,
   * and will be held until next time. */
  //m_num_held_bits缓冲区中已有的比特数
  //num_total_bits写入uiBits之后,缓冲区中的总比特数。
  UInt num_total_bits = uiNumberOfBits + m_num_held_bits;
  //next_num_held_bits表示缓冲区去中使用的比特数
  UInt next_num_held_bits = num_total_bits % 8;

  /* form a byte aligned word (write_bits), by concatenating any held bits
   * with the new bits, discarding the bits that will form the next_held_bits.
   * eg: H = held bits, V = n new bits        /---- next_held_bits
   * len(H)=7, len(V)=1: ... ---- HHHH HHHV . 0000 0000, next_num_held_bits=0
   * len(H)=7, len(V)=2: ... ---- HHHH HHHV . V000 0000, next_num_held_bits=1
   * if total_bits < 8, the value of v_ is not used */
  //将数据执行移位操作,放入next_held_bits中
  UChar next_held_bits = uiBits << (8 - next_num_held_bits);

  //判断比特数是否大于8,若大于,表示缓冲区满,要先写入比特流中
  if (!(num_total_bits >> 3))
  {//不大于8,写入缓冲区尾部
    /* insufficient bits accumulated to write out, append new_held_bits to
     * current held_bits */
    /* NB, this requires that v only contains 0 in bit positions {31..n} */
    m_held_bits |= next_held_bits;
    m_num_held_bits = next_num_held_bits;
    return;
  }
  
  /* topword serves to justify held_bits to align with the msb of uiBits */
  //写入部分数据
  UInt topword = (uiNumberOfBits - next_num_held_bits) & ~((1 << 3) -1);
  UInt write_bits = (m_held_bits << topword) | (uiBits >> next_num_held_bits);
  //判断num_total_bits的长度:32,24,16,8
  switch (num_total_bits >> 3)
  {
  case 4: m_fifo->push_back(write_bits >> 24);
  case 3: m_fifo->push_back(write_bits >> 16);
  case 2: m_fifo->push_back(write_bits >> 8);
  case 1: m_fifo->push_back(write_bits);
  }
  //更新计数参数
  m_held_bits = next_held_bits;
  m_num_held_bits = next_num_held_bits;
}

CABAC:

CABAC(上下文自适应的二进制算术编码)基于算术编码,在HEVC中,除了参数集、SEIslice头部之外,其余的所有数据都使用CABAC来进行熵编码。

     CABAC有三个步骤:

   1、初始化,构建上下文概率模型
     2、根据上下文概率模型获取语法元素的概率,对语法元素进行熵编码
   3、根据编码结果更新上下文概率模型

1.初始化

 

    初始化上下文模型,就是指初始化和上下文模型有关的两个变量:MPSδMPS是最大概率符号,它表示待编码符号可能出现符号,对于二进制算术编码来说,MPS0或者1,相反,LPS表示待编码符号不可能出现的符号,对于二进制算术编码来说,LPS也是01δ表示概率的状态索引,它的值与LPS的概率值是相对应的,δ值随着LPS概率的更新而变化。MPSδ唯一的确定上下文模型的状态,以及后续如何对上下模型进行更新。
    计算MPSδ需要一个初始值initValueinitValue的值与slice的类型、初始量化参数有关。HEVC为每一个语法元素都定义了不同的initValue,为了方便,可以通过slice的类型和量化参数查表来得到initValue的值。initValue表示起始概率值。

    我们以MPSδ来表示上下文概率模型,或者说,上下文概率模型的主要参数是MPSδ

 

初始化的入口函数

    它的主要功能是:获取QPslice的类型,然后调用ContextModel3DBuffer::initBuffer进行上下文概率模型的初始化。


Void TEncSbac::resetEntropy           ()
{
  Int  iQp              = m_pcSlice->getSliceQp();
  SliceType eSliceType  = m_pcSlice->getSliceType();
  
  Int  encCABACTableIdx = m_pcSlice->getPPS()->getEncCABACTableIdx();
  //帧内,Bslice,
  if (!m_pcSlice->isIntra() && (encCABACTableIdx==B_SLICE || encCABACTableIdx==P_SLICE) && m_pcSlice->getPPS()->getCabacInitPresentFlag())
  {
    eSliceType = (SliceType) encCABACTableIdx;
  }
  //初始化各个模型的缓存
  m_cCUSplitFlagSCModel.initBuffer       ( eSliceType, iQp, (UChar*)INIT_SPLIT_FLAG );
  //split标志的上下文
  m_cCUSkipFlagSCModel.initBuffer        ( eSliceType, iQp, (UChar*)INIT_SKIP_FLAG );
  //skip标志的上下文
  //merge标志、索引上下文
  m_cCUMergeFlagExtSCModel.initBuffer    ( eSliceType, iQp, (UChar*)INIT_MERGE_FLAG_EXT);
  m_cCUMergeIdxExtSCModel.initBuffer     ( eSliceType, iQp, (UChar*)INIT_MERGE_IDX_EXT);
  //PartSize上下文
  m_cCUPartSizeSCModel.initBuffer        ( eSliceType, iQp, (UChar*)INIT_PART_SIZE );
  //预测上下文
  m_cCUPredModeSCModel.initBuffer        ( eSliceType, iQp, (UChar*)INIT_PRED_MODE );
  //帧内预测
  m_cCUIntraPredSCModel.initBuffer       ( eSliceType, iQp, (UChar*)INIT_INTRA_PRED_MODE );
  //色度预测
  m_cCUChromaPredSCModel.initBuffer      ( eSliceType, iQp, (UChar*)INIT_CHROMA_PRED_MODE );
  //帧间角度
  m_cCUInterDirSCModel.initBuffer        ( eSliceType, iQp, (UChar*)INIT_INTER_DIR );
  //mv残差
  m_cCUMvdSCModel.initBuffer             ( eSliceType, iQp, (UChar*)INIT_MVD );
  m_cCURefPicSCModel.initBuffer          ( eSliceType, iQp, (UChar*)INIT_REF_PIC );
  m_cCUDeltaQpSCModel.initBuffer         ( eSliceType, iQp, (UChar*)INIT_DQP );
  m_cCUQtCbfSCModel.initBuffer           ( eSliceType, iQp, (UChar*)INIT_QT_CBF );
  m_cCUQtRootCbfSCModel.initBuffer       ( eSliceType, iQp, (UChar*)INIT_QT_ROOT_CBF );
  //符号上下文
  m_cCUSigCoeffGroupSCModel.initBuffer   ( eSliceType, iQp, (UChar*)INIT_SIG_CG_FLAG );
  m_cCUSigSCModel.initBuffer             ( eSliceType, iQp, (UChar*)INIT_SIG_FLAG );
  //最后一个X
  m_cCuCtxLastX.initBuffer               ( eSliceType, iQp, (UChar*)INIT_LAST );
  //最后一个Y
  m_cCuCtxLastY.initBuffer               ( eSliceType, iQp, (UChar*)INIT_LAST );
  m_cCUOneSCModel.initBuffer             ( eSliceType, iQp, (UChar*)INIT_ONE_FLAG );
  m_cCUAbsSCModel.initBuffer             ( eSliceType, iQp, (UChar*)INIT_ABS_FLAG );
  m_cMVPIdxSCModel.initBuffer            ( eSliceType, iQp, (UChar*)INIT_MVP_IDX );
  m_cCUTransSubdivFlagSCModel.initBuffer ( eSliceType, iQp, (UChar*)INIT_TRANS_SUBDIV_FLAG );
  m_cSaoMergeSCModel.initBuffer      ( eSliceType, iQp, (UChar*)INIT_SAO_MERGE_FLAG );
  m_cSaoTypeIdxSCModel.initBuffer        ( eSliceType, iQp, (UChar*)INIT_SAO_TYPE_IDX );
  m_cTransformSkipSCModel.initBuffer     ( eSliceType, iQp, (UChar*)INIT_TRANSFORMSKIP_FLAG );
  m_CUTransquantBypassFlagSCModel.initBuffer( eSliceType, iQp, (UChar*)INIT_CU_TRANSQUANT_BYPASS_FLAG );
  // new structure
  m_uiLastQp = iQp;
  //二值化的操作
  m_pcBinIf->start();
  
  return;
}

根据initValue和量化参数计算MPSδ

    在下面的函数中,slopeoffsetinitState都是中间变量,mpState表示MPSm_ucState表示δ

获取概率进行熵编码

    语法元素对应的上下文模型初始化完成之后,开始进行二进制算术编码。二进制算术编码是对语法元素对应的二进制比特串进行算术编码。二进制算术编码包含两种方式:旁路方式和常规方式。在旁路编码方式中,二进制串的符号的概率是相同的,也不需要更新上下文概率模型;在常规方式中,二进制串中符号的概率可以由上下文模型中得到,对每一个符号编码完成之后都需要对上下文模型进行更新。使用常规方式还是旁路方式是由语法元素决定的,HEVC文档指明了哪些语法元素使用旁路方式哪些语法元素使用常规方式。

    在对语法元素进行编码之前需要对它进行二进制化。

 

二进制化

    理论上,HEVC的二进制方法有:
    1、一元码 
    2、截断一元码 
    3、指数哥伦布码
    4、截断莱斯码
    5、定长码

    由于在实际中,由于很多语法元素的值都是0或者1,因此,很多语法元素不需要二进制化就可以直接进行编码,只有少部分才会进行二进制化。例如mvp-indexdelta-qp等语法元素使用截断一元码进行二进制化;mvd等语法元素使用指数哥伦布来进行二进制化。

一元码

    假设语法的元素值是x,那么它对应的一元码由前缀x1和后缀一个0构成:11...10。假设x=5,那么它的一元码是111110

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;
}

截断一元码:

    1、把语法元素之转换成一元码,假如语法元素值是x,那么它的一元码由起始的x1和最后一个0组成。
    2、给定一个最大的可能值cMaxbins的长度不能超过cMax,如果超过,那么就对bins的尾部进行截断
    3、例如,给定一个语法元素的值是5,cMax4
        15对应的一元码是111110
        2)由于一元码的长度大于cMax,因此需要对它进行截断
        3)截断之后为1111,因此5对应的截断一元码是1111(当cMax等于4时)

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 );
  // 编码x个1(一元码)
  while( --uiSymbol )
  {
    m_pcBinIf->encodeBin( 1, pcSCModel[ iOffset ] );
  }
  // 不需要截断,直接在后面添加0
  if( bCodeLast )
  {
    m_pcBinIf->encodeBin( 0, pcSCModel[ iOffset ] );
  }
  
  return;
}

k阶指数哥伦布码

    它由前缀和后缀构成,前缀和一元码有点相似,由p1和一个0构成,其中p=log2[x/(2^k)+1];后缀是q对应的二进制数,其中q=x+2^k*(1-2^p)HEVC中最常用的是1阶指数哥伦布码。

//指数哥伦布编码
Void TEncSbac::xWriteEpExGolomb( UInt uiSymbol, UInt uiCount )
{//uiSymbol是元素值,uiCount 是阶数
  UInt bins = 0;
  Int numBins = 0;
  
  while( uiSymbol >= (UInt)(1<<uiCount) )
  {//去掉后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 );
}

 1、更新上下文模型。上下文模型的更新主要是对上下文模型的δMPS变量进行更新。如果bin等于MPS,那么δ_new=transMPS[δ];否则δ_new=transLPS[δ],而且如果δ等于0,需要互换MPSLPS
    2、更新编码区间。主要是移动编码区间的下界low或者重新计算区间的长度length。首先计算LPS的子区间的长度length_lps=rangeLPS[δ][(length?6)&3],对应的MPS的子区间的长度length_mps=length-length_lps;如果比特符号xMPS,那么区间下界low不变,length更新为length_mps;否则,low=low+length_mpslength更新为length_lps

encodeBin中的编码流程:

 编码流程:
    1、首先计算LPS对应的子区间的长度,通过查表得到:length_lps=rangeLPS[δ][(length?6)&3],对应的代码是:

UInt  uiLPS   = TComCABACTables::sm_aucLPSTable[ rcCtxModel.getState() ][ ( m_uiRange >> 6 ) & 3 ];  

其中rcCtxModel.getState()返回δ
    2、计算MPS对应的子区间的长度:m_uiRange    -= uiLPS;
    3、如果二进制符号不等于MPSlow=low+length_mpslength更新为length_lps

    4、如果二进制符号等于MPS,下界low不变,length更新为length_mps

//1.该比特对应的上下文模型设置为已编码
//2.计算LPS(最低概率值)
//3.计算新的范围
//4.根据比特值是否和MPS相等,更新上下文模式(最大概率值)。
Void TEncBinCABAC::encodeBin( UInt binValue, ContextModel &rcCtxModel )
{
    //调试的打印信息
  {
    DTRACE_CABAC_VL( g_nSymbolCounter++ )
    DTRACE_CABAC_T( "\tstate=" )
    DTRACE_CABAC_V( ( rcCtxModel.getState() << 1 ) + rcCtxModel.getMps() )
    DTRACE_CABAC_T( "\tsymbol=" )
    DTRACE_CABAC_V( binValue )
    DTRACE_CABAC_T( "\n" )
  }
  
  m_uiBinsCoded += m_binCountIncrement;//比特计数
  //设置已编码标志
  rcCtxModel.setBinsCoded( 1 );
  //查表获取LPS对应的子区间长度
  UInt  uiLPS   = TComCABACTables::sm_aucLPSTable[ rcCtxModel.getState() ][ ( m_uiRange >> 6 ) & 3 ];
  //m_uiRange表示MPS对应的子区间长度
  m_uiRange    -= uiLPS;
  //如果二进制符号不等于MPS
  if( binValue != rcCtxModel.getMps() )
  {//binVal != valMPS 索引概率值将减小,即LPS的概率增大
      //numBit用于归一化
    Int numBits = TComCABACTables::sm_aucRenormTable[ uiLPS >> 3 ];
    //更新low=low+length_mps,使用<<numBits的目的是重归一化
    m_uiLow     = ( m_uiLow + m_uiRange ) << numBits;
    m_uiRange   = uiLPS << numBits;
    rcCtxModel.updateLPS();//用LPS对δ进行更新
    
    m_bitsLeft -= numBits;
  }
  else//binVal == valMPS,概率索引值将增大,即LPS的概率减小
  {//下界low不变,length更新为length_mps(m_uiRange已经等于length_mps)
    rcCtxModel.updateMPS();//更新δ
    if ( m_uiRange >= 256 )//大于256不用归一化
    {
      return;
    }
    
    m_uiLow <<= 1;
    m_uiRange <<= 1;
    m_bitsLeft--;
  }
  // 尝试写到比特流中,先判断当前缓冲区中的空闲空间是否足够,不足的话就写到比特流中,腾出空间  
  testAndWriteOut();
}




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章