看了有一段时间了,把过程中的一些东西记下来,主要留给自己以后看。这些代码写的真的很赏心悦目(比ffmpeg包含的H264解码器好看很多),但是没有一分documentation就越来越恶心了,很多连变量的意义到现在还不清楚。 这次做还没有作深入,先做一个较为大体的框架性的阅读吧,现在先不弄NAL码流的写入,B-frame SI && SP ,deblocking filter,和RDO,当然这些好东西会在以后加上的,RDO必然不能缺,现在先这样着,先“形式化的描述一下”,那些当做模块在添加吧。我用的版本是x264-snapshot-20090216-2245,不算老吧。ok,let‘s go。 1.VS下的工程布局。整个工程分为两个部分,一个是从frame级向下的包括MB Mode selection,ME,MC,DCT及熵编码。这部分被封装成为libx264.lib;另外一部分就是主调部分了,提供参数初始化,根据参数构造编码器x264_t *h(其实这个工程面向对象的思想很浓重),其实我们也可以根据自己的需要来写主调部分使用封装好的libx264.lib。
2.好,进入函数吧 1)main=>x264_param_default,对param进行付default值,包括分辨率帧率,video Utility Information中的图像显示比;参考帧的数目,各个功能模块的开关rate control中的诸如method,qp的范围步长;模式分析的方法16x16--4x4,ME的方法范围,是否skip。 可以根据命令行参数更改param,这些将来会构造一个编码器,这部分在x264_encoder_open中,里面还进行了一些初始化比如将一些函数指针指向一些target,zigzag,dct初始化等等。 2)Encode内。思路:1.构造编码器2.打开视频序列3.在每一帧时进入核心部分,也就是刚才说的被封装到libx264里面的函数里。 Encode() { x264_picture_alloc( &pic, X264_CSP_I420, param->i_width, param->i_height ); for( i_frame = 0, i_file = 0; b_ctrl_c == 0 && (i_frame < i_frame_total || i_frame_total == 0); ) { p_read_frame( &pic, opt->hin, i_frame + opt->i_seek ); Encode_frame( h, opt->hout, &pic ); } do {Encode_frame( h, opt->hout, NULL );} while(something) } 代码比人会说话,这是我截出来的。在x264_picture_alloc为一个pic分配空间,for里面每一帧读入到pic内,以后进入到frame内循环分块。需要指出YUV420的文件格式。一个字节是一个像素值的大小,Y为全采样,UV在水平和垂直方向都是半采样,在文件内存放时是这样子的结构,文件:{frame1,frame2,...frameEnd};frame:{Y,U,V},所以一帧大小会是width x height x 3/2(Bytes). 知道文件结构,只要会空指针在哪怎么转换,指针函数怎么使,就能读懂这里面诸如p_get_frame_total,p_read_frame等等函数。
另外在最后面的do-while是用来编码最后还在缓存内的B-frame的,可以看到Encode_frame有pic位置为NULL
3)进入libx264部分,开始重要了。 3.1)x264_encoder_encode,我们使用一个thread,我写出框架,一会填充。 x264_encoder_encode(x264_t *h, *pic_in,*pic_out) { x264_reference_update( h );//更新参考序列 if(!pic_in) { x264_frame_t *fenc = x264_frame_pop_unused( h ); x264_frame_copy_picture( h, fenc, pic_in ); x264_frame_push( h->frames.next, fenc ); } if( h->frames.current[0] == NULL ) { x264_stack_align( x264_slicetype_decide, h ); *&^*&%(//B-frame缓存,暂时不讲 } h->fenc = x264_frame_shift( h->frames.current ); do_encode://goto 的标号 //根据帧类型来写slice-type和NAL的一些info h->fdec->i_poc = h->fenc->i_poc = 2 * (h->fenc->i_frame - h->frames.i_last_idr);//主要是这句,用来建立list 0/1用的 x264_reference_build_list( h, h->fdec->i_poc ); //下面写了rate control策略,对B-frame的策略,想码流写入sps,pps。全都不讲呢 x264_slices_write(h); //下面是由于编码P-frame代价过高和其他策略重新按照I-slice编码,有goto x264_encoder_frame_end(); }
这部分主要是参考帧的管理当做3.2吧。 3.2)参考帧的管理,这里必须明白个变量的意义: 3.2.1)First. 4个数组,既是堆栈又是队列,分别命名为reference(真正参考队列),unused(垃圾站,我这么认为),next(未决定类型的帧队列),current(决定类型带编码的队列) second. 变量fenc就是个临时变量,用来传递使用,真正被编码和解码的是h->fenc和h->fdec. 下面详解(这里流程比较乱,加上没有注释和doc,开始比较难懂,代码个人认为简单,所以讲逻辑): 进入Encoder_encode的前提是一帧已经编码完成了,那该帧h->fdec是否可以作为参考帧呢,这进入 x264_reference_update( h );如果是则将其push到refenence数组里,所以这里是时间顺序的。如果refenence数组满了,那把最早的参考帧扔进unused里面。然后h->fdec = x264_frame_pop_unused( h );这样就为h->fdec分配了空间,不至于每次都new 或者calloc一块空间,所以这个unused设计还是很赞的! 同样为临时变量fdec从unused内那一块空间,将pic_in拷贝到里面,然后对fenc进行操作,比如扩充成为16整数倍的分辨率等操作,然后将fenc空间push到next队列。 编码时我需要决定该帧为什么类型IBP,决定好之后放到current数组内(其实如果单进程的话,个人认为一个单位空间就够了)。从current取出后放入h->fenc即代码: h->fenc = x264_frame_shift( h->frames.current ); 3.2.2)在以上都做好了以后,我们需要对该帧建立一个参考队列list0/1,他们的区别是poc顺序不同:for example,当前帧为125,则list0可能为124、123、122、126、127、128;list1可能为126 127 128 124 123 122 建立的过程在x264_reference_build_list( h, h->fdec->i_poc );里面就是一些拷贝和小的优化的冒泡排序,比较简单。
4.下面就是宏块级别的了,进入x264_slice_write while( (mb_xy = i_mb_x + i_mb_y * h->sps->i_mb_width) < h->sh.i_last_mb ) { x264_macroblock_cache_load( h, i_mb_x, i_mb_y ); x264_macroblock_analyse( h ); x264_macroblock_encode( h ); x264_macroblock_cache_save( h ); } 在这里和下一级别的分析中有必要先讲一下这个h->mb.cache(没法讲,就是cache!)。 x264_macroblock_cache_load将参考帧中某位置的(重建后)数据保存进cache,供参考和反复使用。 x264_macroblock_cache_save在分析和编码后将当前块写进cache。 在这些操作里面会有一个很让人看不懂得地方,这时候需要知道它保存时按照什么保存的。x264设计了一个相当nice的一块内存来存放,并且为其编配了一个索引,这样又能直观的理解,又能节省空间增加效率。索引如下:
static const int x264_scan8[16+2*4+3] = { /* Luma */ 4+1*8, 5+1*8, 4+2*8, 5+2*8, 6+1*8, 7+1*8, 6+2*8, 7+2*8, 4+3*8, 5+3*8, 4+4*8, 5+4*8, 6+3*8, 7+3*8, 6+4*8, 7+4*8, /* Cb */ 1+1*8, 2+1*8, 1+2*8, 2+2*8, /* Cr */ 1+4*8, 2+4*8, 1+5*8, 2+5*8, /* Luma DC */ 4+5*8, /* Chroma DC */ 5+5*8, 6+5*8 };
cache中所有数据的存放都是按照上图中位置存放的,黄色和绿色是当前编码的Luma(Y) 和chroma(UV),而白色部分是从list0/1中取出参考帧,并且在x264_macroblock_cache_load时将对应块存放到这个空间里面去。cache里面包含一切有用的信息,这个设计真的很赞的!!
下面真的开始重要了! 4.1 x264_macroblock_analyse( h );对当前宏块(16x16)分析。因为帧内所走的路帧间都走了,并且帧间复杂一些,就只分析帧间了 x264_macroblock_analyse( h ) { x264_mb_analysis_t analysis; x264_mb_analyse_init( h, &analysis, h->mb.i_qp ); if(SLICE_TYPE_I) {^*&^%(((&&;}//不分析 else if(SLICE_TYPE_P ){ h->mc.prefetch_ref( h->mb.pic.p_fref[0][0][h->mb.i_mb_x&3], h->mb.pic.i_stride[0], 0 );//当前块的数据已经从h->fenc整帧拷贝到h->mb里面。 x264_mb_analyse_load_costs( h, &analysis ); x264_mb_analyse_inter_p16x16( h, &analysis ); //子结构分析,分析方法类似16x16
x264_me_refine_qpel( h, &analysis.l0.me16x16 ); } x264_analyse_update_cache( h, &analysis ); } 详细: x264_mb_analyse_init里面初始化一些cost代价值,rdo对于不同量化参数所用的lamda,mv的范围等。 x264_mb_analyse_load_costs( h, &analysis );这里面主要根据qp的不同初始化了analyse->p_cost_mv,这个数组是一个4*4*2048的大小,表示mv里中心越偏离代价也就越大,代价的计算方法有数学公式的。
下面详细讲x264_mb_analyse_inter_p16x16( h, &analysis );
累了,休息。。。。 ------------------------------ Complaint:用x264和ffmpeg,不算工程和各种没用的文件,两个加一起有近4MB的代码,哭死了,我才能读懂多少,一年能全看懂就不赖了,觉得更多的东西都是信息的学的,各种编码啊,原来做东西是用哪看哪吧,不过这东西联系很紧密,岂是能看一块就完了的,我是来学习的,什么都不学就让我做东西,研究生是什么啊,那岂不赤裸裸了啊,我只看一块,我怎么找工作,我为自己的将来没错吧 |
|