xvid有两种编码方式:single pass和twopass
single pass模式编码简单,速度也快,但最终效果不如twopass。
twopass就是视频压制需要经过两次编码,分别为twopass-1st pass(简称1pass)和twopass-2nd pass(简称2pass)
1pass时,编码器会用最高质量编码采集可供第2次运算参考的画面信息,而在2 pass时。编码器会根据第一次压缩获得的信息和用户指定的文件大小,自动分配比特率,使需要高流量的运动画面分配到更多的空间,更高的比特率来保证画面质量。相对的,对于那些不包含太多运动信息的静态画面则用较低的比特率。追求画质的朋友当然会选择这种方式,但运算比single pass更费时。
接下来介绍一些基本概念:
Q值——量化值,它被用来描述1帧的质量,每帧都有一个Q值,取值范围在1-31之间。Q值越小,画质越好,比特率越大
I-frame——关键帧,常被缩写为IF。关键帧是构成一个帧组的第一帧。IF保留了一个场景的所有信息
P-frame——未来单项预测帧,缩写为PF,只储存与之前一个已解压画面的差值
B-frame——双向预测帧,缩写为BF,除了参考之前解压的画面以外,也会参考后一帧的画面信息
编码流程:
各变量的设置:创建xvid_enc_frame_t和xvid_enc_stats_t,分别用于传入参数和统计编码结果。
具体过程:
设置传入图像数据和图像色彩空间
设置传出的码流
设置vol的标志
设置帧的编码类型
设置量化因子
设置运动估计算法集合
设置vop的标志
编码器提供的函数
1,
xvid_global
(NULL, XVID_GBL_INIT, &xvid_gbl_init, NULL);
含义:根据cpu的特性使用相应汇编优化的函数
2,
xvid_encore
(NULL, XVID_ENC_CREATE, &xvid_enc_create, NULL);
含义:初始化编码器。
具体过程:
创建编码器句柄,并根据传入的参数设置各变量的值,并且分配要使用的内存,用于存放重建帧,参考帧(
1/2
像素精度)。以及各种临时变量。并且做好码率控制的初始化。
3,
xvid_encore
(enc_handle, XVID_ENC_ENCODE, &xvid_enc_frame, &xvid_enc_stats);
目的:编码一帧
具体过程:
{
初始化写码流。
如果有必要,转换色彩空间,并且把原始图像拷贝到有边框的图像空间,但是没有扩展边框。
将重建帧交换成参考帧
从帧队列中获取当前帧
设置
Encoder
结构体的
current
结构体的
vol_flags
,
vop_flags
,
motion_flags
,
fcode
,
bcode
和
quant
字段。
调用
call_plugins
,在里面调用
rc_single_before
做码率控制的初始化,以及对
current
结构体的其他变量进一步设置
通过帧号或者
MEanalysis
函数分析来确定编码类型,并且根据用户的设置作修正。
MEanalysis
的原理是,如果某个宏块的残差的
sad
大于该宏块的平均值的偏离,那么使用
intra
方式,否则使用
inter
方式,然后对这些宏块进行统计,得到整帧的编码方式。
如果编码类型是
I_VOP
{
设置
Encoder->mbParam->vol_flags
设置
Encoder->mbParam.par
根据
vol_flags
设置
vop_flags
调用
FrameCodeI
以
I
帧的方式编码
调用
call_plugins
,在里面调用
rc_single_after
,进行码率控制。
}
如果编码类型是
P_VOP
{
用
mbParam.vol_flags
固定住
pEnc->current->vol_flags
调用
FrameCodeP
以
P
帧的方式编码
调用
call_plugins
,在里面调用
rc_single_after
,进行码率控制。
}
}// xvid_encore
4,
static
int
FrameCodeI(Encoder * pEnc, Bitstream * bs)
目的:将一帧图像编码成一个
I
帧
具体过程:
以
XVID_PLG_FRAME
参数调用
call_plugins
,该函数目前的作用是设置
dquant
,可以在该函数中设置最好质量。
调用
SetMacroblockQuants
,为每个宏块设置量化因子,所以也可以在这里设置最好质量
调用
BitstreamWriteVolHeader
,写
vol
调用
set_timecodes
,设置时间编码。
调用
BitstreamPad
,填充
bit
至字节对齐
调用
BitstreamWriteVopHeader
,填写
vop
头
依次读取每一个宏块,进行编码
{
调用
CodeIntraMB
设置编码模式为
intra
,将所有和运动有关的变量设为
0
调用
MBTransQuantIntra
进行变换编码
{
调用
MBTrans8to16
将像素的表示方法从
8bit
扩大到
16bit
调用
MBfDCT
对像素进行变换编码
调用
MBQuantIntra
对
dct
系数进行
intra
方式的量化
调用
MBDeQuantIntra
对
dct
系数进行
intra
方式的反量化
调用
MBiDCT
将恢复的
dct
系数进行反变换
调用
MBTrans16to8
将恢复的
16bit
像素饱和到
8bit
,组成重建宏块
}//MBTransQuantIntra
调用
MBPrediction
作
acdc
预测
{
调用
get_dc_scaler
函数得到量化系数
调用
predict_acdc
得到预测方向以及在该预测方向上的和当前块的同一量化水平的预测值
调用
calc_acdc_bits
以确定是只使用
DC
预测,还是
DCAC
预测。原理是分别作
DC
预测和
DCAC
预测,分别计算在这
2
种情况下需要的码流长度,以确定哪种方式更节约码流。
调用
CodeCoeffIntra_CalcBits
,用于确定各种方式下的码流长度
根据预测模式的不同,恢复成相应的系数
最后计算该宏块的
cbp
}//MBPrediction
调用
MBCoding
将宏块编制成码流
{
调用
CodeBlockIntra
将
intra
宏块编制成码流
{
编码
mcbpc
编码
ac
预测标记
编码
cbpy
对于
6
个块里的每个块
首先编码
DC
系数
调用
CodeCoeffIntra
对剩下的
63
个系数进行编码
}//CodeBlockIntra
}//MBCoding
}//
依次读取每一个宏块,进行编码
填充
bit
,直到字节对齐
5,
static
int FrameCodeP(Encoder * pEnc, Bitstream * bs)
含义:将一帧图片编码成P帧
具体过程:
{
如果参考帧还没有设置边框,那么就调用
image_setedges设置边框
如果需要半像素运动估计,那么就调用
image_interpolate
进行插值
将一帧填充边框后的参考帧,分成
8*8
的小块,对于每个小块进行插值,如下:
调用
interpolate8x8_halfpel_h
进行水平插值
调用
interpolate8x8_halfpel_v
进行垂直插值
调用
interpolate8x8_halfpel_hv
进行对角线插值
用参数
XVID_PLG_FRAME
调用
call_plugins
,该函数目前的作用是设置
dquant
,可以在该函数中设置最好质量。
调用
SetMacroblockQuants
,为每个宏块设置量化因子,所以也可以在这里设置最好质量
调用
MotionEstimation
做运动估计
{
使用
MotionFlags
变量保存要使用的运动算法集合
使用
skip_thresh
保存要达到
skip
模式的阀值
使用
Data
保存运动估计要用到的相应变量
对于每个宏块,依次执行如下操作
{
调用
sad16v
计算本宏块与参考帧对应位置宏块的亮度的残差,将其保存在
pMB->sad16
中,并按照
4
个块的方式分别存放
pMB->sad8[0-3]
中
用
sad00
记录最大亮度块残差的
4
倍
如果还需要考虑色差块的因素
调用
sad8
两次,分别计算
u
分量和
v
分量的残差,都加入
pMB->sad16
中,并且也加入
sad00
中
如果该宏块的量化差值为
0
,并且
sad00
又没有超过
skip
模式的阀值
如果已经考虑了色差因素,或者使用
xvid_me_SkipDecisionP
确认符合
skip
模式。
调用
ZeroMacroblockP
将其编码为
skip
模式,并置标记
pMB->mode = MODE_NOT_CODED
根据采用的运动估计算法不同,做相应的设置
调用
SearchP
做该宏块的运动估计
{
确定是否使用
inter4v
模式,并记录之
调用
get_range
确定运动搜索的范围,并记录在
Data
中
调用
get_pmvdata2
,以获得左,上,右上的运动向量,以及它们对应的
sad
,存入
pmv[1-3]
和
Data->temp[1-3]
。然后计算它们的中值,并且存放于
pmv[0]
,并且把最小的
sad
存放于
Data->temp[0]
设置
Data
的当前宏块的
yuv
字段。设置
Data->RefP[0-5]
为参考帧的同一宏块的整像素
y
,水平半象素
y
,垂直半象素
y
,对角线
y
,
u
,
v
。
设置
Data->lambda16
和
Data->lambda8
,其含义可能是运动向量对带宽的占用折合到
sad
的值
设置
qpel
和方向
如果采用
qpel
,调用
get_qpmv2
计算用
qple
方式下的估计中值,存入
ata->predMV
;否则,
Data->predMV
为
0
。
调用
d_mv_bits
计算
mv
需要的编码
bit
,用于修正
pMB->sad16
和
pMB->sad8[0]
,并将
Data->iMinSAD[0-4]
设置为
pMB->sad16
和
pMB->sad8[0-3]
,也就是
0
向量对应的各
SAD
。
如果不采用率失真决策模型,并且不是当前帧的第一宏块,那么使用一种方法设置阀值
threshA
,否则阀值
threshA
为
512
。
调用
PreparePredictionsP
,对
pmv
作进一步的设置,做运算前的准备。
{
设置
pmv[0]
为
0
向量
设置
pmv[1]
为中值向量的偶数值
设置
pmv[2]
为参考帧相同位置宏块的第
0
块运动向量的偶数值
如果该宏块有左边宏块,设置
pmv[3]
为左边宏块的第
1
块的运动向量的偶数值,否则为
0
如果该宏块有上面宏块,设置
pmv[4]
为上面宏块的第
2
块的运动向量的偶数值,否则为
0
如果该宏块有右上宏块,设置
pmv[5]
为右上宏块的第
2
块的运动向量的偶数值,否则为
0
如果该宏块有右下宏块,设置
pmv[6]
为参考帧的相同宏块的右下宏块的第
0
块的运动向量的偶数值,否则为
0
。
}//PreparePredictionsP
如果使用
inter4v
,设置
CheckCandidate
为
CheckCandidate16
,否则设置为
CheckCandidate16no4v
逐一检查
mpv[1-6]
这六个最可能运动向量,如果发现他们与以前的运动不同,就调用
CheckCandidate
做运动估计,过程如下:
{
检查要做运动估计的运动向量是否越界
通过该运动向量获得所指向数据块的指针
调用
sad16v
,记录下
4
个
8*8
块的
SAD
值,存入
data->temp[0-3]
中,并将他们的和存入临时变量
sad
中。
对
sad
和
data->temp[0]
做基于运动向量的修正。
如果要考虑色差因素,调用
xvid_me_ChromaSAD
计算额外的
SAD
,累加至
sad
中。
如果
sad
小于
data->iMinSAD[0]
,那么设置
data->iMinSAD[0]
,
data->currentMV[0]
,和
data->dir
。注意,此时的
data->dir
记录的不是钻石搜索的方向,而是当前向量是
pmv
数组的第几个元素。
逐一检查
data->temp[0-3]
,如果他们小于
data->iMinSAD[1-4]
,那么修改
data->iMinSAD[1-4]
和
data->currentMV[1-4]
}//CheckCandidate
如果当前最优运动向量,即
Data->iMinSAD[0]
,小于
threshA
?或者当前最优运动向量等于参考帧相同位置宏块的运动向量,并且对应的
SAD
值又比他的小?
就不再做
inter4v
的搜索
否则,就做
inter4v
的搜索
{
使用
make_mask
逐一检查存放于
pmv
的所有运动向量,察看是否位于欲搜索的钻石形的顶点。如果是,则在
mask
变量中标记之。
根据
MotionFlags
确定使用的搜索函数,根据当前设置,
MainSearchPtr
= xvid_me_AdvDiamondSearch
调用
xvid_me_AdvDiamondSearch
进行搜索,过程如下:
{
bDirection
既表明了上次尝试的方向,又表明本次可以尝试的方向
x
,
y
为钻石搜索的位置的中心点坐标
for(;;)
{
如果可以尝试左边,那么调用
CheckCandidate
尝试左边
如果可以尝试右边,那么调用
CheckCandidate
尝试右边
如果可以尝试上边,那么调用
CheckCandidate
尝试上边
如果可以尝试下边,那么调用
CheckCandidate
尝试下边
如果有更好的方向
{
bDirection =
更好的方向
如果更好的方向是左右方向,那么测试该位置的上下方向
否则,那么测试该位置的左右方向
如果这次又找到了更好的方向
将更好的方向累加到
bDirection
将更好的位置存入
x
,
y
}
否则
{
根据去搜索临近未搜索的点,具体规则如下:
如果
bDirection = = 2
,表明搜索方向是趋向右边的,那么搜索当前中心点的右上点和右下点。
如果
bDirection = = 1
,表明搜索方向是趋向左边的,那么搜索当前中心点的左上点和左下点。
如果
bDirection = = 2+4
,表明搜索方向是趋向右上的,那么再搜索当前中心点的左上点,右上点和右下点。
如果
bDirection = = 4
,表明搜索方向是趋向上边的,那么搜索当前中心点的左上点和右上点。
如果
bDirection = = 8
,表明搜索方向是趋向下边的,那么搜索当前中心点的左下点和右下点。
如果
bDirection = = 1+4
,表明搜索方向是趋向左上的,那么再搜索当前中心点的左下点,左上点和右上点。
如果
bDirection = = 2+8
,表明搜索方向是趋向右下的,那么再搜索当前中心点的左下点,左上点和右上点。
如果
bDirection = = 1+8
,表明搜索方向是趋向左下的,那么再搜索当前中心点的左上点,左下点和右下点。
否则的话,则认为本轮搜索没有找到更好的点,那么再搜索当前中心点的左上点,左下点,右上点,右下点。
}
如果没有找到更好的方向,从函数中返回
更新
bDirection
为更好的方向
更新
x
,
y
为更好的位置
}//for(;;)
}//xvid_me_AdvDiamondSearch
如果运动估计算法使用了
XVID_ME_EXTSEARCH16
,那么
{
设置
startMV = Data->predMV
设置
backupMV
为当前最佳运动向量
如果
startMV
和
backupMV
不相等
{
调用
CheckCandidate
计算位置为
startMV
的
SAD
调用
xvid_me_DiamondSearch
做以
startMV
为起点的搜索,过程如下:
{
for(;;)
{
如果可以尝试左边,那么调用
CheckCandidate
尝试左边
如果可以尝试右边,那么调用
CheckCandidate
尝试右边
如果可以尝试上边,那么调用
CheckCandidate
尝试上边
如果可以尝试下边,那么调用
CheckCandidate
尝试下边
如果没有更好的方向,退出
bDirection =
更好的方向
x
,
y =
更好的位置
如果更好的方向是左右方向,那么测试该位置的上下方向
否则,那么测试该位置的左右方向
如果这次又找到了更好的方向
{
bDirection +=
更好的方向
x
,
y =
更好的位置
}
}
}//xvid_me_DiamondSearch
将这次搜索结果和上次搜索结果比较,记录最佳的
SAD
和位置。
}//
如果
startMV
和
backupMV
不相等
设置
startMV = {1
,
1}
设置
backupMV
为当前最佳运动向量
如果
startMV
和
backupMV
不相等
{
调用
CheckCandidate
计算位置为
startMV
的
SAD
调用
xvid_me_DiamondSearch
做以
startMV
为起点的搜索,过程如下:
将这次搜索结果和上次搜索结果比较,记录最佳的
SAD
和位置。
}
}//
如果运动估计算法使用了
XVID_ME_EXTSEARCH16
}//
否则,就做
inter4v
的搜索
如果没有采用
1/4
像素运动估计算法
{
如果采用了
XVID_ME_HALFPELREFINE16
算法
调用
xvid_me_SubpelRefine
按顺时针方向
8
次调用
CheckCandidate16
,得到最好的
1/2
像素位置
}
否则
略
如果当前
SAD
足够小,那么
inter4v = 0
如果采用
inter4v
{
4
次调用
Search8
来搜索
4
个
8*8
块的最佳运动向量,每一次搜索的规则如下:
{
如果采用
1/4
像素运动估计,略。否则
调用
get_pmv2
取得本块的中值
计算第一块以外快的
d_mv_bits
用
Data->lambda8
修正该块当前的
SAD
,但是第
0
块是不用修正的。
如果使用了
XVID_ME_EXTSEARCH8 | XVID_ME_HALFPELREFINE8 | XVID_ME_QUARTERPELREFINE8
,那么
{
Data->RefP[0-3] =
参考帧的整像素,水平半象素,垂直半象素,对角线半象素的对应宏块的对应块的起始地址。
Data->Cur =
当前帧的当前宏块的当前块的起始地址
利用
get_range
得到运动搜索的范围
根据
MotionFlags
的指示,设定运动估计
MainSearchPtr
的算法,当前设置为
MainSearchPtr = xvid_me_AdvDiamondSearch
。
调用
xvid_me_AdvDiamondSearch
做运动估计,其中做
SAD
的函数是
CheckCandidate8
,该函数类似于
CheckCandidate16
如果不采用
1/4
像素运动估计,并且又采用了
XVID_ME_HALFPELREFINE8
,那么调用
xvid_me_SubpelRefine
按顺时针方向
8
次调用
CheckCandidate8
,得到最好的
1/2
像素位置
如果采用了
1/4
像素运动估计,略
}// XVID_ME_EXTSEARCH8 | XVID_ME_HALFPELREFINE8 | XVID_ME_QUARTERPELREFINE8
如果采用
1/4
运动估计
略
否则
记录
pMB->pmvs[block] =
当前找到的最佳位置与预测位置的差值
将这次的搜索存入相应
OldData
的字段,以及
pMB
的相应字段
}// Search8
如果考虑色差的因素,并且又不考虑率失真算法
{
根据是否采用
1/4
像素运动估计算出色差的运动向量
计算
u
,
v
的
SAD
,将其作为
Data->iMinSAD[1]
的修正
}
} //
如果采用
inter4v
否则,
Data->iMinSAD[1]
为足够大的值
}//SearchP
调用
ModeDecision_SAD
确定该宏块的类型
判断该宏块要采取的编码方式,
MODE_INTER
,
MODE_INTER4V
,
MODE_NOT_CODED
,
MODE_INTRA
调用
motionStatsPVOP
做一些统计工作
具体过程略
}//
对于每个宏块,依次执行如下操作
做一些最后的设置
}//MotionEstimation
调用
set_timecodes
设置时间戳
调用
BitstreamWriteVopHeader
写
VOP
头
具体过程略
对于每一个宏块,依次执行如下操作
{
如果该宏块的编码模式是
MODE_INTRA
或者
MODE_INTRA_Q
{
调用
CodeIntraMB
设置编码模式为
intra
,将所有和运动有关的变量设为
0
调用
MBTransQuantIntra
进行变换编码
调用
MBCoding
将该宏块编制成码流
Continue
}
调用
MBMotionCompensation
做运动补偿
{
如果编码模式是
MODE_NOT_CODED
用参考帧的相应宏块替代当前帧的当前宏块
Return
如果编码模式是
MODE_NOT_CODED
或者
MODE_INTER
或者
MODE_INTER_Q
{
如果
mb->mcsel
不为
0
做
GMC
的处理
Return
计算运动向量
dx
,
dy
调用
compensate16x16_interpolate
进行运动补偿
{
如果采用
1/4
像素运动估计
略
否则,调用
get_ref
计算用于运动补偿的参考宏块的指针
调用
4
次
transfer_8to16sub
做亮度块的运动补偿,使得临时数组里存放的是残差,而原始图像里存放的是参考快的数据。
}//compensate16x16_interpolate
计算出用于色差运动补偿的
dx
,
dy
}//MODE_NOT_CODED
或者
MODE_INTER
或者
MODE_INTER_Q
否则,那就是
MODE_INTER4V
{
根据是否使用
1/4
像素运动估计,计算出
4
个色度块的运动向量
以这
4
个运动向量为参数,调用
4
次
compensate8x8_interpolate
,该操作类似于
compensate16x16_interpolate
,不同在于一次只计算一个块。
计算出用于色差运动补偿的
dx
,
dy
}
调用
CompensateChroma
计算色差块的运动补偿
{
调用
interpolate8x8_switch2
计算出
u
的插值
调用
interpolate8x8_halfpel_v
或者
interpolate8x8_halfpel_h
或者
interpolate8x8_halfpel_hv
做实际的插值操作,或者直接返回
调用
transfer_8to16sub_c
做
u
份量的运动补偿
调用
interpolate8x8_switch2
计算出
v
的插值
调用
interpolate8x8_halfpel_v
或者
interpolate8x8_halfpel_h
或者
interpolate8x8_halfpel_hv
做实际的插值操作,或者直接返回
调用
transfer_8to16sub_c
做
v
份量的运动补偿
}//CompensateChroma
}//MBMotionCompensation
如果需要编码,那么用
MBTransQuantInter
进行编码,并把结果返回给
pMB->cbp
{
调用
MBfDCT
进行宏块变换编码
调用
6
次
fdct
调用
MBQuantInter
进行量化
{
对于宏块里的每一块
{
调用
quant_h263_inter
进行量化
如果在量化后,前三个系数为
0
,并且系数的绝对值之和小于阀值,那么标记该块为全
0
块,将标记存入
cbp
。否则,标记为非全
0
块,也将标记存入
cbp
}
}//MBQuantInter
调用
MBDeQuantInter
反量化
{
确定要使用的反量化函数
对于六个块里的每个块,如果
cbp
表示许可,都调用
dequant_h263_inter
反量化
}//MBDeQuantInter
调用
MBiDCT
做反离散余弦变换
对于六个块里的每个块,如果
cbp
表示许可,都调用
idct_int32
反量化
调用
MBTrans16to8
将恢复出的残差构成重建图像
{
确定具体执行的函数,分为
transfer_16to8copy
和
transfer_16to8add
找到该宏块的
y
,
u
,
v
分量起始地址
对于六个块里的每个块,如果
cbp
表示许可,调用相应得函数执行重建。
}// MBTrans16to8
}//MBTransQuantInter
如果无残差,并且编码方式为
MODE_INTER
,并且帧方式是
P
帧,并且向量
2
分量都为
0
,那么可以考虑
skip
模式
如果可以考虑
skip
模式,则做进一步检验,如果检验通过,那么
{
编码模式为
MODE_NOT_CODED
,并且在码流里做标记
Continue
}
调用
MBCoding
将这个宏块写入码流
{
写入非
NOT_CODED
标记
调用
CodeBlockInter
写入码流
{
编码
mcbpc
编码
cbpy
调用
CodeVector
编码运动向量
对六个块,如果
cbp
只是需要编码,调用
CodeCoeffInter
进行编码
}//CodeBlockInter
}// MBCoding
}//
对于每一个宏块,依次执行如下操作
更新
fcode
为下一帧的编码做简单的更新设置
统计该帧编码长度
}// FrameCodeP