信息論與編碼實(shí)驗(yàn)一 灰度圖像的二元霍夫曼編碼和譯碼

一心傀、問題描述和基本要求

對一幅BMP格式的灰度圖像(如個(gè)人證件照片)進(jìn)行二元霍夫曼編碼和譯碼。

二、算法思想

霍夫曼在1952年提出了一中構(gòu)造最佳碼的方法魂务,這種編碼方法被稱為霍夫曼編碼∶谏洌霍夫曼編碼適用于多元獨(dú)立信源粘姜,對于多元獨(dú)立信源來說他是最佳碼。它充分利用了信源概率分布的特性進(jìn)行編碼熔酷,完全依據(jù)字符出現(xiàn)概率來構(gòu)造異字頭的平均長度最短的碼字孤紧,因此它是一種最佳的逐個(gè)符號的編碼方法。
r 元霍夫曼碼的編碼步驟如下:
① 將q個(gè)信源符號按概率分布 P(s_i) 的大小拒秘,以遞減的次序排列起來号显,設(shè)


② 為了使最后一步信源的縮減信源有r個(gè)信源符號,需要對信源S添加t個(gè)概率為0的信源符號躺酒,使擴(kuò)展后的信源符號個(gè)數(shù)q+t滿足

其中押蚤,θ表示縮減的次數(shù),r-1表示每次縮減所減少的信源符號的個(gè)數(shù)羹应,r代表編碼元數(shù)揽碘。
③ 用0,1,…,r-1r個(gè)碼符號分別分配給概率最小的r個(gè)信源符號,并將這兩個(gè)概率最小的信源符號合并成一個(gè)新的符號园匹,并用這r個(gè)最小概率之和作為新符號的概率雳刺,從而得到只包含q-1個(gè)符號的縮減信源S_1
④ 以此繼續(xù)下去,直到縮減信源最后只剩r個(gè)符號為止裸违。最后這r個(gè)符號的概率之和為1掖桦。然后從最后一級縮減信源開始,依編碼路徑由后向前返回供汛,就得出各個(gè)信源符號所對應(yīng)的碼符號序列滞详,即所對應(yīng)的碼字。
霍夫曼編碼過程中生成的碼樹是霍夫曼樹紊馏。假如采用二元編碼料饥,那么他的碼樹二元霍夫曼樹。在數(shù)據(jù)結(jié)構(gòu)中朱监,二元霍夫曼樹也稱為最優(yōu)二元樹岸啡。若對他的葉全部賦權(quán)值,那么權(quán)值乘上葉子的高度的和最小赫编,這也就是二元霍夫曼編碼的平均碼長巡蘸。
進(jìn)行霍夫曼編碼奋隶,有靜態(tài)霍夫曼編碼和自適應(yīng)霍夫曼編碼(常用的算法有Faller-Gallagher-Knuth算法,簡稱FGK算法悦荒,后來針對FGK算法存在的問題唯欣,Vitter進(jìn)行了改進(jìn),稱為Vitter算法)搬味。本實(shí)驗(yàn)采用靜態(tài)霍夫曼編碼境氢。
靜態(tài)霍夫曼編碼,首先掃描第一遍輸入符號流碰纬,統(tǒng)計(jì)各個(gè)符號出現(xiàn)的概率萍聊,之后按照上述算法構(gòu)造二元最優(yōu)樹,再對輸入符號流進(jìn)行第二遍掃描進(jìn)行編碼悦析。
二元的霍夫曼編碼的過程和構(gòu)造一棵最優(yōu)二元樹的過程是等價(jià)的寿桨。一棵最優(yōu)二元樹,若從他的根開始進(jìn)行遍歷操作强戴,左結(jié)點(diǎn)遍歷字符串插入0亭螟,右結(jié)點(diǎn)遍歷字符串插入1,那么最后葉結(jié)點(diǎn)的得到的遍歷字符串即為碼字骑歹。
構(gòu)造一棵二元最優(yōu)樹的算法可以描述為:
① 令S={ w_1,w_2,…,w_t }预烙;
② 從S中取出兩個(gè)最小的權(quán)w_iw_j,畫結(jié)點(diǎn)v_i陵刹,帶權(quán)w_i默伍,畫結(jié)點(diǎn)v_j欢嘿,帶權(quán)w_j衰琐。畫v_iv_j的父親v,連接v_iv炼蹦,v_jv羡宙,令v帶權(quán)w_i+w_j
③ 令S = (S - { w_i,w_j })∪{ w_i+w_j }掐隐;
④ 判斷S是否只有一個(gè)元素狗热,若是,則停止虑省,否則重復(fù)② ③ ④匿刮。
帶有上述S中唯一權(quán)重的結(jié)點(diǎn)v就是最優(yōu)二元樹的根結(jié)點(diǎn)。為了方便取出權(quán)重最小的結(jié)點(diǎn)探颈,我們可以使用優(yōu)先級隊(duì)列(或者堆)來實(shí)現(xiàn)熟丸。那么,構(gòu)造一棵最優(yōu)二元樹的算法可以更加形式化的描述為:

