资源压缩基础:图像压缩原理与嵌入式取舍
做嵌入式GUI,说白了就是跟资源较劲。屏幕越来越大,效果要求越来越高,但Flash和RAM就那么点。图像压缩,就是咱们手里的第一把刀。
我刚开始做嵌入式GUI那会儿,总觉得压缩是上层应用的事。直到有一次,一个简单的开机动画就把256MB的Flash撑爆了……嗯,从那以后我再也不敢小看压缩这回事了。
有损 vs 无损:本质区别在哪?
先搞清楚一个核心问题:压缩到底在干什么?
无损压缩,就是压缩完解压出来,跟原来一模一样。一个像素都不差。有损压缩呢?它会丢掉一些人眼不太敏感的信息,换来更高的压缩比。
核心区别一句话:无损压缩靠的是数据冗余,有损压缩靠的是视觉冗余。
举个例子。一张纯蓝色的图,每个像素都是(0,0,255)。无损压缩会记录成"连续1000个蓝色像素",而不是存1000遍。有损压缩呢?它可能会把相邻几个差不多的蓝色合并成一个,反正人眼看不出区别。
我在项目里遇到过这种情况:用JPEG压缩一张带文字的按钮图,结果文字边缘全是鬼影。为什么?因为JPEG对有高频细节的区域(比如文字边缘)压缩效果很差。这时候就得换无损格式。
| 特性 | 无损压缩 | 有损压缩 |
|---|---|---|
| 还原度 | 100%还原 | 有信息丢失 |
| 压缩比 | 一般2:1~5:1 | 可达10:1~50:1 |
| 适用场景 | 图标、文字、UI控件 | 照片、渐变背景、纹理 |
| 典型格式 | PNG、BMP(RLE) | JPEG、WebP(有损模式) |
PNG、JPEG、BMP:嵌入式里的三选一
这三种格式,做嵌入式GUI的几乎天天见。但怎么选,很多人其实没想清楚。
BMP:最老实,也最占地方
BMP就是不压缩,或者最多做个简单的RLE(游程编码)。每个像素原样存着。好处是解码快,几乎不消耗CPU。坏处是……太占空间了。
一个800×480的32位BMP,算一下:800×480×4 = 1,536,000字节,差不多1.5MB。放几张图,Flash就没了。
注意:BMP的RLE压缩只支持8位及以下的索引色。真彩色BMP的RLE压缩效果很差,基本等于没压。
我个人习惯,BMP只用在两种场景:一是Flash特别大、CPU特别弱的平台;二是做临时帧缓冲,直接操作显存。
PNG:无损之王,但解码有代价
PNG用的是Deflate算法,无损压缩。对UI图标、小控件来说,压缩比很不错。但问题在于解码。
PNG解码需要做几件事:
- 解压Deflate数据流
- 重建滤镜(每行像素的差值还原)
- 如果是索引色,还要查调色板
这些操作在MCU上跑,挺吃力的。我记得有一次在Cortex-M4上解码一张320×240的PNG,花了将近200ms。用户按个按钮,等200ms才看到变化,体验很差。
小技巧:如果非要用PNG,可以预解码成RGB565或索引色格式存到外部Flash。运行时直接读像素数组,不用再解压。
JPEG:压缩率高,但质量要小心
JPEG的压缩率确实诱人。一张照片级的背景图,BMP要几MB,JPEG可能就几十KB。但JPEG有两个坑:
- 块效应:压缩率太高时,8×8的块边界会很明显
- 不支持透明:JPEG没有Alpha通道
我做开机画面时用过JPEG。效果还行,但要注意质量因子。质量因子低于70时,文字和线条边缘就开始糊了。UI上用的JPEG,我建议质量因子不低于85。
| 格式 | 压缩类型 | 解码速度 | 存储效率 | 透明支持 | 嵌入式推荐度 |
|---|---|---|---|---|---|
| BMP | 无/简单RLE | 极快 | 极低 | 支持 | ★★☆☆☆ |
| PNG | 无损(Deflate) | 较慢 | 中等 | 支持 | ★★★★☆ |
| JPEG | 有损(DCT) | 中等 | 高 | 不支持 | ★★★☆☆ |
调色板与索引色:嵌入式GUI的杀手锏
说完了格式,咱们聊聊一个特别适合嵌入式的东西——索引色。
你想想看,一张图标可能只有16种颜色。用32位真彩色存,每个像素4字节。但如果用索引色呢?每个像素只存一个字节的索引值,然后查调色板得到真实颜色。
这就是调色板(Palette)和索引色(Indexed Color)的核心思想。
调色板怎么工作?
调色板就是一个颜色查找表。比如4位索引色:
// 调色板:16种颜色,每种用RGB565表示
uint16_t palette[16] = {
0x0000, // 索引0: 黑色
0xFFFF, // 索引1: 白色
0xF800, // 索引2: 红色
0x07E0, // 索引3: 绿色
0x001F, // 索引4: 蓝色
// ... 其他12种颜色
};
// 像素数据:每个字节存一个索引值
uint8_t pixel_data[] = {
0x01, 0x02, 0x03, 0x04, // 第一行像素
0x04, 0x03, 0x02, 0x01, // 第二行像素
// ...
};
显示的时候,每个像素先读索引值,再到调色板里查颜色,然后送显存。就这么简单。
位深度与存储节省
索引色支持不同的位深度:
| 位深度 | 颜色数 | 每像素字节数 | 适用场景 |
|---|---|---|---|
| 1位 | 2色 | 1/8字节 | 黑白图标、文字 |
| 2位 | 4色 | 1/4字节 | 简单图形 |
| 4位 | 16色 | 1/2字节 | 图标、小控件 |
| 8位 | 256色 | 1字节 | 复杂UI、渐变 |
对比一下:一张32×32的图标,用32位真彩色要4096字节。用4位索引色只要512字节(像素数据)+ 32字节(调色板)。省了将近8倍。
关键点:索引色的存储节省是立竿见影的。但前提是——你的图像颜色数不能超过调色板容量。否则就得做颜色量化,把相近的颜色合并。
颜色量化:怎么把256色变成16色?
颜色量化是个算法活。常用的有:
- 均匀量化:把RGB空间均匀切块,每个块取平均值。简单,但效果一般。
- 中位切分法:每次选颜色分布最分散的维度,切一刀。递归直到得到目标颜色数。效果不错。
- 八叉树量化:用八叉树结构合并颜色节点。速度快,效果好,我比较推荐。
我曾经在项目里用八叉树量化,把一张256色的照片压到16色。人眼几乎看不出区别,但存储从原来的80KB降到了10KB。值不值?太值了。
实用建议:做UI设计时,尽量控制每个控件的颜色数。图标不超过16色,按钮不超过32色。这样可以直接用索引色,省掉量化的麻烦。
知识体系总览
下面这张图,把本章的核心逻辑串起来了。你可以把它当作一个决策流程图:
从原始图像出发,先判断能不能接受有损。能接受,走JPEG路线;不能接受,走PNG或索引色路线。索引色是嵌入式特有的优化手段,配合颜色量化,效果往往比直接用PNG还好。