HEVC码率控制总体流程compressGOP,compressSlice

参考自(9条消息) HEVC码率控制代码追踪(一)(HEVC code tracing-Rate control)_yang-彧的博客-CSDN博客https://blog.csdn.net/cpp12341234/article/details/45766687

(9条消息) HEVC代码学习2:TAppEncTop::encode函数_岳麓吹雪的博客-CSDN博客https://blog.csdn.net/lin453701006/article/details/52792320

先开看看码率控制的总流程

总体流程

首先在encoder的main函数中,调用了encode函数,这是编码开始的地方

调用类cTAppEncTop的成员函数encode。encode主要做的是编码前的准备工作,进行编码器参数、视频文件、以及各类参数等的初始化,为原始YUV分配空间,然后调用TEncTop::encode进行编码,编码完成后打印统计信息。

下面进入到TEncTop::encode函数中去。到这算是从APP类来到了lib类工程当中。TEncTop::encode主要作用是为GOP压缩之前做一些准备工作,包括创建当前图像缓冲区、设定QP是否自适应、根据码率控制模式来确定是否需要先初始化GOP,然后调用TEncGop::compressGOP来压缩GOP。

这里是循环直到编码结束

while ( !bEos )//由bEos控制,对视频帧进行编码{

}

fstream bitstreamFile(m_bitstreamFileName.c_str(), fstream::binary | fstream::out);//以二进制输出方式打开比特流文件
  if (!bitstreamFile)//判断比特流文件是否存在
  {
    fprintf(stderr, "\nfailed to open bitstream file `%s' for writing\n", m_bitstreamFileName.c_str());
    exit(EXIT_FAILURE);
  }

  TComPicYuv*       pcPicYuvOrg = new TComPicYuv; // 定义YUV类
  TComPicYuv*       pcPicYuvRec = NULL;

  // initialize internal class & member variables
  xInitLibCfg();//初始化编码器的参数
  xCreateLib();//创建视频源文件以及编码重建后的二进制视频文件和程序的连接,初始化GOP,SLICE,CU的部分对象函数
  xInitLib(m_isField);//初始化SPS,PPS,GOP,SLICE,CU的部分对象函数,变换和量化类,编码器搜索函数


  printChromaFormat();//打印输入和输出的YUV格式

  // main encoder loop
  Int   iNumEncoded = 0; //记录已编码帧数
  Bool  bEos = false;//控制编码是否结束

  const InputColourSpaceConversion ipCSC  =  m_inputColourSpaceConvert;
  const InputColourSpaceConversion snrCSC = (!m_snrInternalColourSpace) ? m_inputColourSpaceConvert : IPCOLOURSPACE_UNCHANGED;

  list<AccessUnit> outputAccessUnits; ///< list of access units to write out.  is populated by the encoding process

  TComPicYuv cPicYuvTrueOrg;

  // allocate original YUV buffer为原始YUV缓冲区分配内存空间
  if( m_isField )// m_isField表示是否为场编码,默认为否
  {
    pcPicYuvOrg->create  ( m_iSourceWidth, m_iSourceHeightOrg, m_chromaFormatIDC, m_uiMaxCUWidth, m_uiMaxCUHeight, m_uiMaxTotalCUDepth, true );
    cPicYuvTrueOrg.create(m_iSourceWidth, m_iSourceHeightOrg, m_chromaFormatIDC, m_uiMaxCUWidth, m_uiMaxCUHeight, m_uiMaxTotalCUDepth, true);
  }
  else
  {
    pcPicYuvOrg->create  ( m_iSourceWidth, m_iSourceHeight, m_chromaFormatIDC, m_uiMaxCUWidth, m_uiMaxCUHeight, m_uiMaxTotalCUDepth, true );
    cPicYuvTrueOrg.create(m_iSourceWidth, m_iSourceHeight, m_chromaFormatIDC, m_uiMaxCUWidth, m_uiMaxCUHeight, m_uiMaxTotalCUDepth, true );
  }

