Skip to content

LV065-显示JPEG图片

一、JPEG 简介

JPEG(Joint Photographic Experts Group)是由国际标准组织为静态图像所建立的第一个国际数字图像压缩标准,也是至今一直在使用的、应用最广的图像压缩标准。

JPEG 由于可以提供有损压缩,因此压缩比可以达到其他传统压缩算法无法比拟的程度; JPEG 虽然是有损压缩,但这个损失的部分是人的视觉不容易察觉到的部分,它充分利用了人眼对计算机色彩中的高频信息部分不敏感的特点,来大大节省了需要处理的数据信息。

JPEG 压缩文件通常以.jpg 或.jpeg 作为文件后缀名, 关于 JPEG 压缩标准这里就写这么多,其他方面更详细的可以看音视频那部分的笔记:

二、libjpeg 简介

1. 简介

JPEG 压缩标准使用了一套压缩算法对原始图像数据进行了压缩得到.jpg 或.jpeg 图像文件,如果想要在 LCD 上显示.jpg 或.jpeg 图像文件,则需要对其进行解压缩、以得到图像的原始数据,如 RGB 数据。可以分为软解和硬解,硬解就是硬件 JPEG 解码器去解码,但是 imx6ull 中没有这样的解码器,所以这里我们了解一下软解。

既然压缩过程使用了算法,那对.jpg 或.jpeg 图像文件进行解压同样也需要算法来处理,我们不需要自己去造轮子写这样一个算法,感觉还是很麻烦的,我们可以使用别人写好的库、调用别人写好的库函数来解压.jpg 或.jpeg 图像文件。

libjpeg 是一个完全用 C 语言编写的函数库,包含了 JPEG 解码(解压缩) 、 JPEG 编码(创建压缩) 和其他的 JPEG 功能的实现。 可以使用 libjpeg 库对.jpg 或.jpeg 压缩文件进行解压或者生成.jpg 或.jpeg 压缩文件。

libjpeg 是一个开源 C 语言库,它的官网在这里:libjpeg (sourceforge.net)(感觉应该是这个吧)

2. 移植 libJpeg 库

2.1 源码下载

我们打开这个网址:Directory Listing of /files (ijg.org),我这里直接选最新的 v9f 版本:

image-20260107164601351

下载完进行解压:

shell
tar xf jpegsrc.v9f.tar.gz
image-20241013091941704

解压完我们会得到如下文件:

image-20241013092354247

还有很多文件,这里仅展示一部分。

2.2 编译源码

  • 创建一个安装目录
shell
cd ~/9arm-linux-lib/jpeg-9f/
mkdir jpeg_lib_out
  • 配置工程
shell
./configure --host=arm-linux-gnueabihf --prefix=/home/sumu/9arm-linux-lib/jpeg-9f/jpeg_lib_out

我们可以执行./configure --help 查看它的配置选项以及含义, --host 选项用于指定交叉编译得到的库文件是运行在哪个平台,通常将--host 设置为交叉编译器名称的前缀,如 arm-linux-gnueabihf-gcc 前缀就是 arm-linux-gnueabihf; --prefix 选项则用于指定库文件的安装路径, 将家目录下的 9arm-linux-lib/jpeg-9f/ 目录作为 libjpeg 的安装目录。 配置完毕如下图所示:

image-20241013092833361
  • 编译
shell
make
  • 安装 libjpeg
shell
make install
  • 查看生成的文件
shell
sumu@sumu-virtual-machine:~/9arm-linux-lib/jpeg-9f/jpeg_lib_out$ cd ~/9arm-linux-lib/jpeg-9f/jpeg_lib_out
sumu@sumu-virtual-machine:~/9arm-linux-lib/jpeg-9f/jpeg_lib_out$ tree
.
├── bin
│   ├── cjpeg
│   ├── djpeg
│   ├── jpegtran
│   ├── rdjpgcom
│   └── wrjpgcom
├── include
│   ├── jconfig.h
│   ├── jerror.h
│   ├── jmorecfg.h
│   └── jpeglib.h
├── lib
│   ├── libjpeg.a
│   ├── libjpeg.la
│   ├── libjpeg.so -> libjpeg.so.9.6.0
│   ├── libjpeg.so.9 -> libjpeg.so.9.6.0
│   ├── libjpeg.so.9.6.0
│   └── pkgconfig
│       └── libjpeg.pc
└── share
    └── man
        └── man1
            ├── cjpeg.1
            ├── djpeg.1
            ├── jpegtran.1
            ├── rdjpgcom.1
            └── wrjpgcom.1

