3. 图片解码优化:硬件解码 vs 软件解码,常用解码库性能对比与选型
图片解码,说白了就是把硬盘里压缩好的 JPEG、PNG 文件,变成屏幕上能直接显示的像素点阵。这一步要是慢了,整个界面切换都会卡顿。我见过不少项目,CPU 占用率飙到 80%,结果一查,全耗在解码上了。
今天咱们就聊聊解码这条路该怎么选。是走硬件加速,还是靠软件硬扛?常用的那几个库到底差多少?
3.1 硬件解码 vs 软件解码:两条路,各有各的坑
先搞清楚本质区别。
- 软件解码:全靠 CPU 算。JPEG 的 Huffman 解码、IDCT 变换,PNG 的 zlib 解压、filter 还原,全是 CPU 一条条指令跑出来的。
- 硬件解码:把任务丢给 GPU、VPU 或者专用的 JPEG 解码器。CPU 只负责发指令,真正的计算在专用硬件里完成。
听起来硬件解码肯定更快对吧?不一定。我踩过这个坑。
核心结论:硬件解码的延迟更低、吞吐量更高,但启动开销大、灵活性差。软件解码虽然吃 CPU,但移植性好、控制力强。
咱们用一张图把决策逻辑理清楚。
我的经验:在 STM32MP157 平台上,硬件 JPEG 解码一张 1080p 图片只需要 8ms,而软件解码要 45ms。但硬件解码器初始化就要 3ms,如果只解一张小图标,反而更慢。所以我的做法是:大图走硬件,小图走软件。
3.2 三大常用解码库:libjpeg-turbo、libpng、stb_image
选库这件事,我折腾过不少。早期项目用过 libjpeg 原版,慢得让人抓狂。后来换了 libjpeg-turbo,速度直接翻倍。下面逐个说说我的使用感受。
3.2.1 libjpeg-turbo:JPEG 解码的王者
libjpeg-turbo 是 libjpeg 的优化版。它用了 SIMD 指令集(ARM 的 NEON、x86 的 SSE2),把 IDCT 和色彩转换这些热点函数加速了好几倍。
我做过一个对比测试:同样一张 1920x1080 的 JPEG,libjpeg 原版解码耗时 52ms,libjpeg-turbo 只要 18ms。这差距,你想想看。
// libjpeg-turbo 解码示例(简化版)
#include <jpeglib.h>
void decode_jpeg(const char *filename, unsigned char **pixels, int *w, int *h) {
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;
FILE *fp = fopen(filename, "rb");
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_decompress(&cinfo);
jpeg_stdio_src(&cinfo, fp);
jpeg_read_header(&cinfo, TRUE);
cinfo.out_color_space = JCS_RGB; // 直接输出 RGB
jpeg_start_decompress(&cinfo);
*w = cinfo.output_width;
*h = cinfo.output_height;
int row_stride = cinfo.output_width * cinfo.output_components;
*pixels = (unsigned char*)malloc(row_stride * cinfo.output_height);
while (cinfo.output_scanline < cinfo.output_height) {
unsigned char *buffer_array[1] = { *pixels + cinfo.output_scanline * row_stride };
jpeg_read_scanlines(&cinfo, buffer_array, 1);
}
jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
fclose(fp);
}
注意:libjpeg-turbo 默认输出是 BGR 还是 RGB,取决于编译选项。我遇到过因为色彩空间没对齐,图片颜色全偏蓝的情况。建议显式设置 out_color_space = JCS_RGB。
3.2.2 libpng:PNG 解码的标配
PNG 解码比 JPEG 复杂。它要处理 zlib 解压、filter 还原(Sub、Up、Average、Paeth 四种 filter)、以及可选的 alpha 通道。libpng 是官方库,稳定可靠,但速度一般。
我记得有一次优化开机动画,PNG 序列解码占了 40% 的 CPU。后来发现是 filter 还原没走 NEON 优化。libpng 1.6 之后支持了硬件加速,但需要手动开启。
// libpng 解码示例(关键片段)
#include <png.h>
void decode_png(const char *filename, unsigned char **pixels, int *w, int *h) {
FILE *fp = fopen(filename, "rb");
png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
png_infop info = png_create_info_struct(png);
png_init_io(png, fp);
png_read_info(png, info);
*w = png_get_image_width(png, info);
*h = png_get_image_height(png, info);
png_byte color_type = png_get_color_type(png, info);
png_byte bit_depth = png_get_bit_depth(png, info);
// 统一转为 8bit RGBA
if (bit_depth == 16) png_set_strip_16(png);
if (color_type == PNG_COLOR_TYPE_PALETTE) png_set_palette_to_rgb(png);
if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) png_set_expand_gray_1_2_4_to_8(png);
if (png_get_valid(png, info, PNG_INFO_tRNS)) png_set_tRNS_to_alpha(png);
if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_PALETTE)
png_set_filler(png, 0xFF, PNG_FILLER_AFTER);
png_read_update_info(png, info);
int row_bytes = png_get_rowbytes(png, info);
*pixels = (unsigned char*)malloc(row_bytes * (*h));
png_bytep *row_pointers = (png_bytep*)malloc(sizeof(png_bytep) * (*h));
for (int y = 0; y < *h; y++) row_pointers[y] = *pixels + y * row_bytes;
png_read_image(png, row_pointers);
png_destroy_read_struct(&png, &info, NULL);
free(row_pointers);
fclose(fp);
}
小技巧:如果不需要 alpha 通道,用 png_set_strip_alpha() 去掉,能省 25% 的解码时间。我在做菜单图标时就这么干的。
3.2.3 stb_image:单文件、零依赖、够用
stb_image 是 Sean Barrett 写的神器。整个库就一个 .h 文件,没有外部依赖,支持 JPEG、PNG、BMP、GIF、PSD 等十几种格式。代码量极小,非常适合嵌入式环境。
但它的性能不如 libjpeg-turbo 和 libpng。为什么?因为它为了保持单文件简洁,没有做 SIMD 优化。解码一张 1080p JPEG,stb_image 要 35ms,而 libjpeg-turbo 只要 18ms。
// stb_image 解码示例
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
void decode_with_stb(const char *filename, unsigned char **pixels, int *w, int *h) {
int channels;
*pixels = stbi_load(filename, w, h, &channels, STBI_rgb_alpha);
if (*pixels == NULL) {
// 错误处理
return;
}
// 用完记得释放:stbi_image_free(*pixels);
}
一句话总结:stb_image 适合原型验证、小项目、或者对性能要求不高的场景。生产环境的大图解码,还是老老实实用 libjpeg-turbo 和 libpng。
3.3 性能对比:数据说话
我在 i.MX8M Mini 平台(Cortex-A53 @1.6GHz)上做了个对比测试。测试图片:1920x1080 JPEG(质量 85%)、1024x768 PNG 32bit。结果如下:
| 解码库 | 图片格式 | 解码耗时 (ms) | 内存占用 (KB) | CPU 占用率 | 代码体积 |
|---|---|---|---|---|---|
| libjpeg-turbo | JPEG | 18 | 6200 | 35% | ~300KB |
| libjpeg(原版) | JPEG | 52 | 6100 | 78% | ~280KB |
| stb_image | JPEG | 35 | 6300 | 55% | ~120KB |
| libpng | PNG | 42 | 5800 | 60% | ~350KB |
| stb_image | PNG | 68 | 5900 | 82% | ~120KB |
注意:内存占用是解码过程中的峰值,不是最终像素数据。最终像素数据只跟分辨率有关:1920x1080x4 ≈ 8MB。这个逃不掉。
3.4 选型建议:怎么选?
我个人的选型原则是这样的:
- JPEG 大图(壁纸、照片):无脑选 libjpeg-turbo。速度快,稳定,社区活跃。
- PNG 图标、按钮:libpng 是首选。如果项目对代码体积敏感,可以用 stb_image,但要做好性能测试。
- 原型验证、快速迭代:stb_image 最方便。一个 .h 文件搞定,不用折腾交叉编译。
- 混合场景:我建议同时集成 libjpeg-turbo 和 stb_image。大图用前者,小图用后者。代码里做个格式和尺寸判断就行。
避坑指南:我曾经在一个项目里全用 stb_image,结果客户反馈开机动画卡顿。一查,原来是 PNG 序列解码太慢。后来把大图换成 JPEG + libjpeg-turbo,小图保留 stb_image,问题解决。所以,别偷懒,该分开处理就分开处理。
3.5 小结
解码优化这件事,没有银弹。硬件解码快但不灵活,软件解码灵活但吃 CPU。库的选择也一样,libjpeg-turbo 快但体积大,stb_image 小巧但性能一般。
我的建议是:先搞清楚你的瓶颈在哪。是 CPU 不够?还是内存不够?还是代码体积受限?然后对症下药。别一上来就追求极致性能,也别为了省事全用轻量库。平衡,才是嵌入式优化的精髓。