嵌入式使用的轻量级(解)压缩算法

2024-03-14

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

要压缩的数据的属性

  • 每个像素 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 八​​位位组)查找表将 3 基值解码为 4 基值。压缩比很大程度上取决于字体大小,但根据我的典型数据,我可以获得大约 1:2 的压缩比。由于这比 LZ 变体(大约 1:3)差得多,所以我想尝试静态字典方法。

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

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


我确实承认这是一个很好回答我的问题的边缘情况,但由于我对这个问题进行了一些研究,这个答案既描述了我选择的方法,又提供了关于问题本质的更多信息(如果有人遇到这种情况)它。

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

我最终得到的是我在问题中描述的内容的变体。首先,每个字形被分成 trit 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 值系统。

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

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

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

如何建立词典?

如果解压缩算法非常简单且快速,那么真正的挑战在于字典的构建。很容易证明存在最佳字典(对于给定字体给出最少数量的压缩八位字节的字典),但是比我更聪明的人似乎已经证明找到这样的字典的问题是NP完全的。

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

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

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

效果如何?

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

  • 原始未压缩大小:127101
  • 中间值数量:46697
  • Shannon entropies (octet-by-octet):
    • 总计:528914 位 = 66115 个八位位组
    • 三进制数据:176405 位 = 22051 个八位组
    • 中间值:352509 位 = 44064 个八位字节
  • 简单压缩的三进制数据(0=0、10=1、11=中间)(127101 trits):207505 位 = 25939 个八位字节
  • dictionary compressed ternary data: 18492 octets
    • 熵:136778 位 = 17097 个八位位组
  • 字典大小:647 个八位字节
  • 完全压缩数据:647 + 18492 + 46697 = 65836 个八位位组
  • 压缩率:48.2%

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

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

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

实际上,较小的字体很难压缩。这应该不足为奇,因为大部分信息是灰度信息。使用此算法可以有效地压缩非常大的字体大小,但游程压缩是更好的候选者。

什么会更好?

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

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

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

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

这个方向的一个提示是 LZMA2 压缩文件(带有xz在最高压缩率下)完整字体数据(127101 个八位位组)仅为 36720 个八位位组。当然,这种格式不满足任何其他要求(快速解压缩、可以逐个字形解压缩、低 RAM 要求),但它仍然表明数据中的冗余比我的廉价算法能够实现的要多。开发。

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