7 directories, 20 files

3. 安装目录文件夹简介

我们可以看一下上面安装完成后的目录:

image-20241013093248880

bin 目录下包含一些测试工具;include 目录下包含头文件; lib 目录下包含动态链接库文件。

3.1 include

我们看一下 include 目录:

image-20241013093344805

可以看到有 4 个头文件,我们编程的时候只需要包含 jpeglib.h 就可以了。

3.2 lib 目录

我们看一下 lib 目录:

image-20241013093523152

其中的 libjpeg.so 和 libjpeg.so.9 都是符号链接,指向 libjpeg.so.9.2.0。 所以在拷贝到 windows 目录(主要是可能会出现在共享目录)的时候要格外注意。

4. 拷贝到共享目录

我把应用层的代码都是放在虚拟机与 windows 的共享目录中,所以这里要往共享目录拷贝一份,毕竟编译的时候还是要用到库的。

shell
cp -avf ~/9arm-linux-lib/jpeg-9f/jpeg_lib_out ~/1sharedfiles/linux_develop/imx6ull-app-demo/lib/jpeg-9f

复制完成后,软连接可能会有问题,我们看一下:

image-20241013094520195

我们可以手动复制两个,重命名下,方便后面使用:

shell
cd ~/1sharedfiles/linux_develop/imx6ull-app-demo/lib/jpeg-9f/lib
cp libjpeg.so.9.6.0 libjpeg.so.9
cp libjpeg.so.9.6.0 libjpeg.so
image-20241013094701984

5. 移植到开发板

接下来就是将安装目录的文件移植到开发板去。我们确保开发板可以挂载开发板的 nfs 目录,这样我们先将安装目录的文件拷贝到 nfs 服务器目录去:

shell
cp -a jpeg_lib_out ~/4nfs/jpeg-9f

我们看一下文件结构:

shell
sumu@sumu-virtual-machine:~/9arm-linux-lib/jpeg-9f$ cd ~/4nfs/jpeg-9f/
sumu@sumu-virtual-machine:~/4nfs/jpeg-9f$ tree
.
├── bin
│   ├── cjpeg
│   ├── djpeg
│   ├── jpegtran
│   ├── rdjpgcom
│   └── wrjpgcom
├── include
│   ├── jconfig.h
│   ├── jerror.h
│   ├── jmorecfg.h
│   └── jpeglib.h
├── lib
│   ├── libjpeg.a
│   ├── libjpeg.la
│   ├── libjpeg.so -> libjpeg.so.9.6.0
│   ├── libjpeg.so.9 -> libjpeg.so.9.6.0
│   ├── libjpeg.so.9.6.0
│   └── pkgconfig
│       └── libjpeg.pc
└── share
    └── man
        └── man1
            ├── cjpeg.1
            ├── djpeg.1
            ├── jpegtran.1
            ├── rdjpgcom.1
            └── wrjpgcom.1

7 directories, 20 files

可以看到是没问题的。然后我们在开发板挂载 nfs 目录:

image-20241013095131896

我们现在需要将 jpeg-9f 目录中的 bin 目录下的所有测试工具拷贝到开发板 Linux 系统/usr/bin 目录;将 lib 目录下的所有库文件拷贝到开发板 Linux 系统/usr/lib 目录。

拷贝 lib 目录下的库文件时,需要注意符号链接的问题, 不能破坏原有的符号链接; 可以将 lib 目录下的所有文件打包成压缩包的形式,如进入到 lib 目录,执行命令:

shell
tar -czf lib.tar.gz ./*

但是这里我们是在两个 linux 系统之间拷贝文件,这里其实不压缩也可以。我们拷贝之前可以先将开发板出厂系统中已经移植的 libjpeg 库删除,在串口终端执行命令 :

shell
# 我们可以先备份一下
cd /usr/lib
tar -czf libjpeg.bk.tar.gz libjpeg.*
mv libjpeg.bk.tar.gz ~/nfs_temp/
rm -rf /usr/lib/libjpeg.*

接下来我们开始拷贝文件:

shell
cp -pvf nfs_temp/jpeg-9f/bin/* /usr/bin
cp -avf nfs_temp/jpeg-9f/lib/* /usr/lib

拷贝完毕如下图:

image-20241013100606133

我们看一下拷贝过去的软链接是否正常:

shell
ls -alh /usr/lib/libjpeg.*
image-20241013100702438

可以看到都是正常的。

Tips:注意!当出厂系统原有的 libjpeg 库被删除后,将会导致开发板下次启动后, 出厂系统的 Qt GUI 应用程序会出现一些问题,原本显示图片的位置变成了空白,显示不出来了!原因在于 Qt 程序处理图片(对 jpeg 图片解码)时,它的底层使用到了 libjpeg 库,而现在我们将出厂系统原有的 libjpeg 库给删除了,自然就会导致 Qt GUI 应用程序中图片显示不出来(无法对 jpeg 图片进行解码) !这个跟具体的 libjpeg 版本绑定起来的,即使我们将最新编译得到的库文件拷贝到/usr/lib 目录下,也是无济于事,因为版本不同,想要恢复的话我们可以将之前备份的 lib 库重新解压即可。

然后我们,接着执行 libjpeg 提供的测试工具,看看我们移植成功没:

shell
djpeg --help
image-20241013100942668

djpeg 是编译 libjpeg 源码得到的测试工具(在 libjpeg 安装目录下的 lib 目录中) ,当执行命令之后,能够成功打印出这些信息就表示我们的移植成功了!

三、libjpeg 的使用

1. libjpeg 解码基本流程

libjpeg 提供 JPEG 解码、 JPEG 编码和其他的 JPEG 功能的实现, 这里我们暂时只学习 libjpeg 提供的库函数对.jpg/.jpeg 进行解码(解压),得到 RGB 数据。

首先,使用 libjpeg 库需要在我们的应用程序中包含它的头文件 jpeglib.h,该头文件包含了一些结构体数据结构以及 API 接口的申明。 先来看看解码操作的过程:

(1)创建 jpeg 解码对象;

(2)指定解码数据源;

(3)读取图像信息;

(4)设置解码参数;

(5)开始解码;

(6)读取解码后的数据;

(7)解码完毕;

(8)释放/销毁解码对象。

以上便是整个解码操作的过程, 用 libjpeg 库解码 jpeg 数据的时候,最重要的一个数据结构为 struct jpeg_decompress_struct 结构体,该数据结构记录着 jpeg 数据的详细信息, 也保存着解码之后输出数据的详细信息。 除此之外, 还需要定义一个用于处理错误的对象,错误处理对象是一个 struct jpeg_error_mgr 结构体变量。

c
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;

以上就定义了 JPEG 解码对象和错误处理对象。

2. 错误处理

使用 libjpeg 库函数的时候难免会产生错误,所以我们在使用 libjpeg 解码之前,首先要做好错误处理。在 libjpeg 库中,实现了默认错误处理函数,当错误发生时, 例如如果内存不足、文件格式不对等, 则会 libjpeg 实现的默认错误处理函数, 默认错误处理函数将会调用 exit()结束束整个进程;当然,我们可以修改错误处理的方式, libjpeg 提供了接口让用户可以注册一个自定义错误处理函数。错误处理对象使用 struct jpeg_error_mgr 结构体描述,该结构体内容如下所示 :

c
/* Error handler object */

struct jpeg_error_mgr {
  /* Error exit handler: does not return to caller */
  JMETHOD(noreturn_t, error_exit, (j_common_ptr cinfo));
  /* Conditionally emit a trace or warning message */
  JMETHOD(void, emit_message, (j_common_ptr cinfo, int msg_level));
  /* Routine that actually outputs a trace or error message */
  JMETHOD(void, output_message, (j_common_ptr cinfo));
  /* Format a message string for the most recent JPEG error or message */
  JMETHOD(void, format_message, (j_common_ptr cinfo, char * buffer));
#define JMSG_LENGTH_MAX  200	/* recommended size of format_message buffer */
  /* Reset error state variables at start of a new image */
  JMETHOD(void, reset_error_mgr, (j_common_ptr cinfo));

  /* The message ID code and any parameters are saved here.
   * A message can have one string parameter or up to 8 int parameters.
   */
  int msg_code;
#define JMSG_STR_PARM_MAX  80
  union {
    int i[8];
    char s[JMSG_STR_PARM_MAX];
  } msg_parm;

  /* Standard state variables for error facility */

  int trace_level;		/* max msg_level that will be displayed */

  /* For recoverable corrupt-data errors, we emit a warning message,
   * but keep going unless emit_message chooses to abort.  emit_message
   * should count warnings in num_warnings.  The surrounding application
   * can check for bad data by seeing if num_warnings is nonzero at the
   * end of processing.
   */
  long num_warnings;		/* number of corrupt-data warnings */

