一、前言
本篇博客尝试动手实现一个简单的P帧编码。
二、主要实现:
参考图像P1,欲编码图像P2,通过在P1中去进行宏块匹配,来拼凑出图像P3,最后在对编码出的图像P3进行残差补偿来完成简单的图像编码,得到编码后图像P4。整体上只简单的应用了帧间编码的思想。
三、运行结果
1. 参考图像P1
这帧图像就是我们在编码中的参考帧,大多数情况下为I帧。通常情况下参考帧的数量不会是只有1个。
2 欲编码图像P2
这帧图像就是我们想要进行编码的图像的源图像。
3 宏块匹配拼凑出的图像P3
把参考帧P1和欲编码帧P2都划分成8*8的宏块,寻找P1中与P2中相对应的最相近的宏块,记录下宏块的坐标,即运动矢量。然后根据图像P1中的宏块去拼凑出图像P3,理论上P3应该与P2有较大的相似性,并且在图像未变化的区域应该表现出平滑且清晰的画面。图像中人的区域有明显的模糊。
4 编码后图像P4
帧间编码主要是运动检测和运动补偿,宏块匹配得到运动矢量,已经大大缩减了码流,然后再计算P2与P3之间的残差(欲编码图像P2与拼凑出来的类P2的图像P3),得到残差和运动矢量一起进行传输,就可以达到节省码流的目的。下面是运动补偿后的图像。几乎与原图像P2一致。
有趣的现象
事实上即便参考帧和欲编码帧的是完全不同的两幅图像,即没有时域相关性,通过拼凑也是可以得到欲编码帧的大概轮廓,如下
1. 参考图像P1
2 欲编码图像P2
3 宏块匹配拼凑出的图像P3
完全不相关的图像也是根据参考帧拼凑出来了图2的大概模样。
四、实验过程
这只是一个简单的模拟,并没有去做DCT变换和熵编码和传输模块,图像的显示借助的是opencv开源库的Mat,可以更直观的观测到结果。
1、图片读取
注意: imread();这个函数有两个参数,我这个实验使用灰度图去模拟的所有只需要单通道,将第二个参数置0,默认是1(三通道),如果你想读取灰度图,即便你读取的源图片就是灰度图,如果你不将参数置为0的话,那么读取的数据仍然是三通道的数据,这在后面处理data数据会造成错误。
Mat P1 = imread("test11.jpg",0);
resize(P1, P1, Size(640, 360));
Mat P2 = imread("test10.jpg",0);
resize(P2,P2,Size(640,360));
//未经残差补偿的图像。
Mat P3(P1.rows, P1.cols, P1.type());
//残差补偿后的图像
Mat P4(P1.rows, P1.cols, P1.type());
unsigned char* data1 = P1.data;
unsigned char* data2 = P2.data;
//data3是根据参考帧P1拼凑出来的图片,理论上应该与P2很相近。
unsigned char* data3 =P3.data;
2 宏块划分、匹配
将源图像P1和待编码图像P2切分为8*8的小宏块,然后按顺序取出待编码图像P2的宏块在P1中进行匹配。匹配的算法实际上在应用中一般是以待匹配宏块为中心向四周搜索,这里为了实现简单,直接简单粗暴的进行全局搜索,这么做带来的问题就是计算量的暴增。
寻找到匹配快后要记录匹配块的坐标x,y,即运动矢量。
for (int y = 0; y < height; y += 8)
{
for (int x = 0; x < width; x += 8)
{
// 宏块的y数据
int block[64] = { 0 };
//单个宏块 8*8
for (int off_y = 0; off_y < 8; ++off_y)
{
for (int off_x =0; off_x < 8; ++off_x)
{
//像素点对应宏块里的索引坐标
int block_index = (off_y * 8 + off_x);
//像素点对应整幅图像的坐标。
int src_index = (((y + off_y) * width) + (x + off_x));
//取出像素,这里读取的是灰度图,所以读取出来就直接是Y分量,不需要再进行像素格式转换
block[block_index] = data2[src_index];
}
}
//取出像素后,要去P1中去匹配宏块
int ref_x=0, ref_y=0;
block_search_nearest(data1, &ref_x, &ref_y, block, width, height);
}
}
宏块匹配其实就是计算距离矢量,代码实现如下:
int block_diff(int *data1, int * data2)
{
int sum = 0;
for (int j = 0; j < 8; j++)
{
for (int i = 0; i < 8; i++)
{
int c1 = data1[j * 8 + i];
int c2 = data2[j * 8 + i];
sum += (c1 - c2) * (c1 - c2);
}
}
return sum;
}
3 拼凑图像P3
宏块匹配过后记录下运动矢量,即对应宏块坐标,根据坐标在参考帧P1中去拼凑出一帧新的图像P3,P3理论上应该与P2很相似,时域相关性强的情况下应该只有少部分模糊或不同。
for (int ref_y = 0; ref_y < height; ref_y += 8)
{
for (int ref_x = 0; ref_x < width; ref_x += 8)
{
//取出P1中对应的像素,存放到新的图像数据data3中。
if (match_block_list->size() == 0)
{
break;
}
struct Match_Block_Index index;
index = match_block_list->front();
match_block_list->pop_front();
//从data1中拿取数据去拼凑data3
CopyRect(data1,data3,index.x,index.y,ref_x,ref_y,width);
}
}
运动补偿
最后计算图像P2与P3之间的差值,这部分在实际应用中在编码中进行,然后将残差进行网络传输,最后在解码端接收残差,然后与根据参考帧建立的图像进行整合,从而构建出完整的图像。这个模拟中就直接一起进行了。
//残差计算
unsigned char* diff_data = (unsigned char*)malloc(P1.rows * P1.cols);
for (int i = 0; i < P1.rows * P1.cols; i++)
{
diff_data[i] = (data2[i] - data4[i]) / 2;
}
//运动补偿
for (int i = 0; i < P1.rows * P1.cols; i++)
{
data4[i] = data4[i] + diff_data[i]*2;
}
总结:
如果有需要源程序的可以下载我上传的工程,包括一个本篇博客的编码程序完整版,然后还有一个tiny_jpeg.c开源程序,两个工程都是可以直接打开就运行的。、
源代码下载地址: 最简单的视频P帧编码的C++实现和tiny_jpeg工程,vs打开即可运行