前言
本文所说的帧间预测是指根据当前预测块的MV向量和预测所需的参考块数据,计算出当前预测块的预测像素值的过程。该过程得到的预测像素值经过运动补偿后(与反变换量化之后得到的残差像素值相加)可以得到滤波前的重建像素值。
原理说明
一般来说,只要知道MV向量,就可以将MV向量所指向参考块的像素值作为当前块的预测值,但是H264采用1/4亚像素级运动估计方法,MV向量并不是以整数像素点为单位的,而是以1/4像素为单位的。这也就意味着我们需要将MV指向的参考块进行像素内插操作,进而找到MV向量具体指向的位置,同时得到预测像素值。
假设当前4x4块的参考关系如下图所示:
从图中可以看出当前块的左上角P0的坐标为(128,128)MV向量为(-19,-5)参考块左上角G的坐标为(123,126)。由于MV向量以1/4像素为单位,我们可以先将该MV向量做整像素单位对齐,找到该整数像素点的坐标,对齐后的MV向量我们暂时称为MVz,然后计算实际参考的分像素点相对与该整数像素点的向量MVf,可以看出MVf = MV - MVz。如下图所示,MVz为(-20,-8),刚好指向参考块的G像素,MVf为(1,3)指向参考块做了像素内插后的p像素点。同理可得P4的参考的整像素点为M点,实际参考点同样也是像素内插后相对与M点的(1,3)位置,其他像素点依次类推。
图中的a,b,c,d,e,f,g,h,i,j,k,m,n,p,q,r,s等小写字母表示的都是通过像素内插得到的分像素点。那么像素内插过程是如何的呢,H264标准协议第8.4.2.2.1章节有详细描述,这里简单说明一下原理,请看下图:
图中灰色方块代表整数像素点,其余为分数像素点。像素内插遵循以下规则:
(1)首先生成参考图像亮度成分半像素像素。
半像素点(如b,h,m,s)通过对相应整像素点进行6 抽头滤波得出,权重为(1/32 ,-5/32 ,5/8, 5/8, -5/32, 1/32)。例如b 像素点计算如下:
b = round((E - 5F + 20G + 20H - 5I + J) / 32)
类似的,h 由A、C、G、M、R、T 滤波得出。
h = round((A − 5C + 20G + 20M − 5R + T) / 32)
一旦邻近(垂直或水平方向)整像素点的所有像素都计算出,剩余的半像素点便可以通过对6 个垂直或水平方向的半像素点滤波而得。例如,j 由cc, dd, h,m,ee,ff 滤波或者由aa,bb,b,s,gg,hh得出,两者计算出的结果应该是相同的。
j = round((cc − 5dd + 20h + 20m − 5ee + ff) / 1024)
//或者
j = round((aa − 5bb + 20b + 20s − 5gg + hh) / 1024)
(2)半像素点计算出来以后,1/4 像素点就可通过线性内插得出.。
1/4 像素点(如a,c, i, k, d, f, n, q)由邻近像素内插而得,如a像素计算如下:
a = round((G + b) / 2)
注意有4个特殊的1/4像素点e,g,p,r,他们是使用对角线上的两个半像素点得到,如:
e = round((h + b) / 2)
至此像素内插的过程就全部介绍完了。
代码实现
帧间预测需要使用三个输入数据:
(1)当前预测块的MV向量。
(2)当前预测块的参考块的像素重建数据。
(3)当前预测块左上角P0位置坐标。
输出:
(1)当前预测块的预测值。
下列代码展示了当前4x4块左上角P0坐标为(4,4),运动向量MV为(-7,-5)的帧间预测过程。
根据原理可知:
MVz = (-8,-8),MVf = (1,3),因此P0参考的像素点坐标G为(2,2)根据这些信息我们可以计算出整个当前4x4块的预测像素值。
#include <stdio.h>
#include <stdlib.h>
#define PIC_WIDTH 16 //图像宽度
#define PIC_HIGHT 16 //图像高度
#define BLK_WIDTH 4 //当前块宽度
#define BLK_HIGHT 4 //当前块高度
#define REF_POS_X 2 //参考块左上角像素点G的横坐标
#define REF_POS_Y 2 //参考块左上角像素点G的纵坐标
#define MAX_PIX_VALUE 32 //像素点最大像素值
typedef char imgpel;
static inline int imin(int a, int b)
{
return ((a) < (b)) ? (a) : (b);
}
static inline int imax(int a, int b)
{
return ((a) > (b)) ? (a) : (b);
}
static inline int iClip1(int high, int x)
{
x = imax(x, 0);
x = imin(x, high);
return x;
}
/* 该函数以MVf的值命名,因为所有MVf相同的帧内差值过程都是一样的,只是使用的整数像素点不一样而已 */
/* 参数说明: */
/* (1) block: 当前块左上角P0像素地址 */
/* (2) cur_imgY: 参考块左上角G像素相对与整幅图像所在行地址 */
/* (3) block_size_y: 当前块高度 */
/* (4) block_size_y: 当前块宽度 */
/* (5) x_pos: 参考块左上角G像素的横坐标x */
/* (6) shift_x: 整个图像Buffer的宽度,注意这里不一定和图像宽度一致 */
/* (7) max_imgpel_value: 像素值最大值 */
static void get_luma_13(imgpel **block, imgpel **cur_imgY, int block_size_y, int block_size_x, int x_pos, int shift_x, int max_imgpel_value)
{
/* Diagonal interpolation */
int i, j;
imgpel *p0, *p1, *p2, *p3, *p4, *p5;
imgpel *orig_line;
int result;
int jj = 1;
/* 由于MVf(1,3)表示分像素p位置,根据原理可知它是由对角线上两个半像素计算而来 */
/* 因此这里先横向内插 */
for (j = 0; j < block_size_y; j++)
{
p0 = &cur_imgY[jj++][x_pos - 2];
p1 = p0 + 1;
p2 = p1 + 1;
p3 = p2 + 1;
p4 = p3 + 1;
p5 = p4 + 1;
//printf("ver %02d %02d %02d %02d %02d %02d\n", *p0, *p1, *p2, *p3, *p4, *p5);
orig_line = block[j];
for (i = 0; i < block_size_x; i++)
{
//计算s分像素点,公式见原理说明
result = (*(p0++) + *(p5++)) - 5 * (*(p1++) + *(p4++)) + 20 * (*(p2++) + *(p3++));
*(orig_line++) = (imgpel) iClip1(max_imgpel_value, ((result + 16)>>5));
}
}
/* 这里开始进行垂直方向内插 */
p0 = &(cur_imgY[-2][x_pos]);
for (j = 0; j < block_size_y; j++)
{
p1 = p0 + shift_x;
p2 = p1 + shift_x;
p3 = p2 + shift_x;
p4 = p3 + shift_x;
p5 = p4 + shift_x;
orig_line = block[j];
//printf("hor %02d %02d %02d %02d %02d %02d\n", *p0, *p1, *p2, *p3, *p4, *p5);
for (i = 0; i < block_size_x; i++)
{
//计算h分像素点,公式见原理说明
result = (*(p0++) + *(p5++)) - 5 * (*(p1++) + *(p4++)) + 20 * (*(p2++) + *(p3++));
//计算p分像素点,注意这里将h的取平均操作放在这里了
*orig_line = (imgpel) ((*orig_line + iClip1(max_imgpel_value, ((result + 16) >> 5)) + 1) >> 1);
orig_line++;
}
p0 = p1 - block_size_x ;
}
}
int main(int argc, char*argv[])
{
int i;
int j;
char **img_y;
char *img_y_vir;
char *cur_block_y_vir;
char **cur_block_y;
// 分配参考图像空间,这样分配可以使用二维数组访问方式来访问数据,如img_y[0][0]
img_y = (char **)malloc(sizeof(char*) * PIC_HIGHT);
img_y_vir = (char*)malloc(PIC_WIDTH * PIC_HIGHT);
for(i = 0; i < PIC_HIGHT; i++) {
img_y[i] = img_y_vir + i * PIC_WIDTH;
}
// 分配当前块空间,这样分配可以使用二维数组访问方式来访问数据,如cur_block_y[0][0]
cur_block_y = (char **)malloc(sizeof(char*) * BLK_HIGHT);
cur_block_y_vir = (char*)malloc(BLK_WIDTH * BLK_HIGHT);
for(i = 0; i < BLK_HIGHT; i++) {
cur_block_y[i] = cur_block_y_vir + i * BLK_WIDTH;
}
// 这里使用随机函数随机生成参考图像数据
printf("ref pic data:\n");
for(i = 0; i < PIC_HIGHT; i++) {
for(j = 0; j < PIC_WIDTH; j++) {
img_y[i][j] = rand() % 32;
printf("%02d ", img_y[i][j]);
}
printf("\n");
}
//开始针对MVf为(1,3)的情况进行帧间预测,注意如果MV计算出来的MVf是其他值,这里暂不支持。
printf("ref left top point G is (%d, %d)\n", REF_POS_X, REF_POS_Y);
get_luma_13(cur_block_y, &img_y[REF_POS_X], BLK_HIGHT, BLK_WIDTH, REF_POS_Y, PIC_WIDTH, MAX_PIX_VALUE);
printf("cur block data:\n");
for(i = 0; i < BLK_HIGHT; i++) {
for(j = 0; j < BLK_WIDTH; j++) {
printf("%02d ", cur_block_y[i][j]);
}
printf("\n");
}
free(img_y);
free(img_y_vir);
free(cur_block_y);
free(cur_block_y_vir);
return 0;
}
运行结果:
ref pic data:
07 06 09 19 17 31 10 12 09 13 26 11 18 27 03 06
28 02 20 24 27 08 07 13 22 26 14 03 19 31 09 26
06 18 13 23 17 24 03 26 05 29 05 23 24 09 30 20
11 18 13 06 27 20 20 17 14 02 20 01 01 29 28 07
16 09 30 01 01 01 28 07 30 01 30 23 10 28 11 22
15 24 28 10 12 16 27 27 18 15 28 20 12 24 27 28
02 26 30 03 27 26 10 26 27 09 17 06 05 28 28 20
21 24 30 01 09 25 28 27 08 25 15 21 17 11 17 19
05 15 23 00 09 01 26 05 10 11 11 16 08 07 04 29
31 03 30 08 28 27 04 05 20 19 26 05 30 11 25 03
27 16 04 04 17 30 09 28 10 20 12 18 27 16 15 27
19 13 03 16 08 07 21 28 27 15 02 25 26 27 29 21
11 01 26 28 31 03 24 09 24 04 27 19 21 10 14 08
24 18 24 00 25 13 29 20 28 31 14 23 26 11 12 05
12 06 01 11 10 26 21 02 30 16 21 19 27 04 28 19
22 20 19 15 02 16 04 30 15 18 21 09 29 02 14 09
ref left top point G is (2, 2)
cur block data:
08 16 25 24
20 00 08 13
25 07 07 14
21 10 28 19