【问题标题】:Lightweight (de)compression algorithm for embedded use用于嵌入式的轻量级(解)压缩算法
【发布时间】:2017-09-12 06:32:10
【问题描述】:

我有一个带有图形用户界面的低资源嵌入式系统。界面需要字体数据。为了节省只读内存(闪存),需要压缩字体数据。我正在为此寻找一种算法。

要压缩的数据的属性

  • 每像素 8 位的矩形像素图的透明度数据
  • 字体中通常有大约 200..300 个字形(字体以特定大小采样)
  • 每个字形的大小通常为 6x9 到 15x20 像素
  • 有很多零(“无墨水”)和略少的 255(“完全着墨”),否则由于抗锯齿的特性,八位位组的分布相当均匀

压缩算法要求

  • 解压缩算法的重要指标是数据大小加上算法的大小(因为它们将驻留在相同的有限内存中)。
  • 可用于解压的 RAM 非常少;可以将单个字形的数据解压缩到 RAM 中,但不多。
  • 为了使事情变得更加困难,算法必须在 32 位微控制器(ARM Cortex-M 内核)上非常快,因为在将字形绘制到显示器上时需要对其进行解压缩。每个八位字节十个或二十个机器周期都可以,一百个肯定太多了。
  • 为方便起见,完整的数据语料库是先验已知的,并且在压缩阶段有大量可用的处理能力和内存。

结论和想法

  • 由于熵相对较高,仅通过一些可变长度编码打包每个八位字节的简单方法不会产生良好的结果。
  • 任何利用之前解压缩数据的算法似乎都没有问题,因为无法存储其他字形的解压缩数据。这会降低 LZ 算法的效率,因为它们只能引用少量数据。
  • 处理能力的限制似乎排除了大多数按位运算,即解压缩应逐个字节处理数据。这使得霍夫曼编码变得困难,算术编码变得不可能。
  • 这个问题似乎很适合静态字典编码,因为所有数据都是事先已知的,而且数据在本质上有些重复(不同的字形共享相同的形状)。

问题

  • 如何构建好的词典?我知道为某些数据找到最佳字典是一个 np 完全问题,但是有没有相当好的近似值?我尝试了 zstandard 的字典生成器,但结果不是很好。
  • 我的结论中有什么地方我弄错了吗? (我是不是走错了路,遗漏了一些明显的东西?)

迄今为止最好的算法

只是为了提供一些背景信息,我能够弄清楚的最有用的算法如下:

  • 单个字形的字体数据中的所有样本都连接(展平)成一维数组(向量、表格)。
  • 每个样本都有三种可能的状态:0、255 和“其他”。
  • 此信息一次将五个连续样本打包成一个 5 位以三为底的数字 (0..3^5)。
  • 由于八位字节中有一些额外的值(2^8 = 256, 3^5 = 243),它们用于表示较长的 0 和 255 字符串。
  • 对于每个“其他”值,实际值 (1..254) 存储在单独的向量中。

此数据可以快速解压缩,因为可以通过一个较小的(243 x 3 = 729 八​​位字节)查找表将 base-3 值解码为 base-4 值。压缩比高度依赖于字体大小,但使用我的典型数据,我可以得到大约 1:2。由于这比 LZ 变体(大约 1:3)差很多,我想尝试静态字典方法。

当然,通常的 LZ 变体使用 Huffman 或算术编码,这自然使压缩后的数据更小。另一方面,我有所有可用的数据,压缩速度不是问题。这应该可以找到更好的字典。

由于数据的性质,我可以使用有损算法,但在这种情况下,最有可能的有损算法是减少像素数据中量化级别的数量。这不会太大改变底层的压缩问题,我想避免由此产生的位对齐麻烦。

