JPEG 基础概念
-
JPEG 的双重身份
-
既是图像压缩编码标准:由联合图像专家组(Joint Photographic Experts Group)制定,1992 年发布,面向「连续色调静止图像」(如照片、风景图,含渐变色彩的静态图)。
-
也是图像文件格式:文件扩展名为
.jpg或.jpeg,两者完全等价(可互相重命名,文件内容不变)。
-
-
核心优势:高压缩比JPEG 是有损压缩格式,在保证视觉效果接近原图的前提下,文件体积远小于 BMP(无压缩)等格式。例如一张 1024×768 的照片,BMP 约 2.25MB,JPEG 仅需 100-300KB,因此特别适合网络传输和移动设备存储。

-
局限性因有损压缩,反复编解码会导致画质退化;不适合存储文字、图标等「像素级精确」的图像(易出现边缘模糊)。
核心编解码库:libjpeg
要处理 JPEG 文件(解码显示 / 编码生成),需使用专门的库 ——libjpeg,原因是 JPEG 文件经过压缩,无法像 BMP 那样直接读取像素数据。

libjpeg 关键特性
-
开源免费:由独立 JPEG 小组(IJG)维护,官网:www.ijg.org。
-
语言与兼容性:纯 C 语言实现,跨平台(Linux、Windows、嵌入式系统等),支持 JPEG 标准的所有核心功能(压缩、解码、渐进式显示等)。
-
工业级应用:许多知名库 / 工具的底层依赖 libjpeg,例如 OpenCV(计算机视觉库)读取 JPEG 图像时,就是通过 libjpeg 完成解码。

