H.266/VVC技术学习:算术编码

算术编码是一种常用的变长编码方法,和Huffman编码类似,也是对出现概率大的符号赋予短码,出现概率小的符号赋予长码,但算术编码不是简单的将每个信源符号映射成一个码字,而是对整个输入序列分配一个码字,所以平均意义上可以为每个信源符号分配长度小于1的码字。

算术编码的原理参考:熵编码:算术编码

H.266/VVC中采用的是基于上下文的自适应二进制算术编码(Context-based Adaptive Binary Arithmetic Coding,CABAC)。CABAC将二进制算术编码和上下文模型结合起来,其主要特点为:

(1)采用二进制算术编码,将所有语法元素转化为二进制串,消除了乘法运算操作,降低了计算复杂度,提高了编码效率

(2)建立上下文模型,充分利用了符号间的相关性,根据已编码的符号自适应的进行模型更新,进一步提高了编码效率。

VVC并不是对所有语法元素使用算术编码的方法,而是对于不同的语法元素使用不同的编码方法。如对比特流高层特性的语法元素,本身信息量较小,采用定长编码;对于比特流中比例较大的残差系数等信息,使用CABAC编码。

CABAC主要经过以下三个步骤:

  1. 二进制化
  2. 上下文建模
  3. 二进制算术编码

CABAC首先对输入的非二进制语法元素进行二进制化处理,将其唯一地转换为二进制串。如果输入的本身就是二进制语法元素,则可以跳过二进制化步骤。二进制化之后,进入二进制算术编码阶段。二进制算术编码过程中分为常规编码模式和旁路编码模式。在常规编码模式中,首先根据先前编码的语法元素为其选择一个上下文模型,接着,将上下文模型和二进制值一起送到常规编码器中进行编码,并将结果输出到码流中,同时,根据当前编码的二进制值更新上下文模型。在旁路编码模式中,无需对概率模型进行更新,将二进制值0和1看作等概分布,使用各占1/2的固定概率进行编码。

1、二进制化

CABAC仅对二进制符号0或1进行编码,因此对于非二进制符号需要将其转换为二进制串。VVC中常用的二进制化方法包括截断莱码(truncated Rice (TR) )、截断二进制码(truncated binary (TB) )、k阶指数哥伦布码(the k-th order Exp-Golomb (EGk) binarization )和定长码( fixed-length (FL))。
H.266/VVC中的二进制化参考:H.266/VVC熵编码之二进制化 

2、二进制算术编码

算术编码流程:将输入的语法元素经过二进制化转换为一个或者多个bin值,根据bin值对应的ctxId选择上下文模型,并根据当前的bin值选择上下文模型,编码完后,再根据bin值更新上下文模型(更新区间起始点和区间宽度)。

参数介绍:

  • m_low:区间起始点(区间左端点)
  • m_range:区间宽度
  • ctxId:模型对应的索引号
  • lps:小概率符号
  • mps:大概率符号
  • LPS:小概率符号对应的区间宽度
  • MPS:大概率符号对应的区间宽度
  • m_state[0],m_state[1]:模型状态概率,H.266 中采用多概率模型。 
  • m_rate:用于更新模型状态概率

CABAC的算术编码的编码区间宽度m_range的取值为(2^{8},2^{9}),通常区间左侧子区间为mps对应的子区间,右侧子区间为lps对应的子区间。

CABAC的常规编码流程:

  1. 确定区间起始点m_low和区间宽度m_range
  2. 根据ctxId选择相应的上下文模型,再根据m_range和模型对应的状态概率m_state确定LPS,更新m_range=m_range-LPS
  3. 根据模型状态概率确定mps,再判断待编码的bin值是mps还是lps
  4. 更新m_low和m_range,如果bin=lps,则m_Low=m_Low+m_Range,m_range=LPS;若bin=mps,则m_Low=m_Low,m_Range=m_Range
  5. 根据bin值和m_rate更新上下文模型状态概率
  6. 如果新的编码区间宽度m_range超过了(2^{8},2^{9})的范围,需要对m_range进行重归一化,即对m_low和m_range同时左移一定位数,并根据需要输出一定比特。

