数码相册——在LCD上显示JPG图片
- 硬件平台:韦东山嵌入式Linxu开发板(S3C2440.v3)
- 软件平台:运行于VMware Workstation 12 Player下UbuntuLTS16.04_x64 系统
- 参考资料:《嵌入式Linux应用开发手册》、《嵌入式Linux应用开发手册第2版》
- 开发环境:Linux 3.4.2内核、arm-linux-gcc 4.3.2工具链
目录
一、如何在LCD上显示一张图片
对于一张JPG图片,实际上是由多个不同颜色的像素点所组成的,对于LCD实际上也是由多个像素点所组成的,当我们得到JPG图片上的每个像素的颜色信息,按照行列(在内存中表示为二维数组)的方式存储在一块内存空间中,这个内存空间我们称之为Framebuffer
显存。
- 在存储图像信息到显存之前,显存(自行分配)与LCD控制器,通过设置构成一个映射关系(硬件自动完成),关系建立之后,LCD控制器就有能力自动从显存中读取像素数据传输给LCD驱动器。
- 在我们把图像信息存储到显存后想要在LCD上显示出来,我们只关心在软件方面实现LCD上的像素信息的提取与显示,图片信息的规划等等。
对于一个JPG图片,当我们使用二进制软件打开它时,会发现实际上就是一个二进制文件,只是通过了一定的格式显示为图片。
也就是说,实际上JPG图片是通过二进制存储信息,具体可以看这个博客【jpeg图片格式详解】。当我们把这些而二进制信息解析出来时,把所需要的RGB数据发送给LCD,就可以显示一张JPG图片了。
那么如何解析呢?对于写应用程序的我们,肯定需要使用一些开源的库来对JPEG文件进行处理,解压出我们所需要的原始RGB数据,比较常用的就是libjpeg-turbo
了。
二、libjpeg-turbo
的介绍与使用
通过查看官方资料可知:
1、介绍
libjpeg-turbo
是JPEG图像编解码器,它使用SIMD指令(MMX,SSE2,AVX2,NEON,AltiVec)来加速x86,x86-64,ARM和PowerPC系统上的基线JPEG压缩和解压缩,以及在x86,x86-64,ARM和PowerPC系统上的渐进JPEG压缩x86和x86-64系统。在这样的系统上, libjpeg-turbo
通常是 libjpeg
的 2-6倍,其他所有条件都相同。在其他类型的系统上,凭借其高度优化的霍夫曼编码例程, libjpeg-turbo仍然可以大大优于libjpeg。
2、使用
阅读官方libjpeg.txt
文件可以得到如下信息:
- 分配和初始化一个
decompression
结构体
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;
...
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_decompress(&cinfo);
- 提定源文件
FILE * infile;
...
if ((infile = fopen(filename, "rb")) == NULL) {
fprintf(stderr, "can't open %s\n", filename);
exit(1);
}
jpeg_stdio_src(&cinfo, infile);
- 用
jpeg_read_header
获得jpg信息,信息存储在cinfo
这个结构体中的成员变量中
jpeg_read_header(&cinfo, TRUE);
-
设置解压参数,比如放大、缩小
对于这个参数,可以通过设置cinfo
结构体的成员scale_num/scale_denom
构成缩放因子,根据缩放因子的大小实现放大或缩小。
注意:缩放因子默认为1/1,或者没有缩放。目前,仅支持比例缩放M/8是M从1到16,还是M的简化分数如1/2,3/4等)。 -
启动解压:
jpeg_start_decompress
jpeg_start_decompress(&cinfo);
- 循环调用
jpeg_read_scanlines
while (scan lines remain to be read)
jpeg_read_scanlines(...);
- 完成解压
jpeg_finish_decompress(&cinfo);
- 释放
decompression
结构体
jpeg_destroy_decompress(&cinfo);
三、程序编写
流程:
- 初始化LCD
- 清屏
- 分配和初始化一个
decompression
结构体 - 提定源文件
- 用
jpeg_read_header
获得jpg信息并打印源信息 - 设置解压参数,比如放大、缩小
- 启动解压
- 打印输出图片信息
- 计算输出图片一行数据的长度
- 分配显存
- 循环调用
jpeg_read_scanlines
读出输出图片每一行的信息并显示在LCD中 - 释放显存
- 完成解压
- 释放
decompression
结构体
/**
* @file show_jpg2rgb.c
* @brief 把jpg格式的文件使用libjpeg库解压出原始的rgb信息
* @version 1.0 (版本声明)
* @author Dk
* @date July 6,2020
*/
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/fb.h>
#include "jpeglib.h"
#define FB_DEVICE_NAME "/dev/fb0"
#define DBG_PRINTF printf
static int s_FBFD;
static int s_ScreenSize;
static int s_LineWidth;
static int s_PixelWidth;
static struct fb_var_screeninfo s_tVar;
static struct fb_fix_screeninfo s_tFix;
static unsigned char *s_pFbmem;
/**
* @Description: LCD设备初始化函数
* @return 0 - 初始化成功, -1 - 初始化失败
*/
static int FBDeviceInit(void)
{
/* 打开设备:支持读写 */
s_FBFD = open(FB_DEVICE_NAME, O_RDWR);
if (s_FBFD < 0) {
DBG_PRINTF("can not open %s , err code :%d\n", FB_DEVICE_NAME, s_FBFD);
return -1;
}
/* 获得可变信息 */
if (ioctl(s_FBFD, FBIOGET_VSCREENINFO, &s_tVar) < 0) {
DBG_PRINTF("can not get s_tVar\n");
return -1;
}
/* 获得固定信息 */
if (ioctl(s_FBFD, FBIOGET_FSCREENINFO, &s_tFix) < 0) {
DBG_PRINTF("can not get s_tVar\n");
return -1;
}
s_ScreenSize = s_tVar.xres * s_tVar.yres * s_tVar.bits_per_pixel / 8; // 屏幕总像素所占的字节数
s_LineWidth = s_tVar.xres * s_tVar.bits_per_pixel / 8; // 每行像素所占的字节数
s_PixelWidth = s_tVar.bits_per_pixel / 8; // 每个像素所占字节数
/* 直接映射到内存的Framebuffer */
s_pFbmem = (unsigned char *)mmap(NULL, s_ScreenSize, PROT_READ | PROT_WRITE, MAP_SHARED, s_FBFD, 0);
if (s_pFbmem == (unsigned char *)-1) {
DBG_PRINTF("can not mmap\n");
return -1;
}
return 0;
}
/**
* @Description: LCD显示像素函数,根据传入的信息,在(PenX, PenY)位置显示Color
* @param PenX - 像素X座标, PenY - 像素Y座标, Color - 像素RGB值
* @return 0 - 成功, -1 - 不支持该bpp
*/
static int FBShowPixel(int PenX, int PenY, unsigned int Color)
{
unsigned char *pPen8;
unsigned short *pPen16;
unsigned int *pPen32;
unsigned int red, green, blue;
if ((PenX >= s_tVar.xres) || (PenY >= s_tVar.yres))
{
DBG_PRINTF("out of region\n");
return -1;
}
/* 该座标在内存中对应像素的位置 */
pPen8 = s_pFbmem + PenY * s_LineWidth + PenX * s_PixelWidth;
pPen16 = (unsigned short *)pPen8;
pPen32 = (unsigned int *)pPen8;
switch (s_tVar.bits_per_pixel) {
case 8:
*pPen8 = (unsigned char)Color;
break;
case 16:
/* RGB:565 */
red = ((Color >> 16) & 0xff) >> 3;
green = ((Color >> 8 ) & 0xff) >> 2;
blue = ((Color >> 0 ) & 0xff) >> 3;
*pPen16 = (red << 11) | (green << 5) | blue;
break;
case 32:
*pPen32 = Color;
break;
default:
DBG_PRINTF("can not surport %d bpp\n", s_tVar.bits_per_pixel);
return -1;
}
return 0;
}
/**
* @Description: 清屏函数,整个屏幕显示BackColor
* @param BackColor - 所要显示的颜色RGB值
* @return 0 - 成功, -1 - 不支持该bpp
*/
static int FBCleanScreen(unsigned int BackColor)
{
int i;
unsigned char *pPen8;
unsigned short *pPen16;
unsigned int *pPen32;
unsigned int red, green, blue;
pPen8 = s_pFbmem;
pPen16 = (unsigned short *)pPen8;
pPen32 = (unsigned int *)pPen8;
switch (s_tVar.bits_per_pixel) {
case 8:
memset(pPen8, BackColor, s_ScreenSize);
break;
case 16:
/* RGB:565 */
red = ((BackColor >> 16) & 0xffff) >> 3;
green = ((BackColor >> 8 ) & 0xffff) >> 2;
blue = ((BackColor >> 0 ) & 0xffff) >> 3;
BackColor = (red << 11) | (green << 5) | blue;
for (i = 0; i < s_ScreenSize;) {
*pPen16 = BackColor;
pPen16++;
i += 2;
}
break;
case 32:
for (i = 0; i < s_ScreenSize;) {
*pPen32 = BackColor;
pPen32++;
i += 4;
}
break;
default:
DBG_PRINTF("can not surport %dbpp\n", s_tVar.bits_per_pixel);
return -1;
}
return 0;
}
/**
* @Description: LCD显示一行的像素,在y行,x座标范围为xStart~xEnd,显示pRGBArray
* @param xStart - LCD开始显示位置, xEnd - 结束位置, y - 行, pRGBArray - 存储一行像素RGB的数组,
* @return 0 - 显示成功,-1 - 显示失败
*/
static int FBShowLine(int xStart, int xEnd, int y, unsigned char *pRGBArray)
{
int i;
int x;
unsigned int color;
if (y >= s_tVar.yres)
return -1;
if (xStart >= s_tVar.xres)
return -1;
if (xEnd >= s_tVar.xres)
xEnd = s_tVar.xres;
i = xStart * 3; //偏移量,每个颜色在pRGBArray在占据3个字节
for (x = xStart; x < xEnd; x++) {
/* 0xRRGGBB */
color = (pRGBArray[i] << 16) + (pRGBArray[i+1] << 8) + (pRGBArray[i+2] << 0);
i += 3;
FBShowPixel(x, y, color);
}
return 0;
}
int main(int argc, char **argv)
{
int row_stride;
unsigned char *buffer;
FILE *infile;
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;
if (argc != 2) {
DBG_PRINTF("Usage: \n");
DBG_PRINTF("%s <jpg_file>\n", argv[0]);
return -1;
}
/* 初始化LCD */
if (FBDeviceInit()) {
DBG_PRINTF("err FBDeviceInit()\n");
return -1;
}
/* 清屏 */
FBCleanScreen(0);
/* 分配和初始化一个decompression结构体 */
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_decompress(&cinfo);
/* 指定源文件 */
if ((infile = fopen(argv[1], "rb")) == NULL) {
fprintf(stderr, "can't open %s\n", argv[1]);
return -1;
}
jpeg_stdio_src(&cinfo, infile);
/* 用jpeg_read_header获得jpg信息 */
jpeg_read_header(&cinfo, TRUE);
/* 打印源信息 */
printf("source image_width :%d\n", cinfo.image_width);
printf("source image_height:%d\n", cinfo.image_height);
printf("source num_components:%d\n", cinfo.num_components);
/* 设置解压参数,比如放大、缩小
* 缩放因子:scale_num/scale_denom = 1/2
* 范围:M/8 with all M from 1 to 16
*/
printf("enter M/N\n");
scanf("%d/%d", &cinfo.scale_num, &cinfo.scale_denom);
printf("scale to: %d/%d\n", cinfo.scale_num, cinfo.scale_denom);
/* 启动解压 */
jpeg_start_decompress(&cinfo);
/* 打印输出信息 */
printf("out image_width :%d\n", cinfo.output_width);
printf("out image_height:%d\n", cinfo.output_height);
printf("out num_components:%d\n", cinfo.output_components);
row_stride = cinfo.output_width * cinfo.output_components; //一行的数据长度
/* 分配一个buffer */
buffer = malloc(row_stride);
if (buffer == NULL) {
DBG_PRINTF("buffer malloc err\n");
return -1;
}
/* 循环调用jpeg_read_scanlines,一行一行的获得解压数据 */
while (cinfo.output_scanline < cinfo.output_height) {
(void)jpeg_read_scanlines(&cinfo, &buffer, 1);
/* 写到LCD中 */
FBShowLine(0, cinfo.image_width, cinfo.output_scanline, buffer);
}
/* 释放空间 */
free(buffer);
/* 完成解压 */
jpeg_finish_decompress(&cinfo);
/* 释放decompression结构体 */
jpeg_destroy_decompress(&cinfo);
return 0;
}
四、编译与运行
1、前期准备
1.1 解压libjpeg库
执行tar xzf libjpeg-turbo-1.2.1.tar.gz
1.2 配置
进入到/libjpeg-turbo-1.2.1
目录下,执行mkdir tmp
新建一个文件夹
执行./configure --prefix=/work/tools/libjpeg-turbo-1.2.1/tmp/ --host=arm-linux
,设置安装目录
1.3 安装库
执行make
执行make install
1.4 移动头文件和库文件到交叉编译工具链对应文件夹中
安装好的libjpeg需要把它的/include和/lib
目录下的所有文件,分别移动到/4.3.2/arm-none-linux-gnueabi/libc/usr/include
和/4.3.2/arm-none-linux-gnueabi/libc/armv4t/lib
先进入到/tmp
目录下
命令如下:
- 移动
到/lib
目录文件,执行sudo cp *so* /usr/local/arm/4.3.2/arm-none-linux-gnueabi/libc/armv4t/lib -d
- 移动
/include
目录文件,执行sudo cp * /usr/local/arm/4.3.2/arm-none-linux-gnueabi/libc/usr/include -d
1.5 移动库文件的.so
文件到开发板根文件系统的/lib
目录下
先进入到/tmp/
目录下
命令如下:
- 移动到
/lib
目录文件,执行cp *so* /work/nfs_root/fs_mini_mdev_new/lib -d
2、编译运行
2.1 交叉编译jpg2rgb.c
文件
执行arm-linux-gcc -o show_jpg2rgb show_jpg2rgb.c -ljpeg
,生成可执行文件show_jpg2rgb
2.2 把可执行文件和.jpe
图片文件传输到开发板的根文件系统中
2.3 执行
显示图片的原始比例是800 × 600,LCD分辨率480 × 272
执行./show_jpg2rgb 1.jpg
,后根据提示信息输入缩放因子
- 缩放因子1/1
- 缩放因子1/2
- 缩放因子1/3
- 缩放因子1/4
- 缩放因子1/8
- 缩放因子15/8
通过显示的情况与源数据和输出数据可以了解到,使用libjpeg-turbo
缩放时,部分缩放比例是达不到预期的效果。