嵌入式使用的轻量级(解)压缩算法 的相关文章

  • 压缩一组大整数

    我有一组整数 我希望对其有最紧凑的表示 我有以下限制 功能 它被设置 或者换句话说 一个唯一整数的列表 其中顺序并不重要 集合L的大小相对较小 通常为1000个元素 整数遵循 0 到 N 1 之间的均匀分布 其中 N 相对较大 例如 2 3
  • 所有可能的骑士在普罗梅拉的棋盘上移动

    是否有可能用马从初始位置 I J 绕过大小为 N N 的棋盘 并且只访问每个方格一次 define A True A I J false active proctype method bit I 4 bit J 3 bit K 1 bit
  • 对矩阵进行舍入,保留行和列总计

    想要 以保留行和列总计的方式对矩阵进行舍入的 伪 代码 问题从向量开始 X and Y of 非负整数 with Sum X Sum Y 想要圆X Y Sum X 同时保留行和列总计 这是婚姻问题的一种 Xa需要进行一定次数的握手 拨打该号
  • 全局变量在函数调用之间是否刷新?

    我正在编写嵌入式固件 发现有时很难决定何时需要易失性 当我有一个等待中断更改某个布尔标志的函数时 很明显该标志需要是易失性的 因为否则该函数将永远等待 因为编译器没有意识到该值可以通过打断 但是 当我有一个只检查第一行中的标志的短函数时 我
  • Python 中的空填字游戏求解器

    我得到了一个包含填字游戏蓝图的矩阵 当然 它是空的 我们的目标是填补整个难题 这是 Checkio 的一项任务 我已经为此奋斗了相当长一段时间 根据我对复杂性的理解 这个问题没有完美的算法 不过 必须有最好的方法来做到这一点 对吧 我尝试了
  • 更合适地说插入未排序动态数组的摊销 O(1) 与 O(n) ?

    这属于 stackoverflow com help on topic 中的 软件算法 在本例中 是一种将项目添加到动态未排序数组的软件算法 This is chart we made in class about the runtimes
  • 在LPC2148 ARM处理器上创建中断向量的汇编代码

    我最近刚刚开始使用 LPC2148 ARM 处理器 我试图理解一些有关创建中断向量的汇编代码 这是代码 Runtime Interrupt Vectors Vectors b start reset start ldr pc undf un
  • 有效地合并两个数组 - 一个已排序,另一个未排序

    我正在解决一个问题 该问题有一个由 n 个元素组成的排序数组 后跟一个未排序的长度数组 O logn O 平方 n 如何最有效地对整个列表进行排序 在上述两种情况下我应该使用哪种排序 由于将单个元素插入数组并保持其排序是O n 你不可能变得
  • 是否有一种算法可以在线性时间内计算数组反转?

    我知道有多少倒转 en wikipedia org wiki Inversion 28discrete mathematics 29 in an n 元素数组可以在 O n log n 操作使用增强型归并排序 http www geeksf
  • LRU算法,实现这个算法需要多少位?

    我有一个关于 LRU 算法的小问题 如果您有一个包含四个块的高速缓存 那么需要多少位来实现该算法 假设您指的是 4 路组关联缓存 完美 LRU 本质上是按照使用顺序为每一行分配一个精确的索引 您也可以将其视为 年龄 因此 4 个元素中的每一
  • 当给定块大小时反转单链表

    有一个单连接链表 并给出了块大小 例如 如果我的链表是1 gt 2 gt 3 gt 4 gt 5 gt 6 gt 7 gt 8 NULL我的块大小是4然后反转第一个4元素 然后是第二个 4 个元素 问题的输出应该是4 gt 3 gt 2 g
  • 坐标算法 - 绕中心旋转

    通过查看这张图片 我想您会很好地理解我的问题 图片已删除 网址不再有效 现在返回广告 所以基本上我想要一个函数 它接受一个对象作为参数 并根据我之前添加的对象数量为该对象提供正确的坐标 假设我将所有这些对象添加到一个数组中 objectAr
  • 我应该对算法使用递归还是记忆化?

    如果我可以选择使用递归或记忆来解决问题 我应该使用哪一个 换句话说 如果它们都是可行的解决方案 因为它们提供了正确的输出并且可以在我正在使用的代码中合理地表达 那么我什么时候会使用其中一个而不是另一个 它们并不相互排斥 您可以同时使用它们
  • 如何光栅化旋转矩形(通过 setpixel 在 2d 中)

    我有四个 2d 顶点 A B C D 的旋转矩形 我需要在像素缓冲区中 有效地 光栅化 绘制它 使用 setpixel x y 颜色 怎么做 我正在尝试使用一些代码 例如 convertilg a b c d do up down left
  • 使用 HTTPS 时我需要/想要 gzip 压缩吗?

    使用 HTTPS 是否已经包含 透明 内容压缩 或者我是否仍然应该担心与浏览器协商是否压缩我的 Servlet 输出 如果 HTTPS 已经有压缩 是无条件的还是需要配置 协商 启用 默认情况下 TLS 不启用压缩 但它 压缩 是在 TLS
  • 优化计算中使用的 # 个线程的算法

    我正在执行一个操作 我们将其称为CalculateSomeData CalculateSomeData 在连续的 代 中运行 编号为 1 x 整个运行中的代数由CalculateSomeData 的输入参数固定 并且是先验已知的 完成一次生
  • 颜色逻辑算法

    我们正在构建一个体育应用程序 并希望将团队颜色融入到应用程序的各个部分 现在 每个团队都可以使用几种不同的颜色来表示 我想做的是执行检查以验证两个团队颜色是否在彼此一定的范围内 这样我就不会显示两个相似的颜色 因此 如果团队 1 的主要团队
  • JavaScript 中的埃拉托斯特尼筛法对大量数据无限运行

    我一直在尝试写埃拉托斯特尼筛法 http en wikipedia org wiki Sieve of EratosthenesJavaScript 中的算法 基本上我只是按照以下步骤操作 创建从 2 到 n 1 的连续整数列表 令第一个素
  • 无法理解Peterson算法的正确性

    我在这里讨论彼得森算法的一个场景 flag 0 0 flag 1 0 turn P0 flag 0 1 turn 1 while flag 1 1 turn 1 busy wait
  • 在一个区域中拟合二维多边形的算法?

    这有标准吗 算法名称 说 我有 10 个不同大小的多边形 我有一个特定大小的区域 我想知道如何填充该区域中的最多多边形 以及它们是如何拟合的 笔记 多边形可以根据限制集进行旋转 一个可能的名称是包装问题 http en wikipedia