pcPicYuvRec应该是重建后的帧YUV数据,pcPicYuvOrg是原始图像,cPicYuvTrueOrg是没有任何预编码器色彩空间转换的输入文件

TEncGop::encode

正式进入TEncGop::encode函数学习,m_isField表示是否为场编码,默认为否,进入下面的函数

 进入encode后是以GOP为单位进行编码

Void TEncTop::encode( Bool flush, TComPicYuv* pcPicYuvOrg, TComPicYuv* pcPicYuvTrueOrg, const InputColourSpaceConversion snrCSC, TComList<TComPicYuv*>& rcListPicYuvRecOut, std::list<AccessUnit>& accessUnitsOut, Int& iNumEncoded )
{
 //在这里flush为控制编码是否结束,未结束为false
  if (pcPicYuvOrg != NULL)
  {
    // get original YUV,获取原始YUV
    TComPic* pcPicCurr = NULL;

    xGetNewPicBuffer( pcPicCurr );//给当前图像分配新的缓冲区
    pcPicYuvOrg->copyToPic( pcPicCurr->getPicYuvOrg() );//将pcPicYuvOrg的信息赋给当前图像
    pcPicYuvTrueOrg->copyToPic( pcPicCurr->getPicYuvTrueOrg() );//将pcPicYuvTrueOrg的信息赋给当前图像

    // compute image characteristics,计算图像的特征
    如果使用自适应QP,则调用TEncPreanalyzer::xPreanalyze来分析图像并计算用于QP自适应的局部图像特征
    if ( getUseAdaptiveQP() )
    {
      m_cPreanalyzer.xPreanalyze( dynamic_cast<TEncPic*>( pcPicCurr ) );
    }
  }
//注意看这里全为0才可以继续编码
  // 如果接收到的帧数!=0&&(m_iPOCLast==0||flush==true||m_iNumPicRcvd==m_iGOPSize||m_iGOPSize==0) 继续编码,否则return
  if ((m_iNumPicRcvd == 0) || (!flush && (m_iPOCLast != 0) && (m_iNumPicRcvd != m_iGOPSize) && (m_iGOPSize != 0)))
  {
    iNumEncoded = 0;
    return;
  }
  该工程中关于码率控制部分
  //开启码率控制
  if ( m_RCEnableRateControl )
  {
    m_cRateCtrl.initRCGOP( m_iNumPicRcvd );//对整个GOP需要用到的相关参数进行初始化
  }

  // compress GOP,对每一幅picture需要用到的相关参数进行初始化
  m_cGOPEncoder.compressGOP(m_iPOCLast, m_iNumPicRcvd, m_cListPic, rcListPicYuvRecOut, accessUnitsOut, false, false, snrCSC, m_printFrameMSE);
  //释放
  if ( m_RCEnableRateControl ) 
  {
    m_cRateCtrl.destroyRCGOP();
  }

  iNumEncoded         = m_iNumPicRcvd;
  m_iNumPicRcvd       = 0;
  m_uiNumAllPicCoded += iNumEncoded;
}

compressGOP函数

进入compressGOP函数,这个函数进入后就以该GOP每帧为单位进行压缩

一般情况下,输入的系列都是分为GOP,再以GOP为对象进行压缩,所以后续的各种操作都将被compressGOP调用。这里GOPEncoder是定义的一个类TEncGOP的对象,所以m_cGOPEncoder.compressGOP就是调用类TEncGOP的成员函数compressGOP。下面进入到compressGOP函数。这个函数非常的大,但是从我们码率控制的角度来讲只需要关注几段代码就可以。

compressGOP()函数要点:
根据iGOPid逐次递增,对一个GOP内的picture依次编码
根据picture level对当前picture进行码率分配
根据已经编过的picture,来得到当前picture的lambda
得到当前picture的lambda后,对该picture下每一个CTU进行码率分配(compressSlice)
一个picture编完,码流写好了,根据实际编码所用的比特,更新对应level下的lambda参数

iGOPid,compressGOP是对该GOP中的每帧进行压缩

