1. 程式人生 > 其它 >壓縮演算法之LZSS---lzss 演算法實現

壓縮演算法之LZSS---lzss 演算法實現

一、前言

本文是基於我的上一篇部落格《無失真壓縮演算法專題——無失真壓縮演算法介紹》的基礎上來實現的,部落格連結https://blog.csdn.net/qq_34254642/article/details/103651815,這一篇當中實現基本的LZSS演算法功能,先不做改進,所以演算法效率比較低,但是便於理解。寫了Python和C兩個版本,兩種語言的程式碼結構是一樣的,程式碼中都有詳盡的註釋。實現了對任意檔案的壓縮和解壓功能。

 

二、LZSS演算法實現

Python實現

import ctypes
import os

class LZSS():
    def __init__(self, preBufSizeBits):
        self.threshold = 2  #長度大於等於2的匹配串才有必要壓縮
        self.preBufSizeBits = preBufSizeBits  #前向緩衝區佔用的位元位
        self.windowBufSizeBits = 16 - self.preBufSizeBits   #滑動窗口占用的位元位

        self.preBufSize = (1 << self.preBufSizeBits) - 1 + self.threshold #通過佔用的位元位計算緩衝區大小
        self.windowBufSize = (1 << self.windowBufSizeBits) - 1 + self.threshold   #通過佔用的位元位計算滑動視窗大小

        self.preBuf = b''   #前向緩衝區
        self.windowBuf = b''    #滑動視窗
        self.matchString = b''  #匹配串
        self.matchIndex = 0     #滑動視窗匹配串起始下標

    #檔案壓縮
    def LZSS_encode(self, readfilename, writefilename):

        fread = open(readfilename, "rb")
        fwrite = open(writefilename, "wb")
        restorebuff = b''   #待寫入的資料快取區,滿一組資料寫入一次檔案
        itemnum = 0     #8個專案為一組,用來統計當前專案數
        signbits = 0    #標記位元組

        self.preBuf = fread.read(self.preBufSize)   #讀取資料填滿前向緩衝區

        # 前向緩衝區沒資料可操作了即為壓縮結束
        while self.preBuf != b'':
            self.matchString = b''
            self.matchIndex = -1
            #在滑動視窗中尋找最長的匹配串
            for i in range(self.threshold, len(self.preBuf) + 1):
                index = self.windowBuf.find(self.preBuf[0:i])
                if index != -1:
                    self.matchString = self.preBuf[0:i]
                    self.matchIndex = index
                else:
                    break
            #如果沒找到匹配串或者匹配長度為1,直接輸出原始資料
            if self.matchIndex == -1:
                self.matchString = self.preBuf[0:1]
                restorebuff += self.matchString
            else:
                restorebuff += bytes(ctypes.c_uint16(self.matchIndex * (1 << self.preBufSizeBits) + len(self.matchString) - self.threshold))
                signbits += (1 << (7 - itemnum))
            #操作完一個專案+1
            itemnum += 1
            #專案數達到8了,說明做完了一組壓縮,將這一組資料寫入檔案
            if itemnum >= 8:
                writebytes = bytes(ctypes.c_uint8(signbits)) + restorebuff
                fwrite.write(writebytes);
                itemnum = 0
                signbits = 0
                restorebuff = b''

            self.preBuf = self.preBuf[len(self.matchString):]  #將剛剛匹配過的資料移出前向緩衝區
            self.windowBuf += self.matchString  #將剛剛匹配過的資料加入滑動視窗
            if len(self.windowBuf) > self.windowBufSize:  #將多出的資料從前面開始移出滑動視窗
                self.windowBuf = self.windowBuf[(len(self.windowBuf) - self.windowBufSize):]

            self.preBuf += fread.read(self.preBufSize - len(self.preBuf))  #讀取資料補充前向緩衝區

        if restorebuff != b'':  #檔案最後可能不滿一組資料量,直接寫到檔案裡
            writebytes = bytes(ctypes.c_uint8(signbits)) + restorebuff
            fwrite.write(writebytes);

        fread.close()
        fwrite.close()

        return os.path.getsize(writefilename)

    #檔案解壓
    def LZSS_decode(self, readfilename, writefilename):
        fread = open(readfilename, "rb")
        fwrite = open(writefilename, "wb")

        self.windowBuf = b''
        self.preBuf = fread.read(1)  #先讀一個標記位元組以確定接下來怎麼解壓資料

        while self.preBuf != b'':
            for i in range(8):  #8個專案為一組進行解壓
                # 從標記位元組的最高位開始解析,0代表原始資料,1代表(下標,匹配數)解析
                if self.preBuf[0] & (1 << (7 - i)) == 0:
                    temp = fread.read(1)
                    fwrite.write(temp)
                    self.windowBuf += temp
                else:
                    temp = fread.read(2)
                    start = ((temp[0] + temp[1] * 256) // (1 << self.preBufSizeBits))  #取出高位的滑動視窗匹配串下標
                    end = start + temp[0] % (1 << self.preBufSizeBits) + self.threshold  #取出低位的匹配長度
                    fwrite.write(self.windowBuf[start:end])  #將解壓出的資料寫入檔案
                    self.windowBuf += self.windowBuf[start:end]  #將解壓處的資料同步寫入到滑動視窗

                if len(self.windowBuf) > self.windowBufSize:  #限制滑動視窗大小
                    self.windowBuf = self.windowBuf[(len(self.windowBuf) - self.windowBufSize):]

            self.preBuf = fread.read(1)  #讀取下一組資料的標誌位元組

        fread.close()
        fwrite.close()

if __name__ == '__main__':
    Demo = LZSS(7)
    Demo.LZSS_encode("115.log", "encode")
    Demo.LZSS_decode("encode", "decode")




C實現

#include <string.h>
#include <stdio.h>

#define BYTE unsigned char
#define WORD unsigned short
#define DWORD unsigned int

#define TRUE 1
#define FALSE 0

BYTE bThreshold;  //壓縮閾值、長度大於等於2的匹配串才有必要壓縮

BYTE bPreBufSizeBits;  //前向緩衝區佔用的位元位
BYTE bWindowBufSizeBits;  //滑動窗口占用的位元位

WORD wPreBufSize;  //通過佔用的位元位計算緩衝區大小
WORD wWindowBufSize;  //通過佔用的位元位計算滑動視窗大小

BYTE bPreBuf[1024];  //前向緩衝區
BYTE bWindowBuf[8192];  //滑動視窗
BYTE bMatchString[1024];  //匹配串
WORD wMatchIndex;  //滑動視窗匹配串起始下標

BYTE FindSameString(BYTE *pbStrA, WORD wLenA, BYTE *pbStrB, WORD wLenB, WORD *pwMatchIndex);  //查詢匹配串
DWORD LZSS_encode(char *pbReadFileName, char *pbWriteFileName);  //檔案壓縮
DWORD LZSS_decode(char *pbReadFileName, char *pbWriteFileName);  //檔案解壓

int main()
{
	bThreshold = 2;
	bPreBufSizeBits = 6;
	bWindowBufSizeBits = 16 - bPreBufSizeBits;
	wPreBufSize = ((WORD)1 << bPreBufSizeBits) - 1 + bThreshold;
	wWindowBufSize = ((WORD)1 << bWindowBufSizeBits) - 1 + bThreshold;

	LZSS_encode("115.log", "encode");
	LZSS_decode("encode", "decode");
	return 0;
}

BYTE FindSameString(BYTE *pbStrA, WORD wLenA, BYTE *pbStrB, WORD wLenB, WORD *pwMatchIndex)
{
	WORD i, j;

	for (i = 0; i < wLenA; i++)
	{
		if ((wLenA - i) < wLenB)
		{
			return FALSE;
		}

		if (pbStrA[i] == pbStrB[0])
		{
			for (j = 1; j < wLenB; j++)
			{
				if (pbStrA[i + j] != pbStrB[j])
				{
					break;
				}
			}

			if (j == wLenB)
			{
				*pwMatchIndex = i;
				return TRUE;
			}
		}
	}
	return FALSE;
}

DWORD LZSS_encode(char *pbReadFileName, char *pbWriteFileName)
{
	WORD i, j;
	WORD wPreBufCnt = 0;
	WORD wWindowBufCnt = 0;
	WORD wMatchStringCnt = 0;
	BYTE bRestoreBuf[17] = { 0 };
	BYTE bRestoreBufCnt = 1;
	BYTE bItemNum = 0;
	FILE *pfRead = fopen(pbReadFileName, "rb");
	FILE *pfWrite = fopen(pbWriteFileName, "wb");

	//前向緩衝區沒資料可操作了即為壓縮結束
	while (wPreBufCnt += fread(&bPreBuf[wPreBufCnt], 1, wPreBufSize - wPreBufCnt, pfRead))
	{
		wMatchStringCnt = 0;  //剛開始沒有匹配到資料
		wMatchIndex = 0xFFFF;  //初始化一個最大值,表示沒匹配到

		for (i = bThreshold; i <= wPreBufCnt; i++)  //在滑動視窗中尋找最長的匹配串
		{
			if (TRUE == FindSameString(bWindowBuf, wWindowBufCnt, bPreBuf, i, &wMatchIndex))
			{
				memcpy(bMatchString, &bWindowBuf[wMatchIndex], i);
				wMatchStringCnt = i;
			}
			else
			{
				break;
			}
		}

		//如果沒找到匹配串或者匹配長度為1,直接輸出原始資料
		if ((0xFFFF == wMatchIndex))
		{
			wMatchStringCnt = 1;
			bMatchString[0] = bPreBuf[0];
			bRestoreBuf[bRestoreBufCnt++] = bPreBuf[0];
		}
		else
		{
			j = (wMatchIndex << bPreBufSizeBits) + wMatchStringCnt - bThreshold;
			bRestoreBuf[bRestoreBufCnt++] = (BYTE)j;
			bRestoreBuf[bRestoreBufCnt++] = (BYTE)(j >> 8);
			bRestoreBuf[0] |= (BYTE)1 << (7 - bItemNum);
		}

		bItemNum += 1;  //操作完一個專案+1

		if (bItemNum >= 8)  //專案數達到8了,說明做完了一組壓縮,將這一組資料寫入檔案,同時清空快取
		{
			fwrite(bRestoreBuf, 1, bRestoreBufCnt, pfWrite);
			bItemNum = 0;
			memset(bRestoreBuf, 0, sizeof(bRestoreBuf));
			bRestoreBufCnt = 1;
		}

		//將剛剛匹配過的資料移出前向緩衝區
		for (i = 0; i < (wPreBufCnt - wMatchStringCnt); i++)
		{
			bPreBuf[i] = bPreBuf[i + wMatchStringCnt];
		}
		wPreBufCnt -= wMatchStringCnt;

		//如果滑動視窗將要溢位,先提前把前面的部分資料移出視窗
		if ((wWindowBufCnt + wMatchStringCnt) >  wWindowBufSize)
		{
			j = ((wWindowBufCnt + wMatchStringCnt) - wWindowBufSize);
			for (i = 0; i < (wWindowBufSize - j); i++)
			{
				bWindowBuf[i] = bWindowBuf[i + j];
			}
			wWindowBufCnt = wWindowBufSize - wMatchStringCnt;
		}

		//將剛剛匹配過的資料加入滑動視窗
		memcpy((BYTE *)&bWindowBuf[wWindowBufCnt], bMatchString, wMatchStringCnt);
		wWindowBufCnt += wMatchStringCnt;
	}

	//檔案最後可能不滿一組資料量,直接寫到檔案裡
	if (0 != bRestoreBufCnt)
	{
		fwrite(bRestoreBuf, 1, bRestoreBufCnt, pfWrite);
	}

	fclose(pfRead);
	fclose(pfWrite);

	return 0;
}

DWORD LZSS_decode(char *pbReadFileName, char *pbWriteFileName)
{
	WORD i, j;
	BYTE bItemNum;
	BYTE bFlag;
	WORD wStart;
	WORD wMatchStringCnt = 0;
	WORD wWindowBufCnt = 0;
	FILE *pfRead = fopen(pbReadFileName, "rb");
	FILE *pfWrite = fopen(pbWriteFileName, "wb");

	while (0 != fread(&bFlag, 1, 1, pfRead))  //先讀一個標記位元組以確定接下來怎麼解壓資料
	{
		for (bItemNum = 0; bItemNum < 8; bItemNum++)  //8個專案為一組進行解壓
		{
			//從標記位元組的最高位開始解析,0代表原始資料,1代表(下標,匹配數)解析
			if (0 == (bFlag & ((BYTE)1 << (7 - bItemNum))))
			{
				if (fread(bPreBuf, 1, 1, pfRead) < 1)
				{
					goto LZSS_decode_out_;
				}
				fwrite(bPreBuf, 1, 1, pfWrite);
				bMatchString[0] = bPreBuf[0];
				wMatchStringCnt = 1;
			}
			else
			{
				if (fread(bPreBuf, 1, 2, pfRead) < 2)
				{
					goto LZSS_decode_out_;
				}
				//取出高位的滑動視窗匹配串下標
				wStart = ((WORD)bPreBuf[0] | ((WORD)bPreBuf[1] << 8)) / ((WORD)1 << bPreBufSizeBits);  
				//取出低位的匹配長度
				wMatchStringCnt = ((WORD)bPreBuf[0] | ((WORD)bPreBuf[1] << 8)) % ((WORD)1 << bPreBufSizeBits) + bThreshold;
				//將解壓出的資料寫入檔案
				fwrite(&bWindowBuf[wStart], 1, wMatchStringCnt, pfWrite);
				memcpy(bMatchString, &bWindowBuf[wStart], wMatchStringCnt);
			}

			//如果滑動視窗將要溢位,先提前把前面的部分資料移出視窗
			if ((wWindowBufCnt + wMatchStringCnt) > wWindowBufSize)
			{
				j = (wWindowBufCnt + wMatchStringCnt) - wWindowBufSize;
				for (i = 0; i < wWindowBufCnt - j; i++)
				{
					bWindowBuf[i] = bWindowBuf[i + j];
				}
				wWindowBufCnt -= j;
			}

			//將解壓處的資料同步寫入到滑動視窗
			memcpy(&bWindowBuf[wWindowBufCnt], bMatchString, wMatchStringCnt);
			wWindowBufCnt += wMatchStringCnt;
		}
	}

LZSS_decode_out_:

	fclose(pfRead);
	fclose(pfWrite);
	return 0;
}

三、效能分析

因為程式碼都是最基本的實現,並沒有對匹配串搜尋函式進行優化,所以時間效能上比較低,我們這裡只對比壓縮前後文件的位元組大小。有一點要提到的就是程式碼裡面有個threshold引數為2,意思是匹配串大於等於2才有必要進行壓縮,因為(位置,長度)這個標記的輸出長度為16位元位,所以匹配串只有1位元組也用這種方式表示的話顯然起不到壓縮效果。大家還可以發現一個問題,寫入檔案時匹配長度並不是實際值,因為0和1的匹配長度是不存在的,所以乾脆把0當做2,1當做3這樣來看待,就可以把匹配長度擴充兩個長度了。

確定了(位置,長度)的輸出格式為兩個位元組後,接下里就是怎麼選取“位置”和“長度”各自所應該佔用的位元位是多少了,我們可以設定為(8,8),(9,7),(10,6)等等這樣的組合,但是給“位置”設定的位元位一定要大於等於“長度”的位元位,原因是滑動視窗大小要大於等於前向緩衝區大小,不然沒有意義。對於相同的檔案,這兩個引數選取不同,那麼壓縮率也會不同,下面是我對一個log檔案選取不同的引數進行壓縮後的檔案大小對比圖,圖中preBufSizeBits就是指的“長度”所佔的位元位數:

 未壓縮的原始檔案大小是5.06MB,可以清晰的看出preBufSizeBits並不是越小越好,也不是越大越好,因為如果preBufSizeBits小了的話那麼前向緩衝區也就小了,一次能匹配的資料串就小了;如果preBufSizeBits大了的話那麼雖然前向緩衝區變大了,但是滑動視窗會縮小,資料串的匹配範圍就變小了。所以需要選擇一個合適的值才能使檔案壓縮效能最好。

演算法內部的對比就到這裡了,接下來我們和當前流行的ZIP和RAR進行壓縮效能對比,雖然感覺自不量力,但是有對比才有進步嘛。

115.log是原始檔案,encode2到encode8其實就是上邊效能圖裡不同preBufSizeBits時生成的壓縮檔案,decode2到decode8是對應再解壓縮出來的檔案。115.rar是用電腦上RAR軟體壓縮的檔案,115.zip是用電腦上ZIP軟體壓縮的檔案。我們這個演算法最好的效能是壓縮到了197KB,和ZIP的161KB差距不是特別大,和RAR就差了相當一大截了。 因為ZIP其實也是類似的滑動視窗匹配壓縮,所以接下來優化LZSS演算法的話,還是要儘量向ZIP的效能看齊。

四、總結

接下來還會繼續思考LZSS演算法的改進,包括壓縮率和壓縮/解壓時間的效能,因為我本人是嵌入式工程師,所以會思考實現在嵌入式裝置上如何運用資料壓縮,在網上查詢資料的時候也找到了一些開源的壓縮庫,但是使用上可能會存在一些問題和限制,當然最好是我們使用的演算法我們是知根知底的,這樣我們就可以根據專案的需求和特點進行靈活的修改和運用,出了什麼問題也不會太慌。

 

 https://github.com/atomicobject/heatshrink data compression library for embedded/real-time systems lzss base https://blog.csdn.net/qq_34254642/article/details/103741228 無失真壓縮演算法專題——LZSS演算法實現     https://github.com/shuai132/RpcCore/blob/master/test/TypeTest.cpp rpc   https://github.com/nickfox-taterli/LwIP_STM32F4-Discovery STM32F4 LwIP Demo

https://blog.csdn.net/I_canjnu/article/details/106353207 快閃記憶體——磨損均衡

https://blog.csdn.net/m0_37621078/article/details/103282134
https://github.com/StreamAI/LwIP_Projects LwIP_Projects

https://github.com/jiejieTop/LwIP_2.1.2_Comment/tree/master/lwip-2.1.2/src 

https://blog.csdn.net/m0_37621078/article/details/103282134 TCP/IP協議棧之LwIP(十一)--- LwIP協議棧移植