VVC解码端的常规编码流程如下图所示:

(其中pStateidx1和pStateIdx2指代m_state[0],m_state[1],ivlCurrRange指代m_range,valMps指代mps,ivlLpsRange指代LPS,ivlOffset指代m_low)

 VVC解码端的重归一化流程如下图所示:

VTM中代码实现:

template <class BinProbModel>
void TBinEncoder<BinProbModel>::encodeBin( unsigned bin, unsigned ctxId ) //常规编码
{
  BinCounter::addCtx( ctxId );
  BinProbModel& rcProbModel = m_Ctx[ctxId]; //获得相应上下文模型
  uint32_t      LPS         = rcProbModel.getLPS( m_Range ); //获得LPS的区间长度

  DTRACE( g_trace_ctx, D_CABAC, "%d" " %d " "%d" "  " "[%d:%d]" "  " "%2d(MPS=%d)"  "  " "  -  " "%d" "\n", DTRACE_GET_COUNTER( g_trace_ctx, D_CABAC ), ctxId, m_Range, m_Range - LPS, LPS, ( unsigned int ) ( rcProbModel.state() ), bin == rcProbModel.mps(), bin );

  m_Range   -=  LPS; //更新 range = range - LPS(即MPS)
  if( bin != rcProbModel.mps() ) //如果当前符号是lps
  {
    int numBits   = rcProbModel.getRenormBitsLPS( LPS ); // 通过查表获取需要左移的位数(用于重归一化)
    m_bitsLeft   -= numBits;
    m_Low        += m_Range; //移动m_low
    m_Low         = m_Low << numBits;
    m_Range       = LPS   << numBits;
    if( m_bitsLeft < 12 ) // m_bitsLeft 表示存储(m_Low)的寄存器中还剩余多少 bit
    {
      writeOut();
    }
  }
  else //如果当前符号是mps,则不需要更新m_low和m_range
  {
    if( m_Range < 256 ) // 如果更新后的区间宽度小于256,则需要重归一化
    {
      int numBits   = rcProbModel.getRenormBitsRange( m_Range ); // 获取需要左移的位数:1
      m_bitsLeft   -= numBits;
      m_Low       <<= numBits; // 输出比特
      m_Range     <<= numBits; // 扩大区间宽度
      if( m_bitsLeft < 12 )
      {
        writeOut();
      }
    }
  }
  rcProbModel.update( bin ); //更新模型概率状态
  BinEncoderBase::m_BinStore.addBin( bin, ctxId );
}

 其中,getLPS函数用来得到lps的区间宽度,mps函数用来计算大概率符号值

  uint8_t getLPS(unsigned range) const
  {
    uint16_t q = state();
    if (q & 0x80) //如果 q > 128
      q = q ^ 0xff; //q按位取反后小于127 
    return ((q >> 2) * (range >> 5) >> 1) + 4; // 返回LPS的区间长度
  }
  uint8_t state() const { return (m_state[0] + m_state[1]) >> 8; } // 表示P(1)的概率 pState
  uint8_t mps() const { return state() >> 7; } // valMps 返回MPS的值0 or 1

上下文模型的初始化与更新

在算术编码开始前,需要初始化上下文模型,上下文模型的初始化是为了利用每个模型的 initValue 得到一个初始的模型概率状态(m_state[0], m_state[1]),并在模型中加入了一个窗口参数(WindowSize),由此得到了一个控制上下文模型更新的参数 m_rate。

