先开看看码率控制的总流程
总体流程
首先在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进行编码)
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只是预估,根据预估进行编码