Void TEncGOP::compressGOP( Int iPOCLast, Int iNumPicRcvd, TComList<TComPic*>& rcListPic,
                           TComList<TComPicYuv*>& rcListPicYuvRecOut, std::list<AccessUnit>& accessUnitsInGOP,
                           Bool isField, Bool isTff, const InputColourSpaceConversion snr_conversion, const Bool printFrameMSE )
{
  // TODO: Split this function up.

  TComPic*        pcPic = NULL;
  TComPicYuv*     pcPicYuvRecOut;
  TComSlice*      pcSlice;
  TComOutputBitstream  *pcBitstreamRedirect;
  pcBitstreamRedirect = new TComOutputBitstream;
  AccessUnit::iterator  itLocationToPushSliceHeaderNALU; // used to store location where NALU containing slice header is to be inserted

  xInitGOP( iPOCLast, iNumPicRcvd, isField );//GOPsize特殊情况处理

  m_iNumPicCoded = 0;
  SEIMessages leadingSeiMessages;
  SEIMessages nestedSeiMessages;
  SEIMessages duInfoSeiMessages;
  SEIMessages trailingSeiMessages;
  std::deque<DUData> duData;
  SEIDecodingUnitInfo decodingUnitInfoSEI;
 for ( Int iGOPid=0; iGOPid < m_iGopSize; iGOPid++ )
  {
    m_pcCfg->setEncodedFlag(iGOPid, false); //重置标志,指示图片是否已编码
  }
for ( Int iGOPid=0; iGOPid < m_iGopSize; iGOPid++ )

{

...

Int frameLevel = m_pcRateCtrl->getRCSeq()->getGOPID2Level( iGOPid );

 m_pcRateCtrl->initRCPic( frameLevel );//帧级初始化

...

}

调用了xInitGOP函数,GOPsize对于第1帧(iPOCLast == 0)特殊处理

如果是!isField而且编码的是第一帧,GOPsize=1

Void TEncGOP::xInitGOP( Int iPOCLast, Int iNumPicRcvd, Bool isField )
{
  assert( iNumPicRcvd > 0 );
  //  Exception for the first frames
  if ( ( isField && (iPOCLast == 0 || iPOCLast == 1) ) || (!isField  && (iPOCLast == 0))  )
  {
    m_iGopSize    = 1;
  }
  else
  {
    m_iGopSize    = m_pcCfg->getGOPSize();
  }
  assert (m_iGopSize > 0);

  return;
}

下面就是码率控制部分

先获得该帧的level,还有该帧的目标比特

如果配置文件对序列第一帧指定了初始QP,则基于这个QP计算出lamda

如果是I帧,需要进行目标比特的修正等

获取该序列剩余的平均比特,作为实参传入getRefineBitsForIntra函数进行修正,得到的就是I帧的目标比特,I帧目标比特不少于200,如果小于200就初始化为200bits

该I帧目标比特已知,调用getLCUInitTargetBits函数初始化I帧每个LCU的目标比特,计算该帧的预估lambda和QP

 list<TEncRCPic*> listPreviousPicture = m_pcRateCtrl->getPicList();
        m_pcRateCtrl->getRCPic()->getLCUInitTargetBits();//初始化I帧每个LCU的目标比特
        lambda  = m_pcRateCtrl->getRCPic()->estimatePicLambda( listPreviousPicture, pcSlice->getSliceType());
        sliceQP = m_pcRateCtrl->getRCPic()->estimatePicQP( lambda, listPreviousPicture );

P帧情况同理,和I帧相比只是少了一个修正,P帧每个LCU的比特分配在estimatePicLambda函数中

虽然计算出来QP但这里的QP其实是sliceQP,片QP,涉及到了每一帧的Slice划分,具体怎么操作暂不清楚

设置该SLICE的QP,开始根据qp进行码率控制

到这里该帧的帧层的码率控制结束,开始进入Slice码率控制

compressGOP是以该GOP每一帧进行码率控制,compressSlice是以该Slice下每个LCU进行码率控制。

compressSlice

compressSlice主要完成的功能就是Slice层编码参数的初始化,我的理解就是CU层,上面的compressGOP我们已经得知了每帧的目标比特以及该帧下每个LCU的目标比特,以及lambda和QP,compressSlice会调用compressCtu(其中会调用xCompressCU,对CU进行划分)和encodeCtu(其中对调用xEncodeCU,对CU进行编码)

参考自(9条消息) HEVC代码学习34:compressSlice函数_岳麓吹雪的博客-CSDN博客https://blog.csdn.net/lin453701006/article/details/78673978

HM编码器代码阅读(10)——片的编码https://blog.csdn.net/nb_vol_1/article/details/51151803

nextCtuTsAddr是该LCU的条带地址,从0开始,直到大于LCU数量跳出循环

这个函数也很大,函数主要是设置一些参数和初始化一些东西,然后对片中的每一个LCU调用initCU(初始化CU)和compressCU(对CU编码)和encodeCU(对CU进行熵编码,目的是选择最优参数)。

TEncSlice::compressSlice函数的详解:
(1)计算当前slice的开始CU和结束CU
(2)初始化Sbac编码器
(3)再把slice的熵编码器设置为Sbac编码器
(4)重置熵编码器(主要是上下文的重置)
(5)取得二值化编码器
(6)取得比特计数器
(7)遍历slice中的每一个LCU
     ①初始化LCU
     ②调用compressCU,用于求最优模式
     ③调用encodeCU,进行熵编码
 

阅读这部分代码得先清楚TComDataCU这个类,这个类记录了LCU的编码信息

oid TEncSlice::compressSlice( TComPic* pcPic, const Bool bCompressEntireSlice, const Bool bFastDeltaQP )
{
  // if bCompressEntireSlice is true, then the entire slice (not slice segment) is compressed,
  //   effectively disabling the slice-segment-mode.

  UInt   startCtuTsAddr;//当前Slice起始CTU地址
  UInt   boundingCtuTsAddr; //当前SliceCTU边界地址
  TComSlice* const pcSlice            = pcPic->getSlice(getSliceIdx());//当前Slice
  pcSlice->setSliceSegmentBits(0);//将Slice中当前bit置零
  //计算当前Slice中的起始CTU和边界CTU,注意计算出来的是TS地址,需要转为RS地址
  xDetermineStartAndBoundingCtuTsAddr ( startCtuTsAddr, boundingCtuTsAddr, pcPic );
  //bCompressEntireSlice默认为false
  if (bCompressEntireSlice)
  {
    boundingCtuTsAddr = pcSlice->getSliceCurEndCtuTsAddr();
    pcSlice->setSliceSegmentCurEndCtuTsAddr(boundingCtuTsAddr);
  }
  //初始化总bit、RD cost和失真
  // initialize cost values - these are used by precompressSlice (they should be parameters).
  m_uiPicTotalBits  = 0;
  m_dPicRdCost      = 0; // NOTE: This is a write-only variable!
  m_uiPicDist       = 0;
  //初始化熵编码器
  m_pcEntropyCoder->setEntropyCoder   ( m_pppcRDSbacCoder[0][CI_CURR_BEST] );
  //根据当前Slice设置熵编码参数
  m_pcEntropyCoder->resetEntropy      ( pcSlice );
  //加载熵编码器SBAC
  TEncBinCABAC* pRDSbacCoder = (TEncBinCABAC *) m_pppcRDSbacCoder[0][CI_CURR_BEST]->getEncBinIf();
  pRDSbacCoder->setBinCountingEnableFlag( false );
  pRDSbacCoder->setBinsCoded( 0 );

  TComBitCounter  tempBitCounter;
  //帧每行的CTU个数
  const UInt      frameWidthInCtus = pcPic->getPicSym()->getFrameWidthInCtus();
  
  m_pcCuEncoder->setFastDeltaQp(bFastDeltaQP);

  //------------------------------------------------------------------------------
  //  Weighted Prediction parameters estimation.
  //------------------------------------------------------------------------------
  // calculate AC/DC values for current picture
  //默认关闭
  if( pcSlice->getPPS()->getUseWP() || pcSlice->getPPS()->getWPBiPred() )
  {
    xCalcACDCParamSlice(pcSlice);
  }

  const Bool bWp_explicit = (pcSlice->getSliceType()==P_SLICE && pcSlice->getPPS()->getUseWP()) || (pcSlice->getSliceType()==B_SLICE && pcSlice->getPPS()->getWPBiPred());
  //bWp_explicit默认为false
  if ( bWp_explicit )
  {
    //------------------------------------------------------------------------------
    //  Weighted Prediction implemented at Slice level. SliceMode=2 is not supported yet.
    //------------------------------------------------------------------------------
    if ( pcSlice->getSliceMode()==FIXED_NUMBER_OF_BYTES || pcSlice->getSliceSegmentMode()==FIXED_NUMBER_OF_BYTES )
    {
      printf("Weighted Prediction is not supported with slice mode determined by max number of bins.\n"); exit(0);
    }

    xEstimateWPParamSlice( pcSlice, m_pcCfg->getWeightedPredictionMethod() );
    pcSlice->initWpScaling(pcSlice->getSPS());

    // check WP on/off
    xCheckWPEnable( pcSlice );
  }

#if ADAPTIVE_QP_SELECTION
  if( m_pcCfg->getUseAdaptQpSelect() && !(pcSlice->getDependentSliceSegmentFlag()))
  {
    // TODO: this won't work with dependent slices: they do not have their own QP. Check fix to mask clause execution with && !(pcSlice->getDependentSliceSegmentFlag())
    m_pcTrQuant->clearSliceARLCnt(); // TODO: this looks wrong for multiple slices - the results of all but the last slice will be cleared before they are used (all slices compressed, and then all slices encoded)
    if(pcSlice->getSliceType()!=I_SLICE)
    {
      Int qpBase = pcSlice->getSliceQpBase();
      pcSlice->setSliceQp(qpBase + m_pcTrQuant->getQpDelta(qpBase));
    }
  }
#endif



  // Adjust initial state if this is the start of a dependent slice.
  {
    const UInt      ctuRsAddr               = pcPic->getPicSym()->getCtuTsToRsAddrMap( startCtuTsAddr); //CTU的RS地址
    const UInt      currentTileIdx          = pcPic->getPicSym()->getTileIdxMap(ctuRsAddr);//当前Tile序号
    const TComTile *pCurrentTile            = pcPic->getPicSym()->getTComTile(currentTileIdx);//当前Tile
    const UInt      firstCtuRsAddrOfTile    = pCurrentTile->getFirstCtuRsAddr(); //Tile中第一个CTU的地址
    if( pcSlice->getDependentSliceSegmentFlag() && ctuRsAddr != firstCtuRsAddrOfTile )//独立Slice且非第一个Tile时启用
    {
      // This will only occur if dependent slice-segments (m_entropyCodingSyncContextState=true) are being used.
      if( pCurrentTile->getTileWidthInCtus() >= 2 || !m_pcCfg->getEntropyCodingSyncEnabledFlag() )
      {
        m_pppcRDSbacCoder[0][CI_CURR_BEST]->loadContexts( &m_lastSliceSegmentEndContextState );
      }
    }
  }

  // for every CTU in the slice segment (may terminate sooner if there is a byte limit on the slice-segment)
   //遍历Slice中的每一个CTU,对CTU进行编码
  for( UInt ctuTsAddr = startCtuTsAddr; ctuTsAddr < boundingCtuTsAddr; ++ctuTsAddr )
  {
    const UInt ctuRsAddr = pcPic->getPicSym()->getCtuTsToRsAddrMap(ctuTsAddr);
    // initialize CTU encoder
    //当前CTU
    TComDataCU* pCtu = pcPic->getCtu( ctuRsAddr );
    //初始化CTU
    pCtu->initCtu( pcPic, ctuRsAddr );

    // update CABAC state
    const UInt firstCtuRsAddrOfTile = pcPic->getPicSym()->getTComTile(pcPic->getPicSym()->getTileIdxMap(ctuRsAddr))->getFirstCtuRsAddr();
    const UInt tileXPosInCtus = firstCtuRsAddrOfTile % frameWidthInCtus;
    const UInt ctuXPosInCtus  = ctuRsAddr % frameWidthInCtus;
    //如果当前LCU是该片第一个,设置熵编码参数
    if (ctuRsAddr == firstCtuRsAddrOfTile)
    {

      m_pppcRDSbacCoder[0][CI_CURR_BEST]->resetEntropy(pcSlice);
    }
    else if ( ctuXPosInCtus == tileXPosInCtus && m_pcCfg->getEntropyCodingSyncEnabledFlag())
    {
      // reset and then update contexts to the state at the end of the top-right CTU (if within current slice and tile).
      m_pppcRDSbacCoder[0][CI_CURR_BEST]->resetEntropy(pcSlice);
      // Sync if the Top-Right is available.
      TComDataCU *pCtuUp = pCtu->getCtuAbove();
      if ( pCtuUp && ((ctuRsAddr%frameWidthInCtus+1) < frameWidthInCtus)  )
      {
        TComDataCU *pCtuTR = pcPic->getCtu( ctuRsAddr - frameWidthInCtus + 1 );
        if ( pCtu->CUIsFromSameSliceAndTile(pCtuTR) )
        {
          // Top-Right is available, we use it.
          m_pppcRDSbacCoder[0][CI_CURR_BEST]->loadContexts( &m_entropyCodingSyncContextState );
        }
      }
    }

    // set go-on entropy coder (used for all trial encodings - the cu encoder and encoder search also have a copy of the same pointer)
    m_pcEntropyCoder->setEntropyCoder ( m_pcRDGoOnSbacCoder );
    m_pcEntropyCoder->setBitstream( &tempBitCounter );
    tempBitCounter.resetBits();
    m_pcRDGoOnSbacCoder->load( m_pppcRDSbacCoder[0][CI_CURR_BEST] ); // this copy is not strictly necessary here, but indicates that the GoOnSbacCoder
                                                                     // is reset to a known state before every decision process.

    ((TEncBinCABAC*)m_pcRDGoOnSbacCoder->getEncBinIf())->setBinCountingEnableFlag(true);

    Double oldLambda = m_pcRdCost->getLambda();


注意看105行

TComDataCU* pCtu = pcPic->getCtu( ctuRsAddr );
    //初始化CTU
    pCtu->initCtu( pcPic, ctuRsAddr );

pCtu是从该帧的LCU数组里m_pictureCtuArray[ctuRsAddr]里获取到的,同时该LCU也被作为

compressCtu( pCtu )的传入参数(指针传入),encodeCtu( pCtu )同样用了compressCtu后的pCtu为参数,也就是说是以compressCtu函数确定后的pCtu进行编码。

码率控制部分代码原理是:
先判断是不是I帧和getForceIntraQP,或者未开启LCU层码率控制,如果是直接以该帧的QP作为当前LCU的QP

先获取当前LCU的bpp,如果是I帧,调用getLCUEstLambdaAndQP函数获取该LCU的estLambda和estQP,这里传的是地址,把QP也获取了

如果是P帧,getLCUEstLambda和getLCUEstQP函数获取QP,然后对QP进行约束, m_pcRateCtrl->setRCQP( estQP )设置QP,这里我不是很懂为什么要给cRateCtrl这个类设置QP

接着就是调用对CU进行划分和编码,compressCtu是对CU进行编码(压缩), 帧内预测,帧间预测编码还有变换编码,注意这个只是尝试进行,然后选出最优熵编码方案,下面的encodeCU才是真正进行熵编码的地方。

encode是真正的进行熵编码

 m_pcCuEncoder->compressCtu( pCtu );

 m_pcCuEncoder->encodeCtu( pCtu );

当编码完成后更新参数,上面的QP只是预估,根据预估进行编码


 


 

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值