  /* These fields point to the table(s) of error message strings.
   * An application can change the table pointer to switch to a different
   * message list (typically, to change the language in which errors are
   * reported).  Some applications may wish to add additional error codes
   * that will be handled by the JPEG library error mechanism; the second
   * table pointer is used for this purpose.
   *
   * First table includes all errors generated by JPEG library itself.
   * Error code 0 is reserved for a "no such error string" message.
   */
  const char * const * jpeg_message_table; /* Library errors */
  int last_jpeg_message;    /* Table contains strings 0..last_jpeg_message */
  /* Second table can be added by application (see cjpeg/djpeg for example).
   * It contains strings numbered first_addon_message..last_addon_message.
   */
  const char * const * addon_message_table; /* Non-library errors */
  int first_addon_message;	/* code for first string in addon table */
  int last_addon_message;	/* code for last string in addon table */
};

error_exit 函数指针便指向了错误处理函数。使用 libjpeg 库函数 jpeg_std_error()会将 libjpeg 错误处理设置为默认处理方式。如下所示:

c
//初始化错误处理对象、并将其与解压对象绑定
cinfo.err = jpeg_std_error(&jerr);

如果我们要修改默认的错误处理函数,可这样操作:

c
void my_error_exit(struct jpeg_decompress_struct *cinfo)
{
	/* ... */
}
cinfo.err.error_exit = my_error_exit;

3. 创建解码对象

要使用 libjpeg 解码 jpeg 数据,这步是必须要做的。

c
jpeg_create_decompress(&cinfo);

在创建解码对象之后,如果解码结束或者解码出错时,需要调用 jpeg_destroy_decompress 销毁/释放解码对象,否则将会内存泄漏。

4. 设置数据源

就是设置需要进行解码的 jpeg 文件,使用 jpeg_stdio_src()函数设置数据源:

c
FILE *jpeg_file = NULL;
// 打开.jpeg/.jpg 图像文件
jpeg_file = fopen("./jpeg-853*480.jpg", "r"); // 只读方式打开
if (NULL == jpeg_file) 
{
    perror("fopen error");
    return -1;
}
// 指定图像文件
jpeg_stdio_src(&cinfo, jpeg_file);

待解码的 jpeg 文件使用标准 I/O 方式 fopen 将其打开。 除此之外, jpeg 数据源还可以来自内存中、而不一定的是文件流。

5. 读取 jpeg 文件的头信息

这个和创建解码对象一样,是必须要调用的,是约定。 因为在解码之前,需要读取 jpeg 文件的头部信息,以获取该文件的信息,这些获取到的信息会直接赋值给 cinfo 对象的某些成员变量。

c
jpeg_read_header(&cinfo, TRUE);

调用 jpeg_read_header()后,可以得到 jpeg 图像的一些信息,如 jpeg 图像的宽度、高度、 颜色通道数以及 colorspace 等,这些信息会赋值给 cinfo 对象中的相应成员变量,如下所示:

c
cinfo.image_width //jpeg 图像宽度
cinfo.image_height //jpeg 图像高度
cinfo.num_components //颜色通道数
cinfo.jpeg_color_space //jpeg 图像的颜色空间

支持的颜色包括如下几种:

c
/* Known color spaces. */

typedef enum {
	JCS_UNKNOWN,		/* error/unspecified */
	JCS_GRAYSCALE,		/* monochrome */
	JCS_RGB,		/* red/green/blue, standard RGB (sRGB) */
	JCS_YCbCr,		/* Y/Cb/Cr (also known as YUV), standard YCC */
	JCS_CMYK,		/* C/M/Y/K */
	JCS_YCCK,		/* Y/Cb/Cr/K */
	JCS_BG_RGB,		/* big gamut red/green/blue, bg-sRGB */
	JCS_BG_YCC		/* big gamut Y/Cb/Cr, bg-sYCC */
} J_COLOR_SPACE;

6. 设置解码处理参数

在进行解码之前,我们可以对一些解码参数进行设置, 这些参数都有一个默认值,调用 jpeg_read_header()函数后,这些参数被设置成相应的默认值。

直接对 cinfo 对象的成员变量进行修改即可,这里介绍两个比较有代表性的解码处理参数:

(1)输出的颜色(cinfo.out_color_space): 默认配置为 RGB 颜色,也就是 JCS_RGB;

