一、概述
人类所获取的外界知识中约有80%以上的信息来自于视觉,很多情况下图像所承载的信息比任何其他形式的信息都更真切,更丰富,获取也更便捷。一幅实用的数字图像的数据量是巨大的,这给图像的传输和存储带来相当大的困难。在保证一定的图像质量和满足任务要求条件下,减少原始图像数据量的处理过程也就是图像压缩是非常关键的一个步骤。
在无损压缩算法中,一种有效地减少像素间冗余的技术就是单独处理图像的位平面,这种技术被称为位平面编码。它是以将一幅多级图像分解为一系列二值图像并对每幅二值图像进行压缩的原理为基础的。该算法应用也比较广泛,在JPEG2000中,把量化后的系数组织成二进制位平面,从最高有效位平面开始,依次对每个位平面上的小波系数位进行编码。该编码算法的输出是按数据对重建图像质量的重要性排列的,因此可以按一定的压缩比在任意时刻截断输出码流,而获得当时条件下可能获得的最好质量,具有良好的灵活性和可分级性,这就是所说的嵌入特性,可用于渐进图像传输。本文以灰度图像为例利用这种算法进行程序设计。
二、算法简介
灰度图像的每个像素值为8比特,可以表示为以2为基底的多项式,即
(1)
基于这一性质,将多值图像分解为一个二值图像序列的简单方法是将式(1)中多项式8个系数分成8个一比特的平面。因此,一幅灰度图像可以如此变换成8幅二值图像,每幅二值图像成为一个比特平面。第i个比特平面由所有像素的第i个比特组成(i=0,1,…7)。由多值图像分解成一组比特平面,这种变换是可逆的,即由这组比特平面可以完全重建原始的多值图像。
每一比特平面可以分成若干个p*q个像素大小的块。比特平面经过划分,出现三种不同结构的块,即全0块,全1块,以及0和1混合的块。统计表明,前两种结构的块最常出现。一种编码做法是比较全0块和全1块出现的概率,对出现概率高的分配比特码字0,对另一个分配比特码字11,对混合块分配10作前缀码,其后跟pq比特,表示该块的具体图案。
若以p(0)表示全0块出现的概率,以p(1)表示全1块出现的概率,则混合块出现的概率为1-p(0)-p(1),这个编码方案的压缩比为
(2)
图像相邻的灰度值具有很强的相关性,所以高比特位具有大量的全0块和全1块。相邻像素间的较小差别主要体现在低比特位上,混合块出现的概率较高,对编码效率有一定的影响。
但是,有时像素值的一个微小改变会对比特平面的复杂程度造成重大影响。当像素值127(01111111)与128(10000000)相邻,全部8个比特平面都会出现从0到1的转变,从而在所有比特平面都破坏了全0块或全1块的出现,降低了编码效率。为了解决这一问题,我们引进格雷码,其突出特点为代表相邻两个数值的码字只有1位发生转变。比如127,128的格雷码分别为11000000,01000000,只有第七比特平面含有从0到1的转变,增大了全0块全1块出现的概率,有利于编码效率的提高。
下面通过一个“图像比特平面编码程序”的具体实例讨论如何利用VC++实现图像读取、比特平面编码、生成压缩文件、解码和重建图像的功能。具体开发环境:Windows XP, Microsoft Visual C++ 6.0。
三、程序分析
打开图像,将灰度值存入图像数组,并显示(本文中代码省略)。
将图像各个像素灰度值转换为格雷码,并存入代表8个比特平面的数组。
相应于(1)中的多项式,8比特的格雷码可由下式得到。
(3)
for(i=0;i<lx*ly;i++)
{
//ip中存储各像素灰度值
a=(int)ip[i];
b=a/2;
//当前像素对应的格雷码
GrayCode=a^b;
//把格雷码存入8个比特平面对应的数组
for(j=0;j<8;j++)
{
iip[j][i]=GrayCode%2;
GrayCode/=2;
}
}
对各个比特平面进行扫描,取p=q=4,各比特平面中p*q小块如果为全0块,开头一位放2,如果为全1块,开头一位放3,而混合块开头一位是0或1,编码时可以相区别。同时统计全0块和全1块出现的次数,返回0或1,编码时可以区别处理。下面为进行扫描的函数:
int scan()
{
int zero,one,i,j,z,k,binary,sign;
zero=one=0;
for(i=0;i<(ly/p);i++)
{
for(j=0;j<(lx/q);j++)
{
binary=iip[I][i*p*lx+j*q]; sign=0;
//判断是否为混合块,一旦有不相等的数值,跳出循环
for(z=0;z<p;z++)
{
for(k=0;k<q;k++)
{
if(binary!=iip[I][(i*p+z)*lx+(j*q+k)])
{
sign=1;break;
}
}
if(sign==1) break;
}
//是全0块或全1块
if((z==p)&&(k==q))
{
if(binary==0)
{
zero++;//0计数加1
iip[I][i*p*lx+j*q]=2;//全0块标志
}
else
{
one++;//1计数加1
iip[I][i*p*lx+j*q]=3;//全1块标志
}
}
}
}
if(one>zero) return 1;
else return 0;
}
编码从高位比特平面到低位比特平面进行。对于全0块或全1块,如果出现概率高,分配1比特码字0;如果出现概率低,分配2比特码字11。如果是混合块,先写入前缀码10,再写入该块具体图案。由于读写文件的最小单位是字节,而编码是不定长的,所以我们把每32位编码对应为一个unsigned int 型数据,依次把各个unsigned int 型数据写入文件。最后几位编码如果不够32位,把这几位编码的右端补0,凑够32位后写入文件。解码时,当所有位平面解码完成后,就不再继续向下读数据文件,所以上面右端补0的做法不影响解码的正确性,实现的核心代码如下:
//创建压缩文件
CFile MyFile1("D:\\test.dat",CFile::modeCreate|CFile::modeWrite);
nCode=nN=0;
//编码从高位比特平面到低位比特平面
for(i1=7;i1>=0;i1--)
{
for(i=0;i<(ly/p);i++)
{
for(j=0;j<(lx/q);j++)
{
bi=iip[i1][i*p*lx+j*q];
//该块为全0或全1块且出现的概率高,分配1比特码字0
if(((bi==2)&&(c[i1]==0))||((bi==3)&&(c[i1]==1)))
{
nCode+=0;nN++;
if(nN==32)
{
MyFile1.Write((void*)(&nCode),4);
nN=0;nCode=0;
}
else
nCode<<=1;
}
//该块为全0或全1块且出现的概率低,分配2比特码字11
else if(((bi==2)&&(c[i1]==1))||((bi==3)&&(c[i1]==0)))
{
for(l=0;l<2;l++)
{
nCode+=1;
nN++;
if(nN==32)
{
MyFile1.Write((void*)(&nCode),4);
nN=0;
nCode=0;
}
else
nCode<<=1;
}
}
//混合块先写入前缀码10,再写入该块具体图案
else
{
//写入10
for(l=0;l<2;l++)
{
nCode+=(1-l);nN++;
if(nN==32)
{
MyFile1.Write((void*)(&nCode),4);
nN=0;nCode=0;
}
else
nCode<<=1;
}
//写入该块具体图案
for(z=0;z<p;z++)
{
for(k=0;k<q;k++)
{
nCode+=iip[i1][(i*p+z)*lx+j*q+k];nN++;
if(nN==32)
{
MyFile1.Write((void*)(&nCode),4);
nN=0;nCode=0;
}
else
nCode<<=1;
}//k
}//z
}//else
}//j
}//i
}//i1
MyFile1.Close();
由压缩文件解码,重建图像。解码为编码的逆过程,实现的核心代码如下:
CFile MyFile2("D:\\test.dat",CFile::modeRead);
while(1)
{
MyFile2.Read((void*)(&nCode1),4);
n1=31;
//把编码拆入数组
while(n1>=0)
{
st[n1--]=nCode1%2;nCode1/=2;
}
for(i=0;i<32;i++)
{
if(read==1)
{
oop[num][n++]=st[i];
if(n==p*q)
{
n=0;num++;read=0;
if(num==((lx/q)*(ly/p)))
{
trans();//一个比特平面解码完成,存入数组相应位置
if(nu==-1)
{
sig=1;break;
}
else
{
num=0;s1=c[nu];s2=1-s1;
}
}
}
}
else if(con==1)
{
if(st[i]==1)
{
oop[num++][0]=s2+2;
if(num==((lx/q)*(ly/p)))
{
trans();//一个比特平面解码完成,存入数组相应位置
if(nu==-1)
{
sig=1;break;
}
else
{
num=0;s1=c[nu];s2=1-s1;
}
}
}
else
{
read=1;
}
con=0;
}
else
{
if(st[i]==0)
{
oop[num++][0]=s1+2;
if(num==((lx/q)*(ly/p)))
{
trans();//一个比特平面解码完成,存入数组相应位置
if(nu==-1)
{
sig=1; break;
}
else
{
num=0;s1=c[nu];s2=1-s1;
}
}
}
else
con=1;
}
}//i
if(sig==1) break;
}
MyFile2.Close();
以下为解码过程中用到的一个转换函数,作用为把各个比特平面的数组全0块,全1块全部填入0或1,混合块填入具体图案。
void trans()
{
int d1,d2,i,j,z,k;
for(i=0;i<(ly/p);i++)
{
for(j=0;j<(lx/q);j++)
{
d1=oop[i*(lx/q)+j][0];
//全0块填入0或1
if((d1==2)||(d1==3))
{
for(z=0;z<p;z++)
{
for(k=0;k<q;k++)
{
op[nu][(i*p+z)*lx+j*q+k]=d1-2;
}
}
}
//混合块填入具体图案
else
{
for(z=0;z<p;z++)
{
for(k=0;k<q;k++)
{
d2=oop[i*(lx/q)+j][z*q+k];
op[nu][(i*p+z)*lx+j*q+k]=d2;
}
}
}
}//j
}//i
nu--;
}
将格雷码转为二进制码,并存入图像数组。
相应于(1)中的多项式,8比特的二进制码可由下式得到。
(4)
对应的程序如下:
for(i=0;i<lx*ly;i++)
{
//转换为二进制
nValue=nValue1=op[7][i];
for(j=6;j>=0;j--)
{
nValue1=(op[j][i])^nValue1;
nValue<<=1;
nValue+=nValue1;
}
//存入图像数组
op1[i]=nValue;
}
最后,显示解码后图像(本文中代码省略)。
四、结语
对256*256像素(64KB)的几幅灰度图像进行测试,结果如下。
sun |
tree |
baboon |
lena |
cat |
34.8K |
53.4K |
61.4K |
50.4K |
52.3K |
对512*512像素(256KB)的几幅灰度图像进行测试,结果如下。
goldhill |
lena |
barbara |
boat |
Baboon |
207K |
189K |
212K |
208K |
243K |
对于上述结果可以进一步处理。如果一个编码块中比特数值大部分为0,只有很少的1,利用人的视觉心理特性,可将该块视为全0块处理。此时视觉上可能感觉不到明显的失真而
予以接受。反之也可将1很多而0很少的块视为全1块处理。这种利用视觉心理特性的编码,尤其在低位的比特平面上使用,不会引起明显的可察觉失真(因其产生的客观失真量小),可收到较好效果。上述处理使全0块全1块出现的概率p(0)p(1)上升,从而提高压缩效率。解码前后的图像对比如下图所示。
解码前图像 解码后图像

图 解码前后的图像对比
|