初始化優(yōu)先級隊(duì)列PQ
把所有帶權(quán)重的結(jié)點(diǎn)放入PQ中
while PQ的大小 > 1:
    取出PQ的頂端結(jié)點(diǎn)v_1伪节,帶權(quán)重w_1
    取出PQ的頂端結(jié)點(diǎn)v_2光羞,帶權(quán)重w_2
    構(gòu)造新結(jié)點(diǎn)Father绩鸣,使之左孩子為Node1,右孩子為Node2纱兑,權(quán)重為w_1+w_2
    將Father放入到PQ中

帶有PQ的唯一權(quán)重的對應(yīng)節(jié)點(diǎn)是構(gòu)造好最優(yōu)二元樹的根結(jié)點(diǎn)呀闻。
之后,再對構(gòu)造好的最優(yōu)二元樹進(jìn)行遍歷即可得到碼字潜慎。
靜態(tài)霍夫曼編碼容易實(shí)現(xiàn)捡多,但要提前統(tǒng)計(jì)被編碼對象中的符號出現(xiàn)概率,因此必須對輸入符號流進(jìn)行兩遍掃描勘纯,這也限制了靜態(tài)霍夫曼編碼的應(yīng)用局服。

三、源代碼

#!/usr/bin/env python3
import os
import pickle
from math import log
from queue import PriorityQueue, Queue
# 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, imread, imshow, imwrite, absdiff
# python3 -m pip install prettytable
from prettytable import PrettyTable

class StaticHuffNode:
    def __init__(self, s: int, weight: int, lchild=None, rchild=None, parent=None):
        self.s = s              # 信源符號
        self.weight = weight    # 權(quán)重
        self.parent = parent    # 父親
        self.lchild = lchild    # 左子樹
        self.rchild = rchild    # 右子樹
        self.word = ""          # 碼字

    def __lt__(self, other):
        return self.weight < other.weight

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

def traverse(root, s: str, NodeStringMap: dict):
    if (root.lchild == None) and (root.rchild == None):
        root.word = s
        NodeStringMap[root.s] = root.word
        return
    traverse(root.lchild, s + "0", NodeStringMap)
    traverse(root.rchild, s + "1", NodeStringMap)

