信息论与编码实验二 灰度图像的二元Fano编码和译码

一、问题描述和基本要求

对一幅BMP格式的灰度图像进行二元Fano编码、译码。

二、算法思想

1949年,费诺(R.M. Fano)提出了一种编码方法,称之为费诺码或Fano码。它属于概率匹配编码,但一般也不是最佳的编码方法,只有当信源的概率分布呈现分布形式P(a_i)=s^{-l_i} (i=1,2,…,r)的条件下,才能达到最佳码的性能。r元费诺码的编码步骤如下:
① 将q个信源符号按概率分布P(s_i)的大小,以递减的次序排列起来,设


②将排列好的信源符号按概率值划分成r大组,使每组的概率之和接近于相等,并对每组各赋予一个r元码符号0,1,…,r-1
③ 将每一大组的信源符号再分成r组,使划分后的两个组的概率之和接近于相等,再分别赋予一个r元码符号0,1,…,r-1
④ 依次下去,直至每个小组只剩一个信源符号为止。
⑤ 将逐次分组过程中得到的码元排列起来就是各信源符号的编码。
费诺码的编码方法实际上是一种构造码树的方法,所以费诺码是即时码,并且它考虑了信源的统计特性,使概率大的信源符号能对应码长较短的码字,从而有效地提高了编码效率。
对于本课设的费诺编码,首先扫描第一遍输入符号流,统计各个符号出现的概率,之后按照上述算法构造费诺编码,再对输入符号流进行第二遍扫描进行编码。
根据费诺编码的原理,将满足上述条件的概率序列p_i p_{i+1}…p_j,断开为两个子序列p_i p_{i+1}…p_kp_k p_{k+1}…p_j,然后对这两个子序列对应的各事件分别进行0和1编码,递归地对子序列重复该过程,直到每个子序列的长度都为1,就可以得到每个事件的编码。这显然就是分治法的思想,因此可以用分治法来解决费诺编码问题。由上式可知,费诺编码的关键问题就是找到分界点k,使得k两侧的概率序列的和相差最小。
为求得分界点k,一个很自然的想法就是先找到序列概率和的一半对应的两侧位置k_1k_2,然后观测这两个分界点哪个使得分界点两侧的概率和相差最小。
设计以下递归结构F(start, end, possibility, string), 其中start为开始位置,end为结束位置,possibility仅为计算方便,string为当前递归结构对应的码字,那么F的形式化描述为:

F(start,end,possibility,string):
    if start == end:
        p_start对应的信源符号s_start的码字为string
        return
    找到符合上述条件的k_1和k_2,分别计算偏差值d_1和d_2,选择偏差值最小的k
    F(start,k,sum(p_start,p_{start+1},…,p_k ),string+"0")
        F(k+1,end,sum(p_{k+1},p_{k+2},…,p_end ),string+"1")

由上述递归结构F即可求得每个信源符号的码字,从而可以对文件进行编码和解码。

三、源代码

#!/usr/bin/env python3
import os
import pickle
from math import log
# python3 -m pip install matplotlib
import matplotlib.pyplot as plt
# python3 -m pip install numpy
import numpy as np
# python3 -m pip install opencv-python
from cv2 import IMREAD_GRAYSCALE, absdiff, imread, imshow, imwrite
# python3 -m pip install prettytable
from prettytable import PrettyTable

class WriterWrapper:
    def __init__(self, f):
        self.buf = ""
        self.f = f

    def __call__(self, con: str):
        self.buf += con
        while len(self.buf) >= 8:
            out = self.buf[0:8]
            self.buf = self.buf[8:]
            conint = int(out, 2)
            self.f.write(bytes([conint]))

    def flush(self):
        if len(self.buf) > 0:
            self.buf += "0"*(7 - ((len(self.buf) - 1) % 8))
        while len(self.buf) > 0:
            out = self.buf[0:8]
            self.buf = self.buf[8:]
            conint = int(out, 2)
            self.f.write(bytes([conint]))

class ReaderWrapper:
    def __init__(self, f):
        self.buf = ""
        self.f = f

    def next(self):
        if self.buf != "":
            c = self.buf[0]
            self.buf = self.buf[1:]
            return c
        c = self.f.read(1)
        if c == b"":
            return ""
        bstr = bin(ord(c))[2:]
        bstr = "0"*(7 - ((len(bstr) - 1) % 8)) + bstr
        self.buf = bstr
        return self.next()

    def read(self, c : int):
        s = ""
        for i in c:
            s += self.next()
        return s

class Symbol:
    def __init__(self, symbol, weight, pos):
        self.word = ""
        self.pos = pos
        self.weight = weight
        self.symbol = symbol
    
    def __lt__(self, value):
        return self.pos < value.pos

class FanoNode:
    def __init__(self, left = None, right = None):
        self.symbol = None
        self.left = left
        self.right = right

def Fano(start, end, possibility, string, syms) -> FanoNode:
    leaf = FanoNode()
    if start == end - 1:
        leaf.symbol = syms[start]
        syms[start].word = string
        return leaf
    mid = possibility / 2.0
    target = 0.0
    index = start
    while target < mid:
        target += syms[index].pos
        index += 1
    subseq1left = syms[start:index-1]
    subseq1right = syms[index-1:end]
    pos1left = sum(map(lambda x: x.pos, subseq1left))
    pos1right = sum(map(lambda x: x.pos, subseq1right))
    delta1 = abs(pos1left-pos1right)
    subseq2left = syms[start:index]
    subseq2right = syms[index:end]
    pos2left = sum(map(lambda x: x.pos, subseq2left))
    pos2right = sum(map(lambda x: x.pos, subseq2right))
    delta2 = abs(pos2left-pos2right)
    if delta1 < delta2:
        leaf.left = Fano(start, index-1, pos1left, string + "0", syms)
        leaf.right = Fano(index-1, end, pos1right, string + "1", syms)
    else:
        leaf.left = Fano(start, index, pos2left, string + "0", syms)
        leaf.right = Fano(index, end, pos2right, string + "1", syms)
    return leaf

