[实验]无失真信源压缩编码

实验一 无失真信源压缩编码


此文系后续整理,懒得copy,对应的方法可以根据需要可以直接下载代码,附件:

https://download.csdn.net/download/weixin_40744915/10296130

https://download.csdn.net/download/weixin_40744915/10296133

https://download.csdn.net/download/weixin_40744915/10296135

https://download.csdn.net/download/weixin_40744915/10296154


目录

一、实验目的及要求... 1

二、任务分析及方案选择... 1

(一)Huffman编码... 1

(二)游程编码... 1

(三)算术编码... 2

(四)字典码... 3

三、实验原理及过程... 5

(一)Huffman编码实现信源无失真压缩... 5

(二)游程编码与Huffman编码结合实现信源无失真压缩... 13

(三)字典码实现信源无失真压缩... 18

四、实验结果分析与思考... 20

(一)Huffman编码实现信源无失真压缩... 20

(二)游程编码与Huffman编码结合实现信源无失真压缩... 23

(三)字典码实现信源无失真压缩... 24

五、实验小结... 27

 


 


一、实验目的及要求

1、对文本信源,寻求最佳压缩方案,现完整的无失真压缩的编译码算法,完成对文本文件的压缩及解压。

2、构建性能分析模块,实现对信源熵的统计、压缩后的传输率(bits/symbol),以及恢复文本的完整情况进行分析。

二、任务分析及方案选择

香农第一定理从理论上给出了进行无失真信源压缩的理论极值,并论证了理想最佳信源编码的存在性,也就是说,总能找到某种合适的编码方法使编码后信源的信息传输率任意逼近信源的信息熵而不存在任何失真,这样的无失真信源编码在数据压缩技术中又称为熵编码。典型的代表有霍夫曼编码、游程编码、算术编码和字典码,霍夫曼编码主要适用于多元独立的信源,游程编码和算术编码主要使用与二元信源及具有一定相关性的有记忆信源,字典码针对信源的统计特性未知时有较好的性能。本次实验给定的是一个大小较小、无记忆文本文件,所以Huffman编码是应该一个比较合适的方案,下面具体进行原理分析与比较:

(一)Huffman编码

Huffman编码是一种即时码,二元Huffman编码步骤如下:

(1)将q个信源符号按递减概率排列起来,设

(2)用0和1码符号分别分配给概率最小的两个信源符号,并将这两个概率最小的信源符号合并成一个新符号,并用这两个最小概率之和作为新符号的概率,从而得打只包含个符号的新信源,作为原信源的缩减信源;

(3)把缩减信源的符号仍按照概率大小递减排列,再将其最后两个概率最小的符号合并成一个新符号,依次递推,直至缩减信源最后只剩下两个符号为止。将这最后两个符号分别用0和1码表示,最后他们俩的概率之和为1。然后从最后一级缩减信源开始,依照编码路径从后向前返回,就得出各个信源符号所对应的码符号序列,即对应的码字。

但Huffman编码得到的码并非唯一的,因为每次对缩减信源最后两个概率最小的符号,用0还是1表示可以是任意的,所以可能得到码长相同但码字不同的码;另外当缩减信源中缩减合并后的符号的概率与其他信源符号相同时,概率次序也不一定,所以其Huffman编码并不唯一。但由于平均码长没变,所以有相同的编码效率。

后续将给出实验(一)中给出具体过程及步骤。

(二)游程编码

游程编码又称“运行长度编码”或“行程编码”,是一种统计编码,该编码属于无损压缩编码,是栅格数据压缩的重要编码方法。对于二元信源,其输出只有两个符号,‘0’或‘1’。连续出现‘1’符号或者连续出现‘0’符号的个数称为这一段的游程长度,这个游程长度是一个随机变量,可取值从1……无穷,且‘0’和‘1’游程一定是交替出现的。用自然数标记游程长度的这种映射是可逆的,无失真的,而且游程长度越长以及长游程频繁出现时压缩效率就会越高。

同样,由于通过游程的映射变换已经减弱了原来二元序列符号间的相关性,并把它变成了多元序列,这样可以在游程编码的基础上进行其他的变长编码方式,就能进一步压缩信源,提高通信效率。当然要首先测定‘0’游程长度和‘1’游程长度的概率分布,即以游程长度为元素,构造一个新的多元信源进行霍夫曼编码,得到不同游程长度映射的码字,就可以将游程序列变换成码字序列,使信源得到进一步压缩。若二元序列的概率特性已知,由于二元序列和映射所得的游程序列是一一对应的,就可以计算出游程序列的概率特性,根据信息熵的定义分别计算‘0’游程长度信源和‘1’游程长度信源的熵,以及平均游程长度,原信源的二元序列是由‘0’游程和‘1’游程交替出现而组成的,所以原二元序列的熵等于两个游程长度信源熵之和除以它们的平均游程长度之和,即游程变换后的信源信息熵没有变,当‘0’游程和‘1’游程的编码效率都很高时,采用游程编码的编码效率也很高。因此要想编码效率尽可能高,应该尽量提高熵值较大的游程的编码效率。