void BinProbModel_Std::init( int qp, int initId )
{
  int slope = (initId >> 3) - 4;
  int offset = ((initId & 7) * 18) + 1;
  int inistate = ((slope   * (qp - 16)) >> 1) + offset;
  int state_clip = inistate < 1 ? 1 : inistate > 127 ? 127 : inistate;
  const int p1 = (state_clip << 8);
  m_state[0]   = p1 & MASK_0;
  m_state[1]   = p1 & MASK_1;
}
  void setLog2WindowSize(uint8_t log2WindowSize) //根据WindowSize设置pstate的更新速率
  {
    int rate0 = 2 + ((log2WindowSize >> 2) & 3);
    int rate1 = 3 + rate0 + (log2WindowSize & 3);
    m_rate    = 16 * rate0 + rate1;
    CHECK(rate1 > 9, "Second window size is too large!");
  }

上下文模型的更新主要是根据当前编码bin值和更新速率m_rate更新m_state[0],m_state[1]。

 update函数用来更新模型状态概率

  void update(unsigned bin) // 上下文状态概率更新过程
  {
    int rate0 = m_rate >> 4;
    int rate1 = m_rate & 15;

    m_state[0] -= (m_state[0] >> rate0) & MASK_0;
    m_state[1] -= (m_state[1] >> rate1) & MASK_1;
    if (bin)
    {
      m_state[0] += (0x7fffu >> rate0) & MASK_0;
      m_state[1] += (0x7fffu >> rate1) & MASK_1;
    }
  }

3、旁路编码

旁路编码假定符号0或1各占1/2的概率进行编码,其中 0 区间在前,1 区间在后,同时不需要对概率模型进行更新。

为了使区间划分更加简单,不采用直接对区间长度二等分的方法,而是采用保持编码区间长度不变方法使区间下限 m_low加倍的方法实现区间划分,可以直接通过将 m_low左移 1 位实现,这样既达到了同样的效果又省去了在重归一化过程中同时对 m_range 和 m_low 进行的加倍操作。

VVC解码端的旁路编码流程如下:

 VTM代码实现:

void BinEncoderBase::encodeBinEP( unsigned bin )
{
  DTRACE( g_trace_ctx, D_CABAC, "%d" "  " "%d" "  EP=%d \n", DTRACE_GET_COUNTER( g_trace_ctx, D_CABAC ), m_Range, bin );

  BinCounter::addEP();
  // 这里,由于m_range范围为0-510,旁路编码0和1各占一半,因此每编码一次bin需要进行一次重归一化
  // 左移位数为 1,并输出 1 个比特。这里为了更简便,将对 m_Low 的移位操作提前,并保持m_Range 不变。
  m_Low <<= 1;
  if( bin )// 如果编码的符号是1的话(旁路编码中,0的区间在前,1的区间在后,因此需要移动m_low)
  {
    m_Low += m_Range;
  }
  m_bitsLeft--;
  if( m_bitsLeft < 12 )
  {
    writeOut();
  }
}

其中常规编码和旁路编码的最后,当 m_bitsLeft<12 都会调用writeOut函数进行比特输出
VTM代码具体如下:

void BinEncoderBase::writeOut()
{
  unsigned leadByte = m_Low >> ( 24 - m_bitsLeft ); // 将m_low的高8位存在LeadByte中
  m_bitsLeft       += 8;
  m_Low            &= 0xffffffffu >> m_bitsLeft;
  if( leadByte == 0xff )
  {
    m_numBufferedBytes++;
  }
  else
  {
    if( m_numBufferedBytes > 0 )
    {
      unsigned carry  = leadByte >> 8;
      unsigned byte   = m_bufferedByte + carry; // 将上次缓存的8位bit输出
      m_bufferedByte  = leadByte & 0xff;// 将本次m_low的高8位存入缓存器中
      m_Bitstream->write( byte, 8 ); //输出
      byte            = ( 0xff + carry ) & 0xff;
      while( m_numBufferedBytes > 1 )
      {
        m_Bitstream->write( byte, 8 );
        m_numBufferedBytes--;
      }
    }
    else
    {
      m_numBufferedBytes  = 1;
      m_bufferedByte      = leadByte;
    }
  }
}

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值