签到

06月21日
尚未签到

共有回帖数 0

    长街旧港00

    等级:
    我们给大家讲了压缩算法中预测模型与编码器的概念,同时给大家介绍了一个简单的hello_compressor。虽然hello_compressor中预测模型与编码器都有实现,但是这两个简单的核心部件的压缩效率并不高。今天我会给大家介绍更高效的预测模型和编码器,使我们的算法达到更高的压缩效率。

    概率预测模型:

    在hello_compressor中我们用到的是一个上下文长度为2字节的模型,每次预测时返回可能性最大的字符。这个模型会有一个很大的缺点:当可能性大的字符不止一个时,这个模型常常会顾此失彼,给出不正确的预测。例如在英文文章中,there和three都是很常见的单词,假设一篇文章中th之后出现e和出现r的概率各占50%,在我们的预测模型处理到th时,预测结果是e还是r呢?显然无论结果是哪一个,都会有相当大的出错概率。
    为了解决这个问题,我们将预测模型的功能进行拓展,预测时不是给出概率最大的字符,而是给出所有字符的出现概率,让编码器根据这个概率表来对下一个字符进行编码,这就是所谓的概率预测模型(probability model)。
    显然我们的hello_compressor中的编码器是不能利用模型给出的概率表进行编码的,适用这种模型的编码器有我们将要讲到的数字编码,还有哈夫曼编码(很著名的算法,不过我们不打算讲它)、香农-范诺编码等。

    算术编码:

    (本章讲的是简单的基于bit的算术编码器,现在网上的很多算术编码器都是基于byte的,代码有些不同,但算法思想基本一样)
    算术编码算法可能比较抽象,我们先从一个实验讲起。
    假设我们有一张纸一支笔和尺子,现在要我们记录一个二进制序列10110,我们可以用这样的方法:首先画一条32mm的线段,然后将线段二等分,处理第一个bit位,如果为0,使用左边的0..16的区间,如果为1则使用右边的16..32区间。对于我们的数据10110,显然使用的是右边的16..32区间。然后将这个区间二等分,用同样的方法处里下一个bit位。对于我们的数据10110,每次的区间依次为:
    16..32
    16..24
    20..24
    22..24
    22..23
    最后我们将22cm到23cm涂成黑色,完成整个编码过程。
    解码可以用类似的方法完成,同样将32mm的线段分为二等分,如果黑色区间在等分点左边,我们解码出一个bit位0,然后使用左边区间继续解码;黑色区间在等分点右边,我们解码出一个bit位1,然后使用右边区间。这个过程可以一直进行到最后的区间恰好为涂成黑色的区间,我们会发现正好解出了10110这个序列。
    有读者可能会猜到了,只要我们有足够精确的尺子,这个方法实际上可以在很小的纸条上记录任意多的bit位,因为“一尺之棰,日取其半,万世不竭”。实际上也是如此,所以我们也可以说:“给我纸笔尺子,我就能记录整个宇宙的数据!”

    现在我们将这个方法用程序实现,一个最大的问题是在计算机中数据都是离散的0和1,即使是浮点类型也会有有限的精度。也就是说在计算机中并不能完成“无限细分”,到最后我们的区间会退化到一个点上,不能再等分下去。
    一个解决方法是将数据分段,当区前退化到一个点上时我们就将这个点保存起来,然后重新开辟一个新区间对后面的数据进行处理。这个方法写成代码如下(我们的用区间长度是0..65535):
    void encode()
    {
       unsigned short hi_val = 65535;
       unsigned short lo_val = 0;
       int i;
       int next_char;

       while((next_char = getchar()) != EOF)
       {
           for(i = 0; i  8; i++)
         {
               // encode a bit
               if(1 & (next_char  i))
               {
    // bit 1
                   lo_val = lo_val / 2 + hi_val / 2 + 1;
               }
               else
               {
                   // bit 0
                   hi_val = lo_val / 2 + hi_val / 2;
               }

               // the range can no longer split in two, output it and use a new one
               if(hi_val == lo_val)
               {
                   putchar(hi_val / 256);
                   putchar(hi_val % 256);
                   lo_val = 0;
                   hi_val = 65535;
               }
           }
       }

       // the last range stores the ending few bytes, output it
       putchar(hi_val / 256);
       putchar(hi_val % 256);
       return;
    }

    这个方法能不能压缩数据呢,通过上一章的知识我们知道,这个算法没有对输入进行任何预测,显然是不能进行压缩的。实际上我们的16位长度的区间在处理16个bit之后就退化成一个点,而保存这个点恰好需要16位。

    现在我们开始讲数字编码,在hello_compressor中我们有这样一个做法,对于出现概率高的字符,我们用短的编码来表示它。数字编码的算法和上面提到的方法大体相同,只不过不再是等分区间,而是按0和1出现的概率去划分,例如0和1的概率分别是25%和75%,那么对于0..16这个区间,它个两个字区间分别是0..4和4..16。这样做一个直观的效果是对于概率大的bit,区间划分后得到的还是一个比较大的子区间,使得原本只能处理16个bit的区间现在能够处理更多的bit。
    这里给出具体的区间变换公式(假设原区间为a..b,0和1的概率分别为p和1-p):
    左边区间: a..[(b - a) * p]
    右边区间: [(b - a) * p + 1]..b
    使用这两个公式替换掉上面代码中的等分区间部分,就得到了算术编码的算法,下面我们给出一个完整的算术编码的源代码,里面世包含了一个上下文长度为3字节的0-1概率预测模型:

    (为了代码编写方便,我将计算区间的代码放在了预测模型中,也就是说现在的预测模型功能是:输入区间的上下边界,模型根据预测计算出区间的划分点)
    #include stdio.h
    #include stdlib.h
    #include string.h

    // # prediction model
    // #############################################################################
    typedef struct model_type_struct
    {
       unsigned char* m_count[2];
       unsigned int m_context;
    } model_type;

    void model_init(model_type* model)
    {
       model-m_count[0] = malloc(sizeof(unsigned char) * (1  24));
       model-m_count[1] = malloc(sizeof(unsigned char) * (1  24));
       memset(model-m_count[0], 0, sizeof(unsigned char) * (1  24));
       memset(model-m_count[1], 0, sizeof(unsigned char) * (1  24));
       return;
    }

    void model_destroy(model_type* model)
    {
       free(model-m_count[0]);
       free(model-m_count[1]);
       return;
    }

    unsigned short model_predict(model_type* model, unsigned short lo_val, unsigned short hi_val)
    {
       unsigned char count[2] =
       {
           1 + model-m_count[0][model-m_context % (1  24)],
           1 + model-m_count[1][model-m_context % (1  24)],
       };
       return lo_val + (hi_val - lo_val) * count[0] / (count[0] + count[1]);
    }

    void model_update(model_type* model, int bit)
    {
       if((model-m_count[bit][model-m_context % (1  24)] += 1) = 255)
       {
           // avoid overflow
           model-m_count[0][model-m_context % (1  24)] /= 2;
           model-m_count[1][model-m_context % (1  24)] /= 2;
     }
       model-m_context = model-m_context * 2 + bit;
       return;
    }

    // # coder
    // #############################################################################
    void arithmetic_encode(model_type* model)
    {
       unsigned short hi_val = 65535;
       unsigned short lo_val = 0;
       int i;
       int next_char;

       while((next_char = getchar()) != EOF)
       {
           for(i = 0; i  8; i++)
           {
               // encode a bit
               if(1 & (next_char  i))
               {
                   lo_val = model_predict(model, lo_val, hi_val) + 1;
                   model_update(model, 1);
               }
               else
               {
                   hi_val = model_predict(model, lo_val, hi_val);
                   model_update(model, 0);
               }

               // the range can no longer split in two, output it and use a new one
               if(hi_val == lo_val)
               {
                   putchar(hi_val / 256);
                   putchar(hi_val % 256);
                   lo_val = 0;
                   hi_val = 65535;
               }
           }
       }

       // the last range stores the ending few bytes, output it
       putchar(hi_val / 256);
       putchar(hi_val % 256);
       return;
    }

    void arithmetic_decode(model_type* model)
    {
       unsigned short hi_val = 0;
       unsigned short lo_val = 0;
       unsigned short val = 0;
       unsigned short mid;
       int output_char = 0;
       int output_bit_pos = 0;

       while(!feof(stdin))
       {
           // process next range
           if(hi_val == lo_val)
           {
               val = (val * 256) + getchar();
               val = (val * 256) + getchar();
               lo_val = 0;
               hi_val = 65535;
           }

           // decode a bit
           mid = model_predict(model, lo_val, hi_val);
           if(val  mid)
           {
               output_char |= (1  (output_bit_pos++));
               model_update(model, 1);
               lo_val = mid + 1;
           }
           else
           {
               output_char &= ~(1  (output_bit_pos++));
               model_update(model, 0);
               hi_val = mid;
           }
           if(output_bit_pos == 8)
           {
               putchar(output_char);

             output_bit_pos = 0;
           }
       }
       return;
    }

    // # main
    // #############################################################################
    int main(int argc, char** argv)
    {
       static char stdin_buffer[4096];
       static char stdout_buffer[4096];
       static model_type model;

       setbuffer(stdin, stdin_buffer, sizeof(stdin_buffer));
       setbuffer(stdout, stdout_buffer, sizeof(stdout_buffer));

       if(argc == 2 && strcmp(argv[1], "encode") == 0)
       {
           model_init(&model);
           arithmetic_encode(&model);
           model_destroy(&model);
       }
       else if(argc == 2 && strcmp(argv[1], "decode") == 0)
       {
           model_init(&model);
           arithmetic_decode(&model);
           model_destroy(&model);
       }
       else
       {
           fputs("argument: (encode|decode) in.file out.file", stderr);
       }
       return 0;
    }
    用这个程序压缩EGE的graphics.cpp文件,可以压缩到65.4KB,压缩率为29.7%,比上一章的hello_compressor提高了不少。

    编码器的优化:

    上面代码主要还是为了让大家理解算术编码,实际上还有优化的空间。最明显的是在计算划分点时由于整数舍入造成划分的误差,当区间很大时这个误差仅为几万分之一,基本不会对压缩率造成影响,但当区间很小时误差就不能忽略了,一个极端的例子是对于长度为2的区间如1..2,无论输入概率是多少,它的两个子区间都是按50%来算的1..1和2..2。这对压缩效率造成了很大的损失。
    现在我给出一个优化过的编码器,它的主要变化是:不再是将一个区间分划至退化为点的时候输出,而是分划到16位的前8位相同时就输出前8位,然后对整个区间进行一个比例扩大的操作。
    // # coder
    // #############################################################################
    void arithmetic_encode(model_type* model)
    {
       unsigned short hi_val = 65535;
       unsigned short lo_val = 0;
       int i;
       int next_char;

       while((next_char = getchar()) != EOF)
       {

         for(i = 0; i  8; i++)
           {
               if(1 & (next_char  i))
               {
                   lo_val = model_predict(model, lo_val, hi_val) + 1;
                   model_update(model, 1);
               }
               else
               {
                   hi_val = model_predict(model, lo_val, hi_val);
                   model_update(model, 0);
               }

               while(hi_val / 256 == lo_val / 256)
               {
                   putchar(hi_val / 256);
                   lo_val = (lo_val * 256);
                   hi_val = (hi_val * 256) + 255;
               }
           }
       }

       putchar(hi_val / 256);
       putchar(hi_val % 256);
       return;
    }

    void arithmetic_decode(model_type* model)
    {
       unsigned short hi_val = 0;
       unsigned short lo_val = 0;
       unsigned short val = 0;
       unsigned short mid;
       int output_char = 0;
       int output_bit_pos = 0;

       while(!feof(stdin))
       {
           while(hi_val / 256 == lo_val / 256)
           {
               val = (val * 256) + getchar();
          lo_val = (lo_val * 256);
               hi_val = (hi_val * 256) + 255;
           }

           // decode a bit
           mid = model_predict(model, lo_val, hi_val);
           if(val  mid)
           {
               output_char |= (1  (output_bit_pos++));
               model_update(model, 1);
               lo_val = mid + 1;
           }
           else
           {
               output_char &= ~(1  (output_bit_pos++));
               model_update(model, 0);
               hi_val = mid;
           }
           if(output_bit_pos == 8)
           {
               putchar(output_char);
               output_bit_pos = 0;
           }
       }
       return;
    }

    这个改进算法将EGE的graphics.cpp压缩至60.8KB,压缩率为27.6%。顺便说一下,网络上能找到的0-1算术编码基本上都是这个算法。算术编码算法是有专利权的,不过我认为这个专利权完全无效,因为在算术编码发明之前就已经有一个无专利限制的区间编码,算法思想和算术编码很相似(其实我认为两者是同一个算法的不同叙述方式)。

    这里也想说一下,算术编码器已经是一个很成就的算法模型,基本不会有什么大的变动。所以即使你实在看不懂算术编码的代码也没关系,只要知道它大概的思想,会使用这份代码就行了,下一章我们会讲一下预测模型的优化,达到更高的压缩率,到时候就只给出prediction model的代码,这个编码器的代码就不打算再列出来。

    PS: 有些朋友提到我的示范代码不能在Windows中编码运行,主要是因为我用到了一个linux下的setbuffer函数,Windows用户将它用setvbuf替换掉即可(参数要改一下)。此外由于Windows的文件打开模式区分为文本模式和二进制模式,所以要用示范代码处理二进制数据,请在代码中加上:
    ...
    #include io.h

    int main(...)
    {
       /* variant definition */
       ...

       _setmode(_fileno(stdin), _O_BINARY);
       _setmode(_fileno(stdout), _O_BINARY);
       ...
    }

    楼主 2015-11-19 21:04 回复

共有回帖数 0
  • 回 帖
  • 表情 图片 视频
  • 发表

登录直线网账号

Copyright © 2010~2015 直线网 版权所有,All Rights Reserved.沪ICP备10039589号 意见反馈 | 关于直线 | 版权声明 | 会员须知