因此,在后续将给出游程编码和Huffman编码结合的无失真压缩编码,具体在实验(二)中给出具体过程及步骤。

(三)算术编码

虽然Huffman编码是最佳码,但是对于二元信源,必须对二元信源的N次扩展信源进行Huffman编码时,才能使得平均码长接近信源的熵,编码效率才高。必须要计算出所有N长信源序列的概率分布,并构造相应的完整的码树,所以较为复杂。算术编码可以无需计算出所有N长信源序列的概率分布及编出码表,可以直接对输入的信源符号序列进行编码输出,对于很长的信源符号序列来说是一个简单有效的编码方法。算术编码的基本原理是将编码的消息表示成实数0和1之间的一个间隔(Interval),消息越长,编码表示它的间隔就越小,表示这一间隔所需的二进制位就越多。

算术编码用到两个基本的参数:符号的概率和它的编码间隔。信源符号的概率决定压缩编码的效率,也决定编码过程中信源符号的间隔,而这些间隔包含在0到1之间。编码过程中的间隔决定了符号压缩后的输出。

给定事件序列的算术编码步骤如下:

(1)编码器在开始时将“当前间隔” [ L, H) 设置为[0,1)。

(2)对每一事件,编码器按步骤a和b进行处理

a.编码器将“当前间隔”分为子间隔,每一个事件一个。

b.个子间隔的大小与下一个将出现的事件的概率成比例,编码器选择子间隔对应于下一个确切发生的事件相对应,并使它成为新的“当前间隔”。

(3)最后输出的“当前间隔”的下边界就是该给定事件序列的算术编码。

设Low和High分别表示“当前间隔”的下边界和上边界,CodeRange为编码间隔的长度,LowRange(symbol)和HighRange(symbol)分别代表为了事件symbol分配的初始间隔下边界和上边界。上述过程的实现可用伪代码描述如下:

set Low to 0

set High to 1

while there are inputsymbols do

    take a symbol

    CodeRange = High – Low

    High = Low + CodeRange *HighRange(symbol)

    Low = Low + CodeRange * LowRange(symbol)

end of while

output Low

算术码解码过程用伪代码描述如下:

get encoded number

do

    find symbol whose range straddles theencoded number

    output the symbol

    range = symbo.LowValue – symbol.HighValue

    substracti symbol.LowValue from encodednumber

    divide encoded number by range

until no more symbols

由于算术编码可以对于输入的信源符号序列不需计算概率,只需要计算累积分布函数即可进行编码输出,因此编码效率很高。当信源符号序列很长时,平均码长接近信源的信源熵。由于算数编码的效率高、编译码速度快,因此在图像压缩等方面有广泛应用,在方案设计部分给出了其实现的伪代码,由于算术编码有关信源符号序列的累积分布函数的计算是将累积分布函数写成二进位的小数,取小数后的l位(若后面有尾数,就进位到第l位)得到的,所以这样根据二进位小数截去位数可能对于精度有一定影响;算术编码也是一种对错误很敏感的编码方法,如果有一位发生错误就会导致整个消息译错,这是算术编码在实际应用中需要考虑的问题。

(四)字典码

Huffman编码、游程编码、算术编码都是基于统计思想的编码方法,但当统计参数发生了变化的时候,这种静态的统计编码就会产生性能下降。因此在1980年代提出了自适应或者半自适应的动态方法,对于工程实践中无法测定信源的统计概率或者产生统计失配时,具有较好的性能。1977-1978年由以色列的研究人员Ziv和Lempel提出的,习惯称为LZ码,由于把已经编码的字符串存储作为字典使用,又称为字典码。典型的字典码是上述二人于1977、1978年提出的LZ-77码、LZ-78码,以及1984年由韦尔奇改进的LZW码,随后又出现了对LZ码和LZW码的改进或变形的多种字典码,如LZSS码,LZRW1-4码,LZP1-4码等。这类字典码,编译简捷,易于软件实现,使其得到广泛的应用。LZ码是一种通用编码方法,就是指在信源概率统计特性不知或不存在时,对信源进行编码,而且使编码效率仍为很高的一种码。

我们先举例说明字典码的思想。设A,B两人约定在用完全相同的字典下互相通信,A将信文中的每个单词用二元标识符(a,b)代替,a,b是表示单词所在字典中的页数和在此页的位置次序,当逐个单词被替代后,就将标识符序列发给B。B收到标识符序列后可按标识符恢复单词和信文。标志符号的个数显然少于单词符号个数,就实现了文本压缩的目的。另外若对字符序列进行压缩编码,其效率要高于对单个字符的压缩。而且字符串越长,平均码长越短。在压缩编码中又常把字符串或字符序列称为数据流。

