转自:http://blog.sina.com.cn/s/blog_76d4be5f01018bbf.html
经过近1周的时间学习x264,对x264有了一些了解,但是如果需要真正深入了解其结构中编码原理却另外需要很多的时间来学习,在这里只总结一下,我这些日子来对x264的学习,一来可以重新梳理一下知识,整理成条理,二来能够起到备忘的功能。好了,现在开始从主函数开始入手分析吧!
1、主函数的分析
int main( int argc, char **argv )
{
x264_param_t param;
cli_opt_t opt;
int ret;
#ifdef PTW32_STATIC_LIB//定义静态链接库,线程方面的一些相关的东东
pthread_win32_process_attach_np();
pthread_win32_thread_attach_np();
#endif
#ifdef _WIN32
_setmode(_fileno(stdin), _O_BINARY);
_setmode(_fileno(stdout), _O_BINARY);//输出文本的模式的转换
#endif
x264_param_default( ¶m );
if( Parse( argc, argv, ¶m, &opt ) < 0 )
//解析命令行的函数,后面做详细分析
return -1;
signal( SIGINT, SigIntHandler );
//现在到了最重要的编码阶段
ret = Encode( ¶m, &opt );
//根据给定的参数和设置等进行编码,详细编码过程后面详细说明
#ifdef PTW32_STATIC_LIB //静态库的链接
pthread_win32_thread_detach_np();
pthread_win32_process_detach_np();
#endif
return ret;
}
2、函数x264_param_default( ¶m )
该函数定义在common.c中,主要是对param参数设定一些缺省值。完成x264_param_t结构体的初始化。包括cpu自动检测、视频序列参量初始化(均为默认值)、编码参量初始化。
void x264_param_default( x264_param_t *param )
{
memset( param, 0, sizeof( x264_param_t ) );
param->cpu = x264_cpu_detect();
param->i_threads = 1;//并行编码线程为1
param->b_deterministic = 1;//允许非确定性时线程优化
param->i_csp = X264_CSP_I420;
param->i_width = 0;//视频的宽度
param->i_height = 0;//高度
param->vui.i_sar_width = 0;//设置长宽比
param->vui.i_sar_height= 0;
param->vui.i_overscan = 0;
param->vui.i_vidformat = 5;
param->vui.b_fullrange = 0;
param->vui.i_colorprim = 2;
param->vui.i_transfer = 2;
param->vui.i_colmatrix = 2;
param->vui.i_chroma_loc= 0;
param->i_fps_num = 25;
param->i_fps_den = 1;
param->i_level_idc = -1;
param->i_frame_reference = 1;
param->i_keyint_max = 250;
param->i_keyint_min = 25;
param->i_bframe = 0;
param->i_scenecut_threshold = 40;
param->i_bframe_adaptive = X264_B_ADAPT_FAST;
param->i_bframe_bias = 0;
param->b_bframe_pyramid = 0;
param->b_deblocking_filter = 1;
param->i_deblocking_filter_alphac0 = 0;
param->i_deblocking_filter_beta = 0;
param->b_cabac = 1;
param->i_cabac_init_idc = 0;
param->rc.i_rc_method = X264_RC_NONE;
param->rc.i_bitrate = 0;
param->rc.f_rate_tolerance = 1.0;
param->rc.i_vbv_max_bitrate = 0;
param->rc.i_vbv_buffer_size = 0;
param->rc.f_vbv_buffer_init = 0.9;
param->rc.i_qp_constant = 26;
param->rc.f_rf_constant = 0;
param->rc.i_qp_min = 10;
param->rc.i_qp_max = 51;
param->rc.i_qp_step = 4;
param->rc.f_ip_factor = 1.4;
param->rc.f_pb_factor = 1.3;
param->rc.i_aq_mode = X264_AQ_VARIANCE;
param->rc.f_aq_strength = 1.0;
param->rc.b_stat_write = 0;
param->rc.psz_stat_out = "x264_2pass.log";
param->rc.b_stat_read = 0;
param->rc.psz_stat_in = "x264_2pass.log";
param->rc.f_qcompress = 0.6;
param->rc.f_qblur = 0.5;
param->rc.f_complexity_blur = 20;
param->rc.i_zones = 0;
param->pf_log = x264_log_default;
param->p_log_private = NULL;
param->i_log_level = X264_LOG_INFO;
param->analyse.intra = X264_ANALYSE_I4x4 | X264_ANALYSE_I8x8;
param->analyse.inter = X264_ANALYSE_I4x4 | X264_ANALYSE_I8x8
| X264_ANALYSE_PSUB16x16 | X264_ANALYSE_BSUB16x16;
param->analyse.i_direct_mv_pred = X264_DIRECT_PRED_SPATIAL;
param->analyse.i_me_method = X264_ME_HEX;
param->analyse.f_psy_rd = 1.0;
param->analyse.f_psy_trellis = 0;
param->analyse.i_me_range = 16; / *运动估计范围*/
param->analyse.i_subpel_refine = 6;
param->analyse.b_chroma_me = 1; param->analyse.i_mv_range_thread = -1;
param->analyse.i_mv_range = -1;
param->analyse.i_chroma_qp_offset = 0;
param->analyse.b_fast_pskip = 1;
param->analyse.b_dct_decimate = 1; /
param->analyse.i_luma_deadzone[0] = 21;
param->analyse.i_luma_deadzone[1] = 11;
param->analyse.b_psnr = 1;
param->analyse.b_ssim = 1;
param->i_cqm_preset = X264_CQM_FLAT;
memset( param->cqm_4iy, 16, 16 );
memset( param->cqm_4ic, 16, 16 );
memset( param->cqm_4py, 16, 16 );
memset( param->cqm_4pc, 16, 16 );
memset( param->cqm_8iy, 16, 64 );
memset( param->cqm_8py, 16, 64 );
param->b_repeat_headers = 1;
param->b_aud = 0;
}
3、Parse( argc, argv, ¶m, &opt )函数
该函数是解析命令参数的,函数定义在x264.c里面。由于该函数较长,但是比较重要的函数不多,现在来说明下。
(1)int c = getopt_long( argc,
argv,
"8A:B:b:f:hI:i:m:o:p:q:r:t:Vvw",
long_options,
&long_options_index);
getopt_long()解析入口地址的向量,例如我们输入的运行参数为:(-o test.264 foreman.yuv 176x144 ),则运行结果后c得到的值就是-o中的o对应的ASCII码值111,即c=111,该函数的具体实现功能在getopt.c函数里面,可以去参看。然后跳转case ‘o’,判断输出文件名的后缀是什么就执行打开什么文件的格式,执行p_open_outfile(optarg,&opt->hout),则以二进制写入的形式打开输出文件tset.264。由于有个for(;;)循环,所以再进行检测命令行,如果还遇到其他命令则执行相应的操作,否则如果没有其他命令,c就得到-1值,则break跳出for循环。其中有一些打开文件的函数需要去看一下。
4、Encode( x264_param_t *param, cli_opt_t *opt )函数
开始编码的路程,该函数定义如下:
static int Encode( x264_param_t *param, cli_opt_t *opt )
{
x264_t *h;
x264_picture_t pic;
int i_frame, i_frame_total;
int64_t i_start, i_end;
int64_t i_file;
int i_frame_size;
int i_update_interval;
char buf[200];
opt->b_progress &= param->i_log_level < X264_LOG_DEBUG;
i_frame_total = p_get_frame_total( opt->hin );
i_frame_total -= opt->i_seek;
if( ( i_frame_total == 0 || param->i_frame_total < i_frame_total )
&& param->i_frame_total > 0 )
i_frame_total = param->i_frame_total;
param->i_frame_total = i_frame_total;
i_update_interval = i_frame_total ? x264_clip3( i_frame_total / 1000, 1, 10 ) : 10;
if( ( h = x264_encoder_open( param ) ) == NULL )
{
fprintf( stderr, "x264 [error]: x264_encoder_open failed\n" );
p_close_infile( opt->hin );
return -1;
}
if( p_set_outfile_param( opt->hout, param ) )
{
fprintf( stderr, "x264 [error]: can't set outfile param\n" );
p_close_infile( opt->hin );
p_close_outfile( opt->hout );
return -1;
}
x264_picture_alloc( &pic, X264_CSP_I420, param->i_width, param->i_height );
i_start = x264_mdate();
for( i_frame = 0, i_file = 0; b_ctrl_c == 0 && (i_frame < i_frame_total || i_frame_total == 0); )
{
if( p_read_frame( &pic, opt->hin, i_frame + opt->i_seek ) )
break;
pic.i_pts = (int64_t)i_frame * param->i_fps_den;
if( opt->qpfile )
parse_qpfile( opt, &pic, i_frame + opt->i_seek );
else
{
pic.i_type = X264_TYPE_AUTO;
pic.i_qpplus1 = 0;
}
i_file += Encode_frame( h, opt->hout, &pic );
i_frame++;
if( opt->b_progress && i_frame % i_update_interval == 0 )
{
int64_t i_elapsed = x264_mdate() - i_start;
double fps = i_elapsed > 0 ? i_frame * 1000000. / i_elapsed : 0;
double bitrate = (double) i_file * 8 * param->i_fps_num / ( (double) param->i_fps_den * i_frame * 1000 );
if( i_frame_total )
{
int eta = i_elapsed * (i_frame_total - i_frame) / ((int64_t)i_frame * 1000000);
sprintf( buf, "x264 [%.1f%%] %d/%d frames, %.2f fps, %.2f kb/s, eta %d:d:d",
100. * i_frame / i_frame_total, i_frame, i_frame_total, fps, bitrate,
eta/3600, (eta/60)`, eta` );
}
else
{
sprintf( buf, "x264 %d frames: %.2f fps, %.2f kb/s", i_frame, fps, bitrate );
}
fprintf( stderr, "%s \r", buf+5 );
SetConsoleTitle( buf );
fflush( stderr );
}
}
do {
i_file +=
i_frame_size = Encode_frame( h, opt->hout, NULL );
} while( i_frame_size );
i_end = x264_mdate();
x264_picture_clean( &pic );
if( opt->b_progress )
fprintf( stderr, " \r" );
x264_encoder_close( h );
x264_free( mux_buffer );
fprintf( stderr, "\n" );
if( b_ctrl_c )
fprintf( stderr, "aborted at input frame %d\n", opt->i_seek + i_frame );
p_close_infile( opt->hin );
p_close_outfile( opt->hout );
if( i_frame > 0 )
{
double fps = (double)i_frame * (double)1000000 /
(double)( i_end - i_start );
fprintf( stderr, "encoded %d frames, %.2f fps, %.2f kb/s\n", i_frame, fps,
(double) i_file * 8 * param->i_fps_num /
( (double) param->i_fps_den * i_frame * 1000 ) );
}
return 0;
}
(1) parse_qpfile( cli_opt_t *opt, x264_picture_t *pic, int i_frame )函数
static void parse_qpfile( cli_opt_t *opt, x264_picture_t *pic, int i_frame )
{
int num = -1, qp, ret;
char type;
uint64_t file_pos;
while( num < i_frame )
{
file_pos = ftell( opt->qpfile );
ret = fscanf( opt->qpfile, "%d %c %d\n", &num, &type, &qp );
if( num > i_frame || ret == EOF )
{
pic->i_type = X264_TYPE_AUTO;
pic->i_qpplus1 = 0;
fseek( opt->qpfile , file_pos , SEEK_SET );
break;
}
if( num < i_frame )
continue;
pic->i_qpplus1 = qp+1;
if ( type == 'I' ) pic->i_type = X264_TYPE_IDR;
else if( type == 'i' ) pic->i_type = X264_TYPE_I;
else if( type == 'P' ) pic->i_type = X264_TYPE_P;
else if( type == 'B' ) pic->i_type = X264_TYPE_BREF;
else if( type == 'b' ) pic->i_type = X264_TYPE_B;
else ret = 0;
if( ret != 3 || qp < -1 || qp > 51 )
{
fprintf( stderr, "x264 [error]: can't parse qpfile for frame %d\n", i_frame );
fclose( opt->qpfile );
opt->qpfile = NULL;
pic->i_type = X264_TYPE_AUTO;
pic->i_qpplus1 = 0;
break;
}
}
}
(2)Encode_frame( x264_t *h, hnd_t hout, x264_picture_t *pic )函数
static int Encode_frame( x264_t *h, hnd_t hout, x264_picture_t *pic )
{
x264_picture_t pic_out;
x264_nal_t *nal;
int i_nal, i;
int i_file = 0;
if( x264_encoder_encode( h, &nal, &i_nal, pic, &pic_out ) < 0 )
{
fprintf( stderr, "x264 [error]: x264_encoder_encode failed\n" );
}
for( i = 0; i < i_nal; i++ )
{
int i_size;
if( mux_buffer_size < nal[i].i_payload * 3/2 + 4 )
{
mux_buffer_size = nal[i].i_payload * 2 + 4;
x264_free( mux_buffer );
mux_buffer = x264_malloc( mux_buffer_size );
}
i_size = mux_buffer_size;
x264_nal_encode( mux_buffer, &i_size, 1, &nal[i] );
i_file += p_write_nalu( hout, mux_buffer, i_size );
}
if (i_nal)
p_set_eop( hout, &pic_out );
return i_file;
}
(2.1)关于x264_encoder_encode函数较复杂,我这里只只写出该函数的大概实现步骤:
1、讲图片复制一帧并放到一个缓冲器里;
2、选择帧类型;
3、移动一些B帧和一个非B帧到编码队列中;
4、获取将要被编码的帧;
5、进行编码,调用Do encode函数来实现;
6、设置输出图片属性;
7、更新编码器状态;
8、计算和打印统计值;
9、判断是否存放重建的帧。
一些名词的介绍:
I、B、P帧的区别:
I帧:帧内编码帧
I帧特点:
1.它是一个全帧压缩编码帧。它将全帧图像信息进行JPEG压缩编码及传输;
2.解码时仅用I帧的数据就可重构完整图像;
3.I帧描述了图像背景和运动主体的详情;
4.I帧不需要参考其他画面而生成;
5.I帧是P帧和B帧的参考帧(其质量直接影响到同组中以后各帧的质量);
6.I帧是帧组GOP的基础帧(第一帧),在一组中只有一个I帧;
7.I帧不需要考虑运动矢量;
8.I帧所占数据的信息量比较大。
P帧:前向预测编码帧。
P帧的预测与重构:P帧是以I帧为参考帧,在I帧中找出P帧“某点”的预测值和运动矢量,取预测差值和运动矢量一起传送。在接收端根据运动矢量从I帧中找出P帧“某点”的预测值并与差值相加以得到P帧“某点”样值,从而可得到完整的P帧。
P帧特点:
1.P帧是I帧后面相隔1~2帧的编码帧;
2.P帧采用运动补偿的方法传送它与前面的I或P帧的差值及运动矢量(预测误差);
3.解码时必须将I帧中的预测值与预测误差求和后才能重构完整的P帧图像;
4.P帧属于前向预测的帧间编码。它只参考前面最靠近它的I帧或P帧;
5.P帧可以是其后面P帧的参考帧,也可以是其前后的B帧的参考帧;
6.由于P帧是参考帧,它可能造成解码错误的扩散;
7.由于是差值传送,P帧的压缩比较高。
B帧:双向预测内插编码帧。
B帧的预测与重构
B帧以前面的I或P帧和后面的P帧为参考帧,“找出”B帧“某点”的预测值和两个运动矢量,并取预测差值和运动矢量传送。接收端根据运动矢量在两个参考帧中“找出(算出)”预测值并与差值求和,得到B帧“某点”样值,从而可得到完整的B帧。
B帧特点
1.B帧是由前面的I或P帧和后面的P帧来进行预测的;
2.B帧传送的是它与前面的I或P帧和后面的P帧之间的预测误差及运动矢量;
3.B帧是双向预测编码帧;
4.B帧压缩比最高,因为它只反映丙参考帧间运动主体的变化情况,预测比较准确;
5.B帧不是参考帧,不会造成解码错误的扩散。
注:I、B、P各帧是根据压缩算法的需要,是人为定义的,它们都是实实在在的物理帧,至于图像中的哪一帧是I帧,是随机的,一但确定了I帧,以后的各帧就严格按规定顺序排列。
I帧和IDR帧的区别:
IDR(Instantaneous Decoding Refresh)--即时解码刷新。
I和IDR帧都是使用帧内预测的。它们都是同一个东西而已,在编码和解码中为了方便,要首个I帧和其他I帧区别开,所以才把第一个首个I帧叫IDR,这样就方便控制编码和解码流程。IDR帧的作用是立刻刷新,使错误不致传播,从IDR帧开始,重新算一个新的序列开始编码。而I帧不具有随机访问的能力,这个功能是由IDR承担。IDR会导致DPB(DecodedPictureBuffer 参考帧列表——这是关键所在)清空,而I不会。IDR图像一定是I图像,但I图像不一定是IDR图像。一个序列中可以有很多的I图像,I图像之后的图像可以引用I图像之间的图像做运动参考。
对于IDR帧来说,在IDR帧之后的所有帧都不能引用任何IDR帧之前的帧的内容,与此相反,对于普通的I-帧来说,位于其之后的B-和P-帧可以引用位于普通I-帧之前的I-帧。从随机存取的视频流中,播放器永远可以从一个IDR帧播放,因为在它之后没有任何帧引用之前的帧。但是,不能在一个没有IDR帧的视频中从任意点开始播放,因为后面的帧总是会引用前面的帧。
结语:
经过这几天对x264编码器代码的学习,大体上面只是了解了编码器的一个大概,很多编码细节的东西涉及到原理的东西都没有时间去细细体会,所以在后面关于宏块方面以及编码的dct变换等数学知识由于较为复杂,时间上面来不及,所以没有怎么弄明白,时间允许的话再继续补充这方面的知识点。
于广州
2011-12-12