随机推荐

  • FormClosing 中的 DoEvents() 也是邪恶的吗?

    在另一个问题上我建议使用 private void Form1 FormClosing object sender FormClosingEventArgs e while processingLock Application DoEven
  • Access SQL 似乎将日期视为 dd/mm/yyyy?

    我在 MS Access 中有一个表 其中包含员工详细信息 tblStaff Employee Number Employee Name Dept 205147 Joe Bloggs IT 205442 John Doe Accounts
  • 带弹簧靴泽西的执行器

    我在我的网络应用程序中使用 Jersey starter org springframework boot spring boot starter jersey 1 4 2 RELEASE 尝试将 Actuator 端点集成到我的应用程序中
  • 将动态加载的函数提交到 ProcessPoolExecutor

    我想提交一个动态加载的函数到concurrent futures ProcessPoolExecutor 这是一个例子 有module py其中包含该功能 Content of module py def func return 1 然后
  • 移动 Numpy 数组的最快方法

    我正在运行一些模拟 其中涉及反复将 2D Numpy 数组中的值与其 邻居 进行比较 例如 索引位置的值 y x 与索引位置的值进行比较 y 1 x 来自同一个数组 目前我正在使用这样的函数 example of the typical s
  • 扩展显示上的 Eclipse 缩放问题

    我的设置是一台戴尔 XPS 13 笔记本电脑 3200 x 1800 Windows 10 两台扩展显示器 1680x1050 通过戴尔 Thunderbolt 坞站连接到笔记本电脑 我的问题是在xps13 eclipse上显示正常 但在扩
  • 错误:System.Environment.SpecialFolder”不包含“CommonApplicationData”的定义

    我有将文件保存在目录中的文件夹中的代码 string timestamp DateTime Now ToString MM dd yyyy HH mm ss var file File Create Owe Data txt timesta
  • 如何将 getimagesize() 与 $_FILES[''] 一起使用?

    我正在做一个图像上传处理程序 我希望它能够检测用户上传的图像的尺寸 所以我从以下开始 if isset FILES image etc 我有 list width height getimagesize 我应该如何一起使用它们 多谢 你可以
  • 使用 MVC3/.NET 异步文件上传器?

    大家 我是一名学生 对 NET 特别是 MVC3 开发很陌生 目前 在我的一个项目部分中有一个表单 其中包含几个文本字段和两个文件输入 考虑到上传的文件可能很大 我想使用异步文件上传器 它可以显示文件上传的进度 这样用户在后面上传文件时就不
  • 从证书中读取备用名称

    我想编写一段代码来读取用户主体名称来自其他名称 under 科目选择证书上的名称 我有 X509 证书 我做了 证书是 X509Certificate 对象 Collection san certificate getSubjectAlte
  • 远程连接到 Amazon RDS MySql

    我正在尝试从我的计算机连接到 Amazon RDS 上的 MySql 使用MySql WorkBench or HeidiSql甚至是console Mysql exe我一直收到这个错误 ERROR 2003 HY000 Can t con
  • 当一个接口“继承”另一个接口时,您怎么称呼它?

    如果我有 B 类 A 我说 B类遗传A 类 或 B 类派生自 A 类 但是 如果我有 class B ISomeInterface 说 B继承ISomeInterface 是错误的 正确的说法是 B实施ISomeInterface 但是 说
  • scipy.curve_fit() 返回多行

    我是 python 新手 尝试使用以下代码来适应数据集分布 实际数据是一个包含两列的列表 预测市场价格和实际市场价格 我试图使用scipy curve fit 但它给了我在同一个地方绘制的许多线条 任何帮助表示赞赏 import the n
  • 我可以从 Dapper 查询返回多个派生类型的集合吗

    我有一个与此类似的类结构 public abstract class Device public int DeviceId get set Additional Properties public class DeviceA Device
  • Mysql:将 NOT NULL 列更新为 null 时未收到错误

    为什么mysql在更新非空列时接受空数据 然后将数据转换为0 我期待一个错误 但它没有显示出来 如果有人尝试将非空列更新为空 我如何得到错误 我需要它 以便在出现错误时可以回滚事务 数据库中是否需要任何配置来执行此操作 谢谢 您还没有指定您
  • 如何正确检索表 ID

    根据数据库理论 数据库中的任何表都可以通过其完全限定名称来成功识别 catalog name schema name table name 在 SQL Server 中检索表 id 的方法是 SELECT object id table n
  • 默认移动构造函数与默认复制构造函数与默认赋值运算符

    为什么 C 编译器对自动生成的移动构造函数比对自动生成的复制构造函数或赋值运算符有更多限制 仅当用户未定义任何内容时 才会生成自动生成的移动构造函数 即 构造函数 复制 赋值 析构函数 仅当用户未分别定义复制构造函数或赋值运算符时 才会生成
  • 日期查询适用于 _id 但不适用于日期值 - MongoDB

    所以 我几个小时以来一直在尝试 但没有得到任何结果 我有一个 MongoDB 集合 它有一个日期值 scrape systemTime 我将其插入scrape systemTime new Date 我试图通过使用以下方法获得早一周的结果
  • 与逃亡者一起离开差异视图

    有了 vim 逃亡者 有没有一种简单的方法来 取消分割 Gedit 返回工作树中的当前对象 E g when in Gcommit Gstatus buffers you would press D to enter side by sid
  • 嵌入式使用的轻量级(解)压缩算法

    我有一个带有图形用户界面的低资源嵌入式系统 该界面需要字体数据 为了节省只读存储器 闪存 需要压缩字体数据 我正在寻找一种用于此目的的算法 要压缩的数据的属性 每个像素 8 位的矩形像素图的透明度数据 字体中通常有大约 200 300 个字