LZ-77编码算法的主要思想是把已输入的数据流存储起来,作为字典使用。编码器为输入流开设一个滑动窗口,其长可达几千字节,将输入的数据存在窗内,做字典使用,窗口右侧是待编码数据流的缓存器,长只有几十字节。用三元标识(K,Ɩ,d)给数据编码,K是窗内从尾自又向左搜索的字符位数,称为移位数,Ɩ是窗内可以与窗外有相同符号的字符串的长,称为匹配串长度,d是窗外已找到匹配的串的后面串的首字符。以自然对数2.71828182845为例:

(1)开始时,窗内是空的,要输入当前位置的2,窗内没有与2匹配的串,故K,Ɩ皆为0,把2编码为(0,0,2),输入2。下面输入的数据为.718也与2的情况相同,窗内只有刚输入的2,没有与.718分别相匹配的串,故只能将.718分别编码为(0,0,.),(0,0,7),(0,0,1),(0,0,8),分别将其输入窗内。

(2)现窗内已有2.718存入,当前位置是e的小数第4位2,在窗内从尾由右向左搜索到第5位处(包括小数点),有窗内左边的第5个数字2与当前位置2相同,即匹配串长Ɩ为1,当前位置2后面的串的首字符为8,得K=5,Ɩ=1,故编码为(5,1,8),表示窗内字典从右向左第5位有一个长为1的匹配串“2”和当前位置的数据串“2”相匹配,后面串的首字符为8,并将28同时输入。

(3)现窗内字典为2.71828,当前输入数据为1,编码器在窗内从尾自右向左搜索,移动4位,搜索到长度Ɩ=4的与当前位置相匹配的串:1828,下面串的首位为4,故编码为(4,4,4)。将18284输入窗内;

(4)由于字典内没有与5,9,0相匹配的串,故均是0移位,0匹配串长的无匹配字符,编码为(0,0,5),(0,0,9),(0,0,0),并将5,9,0输入窗内。

(5)在窗内搜索到第4位,有“45”与当前位置45匹配,串长Ɩ=2,后续段首字符为2。故编码为(4,2,2)。并又将452输入窗内。以下重复上述步骤,可逐步将数据流编码并输入窗内。所有的标识(K,Ɩ,d)序列就是所得的压缩编码序列。

LZ-77码的译码器要有一个与编码器字典窗口等长的缓存器,可以边译码,边建立译码字典,即编码字典不用传送,译码器可自动生成。这种码隐含了假设条件:输入数据产生模式是相距很近的。

因此,在后续将字典码的无失真压缩编码,具体在实验(三)中给出具体过程及步骤。

三、实验原理及过程

(一)Huffman编码实现信源无失真压缩

1、构建Huffman编码树的过程:

(1)创建n个初始化的Huffman树,每个树只包含单一的叶节点,叶节点纪录对应的字母和该字母出现的频率(weight);

(2)按照weight从小到大对其进行所有的Huffman树进行排序,取出其中weight最小的两棵树,构造一个新的Huffman树,新的Huffman树的weight等于两棵子树的weight之和,然后再加入到原来的Huffman树数组当中;

(3)反复上面的2中的操作,直到该数组当中只剩下一棵Huffman树,那么最后剩下来的那棵Huffman树就是我们构造好的Huffman编码树;

2、文本压缩过程:

(1)对需要被压缩的文件以二进制方式读入,然后以byte为单位读取里面的值,然后将其转为int值,一共有[0~255]共256个值,遍历文件所有的byte,统计出现每个值的次数,作为后面叶子节点的权重(weight);

(2)使用上面统计的每个字符对应的权重,构造对应的Huffman编码树,然后分配每个字符对应的Huffman编码;

(3)在压缩文件的开始处,将每个字符及其对应的权重(weight)以二进制的方式保存到压缩文件中去,这样在对压缩文件解压是就可以根据源文件中每个字符出现的频率去构造相应的Huffman树,便于对压缩文件进行解压缩的操作;

(4)遍历文件的每一个byte,然后以步骤(2)中生成的Huffman编码,将该字节替换为编码后对应的01组合;

(5)将步骤(4)中生成的编码后的字符串,每8位作为一个单位,转为相应的byte,然后以二进制的方式写入到压缩文件中;

python实现如下:

