3.2 Xvid视频编码分析
Xvid CODEC包括视频编码算法和视频解码算法。Xvid于2001年创立,是MPEG-4视频编码算法的工程实现。Xvid的早期版本(0.9.x)实现了MPEG-4 SP版本的编解码, Xvid 1.0版本及其子版本实现了MPEG-4 ASP视频压缩,包括所有高级编码工具如码流控制、B帧编码、1/4像素运动补偿和全局运动补偿GMC等。即将开始研发的Xvid 2.0增加了对MPEG-4/AVC(Advanced Video Coding)等更高档级的编解码支持,编码压缩性能相比早期版本将有大幅提升。
Xvid编解码算法的工作流程基本都包含三个过程:初始化CODEC、使用CODEC、销毁CODEC。初始化CODEC过程完成CODEC的句柄创建、内存空间分配;循环使用CODEC完成视频图像的MPEG-4编码、MPEG-4码流的解码;销毁CODEC完成初始化的反工作。Xvid使用标准C语言编程,同时CODEC的核心模块:图像类型转换、DCT变换、量化/反量、SAD值计算、运动补偿MC、CBP计算等都实现了多个CPU如x86的32/64位、ia64等的汇编优化。
3.2.1 MPEG-4视频编码原理
MPEG-4视频编码是典型的混合编码技术,即使用变换、量化和编码三步骤对图像数据或图像差值做处理。I帧(关键帧Key Frame)是帧内编码,即对图像数据做编码,利用了图像的空间冗余;P帧编码主要是利用前向预测,对宏块差值编码,利用了图像的时间冗余。统计来看,鉴于I帧的多帧间隔,主流编码是P帧。P帧编码中的技术核心是运动估计,即搜索与当前宏块的最佳匹配块,搜索的窗口越大,宏块更匹配,但随之运算量也就越大。运动估计技术(ME:Motion Estimation)是任何视频编码算法的核心。这里以图像大小为352×288、格式为I420、编码I、P帧为例介绍MPEG-4视频编码流程,如图3-1所示。
图3-1 MPEG-4视频编码流程框图
MPEG-4算法对一帧图像进行编码的过程如下:
● 从文件中读取一帧图像,大小为352×288×3/2(包含了亮度和色度)。
● 以宏块为单位进行编码。一帧图像中所有宏块编码完成,也就完成了对当前帧的编码。
● 对当前宏块进行运动估计(如果是I帧,则不用),根据SAD值决定匹配块。
● 对8×8块进行编码,按照其在宏块中排列的顺序进行。首先将当前宏块同参考宏块作差值,然后再对差值进行编码(I帧不用)。
● 对当前编码块(或者经过运动补偿的差值)进行DCT变换。
● 对DCT变换后的系数进行量化。
● 反量化模块是量化模块的逆过程。
● 反DCT模块是DCT模块的逆过程。
● AC/DC预测,即在编码I帧时,对当前宏块的第一行或者第一列系数同它周围的某一块作一个差值,进一步增加零系数,降低比特率。
● 反DCT后的宏块作运动补偿得到当前宏块的重建值,称为重建宏块。
● 码流合并模块,形成最后的视频编码码流。码流由码流头开始,然后是具体的帧。编码之前,模块首先向输出流文件中写入码流头信息。然后写入第一帧的内容,每一帧同样由帧头信息开始,帧头信息同当前的编码内容是紧密相关的,后面的帧数据必须完全符合它规定的一些特性,譬如当前帧的量化值、运动补偿的搜索范围等。接下来是具体的帧数据,并且是按照宏块组织的。宏块内容包含当前宏块的编码信息,例如当前宏块是否编码、编码类型等,接着是运动矢量的数据,最后是具体的六个块的数据。
3.2.2 Xvid视频编码过程
Xvid为了检测运行平台的CPU,在编解码器初始化前,给开发者提供了可以指定平台的接口,另外程序也能够自动检测CPU。然后针对不同的平台初始化核心模块的函数指针。注意,在启动编码和解码前均需要做上述初始化工作。
//……………….. Xvid_gbl_init.cpu_flags=0; // 程序自动检测 Xvid_global(NULL, XVID_GBL_INIT, &Xvid_gbl_init, NULL); //……………….. int Xvid_global(void *handle, int opt, void *param1, void *param2) { switch(opt) { case XVID_GBL_INIT: //初始化内部的函数指针、VLC码表和图像空间转换函数 return Xvid_gbl_init((Xvid_gbl_init_t*)param1); case XVID_GBL_INFO: //返回编码器的一些信息通知给主机 return Xvid_gbl_info((Xvid_gbl_info_t*)param1); case XVID_GBL_CONVERT: //颜色转换 return Xvid_gbl_convert((Xvid_gbl_convert_t*)param1); default : return XVID_ERR_FAIL; } }
上述代码在初始化编码前调用,主要是初始化内部的函数指针,如通过检测CPU平台,使用MMX或SSE指令优化的函数初始化函数指针,创建VLC熵编码的码表等。
Xvid的MPEG-4编码算法是以一个函数、及其三个不同的参数传递来完成。
/*------------------------------编码器操作-------------------------------*/ #define XVID_ENC_CREATE 0 /* 创建编码器实例;0代表成功*/ #define XVID_ENC_DESTROY 1 /* 销毁编码器实例;0代表成功*/ #define XVID_ENC_ENCODE 2 /* 编码一帧图像:返回编码后的字节数 * 0表示该帧不用写入文件(如编码器滞后或延迟) */ /*-----------------------------编码器入口函数-----------------------------*/ extern int Xvid_encore(void *handle, int opt, void *param1, void *param2);
编码器的入口函数Xvid_encore的实现如下:
int Xvid_encore(void *handle, int opt, void *param1, void *param2) { switch (opt) { case XVID_ENC_ENCODE: //Xvid编码一帧图像 return enc_encode((Encoder *) handle,(Xvid_enc_frame_t *) param1,(Xvid_enc_stats_t *) param2); case XVID_ENC_CREATE: //Xvid创建编码器 return enc_create((Xvid_enc_create_t *) param1); case XVID_ENC_DESTROY: //Xvid销毁编码器 return enc_destroy((Encoder *) handle); default: return XVID_ERR_FAIL; } }
上述程序是编码器的所有功能函数,创建编码器实例、使用编码器编码图像、销毁编码器。在应用层,编码器可以有多个,这就需要通过编码句柄Handle来控制不同的编码器实例。创建编码器enc_create、销毁编码器enc_destroy均被调用一次。编码视频序列时,循环调用enc_encode实现图像帧编码。
下面就分别详细介绍这三个函数的功能及其实现过程。
1.创建Xvid编码器
创建MPEG-4编码器,首先创建编码器实例句柄,然后在该句柄下实现对编码器的参数配置、图像参数获取和空间申请等工作,从而在多路编码工作时,可以通过句柄来控制每一路的编码。
函数enc_create()流程如图3-2所示。根据该流程,实现过程如下:
图3-2 enc_create()流程图
int enc_create(Xvid_enc_create_t*create) { Encoder *pEnc; /* 创建编码器指针,注意内存对齐*/ pEnc = (Encoder *) Xvid_malloc(sizeof(Encoder), CACHE_LINE); if(pEnc==NULL) return XVID_ERR_MEMORY; memset(pEnc,0,sizeof(Encoder)); /*初始化编码器句柄,所有指针清空*/ pEnc->mbParam.profile = create->profile; pEnc->mbParam.global_flags = create->global; /*编码器全局参数标志*/ pEnc->mbParam.width=create->width; /*视频图像的宽*/ pEnc->mbParam.height=create->height; /*视频图像的高*/ pEnc->mbParam.mb_width=(pEnc->mbParam.width+15)/16; /*图像的宏块宽度*/ pEnc->mbParam.mb_height=(pEnc->mbParam.height+15)/16; /*图像的宏块高度*/ /*扩展后的图像大小*/ pEnc->mbParam.edged_width = 16 * pEnc->mbParam.mb_width + 2 * EDGE_SIZE; pEnc->mbParam.edged_height = 16 * pEnc->mbParam.mb_height + 2 * EDGE_SIZE; pEnc->mbParam.fincr=MAX(create->fincr,0); /*帧率递增模式*/ pEnc->mbParam.fbase=create->fincr<=0?25:create->fbase; /*帧率大小*/ pEnc->mbParam.frame_drop_ratio = MAX(create->frame_drop_ratio, 0);/*丢帧率*/ /*最大关键帧间隔,即I帧间的最大间隔,如果没有设置则默认250*/ pEnc->mbParam.iMaxKeyInterval = create->max_key_interval <= 0 ? (10 * (int)pEnc->mbParam.fbase) / (int)pEnc->mbParam.fincr : create->max_key_interval; /*分配当前编码帧和参考帧空间 */ pEnc->current = Xvid_malloc(sizeof(FRAMEINFO), CACHE_LINE); pEnc->reference = Xvid_malloc(sizeof(FRAMEINFO), CACHE_LINE); if (pEnc->current == NULL || pEnc->reference == NULL) goto Xvid_err_memory1; /* 为当前帧和参考帧的所有宏块结构分配空间 */ pEnc->current->mbs = Xvid_malloc(sizeof(MACROBLOCK) * pEnc->mbParam.mb_width * pEnc->mbParam.mb_height, CACHE_LINE); pEnc->reference->mbs = Xvid_malloc(sizeof(MACROBLOCK) * pEnc->mbParam.mb_width * pEnc->mbParam.mb_height, CACHE_LINE); if (pEnc->current->mbs == NULL || pEnc->reference->mbs == NULL) goto Xvid_err_memory2; /*图像指针清空*/ image_null(&pEnc->current->image); image_null(&pEnc->reference->image); image_null(&pEnc->vInterH); image_null(&pEnc->vInterV); image_null(&pEnc->vInterHV); /* 创建当前视频帧图像空间 */ if (image_create(&pEnc->current->image, pEnc->mbParam.edged_width,pEnc->mbParam.edged_height) < 0) goto Xvid_err_memory3; /* 创建参考视频帧图像空间 */ if (image_create(&pEnc->reference->image,pEnc->mbParam.edged_width,pEnc->mbParam.edged_height)< 0) goto Xvid_err_memory3; /* 创建vInterH参考帧图像空间 */ if (image_create(&pEnc->vInterH, pEnc->mbParam.edged_width, pEnc->mbParam.edged_height) < 0) goto Xvid_err_memory3; /* 创建vInterV参考帧图像空间 */ if (image_create(&pEnc->vInterV, pEnc->mbParam.edged_width, pEnc->mbParam.edged_height) < 0) goto Xvid_err_memory3; /* 创建vInterHV参考帧图像空间 */ if (image_create(&pEnc->vInterHV, pEnc->mbParam.edged_width, pEnc->mbParam.edged_height) < 0) goto Xvid_err_memory3; /* 初始化B帧编码序列参数*/ pEnc->queue_head = 0; pEnc->queue_tail = 0; pEnc->queue_size = 0; pEnc->queue = Xvid_malloc((1) * sizeof(QUEUEINFO),CACHE_LINE); /* 图像编码序列 */ if (image_create(&pEnc->queue[0].image, pEnc->mbParam.edged_width,pEnc->mbParam.edged_height) < 0) goto Xvid_err_memory5; /*时间戳清零*/ pEnc->mbParam.m_stamp = 0; pEnc->current->stamp = 0; pEnc->reference->stamp = 0; pEnc->iFrameNum=0; /*编码图像帧数计数器清零*/ create->handle=(void*)pEnc; /*返回编码器的句柄 */ return 0; /*ok*/ /*如果出错的话,释放以前分配的内存*/ Xvid_err_memory5: image_destroy(&pEnc->queue[0].image, pEnc->mbParam.edged_width,pEnc->mbParam.edged_height); Xvid_free(pEnc->queue); /* 释放图像空间*/ image_destroy(&pEnc->current->image, pEnc->mbParam.edged_width,pEnc->mbParam.edged_height); image_destroy(&pEnc->reference->image, pEnc->mbParam.edged_width,pEnc->mbParam.edged_height); image_destroy(&pEnc->vInterH, pEnc->mbParam.edged_width,pEnc->mbParam.edged_height); image_destroy(&pEnc->vInterV, pEnc->mbParam.edged_width,pEnc->mbParam.edged_height); image_destroy(&pEnc->vInterHV, pEnc->mbParam.edged_width,pEnc->mbParam.edged_height); Xvid_err_memory2: /* 释放当前帧和参考帧的mbs宏块内存*/ Xvid_free(pEnc->current->mbs); Xvid_free(pEnc->reference->mbs); Xvid_err_memory1: /* 释放当前帧和参考帧的内存*/ Xvid_free(pEnc->current); Xvid_free(pEnc->reference); /* 释放编码器*/ Xvid_free(pEnc); Xvid_err_memory3: create->handle = NULL; return XVID_ERR_MEMORY; }
上述代码完成了编码器的创建、编码参数的初始化、工作空间的申请等任务。编码器的初始化函数可以不用考虑优化,因为这个函数仅被调用一次,与编码函数被多次循环调用不同。
2.Xvid编码一帧图像
对输入的一帧编码图像,Xvid根据程序判定或用户强制类型进行MPEG-4编码。编码器返回编码后的码流和长度以及编码信息的统计。
函数enc_encode()流程如图3-3所示。根据该流程,实现过程如下:
图3-3 enc_encode()流程图
int enc_encode(Encoder * pEnc, Xvid_enc_frame_t * xFrame, Xvid_enc_stats_t * stats) { Xvid_enc_frame_t * frame; int type; Bitstream bs; //存储码流 xFrame->out_flags = 0; //初始化码流指针,以便于存储编码后的码流 BitstreamInit(&bs, xFrame->bitstream, 0); //图像颜色空间为I420,即YUV的比例是4 : 2 : 0(或4 : 1 : 1) if(xFrame->input.csp!=XVID_CSP_NULL) { q = &pEnc->queue[0]; //读入图像帧到编码器的序列组中,input.plane => q->image if (image_input(&q->image, pEnc->mbParam.width, pEnc->mbParam.height, pEnc->mbParam.edged_width, (uint8_t**)xFrame->input.plane, xFrame->input.stride, xFrame->input.csp, 0)) { return XVID_ERR_FORMAT; //图像格式错误 } q->frame = *xFrame; pEnc->queue_tail = 0; pEnc->queue_size++; } /*——————————分析图像编码队列———————— */ if(pEnc->queue_size==0){ /*队列为空*/ if(xFrame->input.csp==XVID_CSP_NULL) /*没有图像继续输入*/ return XVID_ERR_END; /*编码完毕,没有图像输出*/ goto done; /*没有图像编码;编码器延迟*/ } SWAP(FRAMEINFO*,pEnc->current,pEnc->reference); /*交换两图像的指针*/ image_swap(&pEnc->current->image,&pEnc->queue[0].image); /*从序列组中读入一帧到编码器中*/ frame = &pEnc->queue[0].frame; pEnc->queue_head = 0; pEnc->queue_size--; /*——————————初始化编码器的当前帧编码参数——————————*/ pEnc->current->fincr=pEnc->mbParam.fincr>0?pEnc->mbParam.fincr:frame->fincr; inc_frame_num(pEnc); /*修改时间戳、编码计数器*/ pEnc->current->vol_flags=frame->vol_flags; /*获取输入的VOL标志 */ pEnc->current->vop_flags=frame->vop_flags; /*获取输入的VOP标志*/ pEnc->current->motion_flags=frame->motion; /*获取输入的Motion标志*/ pEnc->current->fcode=pEnc->mbParam.m_fcode; /*获取ME的搜索窗口*/ /*——————————选择编码帧类型和量化——————————————*/ type = frame->type; pEnc->current->quant = frame->quant; if (type > 0){ type = type2coding(type); }else{ /*XVID_TYPE_AUTO*/ if (pEnc->iFrameNum == 0 || (pEnc->mbParam.iMaxKeyInterval > 0 && pEnc->iFrameNum >= pEnc->mbParam.iMaxKeyInterval)) { pEnc->iFrameNum = 0; type=I_VOP; //强制编码类型I帧编码 }else{ type=P_VOP; //强制编码类型P帧编码 } } if (type != I_VOP) pEnc->current->vol_flags = pEnc->mbParam.vol_flags; /*编码器的编码帧计数*/ pEnc->iFrameNum++; if (type == I_VOP) { pEnc->iFrameNum = 1; pEnc->mbParam.vol_flags=pEnc->current->vol_flags; /*更新I_VOP的VOL标志*/ FrameCodeI(pEnc,&bs); /*图像为I帧编码 */ xFrame->out_flags |= XVID_KEYFRAME; } else { /* (type == P_VOP || type == S_VOP) */ FrameCodeP(pEnc,&bs); /*图像为P帧编码 */ } /*为统计编码器的各个参数,保存当前帧的编码参数:编码类型、量化步长、*/ /*码流长度、被编码宏块和没有编码的宏块数目等。 */ #if 1 stats->type =pEnc->current->coding_type+1; //帧编码类型:I/P/B stats->quant =pEnc->current->quant; //量化步长 stats->vol_flags =pEnc->current->vol_flags; //VOL标志 stats->vop_flags =pEnc->current->vop_flags; //VOP标志 stats->length =pEnc->current->length; //当前帧编码后的码流长度 stats->kblks =pEnc->current->sStat.kblks; //以intra模式编码的宏块数目 stats->mblks =pEnc->current->sStat.mblks; //以inter模式编码的宏块数目 stats->ublks =pEnc->current->sStat.ublks; //未编码的宏块数目 #endif done: return BitstreamLength(&bs); /*编码完毕,计算压缩后的码流长度*/ }
上述过程实现对输入一帧图像的MPEG-4编码,编码方式可以是外部强制定义,也可以根据I帧间隔计算当前帧的编码类型。从上述代码分析可知,Xvid的MPEG-4 SP编码算法由两个函数FrameCodeI和FrameCodeP来实现真正的图像编码。
1)FrameCodeI
I帧编码又称为关键帧编码,是消除图像的空间冗余。I帧编码与静态图像的编码算法有些类似,如JPEG。I帧编码流程如图3-4所示。
图3-4 I帧编码流程框图
根据I帧编码的流程,具体的实现过程如下:
int FrameCodeI(Encoder * pEnc, Bitstream * bs) { int bits = BitstreamPos(bs); uint32_t mb_width =pEnc->mbParam.mb_width; /*宏块宽*/ uint32_t mb_height =pEnc->mbParam.mb_height; /*宏块高*/ uint32_t edged_width =pEnc->mbParam.edged_width; /*扩展后的图像宽度*/ DECLARE_ALIGNED_MATRIX(dct_coeff , 6, 64, int16_t, CACHE_LINE);/*DCT系数*/ DECLARE_ALIGNED_MATRIX(qnt_coeff , 6, 64, int16_t, CACHE_LINE);/*DCT系数量化结果*/ uint16_t x, y; pEnc->current->coding_type=I_VOP; /*当前边编码类型I_VOP*/ pEnc->current->mbs[0].quant=pEnc->current->quant; /*获取量化步长*/ SetMacroblockQuants(&pEnc->mbParam,pEnc->current); /*设置每个宏块的量化大小*/ BitstreamWriteVolHeader(bs, &pEnc->mbParam, pEnc->current);/*写VOL头信息*/ set_timecodes(pEnc->current,pEnc->reference,pEnc->mbParam.fbase); BitstreamPad(bs); /*写VOP头信息*/ BitstreamWriteVopHeader(bs, &pEnc->mbParam, pEnc->current, 1, pEnc->current->mbs[0].quant); /*初始化统计编码信息*/ pEnc->current->sStat.iTextBits = 0; pEnc->current->sStat.kblks = mb_width * mb_height; pEnc->current->sStat.mblks = pEnc->current->sStat.ublks = 0; for (y = 0; y < mb_height; y++) for (x = 0; x < mb_width; x++) { MACROBLOCK *pMB = &pEnc->current->mbs[x + y * pEnc->mbParam.mb_width]; CodeIntraMB(pEnc, pMB); /*图像数据DCT变换、量化、反量化、IDCT变换,更新当前图像数据*/ MBTransQuantIntra(&pEnc->mbParam, pEnc->current, pMB, x, y,dct_coeff, qnt_coeff); /*AC/DC预测*/ MBPrediction(pEnc->current, x, y, pEnc->mbParam.mb_width, qnt_coeff); /*系数VLC编码*/ MBCoding(pEnc->current, pMB, qnt_coeff, bs, &pEnc->current->sStat); } BitstreamPadAlways(bs); /*填充对齐*/ pEnc->current->length=(BitstreamPos(bs)-bits)/8; /*获取当前帧编码后的码流长速*/ pEnc->mbParam.m_fcode=1; /*强制运动估计的搜索窗口*/ pEnc->current->is_edged=0; /*没有扩展*/ return 1; /*intra*/ }
I帧编码比较简单,编码过程一目了然。AC/DC是对宏块变换系数的第一行和第一列做预测,以进一步增加零系数的数目,提高压缩比。
2)FrameCodeP
P帧编码是帧间编码,利用前面已经编码、解码重建的帧作为参考,插值出多个参考帧,在上述参考帧中搜索最佳匹配块。对当前编码块和匹配块相减,对残差做DCT、量化和编码。不同的搜索算法、不同的搜索窗口搜索的匹配块运动向量(MV)、匹配相似度等有所不同。
在图像帧P帧编码中,有一定数量的Intra模式的块编码,其他绝大部分为Inter模式或not_coded不编码模式。算法支持当整整的Intra块编码模式超过一定数量时,强制为I帧编码。
Inter为帧间编码,即对残差进行编码。Not_coded表示当前宏块没有编码,为零块,因此在解码重建时,是直接把参考帧的块复制过来作为解码图像,这种块模式的宏块一般都是背景图像。
P帧编码FrameCodeP()的流程如图3-5所示,根据该流程,P帧编码的过程如下:
图3-5 P帧编码流程框图
int FrameCodeP(Encoder * pEnc,Bitstream * bs) { int bits = BitstreamPos(bs); int iLimit,x,y; int bIntra=0, skip_possible; FRAMEINFO*const current=pEnc->current; /*当前编码帧*/ FRAMEINFO*const reference=pEnc->reference; /*重建参考帧*/ MBParam*const pParam=&pEnc->mbParam; /*宏块编码结构信息*/ const uint32_t mb_width=pParam->mb_width; /*宏块宽度*/ const uint32_t mb_height=pParam->mb_height; /*宏块高度*/ const uint32_t edged_width=pParam->edged_width; /*扩展图像宽度*/ DECLARE_ALIGNED_MATRIX(dct_coeff , 6, 64, int16_t, CACHE_LINE); /*DCT系数*/ DECLARE_ALIGNED_MATRIX(qnt_coeff , 6, 64, int16_t, CACHE_LINE); /*DCT系数量化结果*/ /*初始化编码信息*/ pParam->m_rounding_type = 1 - pParam->m_rounding_type; current->rounding_type = pParam->m_rounding_type; current->fcode = pParam->m_fcode; current->coding_type = P_VOP; pEnc->current->mbs[0].quant = pEnc->current->quant; SetMacroblockQuants(&pEnc->mbParam,current); /*初始化宏块量化步长*/ /*运动估计模块,把当前图像与上一帧的重建帧做最佳匹配,根据SAD抉择*/ /*每个宏块的编码模式Intra、Inter、not_coded,如果Intra块超过50%,则bIntra=1*/ iLimit=(mb_width*mb_height)>>1; /*当Intra块超过宏块的50%时,强制I帧编码*/ bIntra = MotionEstimation(&pEnc->mbParam, current, reference,iLimit); if(bIntra==1) return FrameCodeI(pEnc,bs); /*bIntra为1则执行I帧编码,如场景切换*/ set_timecodes(current,reference,pParam->fbase); /*写VOP头信息到码流*/ BitstreamWriteVopHeader(bs, &pEnc->mbParam, current, 1, current->mbs[0].quant); /*初始化状态统计变量*/ current->sStat.iTextBits = current->sStat.iMvSum = current->sStat.iMvCount = current->sStat.kblks = current->sStat.mblks = current->sStat.ublks = 0; /*以宏块为单位循环编码*/ for (y = 0; y < mb_height; y++) { for (x = 0; x < mb_width; x++) { MACROBLOCK *pMB = ¤t->mbs[x + y * mb_width]; bIntra = (pMB->mode == MODE_INTRA); if(bIntra){ /*Intra块编码,即I帧编码的核心函数*/ CodeIntraMB(pEnc, pMB); MBTransQuantIntra(&pEnc->mbParam, current, pMB, x, y, dct_coeff, qnt_coeff); MBPrediction(current, x, y, mb_width, qnt_coeff); MBCoding(current, pMB, qnt_coeff, bs, ¤t->sStat); continue; } /*运动补偿,根据运动向量确定具体的宏块参考位置,并做差值计算,结果存放dct_coeff*/ MBMotionCompensation(pMB, x, y, pRef, pCur, dct_coeff, pParam->edged_width); /*差值做DCT、量化、反量化、IDCT,并更新到当前图像*/ if(pMB->mode!=MODE_NOT_CODED) /*差值编码,即差值不全为零*/ pMB->cbp=MBTransQuantInter(&pEnc->mbParam,current, pMB, x, y, dct_coeff, qnt_coeff); skip_possible = (pMB->cbp == 0) && (pMB->mode == MODE_INTER); if (current->coding_type == P_VOP) skip_possible &= ( (pMB->mvs[0].x == 0) && (pMB->mvs[0].y == 0) ); /*宏块没有编码,跳过该宏块,继续编码*/ if ( (pMB->mode == MODE_NOT_CODED) || (skip_possible)) { pMB->mode = MODE_NOT_CODED; MBSkip(bs); continue; } /*宏块编码,即差值的DCT系数量化值做VLC编码*/ MBCoding(current, pMB, qnt_coeff, bs, &pEnc->current->sStat); } } pParam->m_fcode=1; /*运动估计ME搜索窗口*/ pEnc->current->is_edged=0; /*没有做扩展*/ BitstreamPadAlways(bs); /*填充编码后的比特流*/ current->length=(BitstreamPos(bs)-bits)/8; /*编码后的码流长度*/ return 0;
在P帧编码前,首先对整帧做运动估计,确定每个宏块的编码模式为,Intra、Inter、Not_coded。如果Intra块的数量超过宏块总数的一定比例,则强制该帧做I帧编码。P帧编码中,既有Inter块的编码也有Intra块的编码,同时对零块不作编码Not_coded,即跳过该宏块MBSkip。另外,为了提高编码效率,编码器中省略了图像扩展的模块。但是需要特别注意,为了保证解码器解码出的图像正确,在创建编、解码器的图像帧空间时(image_create函数),应使初始化为确定的相同的值,否则解码器重建的图像边缘会出错。
3)Intra宏块变换、量化/反量化、反变换
图像Intra块的变换与量化一般是放置在一起。变换采用离散余弦变换(DCT),量化采用H.263的均匀量化。并且量化采取的是查表、移位的方式,从而避免了除法。实现该功能的函数是MBTransQuantIntra(),其代码如下:
void MBTransQuantIntra(const MBParam * const pParam, const FRAMEINFO * const frame, MACROBLOCK*const pMB, const uint32_t x_pos,const uint32_t y_pos, int16_t data[6*64], int16_t qcoeff[6*64]) { /* 从图像空间读取宏块数据,并做16位扩展 */ MBTrans8to16(pParam, frame, pMB, x_pos, y_pos, data); /* 宏块做DCT变换 */ MBfDCT(pParam, frame, pMB, x_pos, y_pos, data); /* 宏块DCT系数做量化 */ MBQuantIntra(pParam, frame, pMB, data, qcoeff); /* 宏块DCT系数做反量化 */ MBDeQuantIntra(pParam, pMB->quant, data, qcoeff); /* 宏块做IDCT变换 */ MBiDCT(data, 0x3F); /* 把重建的宏块更新到当前编码帧,直接复制*/ MBTrans16to8(pParam, frame, pMB, x_pos, y_pos, data, 0, 0x3F); }
上述程序实现宏块(包括一个16×16的亮度块,两个8×8的色度快)的变换与量化处理。首先扩展图像为16位,为DCT做数据源,接着做变换和量化。最后为保证与解码图像一致,做反DCT和反量化,将重建值更新到编码图像。
4)Inter宏块变换、量化/反量化、反变换
Inter块的处理同Intra块基本相同,只是根据CBP编码模式决定那些块的重建值更新编码图像。实现函数为MBTransQuantInter(),其代码实现如下:
uint8_t MBTransQuantInter(const MBParam * const pParam, const FRAMEINFO * const frame, MACROBLOCK*const pMB, const uint32_t x_pos,const uint32_t y_pos, int16_t data[6 * 64], int16_t qcoeff[6 * 64]) { uint8_t cbp; /*MC中已经做了减法,得到了图像差值 */ /* 宏块DCT*/ MBfDCT(pParam, frame, pMB, x_pos, y_pos, data); /* Set the limit threshold */ /* 宏块DCT系数量化 */ cbp = MBQuantInter(pParam, frame, pMB, data, qcoeff, 0, 1); /* 宏块DCT系数反量化 */ MBDeQuantInter(pParam, pMB->quant, data, qcoeff, cbp); /* 宏块反DCT变换 */ MBiDCT(data, cbp); /* 重建的宏块数据更新到当前编码帧,做加操作,与MC相反 */ MBTrans16to8(pParam, frame, pMB, x_pos, y_pos, data, 1, cbp); return(cbp);/* 返回块编码模式CBP(Coded Block Pattern)*/ }
上述代码实现Inter宏块的变换与量化处理,需特别注意CBP的获取与应用。
5)变换、量化后的系数做VLC熵编码
具体代码如下:
void MBCoding(const FRAMEINFO*const frame, MACROBLOCK*pMB, int16_t qcoeff[6 * 64], Bitstream * bs, Statistics * pStat) { if (frame->coding_type != I_VOP) BitstreamPutBit(bs,0); /* 编码模式:not_coded*/ if (pMB->mode == MODE_INTRA) CodeBlockIntra(frame,pMB,qcoeff,bs,pStat); /*Inter宏块编码,MV、系数VLC*/ else CodeBlockInter(frame,pMB,qcoeff,bs,pStat); /*Intra宏块系数VLC*/ }
对于Inter宏块编码中,首先写mcbpc、cbpy、MV,最后写宏块系数。对于Intra宏块编码中,首先写mcbpc、AC预测方向、cbpy,最后写宏块系数。对于图像或图像差值做变换、量化/反量化、反变换、编码等底层核心模块,为了提高效率,一般要汇编优化、改写。在后面的两节中,分别介绍采用MMX/SSE2和DM642两个平台优化核心模块。这也符合视频编码算法开发应用的思路:框架用C语言编程、核心模块用汇编优化。
3.Xvid销毁编码器
销毁编码器实例对应于创建编码器实例的工作,即把所有申请的内存释放。
int enc_destroy(Encoder * pEnc) { /*释放图像序列空间*/ image_destroy(&pEnc->queue[0].image, pEnc->mbParam.edged_width,pEnc->mbParam.edged_height); Xvid_free(pEnc->queue); /*释放当前编码帧的图像空间*/ image_destroy(&pEnc->current->image, pEnc->mbParam.edged_width,pEnc->mbParam.edged_height); /*释放参考帧的图像空间*/ image_destroy(&pEnc->reference->image, pEnc->mbParam.edged_width,pEnc->mbParam.edged_height); /*释放参考帧的水平插值帧的图像空间*/ image_destroy(&pEnc->vInterH, pEnc->mbParam.edged_width,pEnc->mbParam.edged_height); /*释放参考帧的垂直插值帧的图像空间*/ image_destroy(&pEnc->vInterV, pEnc->mbParam.edged_width,pEnc->mbParam.edged_height); /*释放参考帧的水平、垂直(即斜角方向)插值帧的图像空间*/ image_destroy(&pEnc->vInterHV, pEnc->mbParam.edged_width,pEnc->mbParam.edged_height); /*释放宏块编码空间*/ Xvid_free(pEnc->current->mbs); Xvid_free(pEnc->current); /*释放空间*/ /*释放宏块编码空间*/ Xvid_free(pEnc->reference->mbs); Xvid_free(pEnc->reference);/*释放空间*/ Xvid_free(pEnc); /*释放编码器的空间*/ return 0; }
Xvid的MPEG-4视频编码有多种参数可供选择。开发者可以根据自己的指标要求和不同的实现平台,组合编码参数或删减无用的代码以提高编码器的效率。一个典型的嵌入式MPEG-4视频编码算法一般是不支持B帧编码、1/4像素精度或MPEG量化模式的,但是可采用浮点的码流控制机制以适应网络传输。对于通用CPU,如Intel系列CPU,在实现视频编码以验证算法的效率时,可以选择较多的功能,但是实时性可能不高。不过,随着CPU主频等性能的提升,软压缩得到了极大的发展,对硬件视频压缩有一定的影响。