写在前面的话:记录视频编码课程的内容,都是为了学习巩固,有什么不对的地方还希望各位大佬指正出来,不胜感激。介绍较少,主要是方便自己回顾。上课不想听课学习所以陪舍友看论文,回想视频编码内容发现忘了不少有用的东西,所以还是记录一下,担心以后报告没了看自己写的代码都已经看不懂了。前两次实验的内容已不想再整理,这个实验花费的精力确实不少,不过收获也不少。文中的两张原理图来自课程中的PPT。
文末附部分关键的代码,仅作参考,详细代码请去我的资源区下载,不然孩子也没有积分下载一些东西,仅需2积分,所有内容仅供学习使用!
1.实验要求
- block size 分别以 16*16 和 4*4 为基本单元,search area 分别为 32*32 和 64*64 的情况下,进行 integer pixel 级的 motion estimation 运动估计,分别得出四种不同情况下的residual images 残差图像和 motion vectors 运动矢量。
- 将得到的残差图像进行 DCT 变换和量化以后,做反量化,反变换(量化步长可以选择 4 或者 8,尝试对比实验效果),利用前一帧视频图像对当前图像进行重建恢复,并比较解码以后的图像与原图像之间的视觉质量。
2. 理论分析
压缩涉及一对互补的系统,一个压缩器(编码器)和一个解压缩器(解码器)。编码器/解码器对通常被称为 CODEC。对于视频的压缩需要去除时域和空间域的冗余。
其中用到了运动补偿。运动补偿是一种描述相邻帧(相邻在这里表示在编码关系上相邻,在播放顺序上两帧未必相邻)差别的方法,具体来说是描述前面一帧的每个小块怎样移动到当前帧中的某个位置去。其目的是通过消除冗余来提高压缩比。在本次实验中采用不同的block_size(4,16)和 search_area(32,64)进行运动补偿,即分块运动补偿,其中平移的大小即为得到的运动矢量。
DCT 变换与量化都是对图像进行压缩的一个前提,实验要求是采用步长 4、8 的量化,并没有采用 Zigzag 编码,但是在一定程度上已经进行了压缩,去除了较小值。
连续两帧编码流程图(图有点糊了,从报告里截的,放大看吧):
连续两帧解码流程图:
3. 实验过程
- 从视频截取两帧图像对其做裁剪
video_full_path="./videoData/data.flv"
cap = cv2.VideoCapture(video_full_path)
print(cap.isOpened())
num = 1
for _ in range(2):
success, frame = cap.read()
params = []
params.append(1)
cv2.imwrite(f"videoData/capture_{num}.jpg", frame, params)
num += 1
cap.release()
image_files = glob.glob('./videoData/*.jpg')
capture_1_ori = cv2.imread(image_files[0])
capture_2_ori = cv2.imread(image_files[1])
capture_1 = capture_1_ori[200:712, 700:1212]
capture_2 = capture_2_ori[200:712, 700:1212]

转换为 Y 通道并显示灰度图
分别使用 block_size=4,search_area=32;block_size=4,search_area=64;block_size=16,search_area=32;block_size=16,search_area=64 进行运动估计并绘制运动矢量场。
block_size=4,search_area=32 的运动矢量场:

block_size=4,search_area=64 的运动矢量场:

block_size=16,search_area=32 的运动矢量场:

block_size=16,search_area=64 的运动矢量场:

- 显示其预测图像,并与前两帧图像进行对比

- 得到残差图像

进行 DCT 变换
进行量化,首先取 step=4 进行量化,此时对于 block_size=4,search_area=32 的结果已经大多数值已为 0,因此第二次采用了 step=2 进行量化
反量化,IDCT 变换
step=4 的重建后的残差图像:

step=2 的重建后的残差图像:

- 根据保存的运动矢量进行图像重建,其最终的重建效果并不理想(有很严重的块效应)

- 计算PSNR