def compress(inputfilename,outputfilename):

    """

    压缩文件,参数有

    inputfilename:被压缩的文件的地址和名字

    outputfilename:压缩文件的存放地址和名字

    """

    #1. 以二进制的方式打开文件

    f = open(inputfilename,'rb')

    filedata = f.read()

    # 获取文件的字节总数

    filesize = f.tell()

    print('原文件大小为:%d' % filesize)

 

    # 2. 统计 byte的取值[0-255] 的每个值出现的频率

    # 保存在字典 char_freq中

    char_freq = {}

    for x in range(filesize):

        tem = filedata[x]

        if tem in char_freq.keys():

            char_freq[tem] = char_freq[tem] + 1

        else:

            char_freq[tem] = 1

 

    # 3. 开始构造原始的huffman编码树 数组,用于构造Huffman编码树

    list_hufftrees = []

    for x in char_freq.keys():

        # 使用 HuffTree的构造函数 定义一棵只包含一个叶节点的Huffman树

        tem = HuffTree(0, x, char_freq[x],None, None)

        # 将其添加到数组 list_hufftrees 当中

        list_hufftrees.append(tem)

 

    # 4. 步骤2中获取到的 每个值出现的频率的信息

    # 4.1. 保存叶节点的个数

    length = len(char_freq.keys())

    output = open(outputfilename, 'wb')

 

    # 一个int型的数有四个字节,所以将其分成四个字节写入到输出文件当中

    a4 = length&255

    length = length>>8

    a3 = length&255

    length = length>>8

    a2 = length&255

    length = length>>8

    a1 = length&255

    output.write(six.int2byte(a1))

    output.write(six.int2byte(a2))

    output.write(six.int2byte(a3))

    output.write(six.int2byte(a4))

 

    # 4.2 每个值 及其出现的频率的信息

    # 遍历字典 char_freq

    for x in char_freq.keys():

        output.write(six.int2byte(x))

        #

        temp = char_freq[x]

        # 同样出现的次数 是int型,分成四个字节写入到压缩文件当中

        a4 = temp&255

        temp = temp>>8

        a3 = temp&255

        temp = temp>>8

        a2 = temp&255

        temp = temp>>8

        a1 = temp&255

        output.write(six.int2byte(a1))

        output.write(six.int2byte(a2))

        output.write(six.int2byte(a3))

        output.write(six.int2byte(a4))

   

    # 5. 构造huffman编码树,并且获取到每个字符对应的 编码

    tem = buildHuffmanTree(list_hufftrees)

    value =tem.traverse_huffman_tree(tem.get_root(),'',char_freq)

 

    freq_percent = []

    code_len = []

    for i in range(0, len(value), 3):

       freq_percent.append(value[i+1]/float(filesize))

        code_len.append(len(value[i+2]))

   

    # 计算平均码长

    sum_code_len = sum(np.array(freq_percent) *np.array(code_len))

    print('平均码长:',sum_code_len)

 

    # 计算信源熵

    info_entropy = sum([x*-math.log(x,2) for xin freq_percent])

    print('信源熵:',info_entropy,'bits/symbol')

 

    # 计算编码效率

    #code_eff = info_entropy/sum_code_len

    #print('编码效率:',code_eff)

    # print(tem)

    # time.sleep(1000)

   

    # 6. 开始对文件进行压缩

    code = ''

    for i in range(filesize):

        key = filedata[i]

        code = code + char_freq[key]

        out = 0

        while len(code)>8:

            for x in range(8):

                out = out<<1

                if code[x] == '1':

                    out = out|1

            code = code[8:]

            output.write(six.int2byte(out))

            out = 0

 

    # 处理剩下来的不满8位的code

    output.write(six.int2byte(len(code)))

    out = 0

    for i in range(len(code)):

        out = out<<1

        if code[i]=='1':

            out = out|1

    for i in range(8-len(code)):

        out = out<<1

    # 把最后一位给写入到文件当中

    output.write(six.int2byte(out))

 

    # 6. 关闭输出文件,文件压缩完毕

    output.close()

3、文本解压缩过程:

(1)以二进制读的方式,独处压缩文件中保存的被压缩文件中的每个值出现的频率

(2)使用每个值及其对应的权重,构造对应的Huffman编码树,然后分配每一个字符对应的Huffman编码;

(3)接着以二进制的方式读取压缩文件的内容;

(4)使用步骤(3)中读取出来的压缩后的字符串还原Huffman编码树进行解压缩:

a.初始指针指向编码树的根节点root;

b.循环取出压缩文件中的字符串,如果值为0,则为父节点的左孩子;如果值为1,则为父节点的右孩子;

c.判断指针所指向的是否是叶子节点。如果是叶子节点,则将该叶子节点对应的字符写入解压缩文件中,并将指针重置为编码树的root节点;如果不是叶子节点,继续循环步骤b;

d.直至压缩文件中的字符被取完,此时解压缩工作完毕,解压缩文件中已经均为对应的解码字符。

python实现如下:

