1 压缩效果的分析
1.1 压缩效率的对比
我们采用了两种方式对图像进行压缩编码:
- 第一种是直接使用Huffman编码;
- 第二种是先对数据进行DPCM差分预测编码,然后再进行Huffman编码。
根据 压 缩 比 = 原 图 大 小 : 压 缩 码 流 大 小 (1) 压缩比=原图大小:压缩码流大小\tag{1} 压缩比=原图大小:压缩码流大小(1)得出压缩比:
Huffman Only | DPCM & Huffman |
---|---|
1.41 : 1 1.41:1 1.41:1 | 2.25 : 1 2.25:1 2.25:1 |
DPCM对原始图像经过预测编码以后,对数据的压缩比提升是非常大的,达到了仅Huffman编码的1.6倍。这是为什么呢?我们接下来就对DPCM压缩后的数据进行分析。
1.2 DPCM压缩后的数据分布
使用类似于实验一中统计概率分布的方法,我们得出原图和经过DPCM编码后的数据的概率分布如下。(实现代码置于附录中)
观察图像,我们发现:
-
原图的分布在值域 [ 0 − 225 ] [0-225] [0−225]内较为均匀,而残差图的概率分布非常集中,有点像
Laplace
分布。这是因为量化后,两像素之间的差值集中在 [ 100 − 150 ] [100-150] [100−150]这个区间内。这种分布对于变长的Huffman编码来说,优势是巨大的。我们在进行Huffman编码时,需要编写的信源符号更少,且将大概率的值用短码编写。 -
通过计算原图和残差图的熵,我们发现是完全一致的:
result = 0 pr = residual_data['编码内容'] / residual_data['编码内容'].sum() it = iter(pr) # 创建迭代器对象 for x in pr: if x != 0: result += -x * np.log2(x) print(result)
图 残差图 7.71849 7.71849 这种信源的压缩方式,大大降低了数据量,但是信息量没有变化。换言之,这种编码方式实现了能量集中。唯一的误差来源可能就是DPCM的量化和反量化过程。
1.3 DPCM编码后各图像效果
注:原图带有UV分量,故有细微色彩,但是重建图像只有Y分类,UV被置为128,是完全的灰度图。
我们可以看出,随着量化比特数的降低,重建图像质量肉眼可见的下降。由于采取左向预测,所以从4bit起每一行之间一条一条的印迹非常明显。
残差值由于越来越小,就变得很黑难以看清。
其中1bit的原图需要进行二值化的处理。
1.4 压缩质量
压缩后会带来一定质量的损失。我们采取了PSNR,峰值信噪比
作为图像质量评价指标。
1.4.1 PSNR的定义
- 给定一张大小为
m
×
n
m \times n
m×n的原始干净图像
C
\boldsymbol{C}
C,和一张同大小的重建的噪声图像
N
\boldsymbol{N}
N。
我们计算它俩之间的均方误差 M S E MSE MSE。
M S E = 1 m × n ∑ i = 0 m − 1 ∑ i = 0 n − 1 ∣ ∣ C ( i , j ) − N ( i , j ) ∣ ∣ 2 (2) MSE = \frac{1}{m\times n} \sum_{i=0}^{m-1}\sum_{i=0}^{n-1} ||\boldsymbol{C}(i,j)- \boldsymbol{N}(i,j)||^2\tag{2} MSE=m×n1i=0∑m−1i=0∑n−1∣∣C(i,j)−N(i,j)∣∣2(2)
然后我们计算出原始图像可能出现的最大像素值 M A X C 2 MAX^{2}_{C} MAXC2。对于我们的8位图像数据,这个值为 255 255 255。
最后,我们定义
P
S
N
R
(
d
B
)
PSNR(dB)
PSNR(dB)为:
P
S
N
R
(
d
B
)
=
10
⋅
l
o
g
10
(
M
A
X
C
2
M
S
E
)
(3)
PSNR(dB)= 10 \cdot log_{10}(\frac{MAX^{2}_{C}}{MSE})\tag{3}
PSNR(dB)=10⋅log10(MSEMAXC2)(3)
P S N R PSNR PSNR值越大,就代表失真越少。
1.4.2 计算PSNR
将两图像字节读取到列表后:
MSE = 0
for idx, _ in enumerate(raw):
MSE += (raw[idx] - rec[idx]) ** 2
MSE /= (256 * 256)
PSNR= 10 * np.log10((255**2)/MSE)
print(PSNR)
得到重建图像的 P S N R ( d B ) = 51.177 PSNR(dB) = 51.177 PSNR(dB)=51.177
2 DPCM编码算法
2.1 算法实现
我们将DPCM的算法实现如下。其中量化过程由函数quantify()
完成。
程序的逻辑非常简单,首先开启三个buffer: origin_Buffer
用于存储原始图像数据,reconstructed_buffer
用于存储重建图像数据,residual_buffer
用于存储残差数据。
由于我们采用左侧像素对图像进行DPCM编码,故每行第一个像素需要将原始值传去。其余的像素值进行计算残差、量化;然后重建时反量化。
完整代码在附录贴出。
void getDPCMResidual(unsigned char * origin_Buffer, unsigned char* residual_buffer,unsigned char * reconstructed_buffer,int width,int height) {
/*
:function 该函数采用左侧的像素对图像进行DPCM编码。
:return 无
:output 该函数可以输出残差图像、重建图像
*/
int yuvsize = width * height;
for (int i = 0; i < yuvsize; i++) {
if (i % width == 0) { //若是每行第一个像素
reconstructed_buffer[i] = [i];
residual_buffer[i] = origin_Buffer[i];
}
else {
int dn = origin_Buffer[i] - reconstructed_buffer[i - 1];//范围:-255-255
int dn_after_Q = quantify(dn);//量化到0-255
residual_buffer[i] = dn_after_Q;//存入残差
reconstructed_buffer[i] = residual_buffer[i] * 2 + reconstructed_buffer[i - 1];//重建
}
}
}
量化函数的实现,则是对 [ − 255 , 255 ] [-255,255] [−255,255]的数据先进行右移 1 1 1位,再 + 128 +128 +128,并做上下限,将数据范围变到 [ 0 , 255 ] [0,255] [0,255]中。
unsigned char quantify(int dn) {
/*
:function 该函数用于对输入dn进行值的量化
:return 量化后的值(unsigned char)
*/
dn >>= 1;
int temp = dn + 128;
if (temp > 255) temp = 255;
if (temp < 0) temp = 0;
return temp;
}
2.2 1/2/4 比特的量化设计
先前的运算是将一个9比特的数压缩到8比特(1字节)的量化过程。由于采用右移运算符,量化为1/2/4 比特也非常简单。我们只需要改写quantify()
函数即可。
2.2.1 4bits
量化为1bit,我们只需要将原始9位值右移5位,即可获得4bit的数值。然后将范围进行一个线性映射。
修改量化函数:
unsigned char quantify(int dn) {
dn >>= 5;//范围-16-16
dn += 16;//0-32
return dn;
}
反量化处乘在取用residual时16即可。
2.2.2 2bits
与上原理完全一致:
unsigned char quantify(int dn) {
dn >>= 7;//范围-2~2
dn += 2;//0-4
return dn;
}
反量化处乘在取用residual时64即可。
2.2.3 1bit
量化为1bit,我们只需要将原始值右移8位。图像被二值化,只有两种颜色了。也不需要做值的偏移和防溢出了。反量化时,也只会有两种值,非黑即白。1bit我们还需要对原始值做一个二值化。残差要么是0要么是1。0表示与前值相同,1表示与前值不同。
unsigned char quantify(int dn) {
dn >>= 8;//范围-1~0
dn += 1;//0-1
return dn;
}
图像二值化的过程:
for (int i = 0; i < yuvsize; i++) {
if (origin_Buffer[i] >= 127) origin_Buffer[i] = 255;
else origin_Buffer[i] = 0;
}
反量化则是进行if else的判断,在此略去啦。
3 附录
3.1 输出csv文件
YUV_out << "编码内容,出现次数" << endl;
for (int i = 0; i < 256; i++) {
YUV_out << i << "," << distribution_Y[i] << endl;
}
3.2 Python作图
# -*- coding:utf-8 -*-
import numpy as np
import matplotlib.pyplot as plt
from pandas import read_csv
origin = open('原图.csv')
residual = open('残差图像.csv')
origin_data = read_csv(origin)
residual_data = read_csv(residual)
plt.rcParams['font.sans-serif'] = ['Songti SC'] # 指定默认字体
# YUV的作图同下
fig, rgb = plt.subplots()
rgb.plot(origin_data['编码内容'], origin_data['出现次数'] / origin_data['出现次数'].sum(), label='原图', color='red')
rgb.plot(residual_data['编码内容'], residual_data['出现次数'] / residual_data['出现次数'].sum(), label='残差图', color='blue')
rgb.set_xlabel('像素灰度值/残差值')
rgb.set_ylabel('出现频率')
rgb.set_title('灰度值/残差值频率分布图')
rgb.legend()
plt.show()
result = 0
pr = residual_data['编码内容'] / residual_data['编码内容'].sum()
it = iter(pr) # 创建迭代器对象
for x in pr:
if x != 0:
result += -x * np.log2(x)
print(result)
3.3 DPCM实现
# include<iostream>
# include <fstream>
using namespace std;
unsigned char quantify(int dn) {
/*
:function 该函数用于对输入dn进行值的量化
:return 量化后的值(unsigned char)
*/
dn /= 2;
int temp = dn + 128;
if (temp > 255) temp = 255;
if (temp < 0) temp = 0;
return temp;
}
void getDPCMResidual(unsigned char * origin_Buffer, unsigned char* residual_buffer,unsigned char * reconstructed_buffer,int width,int height) {
/*
:function 该函数采用左侧的像素对图像进行DPCM编码。
:return 无
:output 该函数可以输出残差图像、重建图像
*/
int yuvsize = width * height;
for (int i = 0; i < yuvsize; i++) {
if (i % width == 0) { //若是每行第一个像素
reconstructed_buffer[i] = origin_Buffer[i];
residual_buffer[i] = origin_Buffer[i];
}
else {
int dn = origin_Buffer[i] - reconstructed_buffer[i - 1];//范围:-255-255
int dn_after_Q = quantify(dn);//量化到0-255
residual_buffer[i] = dn_after_Q;//存入残差
reconstructed_buffer[i] = residual_buffer[i] * 2 + reconstructed_buffer[i - 1];//重建
}
}
}
void output_csv(unsigned char* origin_Buffer, unsigned char* residual_buffer) {
/*
:output 导出数据便于分析
*/
RGB_out << "R,G,B" << endl;
for (int i = 0; i < 256; i++) {
RGB_out << distribution_R[i] << "," << distribution_G[i] << "," << distribution_B[i] << endl;
}
YUV_out << "Y,U,V" << endl;
for (int i = 0; i < 256; i++) {
YUV_out << distribution_Y[i] << "," << distribution_U[i] << "," << distribution_V[i] << endl;
}
}
int main(int argc, char** argv) {
/*
:input 输入的yuv图像名 输出的yuv残差文件名 yuv图像的宽和高
:output 输出的yuv残差文件
*/
ifstream originYUV(argv[1], ios::binary);
ofstream residualYUV(argv[2], ios::binary);
ofstream reconstructedYUV("重建图像.yuv", ios::binary);
if (!originYUV || !residualYUV || !reconstructedYUV) {
cout << "open file failed!" << endl;
return 0;
}
int width = atoi(argv[3]);
int height = atoi(argv[4]);
int size = width * height;
int yuvsize = size; //我们无需考虑UV通道,只需考虑Y分量即可,UV通道置为128
unsigned char* origin_Buffer = new unsigned char[yuvsize];
unsigned char* residual_buffer = new unsigned char[yuvsize];
unsigned char* reconstructed_buffer = new unsigned char[yuvsize];
originYUV.read((char*)origin_Buffer, yuvsize);
getDPCMResidual(origin_Buffer, residual_buffer, reconstructed_buffer, width, height);
residualYUV.write((char*)residual_buffer, size);
reconstructedYUV.write((char*)reconstructed_buffer, size);
unsigned char* temp = new unsigned char[yuvsize/2];
for (int i = 0; i < yuvsize / 2; i++) {
temp[i] = 128;
}
output_csv(origin_Buffer, residual_buffer);
residualYUV.write((char*)temp, size / 2);//写UV数据
reconstructedYUV.write((char*)temp, size / 2);
delete[] origin_Buffer;
delete[] residual_buffer;
delete[] reconstructed_buffer;
reconstructedYUV.close();
originYUV.close();
residualYUV.close();
return 0;
}
3.4 计算PSNR
import numpy as np
fraw = open("原始图像.yuv", "rb")
frec = open("重建图像.yuv", "rb")
raw = []
rec = []
i = 0
while i < 256 * 256:
i += 1
buf1 = fraw.read(1)
buf2 = frec.read(1)
if buf1:
buf1 = int.from_bytes(buf1, byteorder='big')
buf2 = int.from_bytes(buf2, byteorder='big')
raw.append(buf1)
rec.append(buf2)
MSE = 0
for idx, _ in enumerate(raw):
MSE += (raw[idx] - rec[idx]) ** 2
MSE /= (256 * 256)
PSNR= 10 * np.log10((255**2)/MSE)
print(PSNR)