解码Linux文件IO之JPEG图像原理与应用

JPEG 基础概念

  • JPEG 的双重身份

    • 既是图像压缩编码标准:由联合图像专家组(Joint Photographic Experts Group)制定,1992 年发布,面向「连续色调静止图像」(如照片、风景图,含渐变色彩的静态图)。

    • 也是图像文件格式:文件扩展名为 .jpg.jpeg,两者完全等价(可互相重命名,文件内容不变)。

      image

  • 核心优势:高压缩比JPEG 是有损压缩格式,在保证视觉效果接近原图的前提下,文件体积远小于 BMP(无压缩)等格式。例如一张 1024×768 的照片,BMP 约 2.25MB,JPEG 仅需 100-300KB,因此特别适合网络传输和移动设备存储。

    image

  • 局限性因有损压缩,反复编解码会导致画质退化;不适合存储文字、图标等「像素级精确」的图像(易出现边缘模糊)。

核心编解码库:libjpeg

要处理 JPEG 文件(解码显示 / 编码生成),需使用专门的库 ——libjpeg,原因是 JPEG 文件经过压缩,无法像 BMP 那样直接读取像素数据。

image

libjpeg 关键特性

  • 开源免费:由独立 JPEG 小组(IJG)维护,官网:www.ijg.org

  • 语言与兼容性:纯 C 语言实现,跨平台(Linux、Windows、嵌入式系统等),支持 JPEG 标准的所有核心功能(压缩、解码、渐进式显示等)。

  • 工业级应用:许多知名库 / 工具的底层依赖 libjpeg,例如 OpenCV(计算机视觉库)读取 JPEG 图像时,就是通过 libjpeg 完成解码。

    image

  • 非系统标准库:Linux、Windows 等系统默认不预装 libjpeg,需手动移植或安装后才能使用。

JPEG 格式的存储顺序(关键:RGB 通道 + 从上到下行顺序)

JPEG 本身存储的是压缩后的 DCT 系数(非直接像素数据),存储顺序需看解压后的结果(以 libjpeg 解码为例,这是嵌入式开发的标准方式)。

像素内部:色彩通道顺序为 RGB(红→绿→蓝)

JPEG 解码时,可通过配置 libjpegout_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.solibjpeg.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_WIDTHy0 + 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 分量顺序。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值