defdecompress(inputfilename, outputfilename):

    """

    解压缩文件,参数有

    inputfilename:压缩文件的地址和名字

    outputfilename:解压缩文件的存放地址和名字

    """

    # 读取文件

    f = open(inputfilename, 'rb')

    filedata = f.read()

    #print(filedata)

    # 获取文件的字节总数

    cf_filesize = f.tell()

    print('压缩文件大小:%d'% cf_filesize)

 

    # 1. 读取压缩文件中保存的树的叶节点的个数

    # 一下读取 4个 字节,代表一个int型的值

    a1 = filedata[0]

    a2 = filedata[1]

    a3 = filedata[2]

    a4 = filedata[3]

    j = 0

    j = j | a1

    j = j << 8

    j = j | a2

    j = j << 8

    j = j | a3

    j = j << 8

    j = j | a4

 

    leaf_node_size = j

 

 

    # 2. 读取压缩文件中保存的相信的原文件中 [0-255]出现的频率的信息

    # 构造一个 字典 char_freq 一遍重建 Huffman编码树

    char_freq = {}

    for i in range(leaf_node_size):

        c = filedata[4 + i * 5 + 0]

 

        # 同样的,出现的频率是int型的,读区四个字节来读取到正确的数值

        a1 = filedata[4 + i * 5 + 1]

        a2 = filedata[4 + i * 5 + 2]

        a3 = filedata[4 + i * 5 + 3]

        a4 = filedata[4 + i * 5 + 4]

        j = 0

        j = j | a1

        j = j << 8

        j = j | a2

        j = j << 8

        j = j | a3

        j = j << 8

        j = j | a4

 

        char_freq[c] = j

 

    # 3. 重建huffman 编码树,和压缩文件中建立Huffman编码树的方法一致

    list_hufftrees = []

    for x in char_freq.keys():

        tem = HuffTree(0, x, char_freq[x],None, None)

        list_hufftrees.append(tem)

 

    tem = buildHuffmanTree(list_hufftrees)

    tem.traverse_huffman_tree(tem.get_root(),'', char_freq)

 

    # 4. 使用步骤3中重建的huffman编码树,对压缩文件进行解压缩

    output = open(outputfilename, 'wb')

    code = ''

    currnode = tem.get_root()

    for x in range(leaf_node_size * 5 + 4,cf_filesize):

        c = filedata[x]

        # c = six.byte2int(filedata[x])

        for i in range(8):

            if c & 128:

                code = code + '1'

            else:

                code = code + '0'

            c = c << 1

 

        while len(code) > 24:

            if currnode.isleaf():

                tem_byte = six.int2byte(currnode.get_value())

                output.write(tem_byte)

                currnode = tem.get_root()

 

            if code[0] == '1':

                currnode = currnode.get_right()

            else:

                currnode = currnode.get_left()

            code = code[1:]

 

    # 4.1 处理最后 24位

    sub_code = code[-16:-8]

    last_length = 0

    for i in range(8):

        last_length = last_length << 1

        if sub_code[i] == '1':

            last_length = last_length | 1

 

    code = code[:-16] + code[-8:-8 +last_length]

 

    while len(code) > 0:

        if currnode.isleaf():

            tem_byte =six.int2byte(currnode.get_value())

            output.write(tem_byte)

            currnode = tem.get_root()

 

        if code[0] == '1':

            currnode = currnode.get_right()

        else:

            currnode = currnode.get_left()

        code = code[1:]

 

    if currnode.isleaf():

        tem_byte =six.int2byte(currnode.get_value())

        output.write(tem_byte)

        currnode = tem.get_root()

 

    # 4. 关闭文件,解压缩完毕

    output.close()

    # 读取文件

    f = open(outputfilename, 'rb')

    filedata = f.read()

    #print(filedata)

    # 获取文件的字节总数

    df_filesize = f.tell()+4

    print('解压缩文件大小:%d'% df_filesize)

    f.close()

 

    result = cf_filesize / df_filesize

    print('压缩率为:%f' % result )

(二)游程编码与Huffman编码结合实现信源无失真压缩

编程思想:通过游程的映射变换已经减弱了原来二元序列符号间的相关性,并把它变成了多元序列,这样可以在游程编码的基础上进行Huffman编码,进一步压缩信源,提高通信效率。所以,在实现过程中,先对原文件进行游程编码,并存放在中间文件中,然后对得到的游程长度文件进行Huffman编码。

实现如下:

while(!feof(ifp))//将游程编码写进中间文件

    {

        fread(&ch2,1,1,ifp);

        if(ch2==ch1)

            count++;

        else

        {

            if(count>=3)

            {

                fwrite(&ch,1,1,ofp1);

                fwrite(&ch1,1,1,ofp1);

                fwrite(&count,1,1,ofp1);

            }

            else

            {

                if(count==1)

                    fwrite(&ch1,1,1,ofp1);

                else if(count==2)

                {

                    fwrite(&ch1,1,1,ofp1);

                    fwrite(&ch1,1,1,ofp1);

                }

 

            }

            ch1=ch2;

            count=1;

        }

        filelength++;

        if(filelength==Filelength)

            break;

    }

    if(count>=3)

    {

        fwrite(&ch,1,1,ofp1);

        fwrite(&ch1,1,1,ofp1);

        fwrite(&count,1,1,ofp1);

    }

    else

    {

        if(count==1)

            fwrite(&ch1,1,1,ofp1);

        else if(count==2)

        {

            fwrite(&ch1,1,1,ofp1);

            fwrite(&ch1,1,1,ofp1);

        }

    }

    fclose(ifp);

    fclose(ofp1);

   ofp1=fopen("C:\\Users\\Administrator\\Documents\\pythonexercise\\untitled\\c\\combined\\combined\\combined\\1.txt","rb");

    Filelength=0;//Filelength为中间文件的字节长度

    while(!feof(ofp1))

    {

        fread(&c,1,1,ofp1);

        huffmannode[c].weight++;

        Filelength++;

    }

    Filelength=Filelength-1;

    huffmannode[c].weight--;//结点下标为ASCII码值

    n=0;

    for(i=0;i<256;i++)

        if(huffmannode[i].weight!=0)

    {

        huffmannode[i].ch=(unsigned char)i;

        n++;//n表示字符出现的种类数

       huffmannode[i].lchild=huffmannode[i].rchild=huffmannode[i].parent=-1;

    }

    m=2*n-1;//哈弗曼树结点总数

    j=0;

    for(i=0;i<256;i++)

        if(huffmannode[i].weight!=0)

    {

        huffmannode[j]=huffmannode[i];

        j++;

    }

    for(i=n;i<m;i++)

       huffmannode[i].lchild=huffmannode[i].rchild=huffmannode[i].parent=-1;

    for(i=n;i<m;i++)

    {

        s1=select(huffmannode,i-1);

        huffmannode[i].lchild=s1;

        huffmannode[s1].parent=i;

        s2=select(huffmannode,i-1);

        huffmannode[i].rchild=s2;

        huffmannode[s2].parent=i;

       huffmannode[i].weight=huffmannode[s1].weight+huffmannode[s2].weight;

    }

    coding(huffmannode,n);

    fseek(ofp1,0,SEEK_SET);

    fwrite(&Filelength,4,1,ofp);//将源文件长度写入目标文件,以便解压

    fseek(ofp,8,SEEK_SET);

    codes[0]=0;

    filelength=0;

    while(!feof(ofp1))//将中间文件每个字符的编码所对应的字符写入目标文件

    {

        filelength++;

        fread(&c,1,1,ofp1);

        for(i=0;i<n;i++)

            if(c==huffmannode[i].ch)break;

        strcat(codes,huffmannode[i].code);

        while(strlen(codes)>=8)

        {

            for(i=0;i<8;i++)

            {

                if(codes[i]=='1')

                    c=(c<<1)|1;

                else

                    c=c<<1;

            }

            fwrite(&c,1,1,ofp);

            lengenth++;

            strcpy(codes,codes+8);

        }

 

        if(filelength==Filelength)

            break;

    }

    if(strlen(codes)>0)//最后有多余的编码补0

    {

        strcat(codes,"00000000");

        for(i=0;i<8;i++)

        {

            if(codes[i]=='1')

                ch1=(ch1<<1)|1;

            else

                ch1=ch1<<1;

        }

        fwrite(&ch1,1,1,ofp);

        lengenth++;

    }

       // lengenth代表源文件的字符的编码所对应的字符个数,源文件编码区的字符个数

    lengenth=lengenth+8;

    fseek(ofp,4,SEEK_SET);

    fwrite(&lengenth,4,1,ofp);

    fseek(ofp,lengenth,SEEK_SET);

    fwrite(&n,4,1,ofp);//n代表哈弗曼叶子总数

    count=0;

    for(i=0;i<n;i++)

    {

       fwrite(&(huffmannode[i].ch),1,1,ofp);

              //把哈弗曼叶子结点所带的字符写进目标文件

        ch1=huffmannode[i].Length;

        fwrite(&ch1,1,1,ofp);//把哈弗曼叶子结点所带字符的编码位数写进目标文件

        if(huffmannode[i].Length%8!=0)

           for(j=huffmannode[i].Length%8;j<8;j++)

           strcat(huffmannode[i].code,"0");

        while(huffmannode[i].code[0]!=0)

        {

            ch1=0;

            for(j=0;j<8;j++)

            {

                if(huffmannode[i].code[j]=='1')

                    c=(c<<1)|1;

                else

                    c=c<<1;

            }

            strcpy(huffmannode[i].code,huffmannode[i].code+8);

            count++;

            fwrite(&c,1,1,ofp);

                     //把哈弗曼结点所带的字符的编码所对应的字符写入目标文件

        }

    }

(三)字典码实现信源无失真压缩

