1. 要求
选择灰度图像,按照行的方式展开像得到一维的向量。按照一维预测的公式:
自行设计预测算法实现一维无损预测压缩。将预测压缩后的一维向量(由预测误差组成),进行一维行程/游程编码。计算原图、最终行程/游程编码压缩后数据所需要的存储空间,计算压缩率。
2. 分析
对于灰度图像的任意一行像素
,其表达式如下,其中
为灰度图像
的列数。
给定系数表达式如下
则的第
个元素的预测值
可表示为
预测误差可表示为
特别地,当时,
不存在,则
更一般地,用矩阵进行简化可得
则
则可以写作如下代码
clear, close all
I = imread('cameraman.tif');
[row, col] = size(I); % 获取I的行数和列数
a = rand(col-1, 1); % col-1个系数
e = zeros(size(I)); % 误差矩阵
for i = 1: row % 遍历所有行
e(i,1) = I(i,j); % 第i行的第1列元素误差等于实际值
for j = 2: col % 从第2列开始遍历
e(i,j) = I(i,j) - I(i,1:j-1) * a(1:j-1);
end
end
为了方便,我使用rand函数生成长度为col-1(对应于上述的n-1)的列向量,其中的元素为0~1之间的随机浮点数。
实际运行上述代码的话,除了精度问题的报错,最大的问题是运行时间长。
3. 编码阶段-矩阵优化
可以观察到,上述循环的本质是逐列计算,因此我们可以从每一列入手,进行优化。
对于灰度图像的第
列像素
, 其表达式如下,其中
为灰度图像
的行数,
表示第
行第
列的值。
于是可以得到
即
于是
其中表示误差矩阵中的第
列
特别地,当时,
不存在,则
因此图像编码阶段的代码可写为
clear, close all
I = imread('cameraman.tif');
[row, col] = size(I); % 获取I的行数和列数
a = rand(col-1, 1); % col-1个系数
e = zeros(size(I)); % 生成误差矩阵
e(:,1) = I(:,1); % 第一列特殊计算
II = double(I); % I的副本,类型为double
for j = 2:col % 遍历列
e(:,j) = II(:,j) - (II(:,1:j-1) * a(1:j-1));
end
e = round(e); % 将误差矩阵取整
这里说下要生成一个的副本,且类型为double,这是因为在下面的矩阵乘法运算的时候,
的默认类型是uint8,范围在0~255,uint8的矩阵和任何矩阵进行乘法运算,结果仍为uint8,即超出255的变为255,低于0的变为0,浮点数自动取整。为了得到较为准确的误差矩阵,需要在double类型上进行运算,于是预先生成一个double类型的
的副本。
4. 图像解码
只需要将上述编码过程逆操作即可得到图像解码对应的代码
J = zeros(size(e));
J(:,1) = e(:,1);
for j = 2: col
J(:,j) = e(:,j) + (J(:,1:j-1) * a(1:j-1));
end
J = uint8(J); %解码图像
至此,可以解释图像编码的应用场景。比如,张三需要将大量图像通过网络传输给李四,由于网络带宽限制,单张图像都无法直接传输,于是张三和李四事先约定了一个编码系数向量A。
在发送端,每一张图片都使用A编码得到一个误差矩阵e;
传输的过程中只需要传输误差矩阵e;
在接收端,只需要用A对误差矩阵e进行解码即可得到原始图像。
我们知道,这里的误差矩阵其实和图像的规模是一致的,即行数和列数都等于图像的行数和列数。
灰度图像中的每一个像素都由8位2进制进行编码表示,实际范围在0~256。
想要传输误差矩阵e,一个方法是使用调优过的系数向量A,使得生成的每一个误差矩阵中的任何一个元素,都可以用小于8位的2进制编码进行表示;另一个方法就是题目要求的,对误差矩阵进行游程编码。
游程编码(Run Length Coding,RLC)是一种简单的熵编码(无损压缩编码)方法。其基本原理是,将具有相同数值的、连续出现的信源符号用“符号+符号出现次数”的形式表示。如“zzxxxxyyyyyzzz”将被编码为“2z4x5y3z”。
这样就存在一个问题:假如图像中相邻的像素均不相等,那么游程编码非但起不到压缩作用,还会因为需要传输像素的出现次数,而使数据量增加一倍。因此,直接对图像进行编码时,游程编码只适用于有较多灰度相同的图像,也就是说图像最好包含大片的平坦区。
想要得到效果出色的游程编码结果,就必须使误差矩阵中出现较多连续的值,而误差矩阵中值分布情况,只与图像本身和编码系数向量A有关。
这里就有几个引人深思的问题:
- 能不能找出一个A,并且给出证明,任意的图像经过A编码得到的误差矩阵,均适合使用游程编码,或者误差矩阵中的每一个元素可以用7位2进制甚至6位2进制进行编码。
- 退而求其次,对于一个图像,能不能通过特定算法,花费较少计算代价计算出一个A,将这个特定的A和它对应的图像一起传输。
5. 游程编码
对于一个列向量,对其游程编码后得到矩阵
,其中第一列表示值,第二列表示这个值连续出现的次数。
代码如下
%% 游程编码
function [L, size1, size2] = RLC(K)
L = [K(1) 1];
Lj = 1;
for i = 2: length(K)
if K(i) == L(Lj,1)
L(Lj,2) = L(Lj,2) + 1;
else
Lj = Lj + 1;
L(Lj,:) = [K(i) 1];
end
end
L_1 = ceil(log2(max(L(:,1))-min(L(:,1))+1));
L_2 = ceil(log2(max(L(:,2))-min(L(:,2))+1));
size1 = L_1 * length(L);
size2 = L_2 * length(L);
end
这里举例说明一下L_1的含义,如果矩阵L的第一列中最大值为260,最小值为20,则max(L(:,1))-min(L(:,1))+1的结果为260-20+1=241,显然20-260之间有241个连续的值,可以被8位2进制所编码表示,ceil(log2(X))计算的就是X能被几位二进制所表示。
L_1和L_2分别表示第一列和第二列能被几位二进制表示,size1和size2分别表示第一列和第二列占用的总位数。
size1+size2就是编码L矩阵所需要的总位数(总位数除以8就是总字节数)
6. 完整代码
clear, close all
I = imread('cameraman.tif');
subplot(1,3,1), imshow(I), title('原始图像')
[row, col] = size(I);
a = -1 + 2 * rand(col-1, 1);
a = a / 200;
e = zeros(size(I));
%% encode
% 由原图像I和预测系数a生成误差矩阵e
e(:,1) = I(:,1); % 第一列特殊计算
II = double(I); % I的副本,类型为double
for j = 2:col % 遍历列
e(:,j) = II(:,j) - (II(:,1:j-1) * a(1:j-1));
end
e = round(e); % 将误差矩阵取整
subplot(1,3,2), imshow(uint8(e)), title('误差矩阵')
%% decode
% 该过程只用到误差矩阵e和预测系数a
J = zeros(size(e));
J(:,1) = e(:,1);
for j = 2: col
J(:,j) = e(:,j) + (J(:,1:j-1) * a(1:j-1));
end
J = uint8(J); % 解码图像
subplot(1,3,3), imshow(J), title('解码图像')
%% run length encode
K = e';
K = K(:);
[L, size11, size12] = RLC(K); % 对K进行游程编码
[L2, size21, size22] = RLC(L(:,2)); % 对游程编码的矩阵的第二列再次进行游程编码
total_size = 8 * length(I(:));
% 图像压缩率 = 原始图像所需空间 / 压缩图像所需空间
ratio = total_size / (size11+size21+size22);
%% 游程编码函数
function [L, size1, size2] = RLC(K)
L = [K(1) 1];
Lj = 1;
for i = 2: length(K)
if K(i) == L(Lj,1)
L(Lj,2) = L(Lj,2) + 1;
else
Lj = Lj + 1;
L(Lj,:) = [K(i) 1];
end
end
L_1 = ceil(log2(max(L(:,1))-min(L(:,1))+1));
L_2 = ceil(log2(max(L(:,2))-min(L(:,2))+1));
size1 = L_1 * length(L);
size2 = L_2 * length(L);
end