在 LCD 上显示 jpeg 图像
JPEG 简介
JPEG(Joint Photographic Experts Group)是第一个国际数字图像压缩标准,由国际标准组织制定
它是应用最广的图像压缩标准
JPEG 提供有损压缩,压缩比高于其他传统压缩算法
虽然有损压缩,但损失部分不易被人眼察觉,利用了人眼对高频信息不敏感的特点
JPEG 压缩文件的后缀名通常是 .jpg 或 .jpeg
libjpeg 简介
JPEG 压缩标准使用算法压缩原始图像数据,生成 .jpg 或 .jpeg 文件
为在 LCD 上显示 .jpg 或 .jpeg 图像,需要解压缩以获得原始 RGB 数据
解压缩过程需要使用特定的算法
自编解压算法复杂,可使用现成的库,如 libjpeg
libjpeg 是用 C 语言编写的库,包含 JPEG 解码、编码及其他功能
libjpeg 是开源的,可以获取其源代码
libjpeg 移植
下载源码包
-
下载地址: http://www.ijg.org/files/
-
以 v9b 为例
编译源码
-
将压缩包文件拷贝到 Ubuntu 系统用户家目录下
-
文件解压
- tar -xzf jpegsrc.v9b.tar.gz
-
在家目录下的 tools 文件夹中创建一个名为 jpeg 的文件夹,该目录作为 libjpeg 库的安装目
录 -
进入到 libjpeg 源码目录 jpeg-9b 中
-
配置工程
-
对交叉编译工具的环境进行初始化
- source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
-
./configure --host=arm-poky-linux-gnueabi --prefix=/home/dt/tools/jpeg/
-
-
编译工程
- make
-
安装
- make install
-
安装目录下的文件夹介绍
-
安装目录下的文件夹
-
bin 目录下包含一些测试工具
-
include 目录下包含头文件
-
lib 目录下包含动态链接库文件
移植到开发板
-
将 bin 目录下的所有测试工具拷贝到开发板 Linux 系统/usr/bin 目录
-
将 lib
目录下的所有库文件拷贝到开发板 Linux 系统/usr/lib 目录-
拷贝 lib 目录下的库文件时,需要注意符号链接的问题,不能破坏原有的符号链接;可以将 lib 目录下
的所有文件打包成压缩包的形式,进入到 lib 目录,执行命令- tar -czf lib.tar.gz ./*
-
将 lib.tar.gz 压缩文件解压到开发板 Linux 系统/usr/lib 目录下
- tar -xzf lib.tar.gz -C /usr/lib
-
-
解压成功之后,接着执行 libjpeg 提供的测试工具,看看移植是否成功,成功打印出这些信息就表示我们的移植成功了
- djpeg --help
libjpeg 使用说明
错误处理
-
使用 libjpeg 库函数时可能会产生错误,因此需要预先做好错误处理
-
libjpeg 实现了默认错误处理函数,当出现内存不足或文件格式不对等错误时,默认函数会调用 exit() 结束进程
-
可以通过 libjpeg 提供的接口注册自定义错误处理函数,以修改错误处理方式
-
错误处理对象使用 struct jpeg_error_mgr 结构体描述
-
error_exit 函数指针指向错误处理函数,jpeg_std_error() 函数将 libjpeg 错误处理设置为默认方式
- //初始化错误处理对象、并将其与解压对象绑定
cinfo.err = jpeg_std_error(&jerr);
- //初始化错误处理对象、并将其与解压对象绑定
-
若要修改默认错误处理函数
- void my_error_exit(struct jpeg_decompress_struct cinfo)
{
/ … */
}
cinfo.err.error_exit = my_error_exit;
- void my_error_exit(struct jpeg_decompress_struct cinfo)
创建解码对象
-
使用 libjpeg 解码 JPEG 数据前,必须创建解码对象,通过调用
- jpeg_create_decompress(&cinfo);
-
解码结束后或解码出错时,必须调用 jpeg_destroy_decompress 销毁解码对象以释放内存,防止内存泄漏
设置数据源
-
设置需要解码的 JPEG 文件,使用 jpeg_stdio_src() 函数设置数据源
-
FILE *jpeg_file = NULL;
//打开.jpeg/.jpg 图像文件
jpeg_file = fopen(“./image.jpg”, “r”); //只读方式打开
if (NULL == jpeg_file) {
perror(“fopen error”);
return -1;
}
//指定图像文件
jpeg_stdio_src(&cinfo, jpeg_file); -
JPEG 数据源不仅限于文件流,还可以来自内存
读取 jpeg 文件的头信息
-
调用 jpeg_read_header(&cinfo, TRUE) 是解码前必须执行的约定操作,用于读取 JPEG 文件头部信息
-
读取头部信息后,JPEG 图像的宽度、高度、颜色通道数和颜色空间等信息会赋值给 cinfo 对象的相应成员变量
- cinfo.image_width //jpeg 图像宽度
cinfo.image_height //jpeg 图像高度
cinfo.num_components //颜色通道数
cinfo.jpeg_color_space //jpeg 图像的颜色空间
- cinfo.image_width //jpeg 图像宽度
-
支持的颜色空间包括
- /* 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;
- /* Known color spaces. /
设置解码处理参数
-
在解码前,可以通过修改 cinfo 对象的成员变量来设置解码参数,这些参数在调用 jpeg_read_header() 后会被设置为默认值
-
两个重要的解码参数包括
-
输出的颜色空间(cinfo.out_color_space):默认值为 JCS_RGB
-
图像缩放操作(cinfo.scale_num 和 cinfo.scale_denom):用于设置解码图像的大小比例,支持的比例有 1/1、1/2、1/4 和 1/8,默认值为 1/1
- 例如,要将输出图像设置为原图的 1/2 大小,可以设置
cinfo.scale_num = 1 ;
cinfo.scale_denom = 2;
- 例如,要将输出图像设置为原图的 1/2 大小,可以设置
-
开始解码
-
设置完参数后,可以调用 jpeg_start_decompress(&cinfo) 开始解码
-
解压缩完成后,解压后的图像信息会填充到 cinfo 结构中
-
填充的图像信息包括
-
输出图像宽度:cinfo.output_width
-
输出图像高度:cinfo.output_height
-
每个像素的颜色通道数:cinfo.output_components(如灰度为1,RGB888为3)
-
读取数据
-
解码后的数据是按行读取的,存储顺序为从左到右、从上到下,每个像素点的颜色或灰度通道数据依次存储
-
对于24-bit RGB图像,每行数据的存储模式为B,G,R,B,G,R,…
-
libjpeg默认解码得到的图像数据格式为BGR888
-
定义BGR888颜色类型:
- typedef struct bgr888_color {
unsigned char red;
unsigned char green;
unsigned char blue;
} attribute ((packed)) bgr888_t;
- typedef struct bgr888_color {
-
每次读取一行数据,计算每行数据需要的空间大小:RGB图像为宽度×3,灰度图为宽度×1
-
分配行缓冲区:
- bgr888_t *line_buf = malloc(cinfo.output_width * cinfo.output_components);
-
可以调用jpeg_read_scanlines()读取数据,一次读一行
- jpeg_read_scanlines(&cinfo, &buf, 1);
-
使用循环读取所有行数据
- while(cinfo.output_scanline < cinfo.output_height)
{
jpeg_read_scanlines(&cinfo, buffer, 1);
//do something
}
- while(cinfo.output_scanline < cinfo.output_height)
结束解码
-
解码完毕之后调用 jpeg_finish_decompress()函数:
- jpeg_finish_decompress(&cinfo);
释放/销毁解码对象
-
当解码完成之后,我们需要调用 jpeg_destroy_decompress()函数销毁/释放解码对象
- jpeg_destroy_decompress(&cinfo);
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;
-
使用 libjpeg 库解码 JPEG 数据时,核心数据结构是 struct jpeg_decompress_struct,它记录 JPEG 数据的详细信息及解码后输出数据的信息
-
还需要定义一个 struct jpeg_error_mgr 结构体变量,用于处理错误
-
示例代码中定义了 JPEG 解码对象 cinfo 和错误处理对象 jerr
libjpeg 应用编程
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include <jpeglib.h>
typedef struct bgr888_color {
unsigned char red;
unsigned char green;
unsigned char blue;
} __attribute__ ((packed)) bgr888_t;
static int width; //LCD X分辨率
static int height; //LCD Y分辨率
static unsigned short *screen_base = NULL; //映射后的显存基地址
static unsigned long line_length; //LCD一行的长度(字节为单位)
static unsigned int bpp; //像素深度bpp
static int show_jpeg_image(const char *path)
{
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;
FILE *jpeg_file = NULL;
bgr888_t *jpeg_line_buf = NULL; //行缓冲区:用于存储从jpeg文件中解压出来的一行图像数据
unsigned short *fb_line_buf = NULL; //行缓冲区:用于存储写入到LCD显存的一行数据
unsigned int min_h, min_w;
unsigned int valid_bytes;
int i;
//绑定默认错误处理函数
cinfo.err = jpeg_std_error(&jerr);//初始化错误处理对象、并将其与解压对象绑定
//打开.jpeg/.jpg图像文件
jpeg_file = fopen(path, "r"); //只读方式打开
if (NULL == jpeg_file) {
perror("fopen error");
return -1;
}
//创建JPEG解码对象
jpeg_create_decompress(&cinfo);
//指定图像文件
jpeg_stdio_src(&cinfo, jpeg_file);
//读取图像信息
jpeg_read_header(&cinfo, TRUE);
printf("jpeg图像大小: %d*%d\n", cinfo.image_width, cinfo.image_height);
//设置解码参数
cinfo.out_color_space = JCS_RGB;//默认就是JCS_RGB
//cinfo.scale_num = 1;
//cinfo.scale_denom = 2;
//开始解码图像
jpeg_start_decompress(&cinfo);
//为缓冲区分配内存空间
jpeg_line_buf = malloc(cinfo.output_components * cinfo.output_width);
fb_line_buf = malloc(line_length);
//判断图像和LCD屏那个的分辨率更低
if (cinfo.output_width > width)
min_w = width;
else
min_w = cinfo.output_width;
if (cinfo.output_height > height)
min_h = height;
else
min_h = cinfo.output_height;
//读取数据
valid_bytes = min_w * bpp / 8;//一行的有效字节数 表示真正写入到LCD显存的一行数据的大小
while (cinfo.output_scanline < min_h) {
jpeg_read_scanlines(&cinfo, (unsigned char **)&jpeg_line_buf, 1);//每次读取一行数据
//将读取到的BGR888数据转为RGB565
for (i = 0; i < min_w; i++)
fb_line_buf[i] = ((jpeg_line_buf[i].red & 0xF8) << 8) |
((jpeg_line_buf[i].green & 0xFC) << 3) |
((jpeg_line_buf[i].blue & 0xF8) >> 3);
memcpy(screen_base, fb_line_buf, valid_bytes);
screen_base += width;//+width 定位到LCD下一行显存地址的起点
}
//解码完成
jpeg_finish_decompress(&cinfo); //完成解码
jpeg_destroy_decompress(&cinfo);//销毁JPEG解码对象、释放资源
//关闭文件、释放内存
fclose(jpeg_file);
free(fb_line_buf);
free(jpeg_line_buf);
return 0;
}
int main(int argc, char *argv[])
{
struct fb_fix_screeninfo fb_fix;
struct fb_var_screeninfo fb_var;
unsigned int screen_size;
int fd;
/* 传参校验 */
if (2 != argc) {
fprintf(stderr, "usage: %s <jpeg_file>\n", argv[0]);
exit(-1);
}
/* 打开framebuffer设备 */
if (0 > (fd = open("/dev/fb0", O_RDWR))) {
perror("open error");
exit(EXIT_FAILURE);
}
/* 获取参数信息 */
ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
line_length = fb_fix.line_length;
bpp = fb_var.bits_per_pixel;
screen_size = line_length * fb_var.yres;
width = fb_var.xres;
height = fb_var.yres;
/* 将显示缓冲区映射到进程地址空间 */
screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0);
if (MAP_FAILED == (void *)screen_base) {
perror("mmap error");
close(fd);
exit(EXIT_FAILURE);
}
/* 显示BMP图片 */
memset(screen_base, 0xFF, screen_size);
show_jpeg_image(argv[1]);
/* 退出 */
munmap(screen_base, screen_size); //取消映射
close(fd); //关闭文件
exit(EXIT_SUCCESS); //退出进程
}