def StaticHuffmanEncodingAndDecoding(counter, originsize, originshape, originfilesize):
    PQ = PriorityQueue()
    table = PrettyTable()
    table.field_names = ["信源符號", "數(shù)量", "概率"]
    for item in flatten:
        counter[item] += 1
    for item in counter:
        table.add_row([item, counter[item], counter[item] / originsize])
        PQ.put(StaticHuffNode(item, counter[item]))
    Hs = sum(map(lambda weight: -weight / originsize *
                 log(weight / originsize, 2), counter.values()))
    table.sortby = "數(shù)量"
    table.reversesort = True
    print("完成")
    print("原圖像分辨率為:{1}*{0}驳遵,作為信源淫奔,共有{2}個(gè)、{3}種信源符號堤结,熵為{4:.2f}, "
          "信源概率表如下:".format(*originshape, originsize, len(counter), Hs))
    print(table)
    print("正在進(jìn)行靜態(tài)霍夫曼編碼...")
    # 采用優(yōu)先級隊(duì)列唆迁,選擇權(quán)重最小的節(jié)點(diǎn),合并后放入隊(duì)列中繼續(xù)操作竞穷,直到隊(duì)列中只剩一個(gè)節(jié)點(diǎn)
    while PQ.qsize() > 1:
        left = PQ.get()
        right = PQ.get()
        PQ.put(StaticHuffNode(0, left.weight + right.weight, left, right))
    NodeStringMap = {}
    # 遍歷霍夫曼樹唐责,獲取每個(gè)信源符號對應(yīng)的碼字
    root = PQ.get()
    traverse(root, "", NodeStringMap)
    table = PrettyTable()
    table.field_names = ["信源符號", "碼字", "碼字長度", "數(shù)量", "概率"]
    Len = 0
    for item in counter:
        table.add_row([item, NodeStringMap[item], len(NodeStringMap[item]),
                       counter[item], counter[item] / originsize])
        Len += counter[item]*len(NodeStringMap[item])
    AveLen = Len / originsize
    print("編碼完成,編碼表如下:")
    table.sortby = "數(shù)量"
    print(table)
    print("信源的熵為{:.2f}瘾带,平均碼長為{:.2f}鼠哥,編碼效率為{:.2f}%".format(
        Hs, AveLen, Hs/AveLen*100))
    f = open("statictarget.bin", "wb")
    print("將編碼后的文件寫入到新文件statictarget.bin中。編碼采用靜態(tài)霍夫曼編碼看政,先寫入圖像尺寸和文件長度朴恳,再寫入編碼表,最后寫入編碼后圖像矩陣")
    # 寫入文件分辨率和長度
    f.write(pickle.dumps(originshape))
    f.write(pickle.dumps(Len))
    # 寫入編碼表
    f.write(pickle.dumps(counter))
    # 寫入圖像矩陣
    wt = WriterWrapper(f)
    for item in flatten:
        wt(NodeStringMap[item])
    wt.flush()
    f.close()
    TarLen = os.path.getsize("statictarget.bin")
    print("新文件statictarget.bin的大小為{}字節(jié)允蚣,源文件大小為{}字節(jié)于颖,壓縮比為{:.2f}%".format(
        TarLen, originfilesize, TarLen/originfilesize*100))
    print("現(xiàn)在開始從新文件statictarget.bin中恢復(fù)圖像信息...")
    f = open("statictarget.bin", "rb")
    # 讀取圖像分辨率
    originshape = pickle.load(f)
    # 讀取長度
    Len = pickle.load(f)
    # 讀取編碼表
    counter = pickle.load(f)
    print("正在進(jìn)行霍夫曼譯碼...")
    PQ = PriorityQueue()
    for item in counter:
        PQ.put(StaticHuffNode(item, counter[item]))
    while PQ.qsize() > 1:
        left = PQ.get()
        right = PQ.get()
        PQ.put(StaticHuffNode(0, left.weight + right.weight, left, right))
    p = root = PQ.get()
    cur = 0
    rd = ReaderWrapper(f)
    imgflatten = []
    while (cur != Len):
        c = rd.next()
        cur += 1
        if c == "0":
            p = p.lchild
        elif c == "1":
            p = p.rchild
        else:
            raise AssertionError("c must be 0 or 1")
            break
        if (p.lchild == None) and (p.rchild == None):
            imgflatten.append(p.s)
            p = root
    imgarr = np.array(imgflatten, dtype=np.uint8)
    imgarr.resize(originshape)
    imwrite("statictarget.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()
    return Len, Hs

if __name__ == "__main__":
    print("請將要壓縮的圖像文件命名為origin.bmp,請保證該圖像為灰度圖像嚷兔。")
    print("讀入源圖像文件...", end="")
    originimg = imread("origin.bmp", IMREAD_GRAYSCALE)
    originfilesize = os.path.getsize("origin.bmp")
    print(f"成功森渐,源文件大小為{originfilesize}字節(jié)")
    print("分析原圖像文件...", end="")
    originshape = originimg.shape
    originsize = originimg.size
    flatten = originimg.flatten()
    counter = dict.fromkeys(set(flatten), 0)
    Len, Hs = StaticHuffmanEncodingAndDecoding(
        counter, originsize, originshape, originfilesize)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市冒晰,隨后出現(xiàn)的幾起案子同衣,更是在濱河造成了極大的恐慌,老刑警劉巖壶运,帶你破解...
    沈念sama閱讀 221,430評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件耐齐,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)蚪缀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評論 3 398
  • 文/潘曉璐 我一進(jìn)店門秫逝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人询枚,你說我怎么就攤上這事违帆。” “怎么了金蜀?”我有些...
    開封第一講書人閱讀 167,834評論 0 360
  • 文/不壞的土叔 我叫張陵刷后,是天一觀的道長。 經(jīng)常有香客問我渊抄,道長尝胆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,543評論 1 296
  • 正文 為了忘掉前任护桦,我火速辦了婚禮含衔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘二庵。我一直安慰自己贪染,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,547評論 6 397
  • 文/花漫 我一把揭開白布催享。 她就那樣靜靜地躺著杭隙,像睡著了一般。 火紅的嫁衣襯著肌膚如雪因妙。 梳的紋絲不亂的頭發(fā)上痰憎,一...
    開封第一講書人閱讀 52,196評論 1 308
  • 那天,我揣著相機(jī)與錄音攀涵,去河邊找鬼铣耘。 笑死,一個(gè)胖子當(dāng)著我的面吹牛汁果,可吹牛的內(nèi)容都是我干的涡拘。 我是一名探鬼主播玲躯,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼据德,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了跷车?” 一聲冷哼從身側(cè)響起棘利,我...
    開封第一講書人閱讀 39,671評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎朽缴,沒想到半個(gè)月后善玫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,221評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡密强,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,303評論 3 340
  • 正文 我和宋清朗相戀三年茅郎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蜗元。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,444評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡系冗,死狀恐怖奕扣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情掌敬,我是刑警寧澤惯豆,帶...
    沈念sama閱讀 36,134評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站奔害,受9級特大地震影響楷兽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜华临,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,810評論 3 333
  • 文/蒙蒙 一芯杀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧雅潭,春花似錦瘪匿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至诚欠,卻和暖如春顽染,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背轰绵。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評論 1 272
  • 我被黑心中介騙來泰國打工粉寞, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人左腔。 一個(gè)月前我還...
    沈念sama閱讀 48,837評論 3 376
  • 正文 我出身青樓唧垦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親液样。 傳聞我的和親對象是個(gè)殘疾皇子振亮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,455評論 2 359

推薦閱讀更多精彩內(nèi)容