4. 实验分析
- 实验中遇到的问题:
- 当图像太大时计算量较大,因此对其进行裁剪。
- 对于边缘图像进行匹配时,通过补 0 来处理越界的问题。
- 结果分析
- 1) 获取的运动矢量问题
- 我存储了两部分,一部分用于绘制运动矢量场,另一部分是保存相对矢量,即一个search_area 内的坐标矢量。是因为绘制运动矢量场需要一个全局坐标,但重建需要的是小区域内的相对坐标。
- 2) 残差图像部分,从结果图上来看,达到了预期效果
- 3) 当进行 DCT 变换后左上角的数值并不是最大值,因此尝试对其直接进行反变换,发现可以无损重建,因此排除其错误的可能。
- 4) 观察重建后的图像,理论分析应该得出重建后的图像应该与原图像无较大差异,但是重建出来的图像效果较差,块效应较为严重,对此我分析以下两点
- a) 相对矢量记录有问题
- b) 重建时获取的相对矢量有问题
- c) 量化步长的问题
- d) 可能是单纯的选取的图像变化差异太小
- 如果是 a) 那么绘制的运动矢量可能就不够准确,但是从我的思路上来讲,程序应该没有问题。(在代码中加入了自己的分析过程,并将其图像显现出来,看到的结果是比较符合理论分析的,所以一直没有找到原因所在,如下图对比匹配的像素块)

