【数据压缩】Huffman编解码器

来源:互联网 发布:java rectangle类 编辑:程序博客网 时间:2024/06/07 16:31

一.实验原理

1.Huffman编码

1)Huffman Coding(霍夫曼编码)是一种无失真编码的编码方式,Huffman编码是可编长编码(VLC)的一种。
2)Huffman编码基于信源的概率统计模型,它的基本思路是:出现概率大的信源符号编短码,出现概率小的编长码。从而实现平均码长最小。
3)在程序实现中常使用一种叫做树的数据结构实现Huffman编码,由它编出的码是即时码。

2.Huffman编码的方法

2.1 统计符号的发生概率;
2.2 把频率从小到大的顺序排列
2.3 每一次选出最小的两个值,作为二叉树的两个叶子节点,将和作为它们的根节点,这两个叶子节点不再参与比较,新的根节点参与比较;
2.4 重复3,知道最后得到和为1的根节点。
2.5 将形成的二叉树的左节点标为0,右节点标为1,把从最上面的根节点到最下面的叶子节点途中遇到的0,1序列串起来,就得到了各个符号的编码。

3.实验中遇到的递归式遍历方法

  • 先序遍历
    先序遍历

  • 中序遍历
    中序变绿

  • 后序遍历
    后序遍历

    综上,Huffman编码结构中, build_symbol_encoder()的函数,实现功能为:对存在的每个字符计算码字。该函数使用的遍历方法应该为递归”中序遍历”;假设已经遍历到最左边的叶节点,由if判断到叶节点的子节点为空,故Return。此时输出一个码子,然后进入下一个右节点。其对节点的遍历顺序应为,中序遍历。仅单纯观察代码易混淆为先序遍历

     if(subtree == NULL)    return;if(subtree->isLeaf)    (*pSF)[subtree->symbol] = new_code(subtree);else{//如果不是叶字节点,就需要往下遍历    build_symbol_encoder(subtree->zero, pSF);//先遍历其左子树    build_symbol_encoder(subtree->one, pSF);//再遍历其右子树}

4.实验中所用到的数据结构分析

 typedef struct huffman_node_tag{    unsigned char isLeaf;//判断该节点是否是叶节点的标志    unsigned long count;//信源中出现频数    struct huffman_node_tag *parent;//父节点指针    union    {        struct        {            struct huffman_node_tag *zero, *one;        };        unsigned char symbol;    };} huffman_node;

huffman_node节点中的有union数据结构,这么设计的优点是节约了空间。当节点为叶节点时,必定不包含子节点指针,当节点非叶节点时,必存在子节点指针。

typedef struct huffman_code_tag{    /* The length of this code in bits. */    unsigned long numbits;    /* The bits that make up this code. The first       bit is at position 0 in bits[0]. The second       bit is at position 1 in bits[0]. The eighth       bit is at position 7 in bits[0]. The ninth       bit is at position 0 in bits[1]. */    unsigned char *bits;} huffman_code;

huffman_code是Huffman码表数据结构,用以储存符号对应的码字。

    typedef struct huffman_info_tag{    double freq;//符号出现频率    unsigned long len_code;//符号对应的码长    unsigned char *code;//符号对应的码字}huffman_info;

huffman_info数据结构,用于储存每个符号的符号频率、码长、码字信息。用于压缩后统计压缩数据,计算压缩效率,以及验证压缩正确性。

  • 分别定义三个数组,数组中储存的是指向结构体的指针。
 #define MAX_SYMBOLS 256typedef huffman_node* SymbolFrequencies[MAX_SYMBOLS];typedef huffman_code* SymbolEncoder[MAX_SYMBOLS];//cai add typedef huffman_info* SymbolInfo[MAX_SYMBOLS];

举例,SymbolFrequencies[MAX_SYMBOLS]储存着256个指向huffman_node这个结构体的指针。

5.getopt()函数简单分析

传入的参数,例如:"-i input.rgb -o output.rgb -c"  "i"与“o”在下列注释中称为key,其对应的“input.rgb”称为value。以及调用getopt时,opt = getopt(argc, argv, "i:o:cdhvm")。"i:o:cdhvm"在下列注释中称为规范字符串
extern char *optarg;extern int optind;extern int opterr;int opterr = 1,     /* if error message should be printed */    optind = 1,     /* index into parent argv vector */    optopt,         /* character checked for validity */    optreset;       /* reset getopt */char    *optarg;        /* argument associated with option */#define BADCH   (int)'?'#define BADARG  (int)':'#define EMSG    ""int getopt(int nargc, char * const *nargv, const char* ostr){    static char *place = EMSG;      /* place向量先置为空 */    char *oli;              /* 用于指向 “规范字符串”的指针 */    if (optreset || !*place) {      /* 如果optreset为1,或者place指向空,都会进入这个判断 */        optreset = 0;               /*进入后,将optreset重置为0*/        if (optind >= nargc || *(place = nargv[optind]) != '-') {       /*判断optind是否比总argc数大,以及argv[opind]的第一个字符是否为'-' .*/            place = EMSG;                                               /*若满足其上任一项,说明字符读取到了最后一步,或者输入有误*/            return (EOF);                                               /*将place指向 argv[optind]*/        }        if (place[1] && *++place == '-') {  /* found "--" */            /*如果place的后一位仍指向某个字符,且该字符也为'-',place往前移一位*/            ++optind;                                                   /*进入下一个参数的读取*/            place = EMSG;            return (EOF);        }    }                   /* option letter okay? */    if ((optopt = (int)*place++) == (int)':' ||                         /*将place当前指向的字符给optopt,如果这个字符为':',就别进行下一步判断啦(?)*/        !(oli = strchr(ostr, optopt))) {                                /*好,不是':'.那就找这个key在“规范字符串”的位置,然后找到的位置返回给oli*/                                                                        /*如果key在规范字符串里找到了,那么返回值不为零,则进不去下面的判断*/        /*                                                                       * if the user didn't specify '-' as an option,         * assume it means EOF.         */        if (optopt == (int)'-')            return (EOF);        if (!*place)            ++optind;        if (opterr && *ostr != ':')            (void)fprintf(stderr,                "%s: illegal option -- %c\n", __FILE__, optopt);        return (BADCH);    }    if (*++oli != ':') {            /* don't need argument */       /*将指向key的指针往后移动一位,如果发现指向的并不是':',说明这个key没有value*/        optarg = NULL;                                              /*将value指针置NULL*/        if (!*place)                                                /*如果这时place也指向空了,那就是进行下一项参数的读取吧,optind加加*/            ++optind;    }    else {                  /* need an argument */                  /*这时后移的Key指针发现了':',说明这个Key有对应的value */        if (*place)         /* no white space */                    /*假如传参者传参时,没用空格分开key 和value ,没事,直接赋值给value指针*/            optarg = place;        else if (nargc <= ++optind) {   /* no arg */                /*这时将参数index加一,发现后面没有其他参数了!本来应该有value的!*/            place = EMSG;            if (*ostr == ':')                return (BADARG);            if (opterr)                (void)fprintf(stderr,                "%s: option requires an argument -- %c\n",      /*告诉传参者,这个选项(key),需要对应的value*/                    __FILE__, optopt);            return (BADCH);        }        else                /* white space */                       /*好的原来你有空格,将已经加一的参数index作为索引,nargv[索引]给你value字符串*/            optarg = nargv[optind];        place = EMSG;        ++optind;    }    return (optopt);            /* 丢出我读到的key */}

以上代码写法,于我这样的初学者读起来较为晦涩,但贵在简洁。指针与标识符的移动增减常隐藏在if的判断条件中,且if()判断条件中的&&与||符的使用相当灵活精妙,理解透彻后对写代码能力提升有益处。但在读代码时,未能考虑代码对于所有不规范输入的应对举措,仍需继续研究。(注释待完善…)


二.实验流程&代码分析

实验流程
1.读入待编码的源文件:huffcode.c:

FILE *in = stdin;FILE *out = stdout;FILE *info =NULL;char memory = 0;//内存操作标识符char compress = 1;//编码、解码操作的标识符  .....if(file_in)    {        in = fopen(file_in, "rb");        if(!in)        {            fprintf(stderr,                    "Can't open input file '%s': %s\n",                    file_in, strerror(errno));            return 1;        }    }    /* If an output file is given then create it. */    if(file_out)    {        out = fopen(file_out, "wb");        if(!out)        {            fprintf(stderr,                    "Can't open output file '%s': %s\n",                    file_out, strerror(errno));            return 1;        }    }    if(memory)//如果memory为1,则程序执行的是写进内存操作    {        return compress ?            memory_encode_file(in, out) : memory_decode_file(in, out);    }    //如果Memory ==0,则程序执行写进file操作。    //如果compress标识符为1,则执行编码操作。反之,进行解码操作    return compress ?        huffman_encode_file(in, out,info) : huffman_decode_file(in, out);    //huff_encode_file传参有三个,为满足实验要求,对该函数进行了改动,info类型为FILE *,是对应输出信息表格文件的文件指针。

2.第一次扫描:统计文件中各个字符出现频率
huffman.c:

    get_symbol_frequencies(SymbolFrequencies *pSF, FILE *in){    int c;    unsigned int total_count = 0;//统计总样点数    /* Set all frequencies to 0. */    init_frequencies(pSF);//将所有字符的频率置0    /* Count the frequency of each symbol in the input file. */    while((c = fgetc(in)) != EOF)//挨个读取字符    {        unsigned char uc = c;//将读取的字符赋值给uc        if(!(*pSF)[uc])//如果字符uc不存在对应的空间,则说明这是第一次遇到的字符            (*pSF)[uc] = new_leaf_node(uc);//则为该字符开辟一块内存空间        ++(*pSF)[uc]->count;//如果不是第一次遇见这个字符,则计数加一        ++total_count;//不论什么情况,总统计数都应加一    }    return total_count;//返回总统计数}
    static huffman_node*    new_leaf_node(unsigned char symbol){//新建一个叶子节点    huffman_node *p = (huffman_node*)malloc(sizeof(huffman_node));    p->isLeaf = 1;//叶子标识符置1    p->symbol = symbol;//将第一次遇到的字符,存入该节点的symbol值中    p->count = 0;//初始化计数值,为0    p->parent = 0;//此时树尚未链接,所以没有父指针    return p;}static huffman_node*new_nonleaf_node(unsigned long count, huffman_node *zero, huffman_node *one){//新建非叶节点    huffman_node *p = (huffman_node*)malloc(sizeof(huffman_node));//第一步开辟空间    p->isLeaf = 0;//叶子标识符置0    p->count = count;//计数值,为两个子树传递上来的count总和    p->zero = zero;//左子树链接    p->one = one;//右子树链接    p->parent = 0;    return p;}

计算每个字符对应的出现频率:

    //形参为SymbolInof类型指针,SymbolFrequencies 类型指针,以及总样点数total_count    void cal_freq(SymbolInfo *si, SymbolFrequencies *sf, unsigned int total_count){    for (int i = 0; i < MAX_SYMBOLS; i++)    {        (*si)[i] = (huffman_info*)malloc(sizeof(huffman_info));    }//为每个huffman_info类型开辟空间    for (int i = 0; i < MAX_SYMBOLS; i++)    {        if (!(*sf)[i])        {//如果字符对应的频率为0,则将其对应的数据部分置为零            (*si)[i]->freq = 0;            (*si)[i]->len_code = 0;            (*si)[i]->code = NULL;        }        else//如果存在sf[i],则将其出现的count处于总样点数,得到频率            (*si)[i]->freq =(double) (*sf)[i]->count / total_count;    }}

3.建立huffman树

    static SymbolEncoder*calculate_huffman_codes(SymbolFrequencies * pSF){    unsigned int i = 0;    unsigned int n = 0;    huffman_node *m1 = NULL, *m2 = NULL;//定义两个临时huffman_node指针变量    SymbolEncoder *pSE = NULL;    /* 将信源符号出现频率大小排序,小概率符号在前*/    qsort((*pSF), MAX_SYMBOLS, sizeof((*pSF)[0]), SFComp);#if 0       printf("AFTER SORT\n");    print_freqs(pSF);#endif    /* 获得文件中所出现的信源符号总数. */    for(n = 0; n < MAX_SYMBOLS && (*pSF)[n]; ++n)        ;    /*     每次合并树,是将2个子树进行一次合并。共有n个子树,最后合并结果是合并为概率为1的“大树”,故合并次数为n-1。     */    for(i = 0; i < n - 1; ++i)    {        /* 将m1.m2置为 当前频率 数最小的两个信源符号 */            m1 = (*pSF)[0];        m2 = (*pSF)[1];        /* 将两个小树合并成大树,需要将子树父指针指向新建的一个非叶指针节点,该节点的count值为两个子树的count相加,上问中有所分析。 */        (*pSF)[0] = m1->parent = m2->parent =            new_nonleaf_node(m1->count + m2->count, m1, m2);        (*pSF)[1] = NULL;        /* 由于最小的两个频率数,进行了合并,所以整体顺序必须进行改变,故需要重新调用qsort()排序 */        qsort((*pSF), n, sizeof((*pSF)[0]), SFComp);    }    /* Build the SymbolEncoder array from the tree. */    pSE = (SymbolEncoder*)malloc(sizeof(SymbolEncoder));    memset(pSE, 0, sizeof(SymbolEncoder));    //pSE为指向指针数组的指针,为其开辟内存空间,并置零。    build_symbol_encoder((*pSF)[0], pSE);    return pSE;}
static voidbuild_symbol_encoder(huffman_node *subtree, SymbolEncoder *pSF){    if(subtree == NULL)//是否已经到了根,是则说明编码结束        return;    if(subtree->isLeaf)//如果是叶节点,则生成码字        (*pSF)[subtree->symbol] = new_code(subtree);    else    {    //如果不是则递归,中序遍历        build_symbol_encoder(subtree->zero, pSF);        build_symbol_encoder(subtree->one, pSF);    }}

生成码字:

    static huffman_code* new_code(const huffman_node* leaf){    unsigned long numbits = 0;//码长    unsigned char* bits = NULL;//码字首地址    huffman_code *p;    while(leaf && leaf->parent)    {        huffman_node *parent = leaf->parent;        unsigned char cur_bit = (unsigned char)(numbits % 8);        //所编位数在当前byte中的位置        unsigned long cur_byte = numbits / 8;        //当前是第几个byte        if(cur_bit == 0)//cur_bit==0意味着开始新byte的编码        {            size_t newSize = cur_byte + 1;            bits = (char*)realloc(bits, newSize);            bits[newSize - 1] = 0; /* Initialize the new byte. */        }        if(leaf == parent->one)            bits[cur_byte] |= 1 << cur_bit;        /*如果当前节点为父节点的右子树,则需要将码字对应位置置1。        * 将1左移cur_bit位,cur_bit位当前的编的位置        * 如果当前节点为父节点的左子树,不需要操作,因为初始化时的8bit已经为0        */        ++numbits;//码长加1        leaf = parent;//节点顺序往上    }    if(bits)        reverse_bits(bits, numbits);    //使用的huffman是自顶向下,而以上方法是从叶子往头的方向编,故需要整个码字逆序。    p = (huffman_code*)malloc(sizeof(huffman_code));    p->numbits = numbits;    p->bits = bits;    return p;    //构建一个huffman_code结构,返回给上层函数}

4.将码表及其他必要信息写入输出文件

static int write_code_table(FILE* out, SymbolEncoder *se, unsigned int symbol_count){    unsigned long i, count = 0;    /* 通过码表类型se,计算出现了多少码符号 count*/    for(i = 0; i < MAX_SYMBOLS; ++i)    {        if((*se)[i])            ++count;    }    /* 改变字节序,然后写入文件头*/    i = htonl(count);    if(fwrite(&i, sizeof(i), 1, out) != 1)        return 1;    /* 改变字节序,写入码元数,即上文提到的“样本数” .挨着写入文件头*/    symbol_count = htonl(symbol_count);    if(fwrite(&symbol_count, sizeof(symbol_count), 1, out) != 1)        return 1;    /* 开始写码表 */    for(i = 0; i < MAX_SYMBOLS; ++i)    {        huffman_code *p = (*se)[i];        if(p)        {            unsigned int numbytes;            /*写入1byte的符号. */            fputc((unsigned char)i, out);            /*写入1byte大小的码长数*/            fputc(p->numbits, out);            /*写入对应的码字 */            numbytes = numbytes_from_numbits(p->numbits);            if(fwrite(p->bits, 1, numbytes, out) != numbytes)                return 1;        }    }    return 0;}
static unsigned long numbytes_from_numbits(unsigned long numbits){    return numbits / 8 + (numbits % 8 ? 1 : 0);    // 假如码长有25 bit,也需要4个byte字节来传送(8*3+1)。}

5.第五次扫描,对源文件进行编码并输出:

static int do_file_encode(FILE* in, FILE* out, SymbolEncoder *se){    unsigned char curbyte = 0;    unsigned char curbit = 0;    int c;    while((c = fgetc(in)) != EOF)//挨个字符读入    {        unsigned char uc = (unsigned char)c;//对读入的字符进行查表        huffman_code *code = (*se)[uc];//进行查表操作        unsigned long i;        for(i = 0; i < code->numbits; ++i)        {            /* 操作与上文构建码表操作类似,新读出的bit右移curbit位后,与先前的操作相与,主观上感觉即 :挨个bit读 */            curbyte |= get_bit(code->bits, i) << curbit;            /* 如果curbit已凑足一个字节,则进行写入文件操作,且curbyte与curbit置0*/            if(++curbit == 8)            {                fputc(curbyte, out);                curbyte = 0;                curbit = 0;            }        }    }    /*     *假如最后一个符号的编码,不足以凑整8个字节,也需要强行写入了。(否则就要被丢弃了)     */    if(curbit > 0)        fputc(curbyte, out);    return 0;}
void draw_huffmaninfo(SymbolInfo *si, SymbolEncoder *se){    FILE * file_info=NULL;    if (file_info = fopen("Huffman_info.xls", "w"))        printf("open xls ok\n");    else        return;     /*绘制表头:*/     /* \t对应xls中换列,\n对应xls中换行*/    fprintf(file_info, "Symbol\t Freq\t Length\t Code\n");    for (int i = 0; i < MAX_SYMBOLS; i++)    {        fprintf(file_info, "%d\t", i);//fprintf 输出当前字符        fprintf(file_info, "%f\t", (*si)[i]->freq);//fprintf输出当前字符的频率        fprintf(file_info, "%d\t", (*si)[i]->len_code);//fprintf输出符号对应的码字长度        if ((*si)[i]->freq != 0)        {            for (unsigned int j = 0; j < (*si)[i]->len_code; j++)            {                unsigned char c = get_bit((*si)[i]->code, j);                fprintf(file_info, "%d", c);            }        }        else{            fprintf(file_info, "0");        }        fprintf(file_info, "\n");//换行符    }    fclose(file_info);}

参考资料:http://blog.163.com/zjh_upc/blog/static/122592008201382565122418/

6.涉及代码分析
void qsort(void base,int nelem,int width,int (*fcmp)(const void ,const void *))为包含在stdilib.h当中的函数,使用快速排序例程,进行排序。
试验中传参的SFcomp为指向函数的指针,具体分析如下:

/*调用举例*/qsort((*pSF), MAX_SYMBOLS, sizeof((*pSF)[0]), SFComp);//调用时的pSF为 指向 指针数组的 指针!@//SFcomp为指向函数的指针,//SFcmop的两个形参类型为空类型指针static int SFComp(const void *p1, const void *p2){    const huffman_node *hn1 = *(const huffman_node**)p1;    const huffman_node *hn2 = *(const huffman_node**)p2;    //因为pSF是指针数组,所以这里传入的参数为指向指针的指针。    /* Sort all NULLs to the end. */    if(hn1 == NULL && hn2 == NULL)//如果两个pSF类型都为空,返回0。        return 0;    if(hn1 == NULL)//如果hn1指向空,返回正数。即让hn2在hn1之前,宏观上看即 hn1往后排        return 1;    if(hn2 == NULL)//如果hn2指向空,返回负数。即让hn1在hn2之前,宏观上看即 hn2往后排        return -1;    //累积进行上述判断的结果:指向空的类型往后排     if(hn1->count > hn2->count)//如果h1—count>h2-count,返回正数,让h2提前。因为我们要实现的是从小到大排序。        return 1;    else if(hn1->count < hn2->count)//如果h1—count<h2-count,返回负数,让h1保持前位。        return -1;    return 0;//如果hn1 hn2都不为空,保持原样。不进行位置调整,返回0}
static unsigned char get_bit(unsigned char* bits, unsigned long i){//从bit[]中获取一位数据    return (bits[i / 8] >> i % 8) & 1;}//bit[i/8]中的i/8用于确定索取的位数在第几个字节中,譬如i = 10,则处在bit[1]中。//取出来的字节,需要右移。右移位数://i%8为右移位数,譬如i = 10,则右移两位。//右移后结果与000000001相与,得到返回的bit位
    //字符串倒序static void reverse_bits(unsigned char* bits, unsigned long numbits){    unsigned long numbytes = numbytes_from_numbits(numbits);    unsigned char *tmp =        (unsigned char*)alloca(numbytes);    unsigned long curbit;    long curbyte = 0;    memset(tmp, 0, numbytes);    for(curbit = 0; curbit < numbits; ++curbit)    {        unsigned int bitpos = curbit % 8;        if(curbit > 0 && curbit % 8 == 0)            ++curbyte;        tmp[curbyte] |= (get_bit(bits, numbits - curbit - 1) << bitpos);    }    memcpy(bits, tmp, numbytes);}

三.实验结果

运行程序后,生成.xls文件:
xls截图

根据其数据生成图表:
ico.

jpg.

moon.yuv
ps:moon.yuv编码集中在符号=128一列,概率接近0.6,分析得出原因:yuv文件内容是大面积灰色。
yuv
以表格形式统计实验数据:

文件类型 平均码长 信源熵 源文件大小(kB) 压缩后文件大小(kB) 压缩比 ppt 8.617 6.681 44 38 1.158 avi 8.016 7.989 718 718 1 ico 12.914 2.978 426 160 2.663 jpg 8.055 7.983 5366 5355 1.002 yuv 10.480 2.379 732 219 3.342 dll 8.883 6.948 440 383 1.149 docx 8.818 7.709 11 12 0.917 rar 7.738 7.349 1.03 1.75 0.589 gif 8.012 7.997 15 16 0.938 bmp 4.238 2.547 451 144 3.132
0 0