if __name__ == "__main__":
    print("请将要压缩的图像文件命名为origin.bmp。")
    print("读入源图像文件...", end="")
    originimg = imread("origin.bmp", IMREAD_GRAYSCALE)
    originfilesize = os.path.getsize("origin.bmp")
    print("成功")
    print("分析原图像文件...", end="")
    originshape = originimg.shape
    originsize = originimg.size
    flatten = originimg.flatten()
    counter = dict.fromkeys(set(flatten), 0)
    table = PrettyTable()
    table.field_names = ["信源符号", "数量", "概率"]
    SymbolStringMap = {}
    syms = []
    for item in flatten:
        counter[item] += 1
    for item in counter:
        syms.append(Symbol(item, counter[item], counter[item] / originsize))
        table.add_row([item, counter[item], counter[item] / originsize])
    syms.sort(reverse=True)
    Hs = sum(map(lambda weight: -weight / originsize *
                 log(weight / originsize, 2), counter.values()))
    table.sortby = "数量"
    table.reversesort = True
    print("完成")
    print("原图像分辨率为:{1}*{0},作为信源,共有{2}个、{3}种信源符号,熵为{4:.2f}, "
          "信源概率表如下:".format(*originshape, originsize, len(counter), Hs))
    print(table)
    print("正在进行费诺编码...")
    Fano(0, len(syms), 1.0, "", syms)
    table = PrettyTable()
    table.field_names = ["信源符号", "码字", "码字长度", "数量", "概率"]
    Len = 0
    for sym in syms:
        table.add_row([sym.symbol, sym.word, len(sym.word),
                       sym.weight, sym.pos])
        Len += sym.weight*len(sym.word)
        SymbolStringMap[sym.symbol] = sym.word
    print("编码完成,平均码长为{:.2f},编码表如下:".format(Len/originsize))
    table.sortby = "数量"
    print(table)
    print("信源的熵为{:.2f},平均码长为{:.2f},编码效率为{:.2f}%".format(Hs, Len/originsize, Hs/Len*originsize*100))
    print("将编码后的文件写入到新文件target.bin中。编码采用费诺编码,先写入图像尺寸和文件长度,再写入编码表,最后写入编码后图像矩阵")
    f = open("target.bin", "wb")
    # 写入文件分辨率和长度
    f.write(pickle.dumps(originshape))
    f.write(pickle.dumps(Len))
    # 写入编码表
    f.write(pickle.dumps(counter))
    # 写入图像矩阵
    wt = WriterWrapper(f)
    for item in flatten:
        wt(SymbolStringMap[item])
    wt.flush()
    f.close()
    TarLen = os.path.getsize("target.bin")
    print("新文件target.bin的大小为{}字节,源文件大小为{}字节,压缩比为{:.2f}%".format(
        TarLen, originfilesize, TarLen/originfilesize*100))
    print("现在开始从新文件target.bin中恢复图像信息...")
    f = open("target.bin", "rb")
    # 读取图像分辨率
    originshape = pickle.load(f)
    # 读取长度
    Len = pickle.load(f)
    # 读取编码表
    counter = pickle.load(f)
    print("正在进行费诺译码...")
    syms = []
    originsize = originshape[0]*originshape[1]
    for item in counter:
        syms.append(Symbol(item, counter[item], counter[item] / originsize))
    syms.sort(reverse=True)
    p = root = Fano(0, len(syms), 1.0, "", syms)
    cur = 0
    rd = ReaderWrapper(f)
    imgflatten = []
    while (cur != Len):
        c = rd.next()
        cur += 1
        if c == "0":
            p = p.left
        elif c == "1":
            p = p.right
        else:
            raise AssertionError("c must be 0 or 1")
            break
        if (p.left == None) and (p.right == None):
            imgflatten.append(p.symbol.symbol)
            p = root
    imgarr = np.array(imgflatten, dtype=np.uint8)
    imgarr.resize(originshape)
    imwrite("target.bmp", imgarr)
    dif = absdiff(originimg, imgarr)
    plt.rcParams['font.sans-serif'] = ['SimHei']
    plt.rcParams['axes.unicode_minus'] = False
    fig = plt.figure('result')
    plt.subplot(1, 3, 1)
    plt.title('原图像')
    plt.imshow(originimg, plt.cm.gray)
    plt.axis('off')
    plt.subplot(1, 3, 2)
    plt.title('编解码后的图像')
    plt.imshow(imgarr, plt.cm.gray)
    plt.axis('off')
    plt.subplot(1, 3, 3)
    plt.title('差异')
    plt.imshow(dif, plt.cm.gray)
    plt.subplots_adjust(wspace=0.15)
    plt.axis('off')
    plt.show()
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,172评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,346评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,788评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,299评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,409评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,467评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,476评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,262评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,699评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,994评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,167评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,499评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,149评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,387评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,028评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,055评论 2 352

推荐阅读更多精彩内容