通过解码器代码的研究,已经对HEVC的编解码技术有了一个初步的认识。现在我们就对照着编码器的代码进一步理解HEVC视频编码算法的各个技术细节。
编码器在整个HM解决方案中的工程名为TAppEncoder,入口点函数位于encmain.cpp文件中:
- int main(int argc, char* argv[])
- {
- TAppEncTop cTAppEncTop;
- // print information
- fprintf( stdout, "\n" );
- fprintf( stdout, "HM software: Encoder Version [%s]", NV_VERSION );
- fprintf( stdout, NVM_ONOS );
- fprintf( stdout, NVM_COMPILEDBY );
- fprintf( stdout, NVM_BITS );
- fprintf( stdout, "\n" );
- // create application encoder class
- cTAppEncTop.create();
- // parse configuration
- try
- {
- if(!cTAppEncTop.parseCfg( argc, argv ))
- {
- cTAppEncTop.destroy();
- return 1;
- }
- }
- catch (po::ParseFailure& e)
- {
- cerr << "Error parsing option \""<< e.arg <<"\" with argument \""<< e.val <<"\"." << endl;
- return 1;
- }
- // starting time
- double dResult;
- long lBefore = clock();
- // call encoding function
- cTAppEncTop.encode();
- // ending time
- dResult = (double)(clock()-lBefore) / CLOCKS_PER_SEC;
- printf("\n Total Time: %12.3f sec.\n", dResult);
- // destroy application encoder class
- cTAppEncTop.destroy();
- return 0;
- }
可以很清楚地看到,整个main函数非常简洁清晰,主要可以分为几大部分,分别是输入软件信息、创建编码器类的实例、解析配置文件、获取开始时间、编码数据、计算耗费时间和销毁编码器类的实例几大部分。我们主要关心的编码过程仅通过调用编码器实例的一个方法实现:
- // call encoding function
- cTAppEncTop.encode();
- Void TAppEncTop::encode()
- {
- fstream bitstreamFile(m_pchBitstreamFile, fstream::binary | fstream::out);
- if (!bitstreamFile)
- {
- fprintf(stderr, "\nfailed to open bitstream file `%s' for writing\n", m_pchBitstreamFile);
- exit(EXIT_FAILURE);
- }
- TComPicYuv* pcPicYuvOrg = new TComPicYuv;
- TComPicYuv* pcPicYuvRec = NULL;
- // initialize internal class & member variables
- xInitLibCfg();
- xCreateLib();
- xInitLib();
- // main encoder loop
- Int iNumEncoded = 0;
- Bool bEos = false;
- list<AccessUnit> outputAccessUnits; ///< list of access units to write out. is populated by the encoding process
- // allocate original YUV buffer
- pcPicYuvOrg->create( m_iSourceWidth, m_iSourceHeight, m_uiMaxCUWidth, m_uiMaxCUHeight, m_uiMaxCUDepth );
- while ( !bEos )
- {
- // get buffers
- xGetBuffer(pcPicYuvRec);
- // read input YUV file
- m_cTVideoIOYuvInputFile.read( pcPicYuvOrg, m_aiPad );
- // increase number of received frames
- m_iFrameRcvd++;
- bEos = (m_iFrameRcvd == m_framesToBeEncoded);
- Bool flush = 0;
- // if end of file (which is only detected on a read failure) flush the encoder of any queued pictures
- if (m_cTVideoIOYuvInputFile.isEof())
- {
- flush = true;
- bEos = true;
- m_iFrameRcvd--;
- m_cTEncTop.setFramesToBeEncoded(m_iFrameRcvd);
- }
- // call encoding function for one frame
- m_cTEncTop.encode( bEos, flush ? 0 : pcPicYuvOrg, m_cListPicYuvRec, outputAccessUnits, iNumEncoded );
- // write bistream to file if necessary
- if ( iNumEncoded > 0 )
- {
- xWriteOutput(bitstreamFile, iNumEncoded, outputAccessUnits);
- outputAccessUnits.clear();
- }
- }
- m_cTEncTop.printSummary();
- // delete original YUV buffer
- pcPicYuvOrg->destroy();
- delete pcPicYuvOrg;
- pcPicYuvOrg = NULL;
- // delete used buffers in encoder class
- m_cTEncTop.deletePicBuffer();
- // delete buffers & classes
- xDeleteBuffer();
- xDestroyLib();
- printRateSummary();
- return;
- }
该函数中首先调用pcPicYuvOrg->create( m_iSourceWidth, m_iSourceHeight, m_uiMaxCUWidth, m_uiMaxCUHeight, m_uiMaxCUDepth )分配YUV数据缓存,然后再while循环中逐帧读取YUV数据、设置当前以编码的帧数、编码当前帧、写出码流,随后做其他清理工作。核心功能实现在m_cTEncTop.encode( bEos, flush ? 0 : pcPicYuvOrg, m_cListPicYuvRec, outputAccessUnits, iNumEncoded )函数中。在该函数中调用m_cGOPEncoder.compressGOP(m_iPOCLast, m_iNumPicRcvd, m_cListPic, rcListPicYuvRecOut, accessUnitsOut)进行编码一个GOP的操作。这个函数奇长无比,用了接近1500行代码,看来实现了很多很多很多的功能。这个碉堡了的函数究竟做了些啥事儿呢?这个函数中大部分内容就是在为了编码当前slice做准备,以及编码完成之后一些辅助操作。实际编码过程的操作由以下函数m_pcSliceEncoder->compressSlice( pcPic )实现。
这又是一个碉堡了的函数,占了将近400行……代码就不贴了,会死人的……简单看下好了。
首先还是各种编码的配置,包括配置熵编码器、初始化CU编码器等。在完成了一长串的设置之后,在compressCU函数中实现对一个CU的编码:
- m_pcCuEncoder->compressCU( pcCU );