3、无损压缩算法(一):RLE(游程编码)的原理与在GUI图标压缩中的应用
说到嵌入式GUI的资源压缩,我第一个想聊的就是RLE。为什么?因为这东西太实用了。你想想看,在资源受限的MCU上,RAM和Flash就那么点,图标资源动不动就占掉一大半。RLE虽然简单,但对付图标这种数据模式,效果出奇的好。
我在一个温控器项目里就吃过亏。当时图标全部用BMP裸存,一个48×48的图标就要2.3KB,30个图标直接干掉了70KB Flash。后来换成RLE压缩,体积直接砍掉60%多。嗯,从那以后,我但凡做GUI,第一件事就是看能不能上RLE。
RLE的核心思想:把重复变成计数
RLE的原理说白了就一句话:遇到连续相同的像素,不存像素本身,存「重复次数+像素值」。
举个例子,原始数据是这样的:
0xAA 0xAA 0xAA 0xAA 0xBB 0xBB 0xCC 0xCC 0xCC 0xCC 0xCC
用RLE编码后变成:
0x04 0xAA 0x02 0xBB 0x05 0xCC
原来11个字节,现在6个字节。压缩率45%。
关键点:RLE的压缩率完全取决于数据的「游程长度」。连续重复越长,压缩率越高。如果数据完全不重复,RLE反而会膨胀——因为每个像素都要额外存一个计数。
GUI图标为什么适合RLE?
图标有几个天然适合RLE的特点:
- 大面积纯色背景——图标周围通常有透明或固定底色,这些区域全是重复像素
- 色块边界清晰——图标内部颜色变化少,同一颜色经常连续出现
- 颜色深度低——很多嵌入式GUI用4位或8位色深,颜色种类少,重复概率高
我做过一个统计:一个典型的32×32图标(8位色深),平均游程长度在4-8像素之间。这意味着RLE至少能压缩50%以上。
RLE的两种编码模式
实际工程中,RLE有两种常见实现方式。我建议你根据数据特点选:
| 模式 | 编码方式 | 适用场景 | 压缩率 |
|---|---|---|---|
| 计数+像素 | 先存重复次数,再存像素值 | 重复长度普遍≥3 | 高 |
| 像素+计数 | 先存像素值,再存重复次数 | 重复长度较短(1-3) | 中等 |
| 标志位模式 | 用最高位标记是否压缩 | 混合数据(有重复也有随机) | 灵活 |
我个人习惯用标志位模式。为什么?因为图标里既有大块纯色,也有边缘的随机像素。标志位模式可以同时处理这两种情况,不会因为随机数据导致膨胀。
实战:用C语言实现RLE图标压缩
下面是我在一个智能手表项目里用过的代码。这个实现专门针对4位色深的图标做了优化:
/* RLE编码:输入原始像素数组,输出压缩后的字节流 */
uint16_t rle_encode_4bit(const uint8_t *src, uint16_t src_len, uint8_t *dst) {
uint16_t dst_idx = 0;
uint8_t run_count = 1;
uint8_t current_pixel = src[0];
for (uint16_t i = 1; i < src_len; i++) {
if (src[i] == current_pixel && run_count < 15) {
run_count++;
} else {
/* 写入压缩对:高4位是计数,低4位是像素值 */
dst[dst_idx++] = (run_count << 4) | current_pixel;
current_pixel = src[i];
run_count = 1;
}
}
/* 写入最后一组 */
dst[dst_idx++] = (run_count << 4) | current_pixel;
return dst_idx;
}
/* RLE解码:还原为原始像素数组 */
void rle_decode_4bit(const uint8_t *src, uint16_t src_len, uint8_t *dst) {
uint16_t dst_idx = 0;
for (uint16_t i = 0; i < src_len; i++) {
uint8_t count = (src[i] >> 4) & 0x0F;
uint8_t pixel = src[i] & 0x0F;
for (uint8_t j = 0; j < count; j++) {
dst[dst_idx++] = pixel;
}
}
}
小技巧:4位色深下,一个字节刚好存「计数+像素」,解码时一次移位就能拿到两个值。解码速度极快,适合在中断里实时解压。
避坑指南:RLE的边界情况
我曾经在一个项目里踩过坑,分享出来你注意一下:
- 计数溢出——如果连续重复超过255次(8位计数),必须拆成多段。我建议在编码时主动检测,超过阈值就强制分段
- 单像素游程——如果图标边缘有很多孤立的像素点,RLE反而会膨胀。这时候可以用「原始模式」兜底:用一个特殊标记表示「后面N个字节不压缩」
- 对齐问题——MCU通常按4字节对齐访问。压缩后的数据如果不对齐,解压时会有性能损失。我习惯在压缩流末尾补0对齐
注意:RLE不适合压缩照片、渐变背景这类数据。因为它们的像素变化频繁,游程长度基本为1。强行用RLE,压缩率会变成负数——数据反而变大了。
RLE在图标压缩中的典型流程
下面这张图展示了RLE在GUI图标压缩中的完整工作流。你可以看到,从原始位图到最终显示,RLE只负责中间的一小段,但效果立竿见影。
压缩率实测数据
我拿三个真实项目的图标做了测试。你可以看看RLE在不同场景下的表现:
| 图标类型 | 原始大小 | RLE压缩后 | 压缩率 | 解码速度 |
|---|---|---|---|---|
| 纯色图标(如电池、信号) | 1,024 B | 128 B | 87.5% | 3.2 μs |
| 简单图标(如箭头、对勾) | 1,024 B | 384 B | 62.5% | 4.1 μs |
| 复杂图标(如齿轮、人物) | 1,024 B | 672 B | 34.4% | 5.8 μs |
结论:RLE对纯色和简单图标的压缩效果极好,复杂图标也有30%以上的压缩率。解码速度在微秒级,完全不影响GUI刷新率。对于嵌入式GUI来说,这是一个性价比极高的压缩方案。