【问题讨论】:

    标签: algorithm embedded compression


    【解决方案1】:

    【讨论】:

    • 我喜欢这个主意!有很多参数是对的。不幸的是,该算法似乎更适合更长的数据。如果我只能参考最后一个,比如 1024 个八位字节,我会错过数据中的很多重复(例如,字形 a、á、à、å 和 ä 有很多共同点,尽管它们在字形表)。此外,我很可能需要一个接一个地压缩字形,以使我能够解压缩单个字形。
    【解决方案2】:

    我承认这是一个很好的回答我的问题的临界案例,但是由于我对这个问题进行了一些研究,这个答案既描述了我选择的方法,又提供了有关问题性质的更多信息应该有人撞到它。

    “正确答案”又名最终算法

    我最终得到的是我在问题中描述的变体。首先,每个字形被分成三元组 0、1​​ 和中间字符。然后使用 256 槽静态字典压缩此三元信息。字典(或查找表)中的每一项都是二进制编码的字符串(0=0, 10=1, 11=中间),在最重要的一端添加一个 1。

    灰度数据(用于中间三元组)散布在对查找表的引用之间。所以,数据基本上是这样的:

    <LUT reference><gray value><gray value><LUT reference>...
    

    灰度值的个数自然取决于从静态字典中查找到的三元数据中的中间三元数。

    解压代码很短,可以很容易地写成一个状态机,只有一个指针和一个给出状态的 32 位变量。像这样的:

    static uint32_t trits_to_decode;
    static uint8_t *next_octet;
    
    /* This should be called when starting to decode a glyph
       data : pointer to the compressed glyph data */
    void start_glyph(uint8_t *data)
    {
        next_octet = data;        // set the pointer to the beginning of the glyph
        trits_to_decode = 1;      // this triggers reloading a new dictionary item
    }
    
    
    /* This function returns the next 8-bit pixel value */
    uint8_t next_pixel(void)
    {
        uint8_t return_value;
    
        // end sentinel only? if so, we are out of ternary data
        if (trits_to_decode == 1)
            // get the next ternary dictionary item
            trits_to_decode = dictionary[*next_octet++];
    
        // get the next pixel from the ternary word
        // check the LSB bit(s)
        if (trits_to_decode & 1)
        {
            trits_to_decode >>= 1;
            // either full value or gray value, check the next bit
            if (trits_to_decode & 1)
            {
                trits_to_decode >>= 1;
                // grayscale value; get next from the buffer
                return *next_octet++; 
            }
            // if we are here, it is a full value
            trits_to_decode >>= 1;
            return 255;
        }
    
        // we have a zero, return it
        trits_to_decode >>= 1;
        return 0;
    }
    

    (代码并没有完全按照这种形式进行测试,所以可能存在拼写错误或其他愚蠢的小错误。)

    移位操作有很多重复。我不太担心,因为编译器应该能够清理它。 (其实左移可能会更好,因为这样可以在移位后使用进位位。但是由于在C中没有直接的方法,我不打扰。)

    另一个优化与字典(查找表)的大小有关。可能有短项和长项,因此它可以构建为支持 32 位、16 位或 8 位项。在这种情况下,必须对字典进行排序,以便小数值指代 32 位项,中值指代 16 位项,大值指代 8 位项,以避免对齐问题。那么查找代码是这样的:

    static uint8_t dictionary_lookup(uint8_t octet)
    {
        if (octet < NUMBER_OF_32_BIT_ITEMS)
            return dictionary32[octet];
        if (octet < NUMBER_OF_32_BIT_ITEMS + NUMBER_OF_16_BIT_ITEMS)
            return dictionary16[octet - NUMBER_OF_32_BIT_ITEMS];
        return dictionary8[octet - NUMBER_OF_16_BIT_ITEMS - NUMBER_OF_32_BIT_ITEMS];
    }
    

    当然,如果每种字体都有自己的字典,那么常量就会变成从字体信息中查找的变量。任何半体面的编译器都会内联该函数,因为它只被调用一次。

    如果量化级别的数量减少,也可以处理。最简单的情况是 4 位灰度级 (1..14)。这需要一个 8 位状态变量来保存灰度级。那么灰度分支会变成:

    // new state value
    static uint8_t gray_value;
    ...
    
        // new variable within the next_pixel() function
        uint8_t return_value;
    
        ...
    
                // there is no old gray value available?
                if (gray_value == 0)
                    gray_value = *next_octet++;
                // extract the low nibble
                return_value = gray_value & 0x0f;
                // shift the high nibble into low nibble
                gray_value >>= 4;
                return return_value;
    

    这实际上允许使用 15 个中间灰度级(总共 17 个级别),它可以很好地映射到线性 255 值系统。

    3 位或 5 位数据更容易打包成 16 位半字并将 MSB 始终设置为 1。然后可以使用与三元数据相同的技巧(移位直到得到 1)。

    应该注意的是,压缩比在某个时候开始恶化。三值数据的压缩量不依赖于灰度级数。灰度级数据是未压缩的,八位字节的数量(几乎)与位数成线性关系。对于典型的字体,8 位灰度数据是总数的 1/2 .. 2/3,但这很大程度上取决于字体和大小。

    因此,从 8 位减少到 4 位(在大多数情况下,这在视觉上是难以察觉的)通常会减少 1/4..1/3 的压缩大小,而减少到 3 位所提供的进一步减少要少得多.使用这种压缩算法,两位数据没有意义。

    如何构建字典?

    如果解压算法非常简单快速,真正的挑战在于字典的构建。很容易证明存在诸如最优字典之类的东西(字典为给定字体提供最少数量的压缩八位位组),但比我更聪明的人似乎已经证明找到这样的字典的问题是 NP 完全的。

    由于我可能相当缺乏该领域的理论知识,我认为会有很好的工具提供相当好的近似值。可能有这样的工具,但我找不到,所以我推出了自己的米老鼠版本。 编辑:早期的算法相当愚蠢;发现了一个更简单更有效的方法

    1. 从 '0'、g'、'1' 的静态字典开始(其中 'g' 表示中间值)
    2. 将每个字形的三元数据拆分为一个三元组列表
    3. 找到最常见的连续项目组合(在第一次迭代时很可能是“0”、“0”)
    4. 用组合替换所有出现的组合并将组合添加到字典中(例如,数据'0','1','0','0','g'将变为'0','1 ', '00', 'g' if '0', '0' 替换为 '00')
    5. 删除字典中所有未使用的项目(至少在理论上它们可能会出现)
    6. 重复步骤 3-5,直到字典已满(即至少 253 轮)

    这仍然是一个非常简单的方法,它可能会给出一个非常次优的结果。它唯一的优点是它有效。

    效果如何?

    一个答案就足够了,但要详细说明一下,这里有一些数字。这是一种具有 864 个字形的字体,典型字形大小为 14x11 像素,每像素 8 位。

    • 原始未压缩大小:127101
    • 中间值个数:46697
    • 香农熵(逐个八位字节):
      • 总计:528914 位 = 66115 个八位字节
      • 三进制数据:176405 位 = 22051 个八位字节
      • 中间值:352509 位 = 44064 个八位字节
    • 简单压缩的三进制数据(0=0、10=1、11=中间)(127101 个三元组):207505 位 = 25939 个八位字节
    • 字典压缩三元数据:18492 个八位字节
      • 熵:136778 位 = 17097 个八位字节
    • 字典大小:647 个八位字节
    • 完整压缩数据:647 + 18492 + 46697 = 65836 个八位字节
    • 压缩:48.2 %

    与八位字节熵的比较非常具有启发性。中间值数据具有高熵,而三元数据可以被压缩。这也可以通过原始数据中的大量值 0 和 255 来解释(与任何中间值相比)。

    我们不做任何事情来压缩中间值,因为似乎没有任何有意义的模式。但是,我们用三元数据明显击败了熵,甚至数据总量也低于熵极限。所以,我们可以做得更糟。

    将量化级别的数量减少到 17 会将数据大小减少到大约 42920 个八位字节(压缩超过 66 %)。熵是 41717 个八位字节,所以算法会像预期的那样稍微变差。

    实际上,较小的字体很难压缩。这应该不足为奇,因为大部分信息都在灰度信息中。使用这种算法可以有效地压缩非常大的字体,但运行长度压缩是一个更好的候选。

    什么会更好?

    如果我知道,我会使用它!但我仍然可以推测。

    Jubatian 表明字体中会有很多重复。变音符号一定是这样,因为 aàäáâå 在几乎所有字体中都有很多共同点。但是,对于大多数字体中的诸如 p 和 b 之类的字母来说,情况似乎并非如此。虽然基本形状很接近,但还不够。 (仔细的逐像素字体设计就是另一回事了。)

    不幸的是,这种不可避免的重复在较小的字体中并不容易被利用。我尝试创建所有可能的扫描线的字典,然后只引用那些。不幸的是,不同扫描线的数量很高,因此引用增加的开销超过了好处。如果扫描线本身可以被压缩,情况会有所改变,但是每条扫描线的八位字节数很少,因此很难进行有效的压缩。这个问题当然取决于字体大小。

    我的直觉告诉我,如果使用比全扫描线更长和更短的运行,这仍然是正确的方法。这与使用 4 位像素相结合可能会产生非常好的结果——前提是有办法创建最佳字典。

    对此方向的一个提示是,完整字体数据(127101 个八位字节)的 LZMA2 压缩文件(xz 处于最高压缩率)只有 36720 个八位字节。当然,这种格式不满足其他要求(解压速度快,可以逐个字形解压缩,RAM 要求低),但它仍然表明数据中的冗余比我的廉价算法所能达到的要多利用。

    字典编码通常在字典步骤之后与霍夫曼或算术编码结合使用。我们不能在这里这样做,但如果可以的话,它会再节省 4000 个八位字节。

    【讨论】:

      【解决方案3】:

      您可以尝试使用带有自定义字典的稀疏表示进行有损压缩。

      每个字形的输出是字典中 1-N 个块的叠加;

      • 大部分 CPU 时间花在预处理上
      • 每个像素的预定解码时间(最大、平均或恒定 N)加法
      • 可控压缩大小(字典大小 + 每个字形 xyn 个代码)

      【讨论】:

      • 这很有趣!如果我理解正确,这个想法是找到一组(字典)字形部分,可以逐个像素地组合到最终字形。从某种意义上说,我会有一个“笔画”库,然后将它们组合成字形。虽然在线性域中事情是最简单的,但我可以想象在这个过程中使用一些饱和算法。不幸的是,我对此的初步试验并不令人鼓舞,但有什么工具可以尝试吗?
      • 这看起来像一个完整的工具 --ux.uis.no/~karlsk/ICTools/ictools.html 无论如何,适用性几乎可以归结为用于组合字母的算法 -- 我没有第一手经验。
      • 谢谢!我去看看!
      【解决方案4】:

      似乎最简单的有损方法是减少每像素的位数。对于这种大小的字形,16 个级别可能就足够了。这会立即将数据减半,然后您可以将现有算法应用到值 0、16 或“其他”值中,以再次减半。

      【讨论】:

      • 这正是我在问题结束时所说的。特别是 4 位像素很容易处理。然而,虽然灰度数据量减半,但暗/灰/亮三元组的数量保持不变。这使我自己的算法效率降低。字典压缩不会遇到同样的问题,但大多数字典压缩算法,如八位字节(不是 4 位实体)。
      【解决方案5】:

      我会选择 Clifford 的答案,即首先将字体转换为每像素 4 位,这足以完成这项任务。

      然后,由于这是一种字体,因此您有很多行重复,即定义一个字符的行与另一个字符的行匹配。以字母“p”和“b”为例,这些字母的中间部分应该相同(如果目标语言使用大量变音符号,您将获得更多匹配)。然后,您的编码器可以首先收集字体的所有不同行,存储这些行,然后每个字符图像由指向行的指针列表形成。

      效率当然取决于字体,根据来源,您可能需要一些预处理才能使用这种方法更好地压缩它。

      如果您想要更多,您可能宁愿选择每像素 3 位或什至每像素 2 位,具体取决于您的目标(有些人会手动调整字体图像),这些可能仍然令人满意。

      这个方法总体来说对于实时显示当然效果很好(你只需要遍历一个指针来获取行数据)。

      【讨论】:

      • 看到我的答案的结尾。只要有一种实用的方法来构造静态字典,它们就很棒......
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2010-12-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多