-
非系统标准库:Linux、Windows 等系统默认不预装 libjpeg,需手动移植或安装后才能使用。
JPEG 格式的存储顺序(关键:RGB 通道 + 从上到下行顺序)
JPEG 本身存储的是压缩后的 DCT 系数(非直接像素数据),存储顺序需看解压后的结果(以 libjpeg 解码为例,这是嵌入式开发的标准方式)。
像素内部:色彩通道顺序为 RGB(红→绿→蓝)
JPEG 解码时,可通过配置 libjpeg 的 out_color_space 参数指定输出通道顺序,默认推荐设置为 JCS_RGB(红→绿→蓝):
- 代码配置:
cinfo.out_color_space = JCS_RGB; cinfo.output_components = 3; - 示例:纯红色像素(R=255, G=0, B=0),解压后存储顺序是
0xFF(R)→0x00(G)→0x00(B),与 LCD 显示的默认 RGB 顺序一致,无需通道反转。
注意:JPEG 原生色彩空间是 YCbCr
JPEG 压缩时会先将 RGB 转为 YCbCr(亮度 + 色差),但解压时 libjpeg 会自动将 YCbCr 转回 RGB(通过 JCS_RGB 配置),开发者无需手动处理通道转换。
图像整体:行顺序为 “从上到下”(左上角为起点)
JPEG 解压后的像素行顺序与人类视觉习惯一致:
- 解压后得到的第 1 行像素数据 → 对应图像的第 1 行(最上方);
- 解压后得到的最后 1 行像素数据 → 对应图像的最后 1 行(最下方);
- 优势:无需反转行顺序,可直接按解压后的顺序写入 LCD 显存,避免 “图像颠倒” 问题。
无行对齐要求:每行字节数 = 宽度 × 3
JPEG 解压后的每行像素数据字节数 = 图像宽度 × 每个像素的通道数(RGB 为 3),正好是整数,无需填充字节,读取时直接按 “宽度 ×3” 字节读取即可,无需处理对齐问题(比 BMP 简单)。
存储顺序对比表
| 对比维度 | BMP(24 位,未压缩) | JPEG(libjpeg 解码后,RGB 格式) |
|---|---|---|
| 色彩通道顺序 | B→G→R(蓝→绿→红),无 Alpha 通道(仅存色彩信息) | R→G→B(红→绿→蓝),解码后默认输出纯 RGB 通道(无 Alpha,若需透明需额外处理) |
| 图像行顺序 | 从下到上(文件第 1 行对应图像最后 1 行,图像原点在左下角) | 从上到下(文件第 1 行对应图像第 1 行,图像原点在左上角,符合多数图形系统默认习惯) |
| 行对齐要求 | 强制 4 字节对齐(每行实际字节数 = 向上取整(宽度×3, 4),不足补 0); 示例:宽度=5 → 5×3=15 字节,补 1 字节至 16 字节 | 无需对齐(每行字节数 = 宽度×3,像素数据连续存储,无补位字节) |
| LCD 显示处理 | 需两步处理: 通道反转(BGR 转 RGB,如代码中交换 B、R 字节); 2行顺序反转(从文件末尾行开始读取) | 可直接使用: 通道顺序(RGB)、行顺序(从上到下)均与 LCD 常见显示要求一致,无需额外反转 |
| 常见错误后果 | 通道未反转:色彩错乱(如红色物体显示蓝色、蓝色显示红色); 行未反转:图像上下颠倒(如天空在下方、地面在上方) | 解码格式错(如误设为 JCS_YCbCr 而非 JCS_RGB):图像偏色(泛黄、泛青,无正常 RGB 色彩); 忽略解码后通道顺序:若 LCD 需 BGR 则会色彩错乱 |
| 补充说明 | 数据无压缩,文件体积大(如 1000×1000 图像约 3MB),但解码速度快(仅需处理对齐和反转) | 解码前为压缩格式(体积小),解码后为未压缩 RGB 数据(体积与同尺寸 24 位 BMP 接近),解码耗时略高 |
libjpeg 库移植(Linux 环境)
移植目的:将 libjpeg 源码编译为目标平台(如 ARM 开发板)可使用的库文件(动态库 .so 或静态库 .a),步骤分「下载→解压→配置→编译→安装」5 步。
步骤 1:下载源码
- 官网下载最新稳定版,例如 jpegsrc.v9f.tar.gz(2024 年计划发布的稳定版,当前最新稳定版为 9e)。
- 注意:选择「Unix 格式压缩包(.tar.gz)」,避免 Windows 格式(.zip)。
步骤 2:解压源码
-
将压缩包传到 Linux 系统的非共享文件夹(共享文件夹可能因权限问题导致编译错误),例如
/home/lib/libjpeg_dir,解压后找到自述文件README,打开README了解libjpeg库的使用规则。 -
执行解压命令:
# 格式:tar zxf 压缩包名 tar zxf jpegsrc.v9f.tar.gz -
解压后生成文件夹
jpeg-9f(源码目录)。
步骤 3:配置编译参数
配置的核心是指定「安装路径」和「目标平台」,通过源码目录下的 configure 脚本实现。
-
先创建安装目录(用于存放编译后的库文件和头文件),建议与源码目录同级,例如:
# 创建安装目录 libjpeg(绝对路径:/home/lib/libjpeg_dir/libjpeg) mkdir /home/lib/libjpeg_dir/libjpeg -
进入源码目录,执行配置命令:
cd jpeg-9f # 进入源码目录 # 配置命令:--prefix 指定安装路径(必须绝对路径),--host 指定目标平台(ARM 开发板用 arm-linux) ./configure --prefix=/home/lib/libjpeg_dir/libjpeg --host=arm-linux -
配置成功标志:生成
Makefile脚本(后续编译依赖此文件)和jconfig.h(配置头文件)。
步骤 4:编译源码
-
执行
make命令,自动读取Makefile编译源码:make -
注意:编译过程中若出现错误(如「未定义函数」「权限不足」),需先解决错误(如安装依赖、检查配置参数),再重新编译。编译成功无报错,会生成
cjpeg(JPEG 编码器)、djpeg(JPEG 解码器)等工具,以及库文件的中间文件。
步骤 5:安装库文件
-
执行
make install,将编译后的库文件、头文件安装到步骤 3 指定的-prefix路径:bash
make install -
安装后目录结构(关键文件):
目录 内容说明 include/头文件: jpeglib.h(核心头文件,程序需包含)、jerror.h(错误处理)等lib/库文件: libjpeg.so(动态库,开发板运行需依赖)、libjpeg.a(静态库)bin/工具: cjpeg(BMP 转 JPEG)、djpeg(JPEG 转 BMP)等
移植后准备
将 include/ 和 lib/ 文件夹拷贝到自己的工程目录(如 project/),与源码文件(main.c)同级,方便后续开发维护:
project/
├─ include/ # 从安装目录拷贝的头文件
├─ lib/ # 从安装目录拷贝的库文件
└─ main.c # 自己的 JPEG 处理代码
libjpeg 核心应用:JPEG 解码(LCD 显示必备)
要在 LCD 上显示 JPEG 图像,需先将 JPEG 压缩数据解码为「像素 RGB 分量」,再将 RGB 数据写入 LCD 显存。解码流程是开发核心,需理解并掌握,共 8 步,每步配完整代码 + 详细注释。
解码核心原理
JPEG 解码本质:将压缩的「DCT 系数、量化表」等数据,反向转换为「像素的 RGB 或 YCbCr 分量」(最终需转 RGB 适配 LCD)。
完整解码代码实现
#include <stdio.h>
#include <stdlib.h>
#include <jpeglib.h> // libjpeg 核心头文件,必须包含
// 函数:解码 JPEG 文件,返回 RGB 像素数据(二维数组:[行][列×3],3 代表 R、G、B 分量)
// 参数:
// filename:待解码的 JPEG 文件路径(如 "./pic/test.jpg")
// width:输出参数,用于存储解码后图像的宽度(像素数)
// height:输出参数,用于存储解码后图像的高度(像素数)
// 返回值:
// 成功:RGB 像素数据指针(需手动 free 释放)
// 失败:NULL
unsigned char* jpeg_decode(const char* filename, int* width, int* height) {
// -------------------------- 步骤 1:创建解码对象与错误处理对象 --------------------------
// 定义 JPEG 解码结构体(存储解码过程的所有信息)和错误处理结构体
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr; // 错误处理对象,用于捕获解码中的错误
// 关联错误处理对象:将错误处理结构体绑定到解码对象,让解码错误能被捕获
cinfo.err = jpeg_std_error(&jerr);
// 创建解码对象:初始化解码结构体,分配内部资源
// 参数:&cinfo - 解码结构体指针(必须非 NULL)
// 返回值:无(若失败会通过 jerr 触发错误)
jpeg_create_decompress(&cinfo);
// -------------------------- 步骤 2:打开 JPEG 文件并绑定到解码对象 --------------------------
// 以二进制方式打开文件(JPEG 是二进制文件,必须用 "rb",避免文本模式转换换行符)
FILE* infile = fopen(filename, "rb");
if (infile == NULL) {
fprintf(stderr, "错误:无法打开文件 %s\n", filename);
jpeg_destroy_decompress(&cinfo); // 打开失败,先释放解码对象
return NULL;
}
// 将文件指针绑定到解码对象:告诉 libjpeg 从哪个文件读取压缩数据
// 参数:
// &cinfo - 解码结构体指针
// infile - 已打开的文件指针(必须是 "rb" 模式)
// 返回值:无
jpeg_stdio_src(&cinfo, infile);
// -------------------------- 步骤 3:读取 JPEG 文件头,获取图像信息 --------------------------
// 读取文件头(包含图像宽、高、色彩空间等信息),并将信息存入 cinfo
// 参数:
// &cinfo - 解码结构体指针
// TRUE - 强制读取完整文件头(若为 FALSE,仅读取部分信息)
// 返回值:
// JPEG_HEADER_OK - 读取成功
// 其他值 - 读取失败(如文件不是 JPEG 格式)
jpeg_read_header(&cinfo, TRUE);
// 从 cinfo 中提取图像宽高,赋值给输出参数
*width = cinfo.image_width; // 解码后图像宽度(像素)
*height = cinfo.image_height; // 解码后图像高度(像素)
printf("JPEG 图像信息:宽=%d 像素,高=%d 像素\n", *width, *height);
// -------------------------- 步骤 4:(可选)设置解码参数 --------------------------
// 若用默认参数(如输出 RGB 色彩、不缩放图像),此步骤可省略
// 示例:设置解码后输出 RGB 格式(默认可能为 YCbCr,需手动指定)
cinfo.out_color_space = JCS_RGB; // JCS_RGB 表示输出 RGB 分量
cinfo.output_components = 3; // 每个像素 3 个分量(R、G、B)
// -------------------------- 步骤 5:开始解码 --------------------------
// 初始化解码流程,准备读取像素数据(需在读取扫描线前调用)
// 参数:&cinfo - 解码结构体指针
// 返回值:无(若失败触发错误)
jpeg_start_decompress(&cinfo);
// -------------------------- 步骤 6:循环读取每行像素数据 --------------------------
// 计算每行像素的字节数:宽度 × 每个像素分量数(RGB 为 3)
int row_stride = *width * 3; // 一行数据的总字节数
// 分配缓冲区:存储一行像素数据(因 libjpeg 推荐每次读一行,避免内存浪费)
// 缓冲区类型为 unsigned char*[](二维数组,每行一个指针)
unsigned char* buffer[1]; // buffer[0] 指向一行数据的首地址
buffer[0] = (unsigned char*)malloc(row_stride); // 分配一行内存
if (buffer[0] == NULL) {
fprintf(stderr, "错误:无法分配行缓冲区\n");
jpeg_abort_decompress(&cinfo); // 终止解码
fclose(infile);
jpeg_destroy_decompress(&cinfo);
return NULL;
}
// 分配总像素缓冲区:存储整个图像的 RGB 数据(高度 × 每行字节数)
unsigned char* rgb_data = (unsigned char*)malloc(*height * row_stride);
if (rgb_data == NULL) {
fprintf(stderr, "错误:无法分配 RGB 总缓冲区\n");
free(buffer[0]);
jpeg_abort_decompress(&cinfo);
fclose(infile);
jpeg_destroy_decompress(&cinfo);
return NULL;
}
// 循环读取每行数据:从 JPEG 中读一行,存入 buffer,再拷贝到 rgb_data
int row_idx = 0; // 当前读取的行号(从 0 开始)
// 循环条件:当前已读行数(cinfo.output_scanline) < 总高度(cinfo.output_height)
while (cinfo.output_scanline < cinfo.output_height) {
// 读取一行像素数据到 buffer
// 参数:
// &cinfo - 解码结构体指针
// buffer - 缓冲区指针(二维数组,每个元素指向一行)
// 1 - 最大读取行数(这里设为 1,每次读一行)
// 返回值:实际读取的行数(正常为 1,到最后一行可能小于 1)
int read_rows = jpeg_read_scanlines(&cinfo, buffer, 1);
if (read_rows > 0) {
// 将当前行数据拷贝到总缓冲区的对应位置
memcpy(rgb_data + row_idx * row_stride, buffer[0], row_stride);
row_idx++; // 行号自增
}
}
// -------------------------- 步骤 结束解码 --------------------------
// 释放解码过程中的临时资源(如量化表、DCT 系数缓冲区)
// 参数:&cinfo - 解码结构体指针
// 返回值:无
jpeg_finish_decompress(&cinfo);
// -------------------------- 步骤 释放资源 --------------------------
free(buffer[0]); // 释放行缓冲区
jpeg_destroy_decompress(&cinfo); // 销毁解码对象,释放所有内部资源
fclose(infile); // 关闭文件
// 返回解码后的 RGB 数据
return rgb_data;
}
// 主函数:测试解码功能
int main() {
const char* jpeg_path = "./pic/test.jpg"; // JPEG 文件路径
int img_width, img_height; // 图像宽高
unsigned char* rgb = NULL; // 存储解码后的 RGB 数据
// 调用解码函数
rgb = jpeg_decode(jpeg_path, &img_width, &img_height);
if (rgb == NULL) {
fprintf(stderr, "解码失败\n");
return -1;
}
// -------------------------- 后续操作:将 RGB 写入 LCD --------------------------
// 此处省略 LCD 写入代码(需根据 LCD 驱动接口实现)
// 核心逻辑:遍历 rgb 数组,将每个像素的 R、G、B 分量写入 LCD 对应坐标的显存
printf("解码成功,RGB 数据大小:%d 字节(%d × %d × 3)\n",
img_width * img_height * 3, img_width, img_height);
// 释放 RGB 数据(使用完后必须释放,避免内存泄漏)
free(rgb);
return 0;
}
JPEG 编码流程
BMP 转 JPEG,需掌握 libjpeg 编码流程(核心 7 步),此处给出关键步骤与代码框架:
// 函数:将 BMP 的 RGB 数据编码为 JPEG 文件
// 参数:
// rgb_data:BMP 解码后的 RGB 数据([行][列×3])
// width:图像宽度(像素)
// height:图像高度(像素)
// jpeg_path:输出 JPEG 文件路径
// quality:压缩质量(1-100,100 为最高质量,最小压缩)
// 返回值:0 成功,-1 失败
int bmp_to_jpeg(unsigned char* rgb_data, int width, int height,
const char* jpeg_path, int quality) {
// 步骤 1:创建编码对象与错误处理对象
struct jpeg_compress_struct cinfo;
struct jpeg_error_mgr jerr;
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_compress(&cinfo);
// 步骤 2:创建输出文件并绑定到编码对象
FILE* outfile = fopen(jpeg_path, "wb");
if (outfile == NULL) {
fprintf(stderr, "无法创建 JPEG 文件\n");
jpeg_destroy_compress(&cinfo);
return -1;
}
jpeg_stdio_dest(&cinfo, outfile);
// 步骤 3:设置编码参数(图像宽高、色彩空间、压缩质量)
cinfo.image_width = width; // 输入图像宽度
cinfo.image_height = height; // 输入图像高度
cinfo.input_components = 3; // 输入分量数(RGB 为 3)
cinfo.in_color_space = JCS_RGB; // 输入色彩空间(RGB)
jpeg_set_defaults(&cinfo); // 设置默认编码参数
jpeg_set_quality(&cinfo, quality, TRUE); // 设置压缩质量
// 步骤 4:开始编码
jpeg_start_compress(&cinfo, TRUE);
// 步骤 5:循环写入每行 RGB 数据
int row_stride = width * 3; // 每行字节数
unsigned char* buffer[1];
buffer[0] = (unsigned char*)malloc(row_stride);
if (buffer[0] == NULL) {
fprintf(stderr, "分配缓冲区失败\n");
jpeg_abort_compress(&cinfo);
fclose(outfile);
jpeg_destroy_compress(&cinfo);
return -1;
}
int row_idx = 0;
while (cinfo.next_scanline < cinfo.image_height) {
// 从 RGB 总数据中拷贝一行到缓冲区(注意 BMP 可能是倒序,需调整行号)
memcpy(buffer[0], rgb_data + (height - 1 - row_idx) * row_stride, row_stride);
// 写入一行数据到 JPEG 文件
jpeg_write_scanlines(&cinfo, buffer, 1);
row_idx++;
}
// 步骤 6:结束编码
jpeg_finish_compress(&cinfo);
// 步骤 7:释放资源
free(buffer[0]);
jpeg_destroy_compress(&cinfo);
fclose(outfile);
return 0;
}
程序编译与开发板调试
编译命令解析
因 libjpeg 是第三方库,编译器默认找不到其头文件和库文件,需通过选项手动指定:
# 编译命令格式(ARM 开发板交叉编译)
arm-linux-gcc main.c -o jpeg_display -I ./include -L ./lib -ljpeg
各选项含义:
I ./include:指定头文件路径(./include是工程中 libjpeg 头文件所在目录);L ./lib:指定库文件路径(./lib是工程中 libjpeg 库文件所在目录);ljpeg:指定链接的库名(libjpeg.so或libjpeg.a,省略前缀lib和后缀.so/.a);jpeg_display:生成的可执行文件名。
开发板调试注意事项
-
动态库依赖:若使用动态库(
libjpeg.so),需将lib/libjpeg.so.9(或对应版本)拷贝到开发板的/lib目录:# 示例:通过 scp 拷贝动态库到开发板(假设开发板 IP 为 192.168.1.100) scp ./lib/libjpeg.so.9 root@192.168.1.100:/lib/ -
路径问题:JPEG 文件路径需与开发板上的实际路径一致(如开发板上 JPEG 存于
/root/pic/test.jpg,代码中路径需改为该地址); -
LCD 越界检查:在 LCD 显示时,需确保图像位置(x, y)+ 图像宽高 ≤ LCD 分辨率(如 LCD 为 800×480,则 x+width ≤ 800,y+height ≤ 480)。
实战注意事项
LCD 任意位置显示 JPEG
- 完成 libjpeg 库移植;
- 编写代码:调用
jpeg_decode函数获取 RGB 数据,再调用 LCD 驱动函数(如lcd_draw_pixel)将 RGB 数据写入指定位置; - 关键注意点:
- 计算显示坐标:若要在(x0, y0)位置显示,需遍历 RGB 数据时,像素坐标为(x0 + x, y0 + y)(x 为 0~width-1,y 为 0~height-1);
- 越界判断:必须确保
x0 + width ≤ LCD_WIDTH且y0 + height ≤ LCD_HEIGHT,否则会写入 LCD 显存非法区域,导致显示错乱。
BMP 转 JPEG
- 先解码 BMP 文件:读取 BMP 头文件,提取宽高和 RGB 数据(BMP 存储为「下到上」,需反转行顺序);
- 调用
bmp_to_jpeg函数将 RGB 数据编码为 JPEG; - 测试验证:用
djpeg工具(libjpeg 自带)将生成的 JPEG 转回 BMP,对比与原 BMP 的差异(因 JPEG 有损,会有细微差异)。
常见问题解决
- 编译报错「找不到 jpeglib.h」:检查
I选项是否指定正确的头文件路径,确保include/下有jpeglib.h; - 开发板运行报错「error while loading shared libraries: libjpeg.so.9: cannot open shared object file」:未将动态库拷贝到开发板
/lib目录,或拷贝的版本不匹配; - 解码后图像颠倒:JPEG 解码后行顺序为「上到下」,若 LCD 要求「下到上」,需反转 RGB 数据的行顺序(如
rgb_data + (height - 1 - y) * row_stride); - 编码后 JPEG 色彩异常:检查输入色彩空间是否为
JCS_RGB,确保 BMP 转 RGB 时未混淆 R、G、B 分量顺序。

935

被折叠的 条评论
为什么被折叠?