编程思想:LZ77算法的主要思路是构造一个三元组(off, len, c),其中off表示窗口中匹配的字符串相对于窗口右边界的位移,len表示可以进行匹配的字符串的长度,c表示需要下一个输出的字符。对一串已经储存在文本中的字符串进行LZ77编码,将三元组输出到一个编码文件中,最后只要我们向压缩时那样维护好滑动的窗口,随着三元组的不断输入,我们在窗口中找到相应的匹配串,缀上后继字符 c 输出(如果 off和 len 都为 0 则只输出后继字符c即可还原出原始数据,同样的,将解码后的字符串也输出到一个文本中。

//字符串的匹配

int match(char *s,intsn,char *t,int tn)

{

    int i=0,j=0,k;

    while(i<sn&&j<tn)

    {

        if(s[i]==t[j])

        {

            i++;

            j++;

        }

        else

        {

            i=i-j+1;

            j=0;

        }

    }

    if(j>=tn)

        k=i-tn;

    else k=-1;              //k是可以匹配的字符串的位置

    return k;

}

 

void code(char *array,intcount1,CODE *code_file,int *pcount2)

{

    int window_start=0,window_end=0;

    *pcount2=0;

    if(count1>0)

    {

        code_file[0].off=0;

        code_file[0].len=0;

        code_file[0].c=array[0];

        (*pcount2)++;        //对字符串第一个字符进行编码输出

    }

    int i=1,j=1,k1=0,k2=0;

    for(;i<count1;i++)

    {

        if(i<window_length)

            window_end=i;      //更新窗口右边界

        else window_end=window_length;

        for(j=1,k1=0,k2=0;;j++)

        {

            k1=k2;

            k2=match(array+i-window_end,window_end,array+i,j);     //匹配结果返回

            if(k2<0)  //没有匹配的

            {

               code_file[*pcount2].off=window_end-k1;

                            //存在可以编码的情况的话,off值为当前字符相对于右窗口的位移

               if((code_file[*pcount2].len=j-1)==0)

                            //在出现新字符的情况下,将off置为0

                    code_file[*pcount2].off=0;

               code_file[*pcount2].c=array[i+j-1];

                            //输出需要输出的下一位不能匹配的字符

                (*pcount2)++;//指针移动

                i=i+j-1;//更新i

                break;

            }

        }

    }

}

 

void decode(CODE*code_file,int count2,char *array,int *pcount3)

{

    *pcount3=0;

    int i=0,j=0;

    for(;i<count2;++i)

    {

        if(code_file[i].len==0)

        {

            array[*pcount3]=code_file[i].c;

            ++(*pcount3);

        }

        else

        {

            for(j=0;j<code_file[i].len;++j)

            {

               array[*pcount3+j]=array[*pcount3-code_file[i].off+j];

            }

            array[*pcount3+j]=code_file[i].c;

            *pcount3=*pcount3+code_file[i].len+1;

        }

    }

}

四、实验结果分析与思考

(一)Huffman编码实现信源无失真压缩

实验结果如下:

图1 Huffman编码的python实现(test1.txt)

图2 Huffman编码的python实现(test2.txt)

由实验结果可知,Huffman编码的信源熵接近于平均码长,对于只有英文字符的test1.txt文件的压缩效果要比既包含中文字符又包含英文字符的test2.txt文件好,英文字符较少,Huffman编码后的平均码长也较短。从运行效果上来看,压缩的比例不是非常满意,压缩率很低,对于test1.txt文件的压缩节省了约43%的存储空间,压缩比达到了1.75倍;但是对于中文字符和英文字符共存的test2.txt文件,只节省了5%的存储空间,压缩比也很低。Huffman编码对于中文的压缩是将一个中文字符分化为两个ASCII编码,于是本质上就将中文压缩转化为单字节的压缩算法,算法从物理上割裂了中文编码蕴含的语义信息,压缩效率上有一定的降低。但是这种将汉字重新形成基于“中文字符”的Huffman压缩算法利于实现,在解压缩的完整性上也有一定的优点,还是具有一定的可观性。

在此次压缩算法的实现过程中,遇到过不少难题。由于Huffman编码在压缩文件中存储的编码信息为各个字符的频率信息和编码信息,在解码的时候对于相同频率的字符出现时,如果没有按照与压缩时按照相同的顺序排列,会在译码时出现一定的错误;发现其中一个字符与文件结尾表示EOF有相同的编码,导致解码过程不稳定,出现一些小问题。如下图所示,如果在解码过程中不加以保证相同频率字符的排序时,相同频率的字符可能会在解码出现解码错误。

图3 解压缩未正确排序结果

 

4 错误原因

4 原则上,该程序可以对任意格式的文件进行压缩,如jpg图像,如下图所示:

图5图像压缩与解压缩效果

压缩效果却并不理想,如下图所示:

图6图像压缩与解压缩性能

思考:应该可以采用动态哈夫曼树的编码来对大文件的压缩进行优化,从而达到更优的压缩率。确定一个初始哈夫曼树,然后根据当前处理过的字符动态的调整哈夫曼树的结构,解压过程同样根据解压过的字符调整哈夫曼树结构。压缩与解压的时间效率有待提高,本程序在压缩及解压过程中对硬盘进行了大量的读写,使速度受到影响,可以通过缓冲带还实现提速,即构造内存缓冲区,每次读写多个字符,既而进行处理。

(二)游程编码与Huffman编码结合实现信源无失真压缩

实验结果如下:

图7游程编码+ Huffman编码的无失真信源压缩编码实现(test1.txt)

图8游程编码+ Huffman编码的无失真信源压缩编码实现(test2.txt)

经过游程编码后再进行Huffman编码的压缩效果比只进行Huffman编码的效果略好,因为文件中还是有很多字符的出现频率为1,2的字符,他们的码长可以达到13位,连续的‘0’字符和‘1’字符长度可以达到4或5,经过游程编码后降为3位,有所降低但压缩效果也没有特别明显,只比单纯的Huffman编码略好一点。

(三)字典码实现信源无失真压缩

实验结果如下:

图9基于字典码的无失真信源压缩编码(test1.txt)

图10基于字典码的无失真信源压缩编码(test2.txt)

由实验结果可知,基于字典码的无失真信源压缩编码比Huffman编码和游程编码+Huffman编码的效果要好很多,压缩性能提升了一倍。字典码不依赖于信源的概率分布,也不必知道有关信源的统计特性,编码方法简单,又能达到很高的压缩编码效率,python的zlib函数库就是使用的LZ77的一种变种,性能也非常好。

图11 python中zlib函数对文本压缩效果

下表对实验中用到的Huffman编码、游程编码+Huffman编码、字典码和zlib编码性能进行了对比,可以发现字典码的效果最好,但不管哪种方法,中文文本的压缩效果都不如英文文本的压缩效果好,主要原因是一个中文字符相当于2个ASCII编码,于是本质上就将中文压缩转化为单字节的压缩算法,算法从物理上割裂了中文编码蕴含的语义信息,压缩效率上有一定的降低。如果想要得到较好的中文文本压缩效果,需要利用好中文文本中数据编码和汉字大字符的特点,以提高中文文本中重复字符串不长导致的压缩比远低于英文文本的缺点。

表1 无失真信源压缩编码性能对比

 

test1文件压缩率

test2文件压缩率

Huffman编码

1.759605

1.054758

游程编码+Huffman编码

1.78

1.11

字典码

2.90833

2.0583

zlib编码

2.486436

1.586691

图12无失真信源压缩编码性能对比

查阅文献,发现可以从以下几个方面改进字典码对于中文文本的压缩效果:

1、直接以汉字字符进行读取,而不对读取的编码进行重编码,不破坏汉字本来所蕴含的语义信息。

2、动态自适应编码输出长度,使编码输出长度岁字典长度增大而增大。字典越大,字典中的长字符串会越多,字典对于文本的自适应性增强,压缩效果应该会提高。

3、重建字典,程序开始压缩时记录输入字节数和输出字节数,程序每压缩一定数量的字节,统计一次压缩率,如果新的压缩率比旧的压缩率大则重建字典,否则,将新的压缩率赋值给旧的压缩率。

另外,斯坦福大学的研究者在刚提交的论文中提出,循环神经网络捕捉长期以来关系的优势可以被用于文件的无损压缩任务中。J. Rissanen 提出了算术编码,这是一个实现已知分布熵边界的有效方法。对于未知分布的数据源(如文本和 DNA),他还设计了算术编码的自适应变体,它可以通过尝试学习条件 k-gram 模型的分布来进行压缩。尽管这种过程的复杂度会随 k 的变化而呈指数级增长,通常上下文会被限制在 k=20 符号。这会导致压缩比例的显著损失,因为模型无法捕捉长期依赖关系。

我们都知道基于循环神经网络的模型善于捕捉长期依赖关系,同时可以较准确地预测下一个字母/单词,在这一研究的论文中,研究人员首先分析和理解了已知熵情况下,合成数据集上 RNN 和算术编码方法的表现,其目的是对各种 RNN 结构的能力和极限进行直观的理解。然后设计了压缩器 DeepZip,包含两个主要模块:基于 RNN 的概率评估器和算术编码模块,具体的实现参见https://web.stanford.edu/class/cs224n/reports/2761006.pdf一文。基于对此前在合成数据集上测试的经验,研究人员使用了文本压缩模型和基因组数据集,包含 128 个单元的 DeepZip 模型压缩效果很好。

图 7 包含 128 个单元的 DeepZip 模型与 GZIP、适应性算术编码-CABAC 的表现对比

图 8 包含 128 个单元的 DeepZip 模型在实际数据集上的表现

五、实验小结

对实验中用到的Huffman编码、游程编码+Huffman编码、字典码和zlib编码性能进行了对比,可以发现字典码的效果最好,但不管哪种方法,中文文本的压缩效果都不如英文文本的压缩效果好,主要原因是一个中文字符相当于2个ASCII编码,于是本质上就将中文压缩转化为单字节的压缩算法,算法从物理上割裂了中文编码蕴含的语义信息,压缩效率上有一定的降低。如果想要得到较好的中文文本压缩效果,需要利用好中文文本中数据编码和汉字大字符的特点,以提高中文文本中重复字符串不长导致的压缩比远低于英文文本的缺点,并给出了几点改进意见。


  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值