(2)图像缩放操作(cinfo.scale_num 和 cinfo.scale_denom): libjpeg 可以设置解码出来的图像的大小,也就是与原图的比例。使用 scale_num 和 scale_denom 两个参数,解出来的图像大小就是 scale_num/scale_denom, JPEG 当前仅支持 1/1、 1/2、 1/4、 和 1/8 这几种缩小比例。 默认是 1/1,也就是保持原图大小。例如要将输出图像设置为原图的 1/2 大小,可进行如下设置:

c
cinfo.scale_num = 1;
cinfo.scale_denom = 2;

7. 开始解码

经过前面的参数设置,我们可以开始解码了,调用 jpeg_start_\decompress()函数:

c
jpeg_start_decompress(&cinfo);

在完成解压缩操作后 ,会将解压后的图像信息填充至 cinfo 结构中。如 ,输出图像宽度 cinfo.output_width,输出图像高度 cinfo.output_height ,每个像素中的颜色通道数 cinfo.output_components(比如灰度为 1,全彩色 RGB888 为 3)等。

一般情况下,这些参数是在 jpeg_start_decompress 后才被填充到 cinfo 中的,如果希望在调用 jpeg_start_decompress 之前就获得这些参数,可以通过调用 jpeg_calc_output_dimensions()的方法来实现。

8. 读取数据

接下来就可以读取解码后的数据了, 数据是按照行读取的, 解码后的数据按照从左到右、 从上到下的顺序存储,每个像素点对应的各颜色或灰度通道数据是依次存储, 例如一个 24-bit RGB 真彩色的图像中,一行的数据存储模式为 B, G, R, B, G, R, B, G, R,...。

libjpeg 默认解码得到的图像数据是 BGR888 格式,即 R 颜色在低 8 位、而 B 颜色在高 8 位。 可以定义一个 BGR888 颜色类型,如下所示:

c
typedef struct bgr888_color {
    unsigned char red;
    unsigned char green;
    unsigned char blue;
} __attribute__ ((packed)) bgr888_t;

每次读取一行数据, 计算每行数据需要的空间大小,比如 RGB 图像就是宽度 x 3(24-bit RGB 真彩色一个像素 3 个字节) ,灰度图就是宽度 x1(一个像素 1 个字节)。

c
bgr888_t *line_buf = malloc(cinfo.output_width * cinfo.output_components);

以上我们分配了一个行缓冲区,它的大小为 cinfo.output_width * cinfo.output_components,也就是输出图像的宽度乘上每一个像素的字节大小。 我们除了使用 malloc 分配缓冲区外,还可以使用 libjpeg 的内存管理器来分配缓冲区。

缓冲区分配好之后,接着可以调用 jpeg_read_scanlines()来读取数据, jpeg_read_scanlines()可以指定一次读多少行,但是目前该函数还只能支持一次只读 1 行;函数如下所示:

c
jpeg_read_scanlines(&cinfo, &buf, 1);

1 表示每次读取的行数,通常都是将其设置为 1。 cinfo.output_scanline 表示接下来要读取的行对应的索引值, 初始化为 0(表示第一行)、 1 表示第二行等,每读取一行数据,该变量就会加 1,所以我们可以通过下面这种循环方式依次读取解码后的所有数据:

c
while(cinfo.output_scanline < cinfo.output_height)
{
    jpeg_read_scanlines(&cinfo, buffer, 1);
    //do something
}

读取一行数据就可以送到显示屏进行显示了,但是需要注意的是,解码后的数据是 RGB888 的格式,但是正点原子出厂系统设置 LCD 为 RGB565 格式,所以这里还需要做一个转换:

c
#define argb8888_to_rgb565(color)   ({ \
            unsigned int temp = (color); \
            ((temp & 0xF80000UL) >> 8) | \
            ((temp & 0xFC00UL) >> 5) | \
            ((temp & 0xF8UL) >> 3); \
            })

9. 结束解码

解码完毕之后调用 jpeg_finish_decompress()函数:

c
jpeg_finish_decompress(&cinfo);

10. 释放/销毁解码对象

当解码完成之后,我们需要调用 jpeg_destroy_decompress()函数销毁/释放解码对象:

c
jpeg_destroy_decompress(&cinfo);

四、显示实例

代码和 Makefile 以及实例图片都在这里:LV18_LCD_DEVICE/05_lcd_show_jpeg · linux-dev-org/imx6ull-app-demo

image-20241013105911586