LV18-01-LCD应用编程-04-显示JPEG图片
本文主要是LCD应用编程——显示JPEG图片的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
PC端开发环境 | Windows | Windows11 |
Ubuntu | Ubuntu20.04.2的64位版本 | |
VMware® Workstation 17 Pro | 17.6.0 build-24238078 | |
终端软件 | MobaXterm(Professional Edition v23.0 Build 5042 (license)) | |
Win32DiskImager | Win32DiskImager v1.0 | |
Linux开发板环境 | Linux开发板 | 正点原子 i.MX6ULL Linux 阿尔法开发板 |
uboot | NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03) | |
linux内核 | linux-4.15(NXP官方提供) |
点击查看本文参考资料
分类 | 网址 | 说明 |
官方网站 | https://www.arm.com/ | ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档 |
https://www.nxp.com.cn/ | NXP官方网站 | |
https://www.nxpic.org.cn/ | NXP 官方社区 | |
https://u-boot.readthedocs.io/en/latest/ | u-boot官网 | |
https://www.kernel.org/ | linux内核官网 | |
其他网站 | kernel - Linux source code (v4.15) - Bootlin | linux内核源码在线查看 |
点击查看相关文件下载
分类 | 网址 | 说明 |
NXP | https://github.com/nxp-imx | NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库 |
https://elixir.bootlin.com/linux/latest/source | 在线阅读linux kernel源码 | |
nxp-imx/linux-imx/releases/tag/rel_imx_4.1.15_2.1.0_ga | NXP linux内核仓库tags中的rel_imx_4.1.15_2.1.0_ga | |
nxp-imx/uboot-imx/releases/tag/rel_imx_4.1.15_2.1.0_ga | NXP u-boot仓库tags中的rel_imx_4.1.15_2.1.0_ga | |
I.MX6ULL | i.MX 6ULL Applications Processors for Industrial Products | I.MX6ULL 芯片手册(datasheet,可以在线查看) |
i.MX 6ULL Applications ProcessorReference Manual | I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网) |
一、JPEG简介
JPEG(Joint Photographic Experts Group)是由国际标准组织为静态图像所建立的第一个国际数字图像压缩标准,也是至今一直在使用的、应用最广的图像压缩标准。
JPEG 由于可以提供有损压缩,因此压缩比可以达到其他传统压缩算法无法比拟的程度; JPEG 虽然是有损压缩,但这个损失的部分是人的视觉不容易察觉到的部分,它充分利用了人眼对计算机色彩中的高频信息部分不敏感的特点,来大大节省了需要处理的数据信息。
JPEG 压缩文件通常以.jpg 或.jpeg 作为文件后缀名, 关于 JPEG 压缩标准这里就写这么多,其他方面更详细的可以看音视频那部分的笔记:
- 01嵌入式开发/04音视频/LV01-图像基础知识/LV01-图像-03-图片格式-03-01-JPEG基础.md
- 01嵌入式开发/04音视频/LV01-图像基础知识/LV01-图像-03-图片格式-03-02-JPEG编解码.md
- 01嵌入式开发/04音视频/LV01-图像基础知识/LV01-图像-03-图片格式-03-03-JPEG协议介绍.md
- 01嵌入式开发/04音视频/LV01-图像基础知识/LV01-图像-03-图片格式-03-04-JPEG中的Exif文件格式标准.md
二、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版本:
下载完进行解压:
1 | tar xf jpegsrc.v9f.tar.gz |
解压完我们会得到如下文件:
还有很多,这里没截全。
2.2 编译源码
- 创建一个安装目录
1 | cd ~/9arm-linux-lib/jpeg-9f/ |
- 配置工程
1 | ./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 的安装目录。 配置完毕如下图所示:
- 编译
1 | make |
编译完成如下图所示:
- 安装libjpeg
1 | make install |
安装完毕如下所示,一般都不会有什么大问题:
- 查看生成的文件
1 | sumu@sumu-virtual-machine:~/9arm-linux-lib/jpeg-9f/jpeg_lib_out$ cd ~/9arm-linux-lib/jpeg-9f/jpeg_lib_out |
3. 安装目录文件夹简介
我们可以看一下上面安装完成后的目录:
bin 目录下包含一些测试工具;include 目录下包含头文件; lib 目录下包含动态链接库文件。
3.1 include
我们看一下include目录:
可以看到有4个头文件,我们编程的时候只需要包含jpeglib.h 就可以了。
3.2 lib目录
我们看一下lib目录:
其中的libjpeg.so 和 libjpeg.so.9 都是符号链接,指向 libjpeg.so.9.2.0。 所以在拷贝到windows目录(主要是可能会出现在共享目录)的时候要格外注意。
4. 拷贝到共享目录
我把应用层的代码都是放在虚拟机与windows的共享目录中,所以这里要往共享目录拷贝一份,毕竟编译的时候还是要用到库的。
1 | cp -avf ~/9arm-linux-lib/jpeg-9f/jpeg_lib_out ~/1sharedfiles/linux_develop/imx6ull-app-demo/lib/jpeg-9f |
复制完成后,软连接可能会有问题,我们看一下:
我们可以手动复制两个,重命名下,方便后面使用:
1 | cd ~/1sharedfiles/linux_develop/imx6ull-app-demo/lib/jpeg-9f/lib |
5. 移植到开发板
接下来就是将安装目录的文件移植到开发板去。我们确保开发板可以挂载开发板的nfs目录,这样我们先将安装目录的文件拷贝到nfs服务器目录去:
1 | cp -a jpeg_lib_out ~/4nfs/jpeg-9f |
我们看一下文件结构:
1 | sumu@sumu-virtual-machine:~/9arm-linux-lib/jpeg-9f$ cd ~/4nfs/jpeg-9f/ |
可以看到是没问题的。然后我们在开发板挂载nfs目录:
我们现在需要将jpeg-9f目录中的 bin 目录下的所有测试工具拷贝到开发板 Linux 系统/usr/bin 目录;将 lib目录下的所有库文件拷贝到开发板 Linux 系统/usr/lib 目录。
拷贝 lib 目录下的库文件时,需要注意符号链接的问题, 不能破坏原有的符号链接; 可以将 lib 目录下的所有文件打包成压缩包的形式,如进入到 lib 目录,执行命令:
1 | tar -czf lib.tar.gz ./* |
但是这里我们是在两个linux系统之间拷贝文件,这里其实不压缩也可以。我们拷贝之前可以先将开发板出厂系统中已经移植的 libjpeg 库删除,在串口终端执行命令 :
1 | 我们可以先备份一下 |
接下来我们开始拷贝文件:
1 | cp -pvf nfs_temp/jpeg-9f/bin/* /usr/bin |
拷贝完毕如下图:
我们看一下拷贝过去的软链接是否正常:
1 | ls -alh /usr/lib/libjpeg.* |
可以看到都是正常的。
Tips:注意!当出厂系统原有的 libjpeg 库被删除后,将会导致开发板下次启动后, 出厂系统的 Qt GUI应用程序会出现一些问题,原本显示图片的位置变成了空白,显示不出来了!原因在于 Qt 程序处理图片(对jpeg 图片解码)时,它的底层使用到了 libjpeg 库,而现在我们将出厂系统原有的 libjpeg 库给删除了,自然就会导致 Qt GUI 应用程序中图片显示不出来(无法对 jpeg 图片进行解码) !这个跟具体的 libjpeg 版本绑定起来的,即使我们将最新编译得到的库文件拷贝到/usr/lib 目录下,也是无济于事,因为版本不同,想要恢复的话我们可以将之前备份的lib库重新解压即可。
然后我们,接着执行 libjpeg 提供的测试工具,看看我们移植成功没:
1 | djpeg --help |
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 结构体变量。
1 | struct jpeg_decompress_struct cinfo; |
以上就定义了 JPEG 解码对象和错误处理对象。
2. 错误处理
使用 libjpeg 库函数的时候难免会产生错误,所以我们在使用 libjpeg 解码之前,首先要做好错误处理。在 libjpeg 库中,实现了默认错误处理函数,当错误发生时, 例如如果内存不足、文件格式不对等, 则会 libjpeg实现的默认错误处理函数, 默认错误处理函数将会调用 exit()结束束整个进程;当然,我们可以修改错误处理的方式, libjpeg 提供了接口让用户可以注册一个自定义错误处理函数。错误处理对象使用 struct jpeg_error_mgr 结构体描述,该结构体内容如下所示 :
1 | /* Error handler object */ |
error_exit 函数指针便指向了错误处理函数。使用 libjpeg 库函数 jpeg_std_error()会将 libjpeg 错误处理设置为默认处理方式。如下所示:
1 | //初始化错误处理对象、并将其与解压对象绑定 |
如果我们要修改默认的错误处理函数,可这样操作:
1 | void my_error_exit(struct jpeg_decompress_struct *cinfo) |
3. 创建解码对象
要使用 libjpeg 解码 jpeg 数据,这步是必须要做的。
1 | jpeg_create_decompress(&cinfo); |
在创建解码对象之后,如果解码结束或者解码出错时,需要调用 jpeg_destroy_decompress 销毁/释放解码对象,否则将会内存泄漏。
4. 设置数据源
就是设置需要进行解码的 jpeg 文件,使用 jpeg_stdio_src()函数设置数据源:
1 | FILE *jpeg_file = NULL; |
待解码的 jpeg 文件使用标准 I/O 方式 fopen 将其打开。 除此之外, jpeg 数据源还可以来自内存中、而不一定的是文件流。
5. 读取 jpeg 文件的头信息
这个和创建解码对象一样,是必须要调用的,是约定。 因为在解码之前,需要读取 jpeg文件的头部信息,以获取该文件的信息,这些获取到的信息会直接赋值给 cinfo 对象的某些成员变量。
1 | jpeg_read_header(&cinfo, TRUE); |
调用 jpeg_read_header()后,可以得到 jpeg 图像的一些信息,如 jpeg 图像的宽度、高度、 颜色通道数以及 colorspace 等,这些信息会赋值给 cinfo 对象中的相应成员变量,如下所示:
1 | cinfo.image_width //jpeg 图像宽度 |
支持的颜色包括如下几种:
1 | /* Known color spaces. */ |
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 大小,可进行如下设置:
1 | cinfo.scale_num = 1; |
7. 开始解码
经过前面的参数设置,我们可以开始解码了,调用 jpeg_start_\decompress()函数:
1 | 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 颜色类型,如下所示:
1 | typedef struct bgr888_color { |
每次读取一行数据, 计算每行数据需要的空间大小,比如 RGB 图像就是宽度 x 3(24-bit RGB 真彩色一个像素 3 个字节) ,灰度图就是宽度x1(一个像素 1 个字节)。
1 | 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 行;函数如下所示:
1 | jpeg_read_scanlines(&cinfo, &buf, 1); |
1 表示每次读取的行数,通常都是将其设置为 1。 cinfo.output_scanline 表示接下来要读取的行对应的索引值, 初始化为 0(表示第一行)、 1 表示第二行等,每读取一行数据,该变量就会加 1,所以我们可以通过下面这种循环方式依次读取解码后的所有数据:
1 | while(cinfo.output_scanline < cinfo.output_height) |
读取一行数据就可以送到显示屏进行显示了,但是需要注意的是,解码后的数据是RGB888的格式,但是正点原子出厂系统设置LCD为RGB565格式,所以这里还需要做一个转换:
1 |
9. 结束解码
解码完毕之后调用 jpeg_finish_decompress()函数:
1 | jpeg_finish_decompress(&cinfo); |
10. 释放/销毁解码对象
当解码完成之后,我们需要调用 jpeg_destroy_decompress()函数销毁/释放解码对象:
1 | jpeg_destroy_decompress(&cinfo); |
四、显示实例
代码和Makefile以及实例图片都在这里:LV18_LCD_DEVICE/05_lcd_show_jpeg · 苏木/imx6ull-app-demo - 码云 - 开源中国 (gitee.com)