- 5) 步长为 2 与 4 重建出来的图像与第二帧图像进行 psnr 值后得到的结果相同,在 4)的基础上个人认为是取得的步长仍然不太符合该图像样本的需求,仍需要通过更多的实验。
- 6) 关于 psnr 还有一个问题,就是同样 step=4,但是 search_area 为 32 的 psnr 值大于 64 的值,这与理论分析并不符合。个人分析认为是与 4)为同一个问题。
总结:首先看图像可以得出,对于不同的 block 大小以及 search_area 大小,重建出来的效果是不一样的,所匹配的区域越广(block 越小,search_area 越大),重建出来的图像越接近原图像。通过该编码的方式,可以有效的对视频帧进行压缩,减少传输压力,但是,并不能无限制的使得 block 小,search_area 大,是因为在图像重建中需要记录其运动矢量。匹配的区域越广,所需要记录的运动矢量就越多,反之就越少。这两者都是需要传输的,因此在传输过程中要针对不同的视频而采取不同的编码策略,从而使得传输达到最大效益化。
5. 实验需要改进的部分
- 更换两帧图像再做一次实验
- 对于每一步查看其中间变量的过程
- 更换不同量化步长进行实验
- 只能针对边长为64的倍数且长宽一致的图像进行处理,这部分没有完善的原因是需要考虑很多因素,当时实验已经很费时间就没有再完善了
6. 部分关键代码
- 获取预测图像以及运动矢量
def get_preimage_motions(width, height, block_sizes, search_areas):
width_num = width // block_sizes
height_num = height // block_sizes
# 运动向量个数
vet_nums = width_num * height_num
# 用来保存运动向量,保存坐标值
motion_vectors = []
# 先赋予空值
motion_vectors = [[0, 0] for _ in range(vet_nums)]
motion_vectors_for_draw = [[0, 0, 0, 0] for _ in range(vet_nums)]
similarity = 0
num = 0
end_num = search_areas//block_sizes
# 计算间隔,用于补0
interval = (search_areas-block_sizes)//2
# 构造模板图像,对于上一帧图像加0
mask_image_1 = np.zeros((width + interval*2, height + interval*2))
mask_image_1[interval:mask_image_1.shape[0]-interval, interval:mask_image_1.shape[1]-interval] = capture_1_Y
mask_width, mask_height = mask_image_1.shape
predict_image = np.zeros(capture_1_Y.shape)
# count = 0
for i in range(height_num):
for j in range(width_num):
# count += 1
# print(f'==================i:{i}=j:{j}==count:{count}=====================')
temp_image = capture_2_Y[i*block_sizes:(i+1)*block_sizes, j*block_sizes:(j+1)*block_sizes]
mask_image = mask_image_1[i*block_sizes:i*block_sizes+search_areas, j*block_sizes:j*block_sizes+search_areas]
# 给定初值,用于比较
temp_res = mad(mask_image[:block_sizes, :block_sizes], temp_image)
for k in range(end_num):
for h in range(end_num):
# 取一个模板
temp_mask = mask_image[k*block_sizes:(k+1)*block_sizes, h*block_sizes:(h+1)*block_sizes]
# 计算mad
res = mad(temp_mask, temp_image)
# 比较,如果大于默认值,则将其设为默认值,并将预测图像赋值,替代当前运动矢量
if res <= temp_res:
temp_res = res
motion_vectors[i*j+j][0], motion_vectors[i*j+j][1] = k, h
motion_vectors_for_draw[i*j+j][0], motion_vectors_for_draw[i*j+j][1], motion_vectors_for_draw[i*j+j][2], motion_vectors_for_draw[i*j+j][3] = i+k, height_num-(j+h), i+interval/block_sizes, height_num-(j+interval/block_sizes)
predict_image[i*block_sizes:(i+1)*block_sizes, j*block_sizes:(j+1)*block_sizes] = temp_mask
# print(motion_vectors_for_draw[i*j+j])
return np.array(predict_image), np.array(motion_vectors), np.array(motion_vectors_for_draw)
- 重建代码
def restruct_image(width, height, block_sizes, search_areas, motion_vectors, residual_image, pre_frame):
width_num = width // block_sizes
height_num = height // block_sizes
# 运动向量个数
vet_nums = width_num * height_num
end_num = search_areas//block_sizes
# 计算间隔,用于补0
interval = (search_areas-block_sizes)//2
# 构造模板图像,对于上一帧图像加0
mask_image_1 = np.zeros((width + interval*2, height + interval*2))
mask_image_1[interval:mask_image_1.shape[0]-interval, interval:mask_image_1.shape[1]-interval] = pre_frame
restruct_image = np.zeros((width, height))
for i in range(height_num):
for j in range(width_num):
temp_image = residual_image[i*block_sizes:(i+1)*block_sizes, j*block_sizes:(j+1)*block_sizes]
mask_image = mask_image_1[i*block_sizes:i*block_sizes+search_areas, j*block_sizes:j*block_sizes+search_areas]
# 给定初值,用于比较
k, h = motion_vectors[(i*j)+j][0], motion_vectors[(i*j)+j][1]
restruct_image[i*block_sizes:(i+1)*block_sizes, j*block_sizes:(j+1)*block_sizes] = temp_image + mask_image[k*block_sizes:(k+1)*block_sizes, h*block_sizes:(h+1)*block_sizes]
return np.array(restruct_image, dtype=np.uint8)
restruct_image_4_32 = restruct_image(width, height, 4, 32, motion_vectors_4_32, y_idct_4_32_recover_4, capture_1_Y)
restruct_image_4_64 = restruct_image(width, height, 4, 64, motion_vectors_4_64, y_idct_4_64_recover_4, capture_1_Y)
restruct_image_16_32 = restruct_image(width, height, 16, 32, motion_vectors_16_32, y_idct_16_32_recover_4, capture_1_Y)
restruct_image_16_64 = restruct_image(width, height, 16, 64, motion_vectors_16_64, y_idct_16_64_recover_4, capture_1_Y)
plt.figure(figsize=(10,10))
plt.subplot(221)
# plt.xticks([])
# plt.yticks([])
plt.xlabel('(a) 4_32')
plt.imshow(restruct_image_4_32, cmap='gray')
plt.subplot(222)
# plt.xticks([])
# plt.yticks([])
plt.xlabel('(b) 4_64')
plt.imshow(restruct_image_4_64, cmap='gray')
plt.subplot(223)
# plt.xticks([])
# plt.yticks([])
plt.xlabel('(c) 16_32')
plt.imshow(restruct_image_16_32, cmap='gray')
plt.subplot(224)
# plt.xticks([])
# plt.yticks([])
plt.xlabel('(d) 16_64')
plt.imshow(restruct_image_16_64, cmap='gray')
psnr_4_32_2 = compute_psnr(restruct_image_4_32_2, capture_2_Y)
psnr_4_64_2 = compute_psnr(restruct_image_4_64_2, capture_2_Y)
psnr_16_32_2 = compute_psnr(restruct_image_16_32_2, capture_2_Y)
psnr_16_64_2 = compute_psnr(restruct_image_16_64_2, capture_2_Y)
print(f'The psnr of 4_32_2 is {psnr_4_32_2:.4f}.')
print(f'The psnr of 4_64_2 is {psnr_4_64_2:.4f}.')
print(f'The psnr of 16_32_2 is {psnr_16_32_2:.4f}.')
print(f'The psnr of 16_64_2 is {psnr_16_64_2:.4f}.')
- 计算PSNR
def compute_psnr(img_in, img_sam):
assert img_in.shape == img_sam.shape, "The sample image's shape is not same as the input!"
mse = np.mean( (img_in/255. - img_sam/255.) ** 2 )
if mse < 1.0e-10:
return 100
return 20 * np.log10(1 / np.sqrt(mse))