运动补偿时域滤波器(Motion compensated temporal filter,MCTF)
VTM 支持运动补偿时域滤波器(Motion compensated temporal filter,MCTF)。MCTF是一种编码前处理工具,即当视频帧进行编码前,对该帧进行时域滤波。该工具由CTC中的TemporalFilter选项控制是否开启。
滤波过程
Step1:
通过使用一个或多个TemporalFilterStrengthFrame#配置选项,可以针对每个GOP中的不同图片调整时域滤波的强度,其中“#”是一个整数。指定的强度适用于POC号可被“#”整除的所有图片;如果多个选项都是这样,则使用“#”值最高的选项的值。
MCTF的设计仅适用于编码时域Id较低的图片。例如,
对于随机访问配置(random access),可以优先地使用以下两个配置:
TemporalFilterStrengthFrame8 : 0.95
TemporalFilterStrengthFrame16 : 1.5
这将对(POC%8)==0 的所有图片应用滤波器,不同图片的滤波强度如下:
其中 n 是图片的POC值。
对于低延迟配置(low-delay),可以优先使用以下配置:
TemporalFilterStrengthFrame4 : 0.4
这将对(POC%4)==0的所有图片使用以下强度的滤波器:
不建议在All Intra配置下应用该滤波器。
Step 2:
应用MCTF,直接从源视频文件中读取当前图片前面的四张图片和后面的四张图片(按显示顺序);因此,可用的图片可能存在于当前GOP之外。
仅当TemporalFilterFutureReference配置选项设置为1时,才会使用显示顺序在当前图片之后的图片。在低延迟配置中,此选项通常设置为0。
Step 3:
对当前图片的每个8x8块估计其相对于可用时域相邻图片的运动。
采用分层运动估计方案,如上图所示。L0是原始分辨率,L1是L0的下采样版本,L2是L1的下采样版本,其中L1的宽度和高度是L0的宽度和高度的一半,L2的宽度和高度是L1的宽度和高度的一半。下采样是通过计算四个相应样本值的平均值来得到的。
在运动估计前,先对当前帧和参考帧按照如上所示进行两次下采样。之后进行运动估计过程:
- 对L2中的每个16x16块执行运动估计。计算每个选定运动矢量的平方差之和,并选择与最小差相对应的运动矢量。
- 在L1中估计运动时,将第 1 步得到的运动矢量用作初始值。然后在L0中对每个16x16块执行同样的运动估计。
- 在L0中估计运动时,将第 2 步得到的运动矢量用作初始值,然后在L0中对每个16x16块执行同样的运动估计。
- 以第 3 步得到的运动矢量作为初始值,使用以下8抽头插值滤波器来估计每个8x8块的分数精度运动。
const int EncTemporalFilter::m_interpolationFilter[16][8] =
{
{ 0, 0, 0, 64, 0, 0, 0, 0 }, //0
{ 0, 1, -3, 64, 4, -2, 0, 0 }, //1 -->-->
{ 0, 1, -6, 62, 9, -3, 1, 0 }, //2 -->
{ 0, 2, -8, 60, 14, -5, 1, 0 }, //3 -->-->
{ 0, 2, -9, 57, 19, -7, 2, 0 }, //4
{ 0, 3, -10, 53, 24, -8, 2, 0 }, //5 -->-->
{ 0, 3, -11, 50, 29, -9, 2, 0 }, //6 -->
{ 0, 3, -11, 44, 35, -10, 3, 0 }, //7 -->-->
{ 0, 1, -7, 38, 38, -7, 1, 0 }, //8
{ 0, 3, -10, 35, 44, -11, 3, 0 }, //9 -->-->
{ 0, 2, -9, 29, 50, -11, 3, 0 }, //10-->
{ 0, 2, -8, 24, 53, -10, 3, 0 }, //11-->-->
{ 0, 2, -7, 19, 57, -9, 2, 0 }, //12
{ 0, 1, -5, 14, 60, -8, 2, 0 }, //13-->-->
{ 0, 1, -3, 9, 62, -6, 1, 0 }, //14-->
{ 0, 0, -2, 4, 64, -3, 1, 0 } //15-->-->
};
Step 4:
根据每个8×8块的最佳的运动矢量对当前图片之前和之后的图片进行运动补偿,以将当前图片中每个块的样本坐标与参考图片中的最佳匹配坐标对齐。
Step 5:
分别对当前图片的亮度和色度分量进行滤波,滤波过程如下所示:
使用以下公式计算当前图片的新样本值In:
其中Io是原始样本的值,Ir(i) 是运动补偿图片 i 中相应样本的值,Wr(I,a)是当可用运动补偿图片的数量等于a时运动补偿图片I的权重。当所有源图片可用时,如TemporalFilterFutureReference等于0,a等于4,如果TemporalFilterFutureReference等于1,则a等于8。
对于亮度样本,权重Wr(i,a) 计算如下:
其中,
对于其余情况的 i 和 a:
调整系数wa和σw的计算用于计算Wr(i,a),如下所示:
其中 noise 和 error 是以8×8的亮度块粒度和4×4的色度块粒度计算的。
对于色度样本,权重Wr(i,a) 计算如下:
其中:
Step 6:
将滤波后的图片进行编码。
VTM中,MCTF的入口函数:filter函数
bool EncTemporalFilter::filter(PelStorage *orgPic, int receivedPoc)
{
bool isFilterThisFrame = false;
if (m_QP >= 17) // disable filter for QP < 17 Qp < 17时,不使用滤波
{
for (map<int, double>::iterator it = m_temporalFilterStrengths.begin(); it != m_temporalFilterStrengths.end(); ++it)
{
int filteredFrame = it->first;
if (receivedPoc % filteredFrame == 0)
{
isFilterThisFrame = true; // 是filteredFrame的整数倍才会进行滤波
break;
}
}
}
if (isFilterThisFrame) // 对当前帧进行滤波
{
int offset = m_FrameSkip;
VideoIOYuv yuvFrames;
yuvFrames.open(m_inputFileName, false, m_inputBitDepth, m_MSBExtendedBitDepth, m_internalBitDepth);
// 跳过前面的帧,直到当前POC - 4
yuvFrames.skipFrames(std::max(offset + receivedPoc - m_range, 0), m_sourceWidth - m_pad[0], m_sourceHeight - m_pad[1], m_chromaFormatIDC);
std::deque<TemporalFilterSourcePicInfo> srcFrameInfo;
int firstFrame = receivedPoc + offset - m_range; // 起始帧 当前帧POC-4
int lastFrame = receivedPoc + offset + m_range; // 最后帧 当前帧POC+4
if (!m_gopBasedTemporalFilterFutureReference)
{
lastFrame = receivedPoc + offset - 1; // 不用未来帧参考
}
int origOffset = -m_range;
// subsample original picture so it only needs to be done once 对原始图片进行子采样,因此只需执行一次
PelStorage origPadded;
origPadded.create(m_chromaFormatIDC, m_area, 0, m_padding);
origPadded.copyFrom(*orgPic);
origPadded.extendBorderPel(m_padding, m_padding);
PelStorage origSubsampled2; // 下采样一次,对应L1层
PelStorage origSubsampled4; // 下采样两次,对于L2层
// 下采样
subsampleLuma(origPadded, origSubsampled2);
subsampleLuma(origSubsampled2, origSubsampled4);
// determine motion vectors 遍历相邻帧 确定运动矢量
for (int poc = firstFrame; poc <= lastFrame; poc++)
{
if (poc < 0)
{
origOffset++;
continue; // frame not available POC号小于0,视频的起始位置处,跳过
}
else if (poc == offset + receivedPoc)
{ // hop over frame that will be filtered 跳过将被滤波的帧
yuvFrames.skipFrames(1, m_sourceWidth - m_pad[0], m_sourceHeight - m_pad[1], m_chromaFormatIDC);
origOffset++;
continue;
}
srcFrameInfo.push_back(TemporalFilterSourcePicInfo());
TemporalFilterSourcePicInfo &srcPic = srcFrameInfo.back();
PelStorage dummyPicBufferTO; // Only used temporary in yuvFrames.read
srcPic.picBuffer.create(m_chromaFormatIDC, m_area, 0, m_padding);
dummyPicBufferTO.create(m_chromaFormatIDC, m_area, 0, m_padding);
if (!yuvFrames.read(srcPic.picBuffer, dummyPicBufferTO, m_inputColourSpaceConvert, m_pad, m_chromaFormatIDC, m_clipInputVideoToRec709Range))
{
return false; // eof or read fail 读取文件失败
}
srcPic.picBuffer.extendBorderPel(m_padding, m_padding);
srcPic.mvs.allocate(m_sourceWidth / 4, m_sourceHeight / 4);
// 进行运动估计
motionEstimation(srcPic.mvs, origPadded, srcPic.picBuffer, origSubsampled2, origSubsampled4);
srcPic.origOffset = origOffset;
origOffset++;
}
// filter 滤波
PelStorage newOrgPic; // 存储滤波后的图
newOrgPic.create(m_chromaFormatIDC, m_area, 0, m_padding);
double overallStrength = -1.0;
for (map<int, double>::iterator it = m_temporalFilterStrengths.begin(); it != m_temporalFilterStrengths.end(); ++it)
{
int frame = it->first;
double strength = it->second;
if (receivedPoc % frame == 0) // 根据POC号确定滤波强度
{
overallStrength = strength;
}
}
// 滤波
bilateralFilter(origPadded, srcFrameInfo, newOrgPic, overallStrength);
// move filtered to orgPic 将滤波后的图拷贝到原始图
orgPic->copyFrom(newOrgPic);
yuvFrames.close();
return true;
}
return false;
}