MPEG-4/H.264视频编解码工程实践
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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 = &current->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, &current->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主频等性能的提升,软压缩得到了极大的发展,对硬件视频压缩有一定的影响。