正点原子I.MX6U-MINI应用篇5嵌入式Linux在LCD上显示BMPJPGPNG图片

Posted 果果小师弟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了正点原子I.MX6U-MINI应用篇5嵌入式Linux在LCD上显示BMPJPGPNG图片相关的知识,希望对你有一定的参考价值。

一、BMP图像介绍与显示

我们常用的图片格式有很多,一般最常用的有三种:JPEG(或 JPG)、PNG、BMP和GIF。其中 JPEG(或JPG)、PNG以及 BMP 都是静态图片,而 GIF 则可以实现动态图片。

BMP(全称 Bitmap)是Window操作系统中的标准图像文件格式,文件后缀名为.bmp,使用非常
广。它采用位映射存储格式,除了图像深度可选以外,图像数据没有进行任何压缩,因此,BMP 图像文件所占用的空间很大,但是没有失真、并且解析BMP图像简单

BMP文件的图像深度可选1bit、4bit、8bit、16bit、24bit 以及32bit,典型的BMP图像文件由四部分组成:

  • ①、BMP文件头(BMP file header),它包含BMP文件的格式、大小、位图数据的偏移量等信息;
  • ②、位图信息头(bitmap information),它包含位图信息头大小、图像的尺寸、图像大小、位平面数、压缩方式以及颜色索引等信息;
  • ③、调色板(color palette),这部分是可选的,如果使用索引来表示图像,调色板就是索引与其对应颜色的映射表;
  • ④、位图数据(bitmap data),也就是图像数据。

一般常见的图像都是以 16 位(R、G、B 三种颜色分别使用 5bit、6bit、5bit 来表示)、24 位(R、G、B 三种颜色都使用 8bit 来表示)色图像为主,我们称这样的图像为真彩色图像,真彩色图像是不需要调色板的,即位图信息头后面紧跟的就是位图数据了

对某些BMP位图文件说并非如此,譬如16色位图、256色位图,它们需要使用到调色板,具体调色板如何使用,我们不关心,本节我们将会以16 位色(RGB565)BMP 图像为例。

图片链接

可以看到该图片的分辨率为800*480,位深度为16bit,每个像素点使用16位表示,也就是RGB565。为了向大家介绍BMP文件结构,接下来使用十六进制查看工具将image.bmp文件打开,文件头部分的内容如下所示:

1.1 bmp文件头

Windows下为bmp文件头定义了如下结构体:

typedef struct tagBITMAPFILEHEADER

	UINT16 bfType;
	DWORD bfSize;
	UINT16 bfReserved1;
	UINT16 bfReserved2;
	DWORD bfOffBits;
 BITMAPFILEHEADER;

结构体中每一个成员说明如下:


从上面的描述信息,再来对照文件数据:

  • 00~01H :0x42、0x4D 对应的 ASCII 字符分别为为B、M,表示这是 Windows 所支持的位图格式,该字段必须是BM才是Windows位图文件。
  • 02~05H :对应于文件大小,0x000BB848=768072字节=750KB,与image.bmp文件大小是相符的。
  • 06~09H :保留字段。
  • 0A~0D :0x00000046=70,即从文件头部开始到位图数据需要偏移70个字节。

bmp文件头的大小固定为 14 个字节。

1.2 位图信息头

同样,Windows 下为位图信息头定义了如下结构体:

typedef struct tagBITMAPINFOHEADER 

	DWORD biSize;
	LONG biWidth;
	LONG biHeight;
	WORD biPlanes;
	WORD biBitCount;
	DWORD biCompression;
	DWORD biSizeImage;
	LONG biXPelsPerMeter;
	LONG biYPelsPerMeter;
	DWORD biClrUsed;
	DWORD biClrImportant;
 BITMAPINFOHEADER;

结构体中每一个成员说明如下:

从上面的描述信息,再来对照文件数据:

  • 0E~11H :0x00000038=56,这说明这个位图信息头的大小为56个字节。
  • 12~15H :0x00000320=800,图像宽度为 800 个像素,与文件属性一致。
  • 16~19H :0x000001E0=480,图像高度为 480 个像素,与文件属性一致;
  • 1A~1BH :0x0001=1,这个值总为 1。
  • 1C~1DH :0x0010=16,表示每个像素占16个bit。
  • 1E~21H :0x00000003=0,bit-fileds 方式。
  • 22~25H :0x000BB802=768002,图像的大小,注意图像的大小并不是BMP文件的大小,而是图像数据的大小。
  • 26~29H :0x00001274=4724,水平分辨率为4724像素/米。
  • 2A~2DH :0x00001274=4724,垂直分辨率为4724像素/米。
  • 2E~31H :0x00000000=0,本位图未使用调色板。
  • 32~35H :0x00000000=0。

只有压缩方式选项被设置为bit-fileds(0x3)时,位图信息头的大小才会等于56字节,否则,为40字节。

1.3 调色板

调色板是单色、16 色、256 色位图图像文件所持有的,如果是 16 位、24 位以及 32 位位图文件,则 BMP文件组成部分中不包含调色板,关于调色板这里不过多介绍,有兴趣可以自己去了解。

1.4 位图数据

位图数据其实就是图像的数据,对于24位位图,使用3个字节数据来表示一个像素点的颜色,对于16位位图,使用2个字节数据来表示一个像素点的颜色,同理,32 位位图则使用4个字节来描述。
BMP位图分为正向的位图和倒向的位图,主要区别在于图像数据存储的排列方式,前面已经给大家解释的比较清楚了,如下如所示(左边对应的是正向位图,右边对应的则是倒向位图):

所以正向位图先存储图像的第一行数据,从左到右依次存放,接着存放第二行,依次这样;而倒向位图,则先存储图像的最后一行(倒数第一行)数据,也是从左到右依次存放,接着倒数二行,依次这样。

1.5 源码

#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>

/**** BMP文件头数据结构 ****/
typedef struct 
    unsigned char type[2];      //文件类型
    unsigned int size;          //文件大小
    unsigned short reserved1;   //保留字段1
    unsigned short reserved2;   //保留字段2
    unsigned int offset;        //到位图数据的偏移量
 __attribute__ ((packed)) bmp_file_header;

/**** 位图信息头数据结构 ****/
typedef struct 
    unsigned int size;          //位图信息头大小
    int width;                  //图像宽度
    int height;                 //图像高度
    unsigned short planes;      //位面数
    unsigned short bpp;         //像素深度 
    unsigned int compression;   //压缩方式
    unsigned int image_size;    //图像大小
    int x_pels_per_meter;       //像素/米
    int y_pels_per_meter;       //像素/米 
    unsigned int clr_used;
    unsigned int clr_omportant;
 __attribute__ ((packed)) bmp_info_header;

/**** 静态全局变量 ****/
static int width;                       //LCD X分辨率
static int height;                      //LCD Y分辨率
static unsigned short *screen_base = NULL; //映射后的显存基地址
static unsigned long line_length;       //LCD一行的长度(字节为单位)

/********************************************************************
 * 函数名称: show_bmp_image
 * 功能描述: 在LCD上显示指定的BMP图片
 * 输入参数: 文件路径
 * 返 回 值: 成功返回0, 失败返回-1
 ********************************************************************/
static int show_bmp_image(const char *path)

    bmp_file_header file_h;
    bmp_info_header info_h;
    unsigned short *line_buf = NULL;    //行缓冲区
    unsigned long line_bytes;   //BMP图像一行的字节的大小
    unsigned int min_h, min_bytes;
    int fd = -1;
    int j;

    /* 打开文件 */
    if (0 > (fd = open(path, O_RDONLY))) 
        perror("open error");
        return -1;
    

    /* 读取BMP文件头 */
    if (sizeof(bmp_file_header) !=
        read(fd, &file_h, sizeof(bmp_file_header))) 
        perror("read error");
        close(fd);
        return -1;
    

    if (0 != memcmp(file_h.type, "BM", 2)) 
        fprintf(stderr, "it's not a BMP file\\n");
        close(fd);
        return -1;
    

    /* 读取位图信息头 */
    if (sizeof(bmp_info_header) !=
        read(fd, &info_h, sizeof(bmp_info_header))) 
        perror("read error");
        close(fd);
        return -1;
    

    /* 打印信息 */
    printf("文件大小: %d\\n"
         "位图数据的偏移量: %d\\n"
         "位图信息头大小: %d\\n"
         "图像分辨率: %d*%d\\n"
         "像素深度: %d\\n", file_h.size, file_h.offset,
         info_h.size, info_h.width, info_h.height,
         info_h.bpp);

    /* 将文件读写位置移动到图像数据开始处 */
    if (-1 == lseek(fd, file_h.offset, SEEK_SET)) 
        perror("lseek error");
        close(fd);
        return -1;
    

    /* 申请一个buf、暂存bmp图像的一行数据 */
    line_bytes = info_h.width * info_h.bpp / 8;
    line_buf = malloc(line_bytes);
    if (NULL == line_buf) 
        fprintf(stderr, "malloc error\\n");
        close(fd);
        return -1;
    

    if (line_length > line_bytes)
        min_bytes = line_bytes;
    else
        min_bytes = line_length;

    /**** 读取图像数据显示到LCD ****/
    /*******************************************
     * 为了软件处理上方便,这个示例代码便不去做兼容性设计了
     * 如果你想做兼容, 可能需要判断传入的BMP图像是565还是888
     * 如何判断呢?文档里边说的很清楚了
     * 我们默认传入的bmp图像是RGB565格式
     *******************************************/
    if (0 < info_h.height) //倒向位图
        if (info_h.height > height) 
            min_h = height;
            lseek(fd, (info_h.height - height) * line_bytes, SEEK_CUR);
            screen_base += width * (height - 1);    //定位到屏幕左下角位置
        
        else 
            min_h = info_h.height;
            screen_base += width * (info_h.height - 1); //定位到....不知怎么描述 懂的人自然懂!
        

        for (j = min_h; j > 0; screen_base -= width, j--) 
            read(fd, line_buf, line_bytes); //读取出图像数据
            memcpy(screen_base, line_buf, min_bytes);//刷入LCD显存
        
    
    else   //正向位图
        int temp = 0 - info_h.height;   //负数转成正数
        if (temp > height)
            min_h = height;
        else
            min_h = temp;

        for (j = 0; j < min_h; j++, screen_base += width) 
            read(fd, line_buf, line_bytes);
            memcpy(screen_base, line_buf, min_bytes);
        
    

    /* 关闭文件、函数返回 */
    close(fd);
    free(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 <bmp_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);

    screen_size = fb_fix.line_length * fb_var.yres;
    line_length = fb_fix.line_length;
    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_bmp_image(argv[1]);

    /* 退出 */
    munmap(screen_base, screen_size);  //取消映射
    close(fd);  //关闭文件
    exit(EXIT_SUCCESS);    //退出进程

1.6 编译程序

我们要想给ARM板编译出程序,需要使用交叉编译工具链,交叉编译的工具链我们已经安装过了,详细请看【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.x.pdf 的第4.3小节。我是用的是arm-linux-gnueabihf交叉编译工具链。使用arm-linux-gnueabihf-gcc -v可以查看交叉编译工具链的版本号。

然后就可以使用下面命令编译出可以在ARM板子上运行的可执行文件了。

arm-linux-gnueabihf-gcc -o bmp_show bmp_show.c
  • 1、arm表示这是编译arm架构代码的编译器。
  • 2、linux表示运行在linux环境下。
  • 3、gnueabihf表示嵌入式二进制接口。
  • 4、gcc表示是gcc工具。

这样编译出来的 led程序才可以在ARM板子上运行。执行file bmp_show命令就可以看出hello是32位LSB的ELF格式文件,目标机架构为ARM,说明这个交叉编译正常,可执行文件可以在ARM板上执行。

1.7 上传程序到开发板执行

开发板启动后通过nfs挂载Ubuntu目录的方式,将相应的文件拷贝到开发板上。简单来说,就是通过NFS在开发板上通过网络直接访问ubuntu虚拟机上的文件,并且就相当于自己本地的文件一样。

开发板想访问/home/zhiguoxin/myproject/alientek_app_development_source这个目录中的文件,就要把/home/zhiguoxin/myproject/alientek_app_development_source挂载到开发板的mnt目录,这样就可以通过nfs来访问/home/zhiguoxin/myproject/alientek_app_development_source了。

因为我的代码都放在/home/zhiguoxin/myproject/alientek_app_development_source这个目录下,所以我们将这个目录作为NFS共享文件夹。设置方法参考移植SQLite3、OpenCV到RV1126开发板上开发人脸识别项目第一章。

Ubuntu IP为192.168.10.100,然后一般都是挂载在开发板的mnt目录下,这个目录是专门用来给我们作为临时挂载的目录。

然后使用MobaXterm软件通过SSH访问开发板。

ubuntu ip:192.168.10.100
windows ip:192.168.10.200
开发板ip:192.168.10.50

在开发板上执行以下命令:

mount -t nfs -o nolock,vers=3 192.168.10.100:/home/zhiguoxin/myproject/alientek_app_development_source /mnt

就将开饭的mnt目录挂载在ubuntu的/home/zhiguoxin/myproject/alientek_app_development_source目录下了。这样我们就可以在Ubuntu下修改文件,然后可以直接在开发板上执行可执行文件了。当然我这里的/home/zhiguoxin/myproject/windows之间是一个共享目录,我也可以直接在windows上面修改文件,然后ubuntu和开发板直接进行文件同步了。

执行应用程序

./bmp_show xiaoya.bmp

1.8 如何得到16位色RGB565格式 BMP图像?

在Windows下我们转换得到的BMP位图通常是24位色的RGB888格式图像,那如何得到RGB565格式 BMP位图呢?当然这个方法很多,这里笔者向大家介绍一种方法就是通过 Photoshop软件来得到 RGB565格式的 BMP 位图。

首先,找一张图片,图片格式无所谓,只要Photoshop软件能打开即可;确定图片之后,我们启动Photoshop软件,并且使用Photoshop软件打开这张图片,打开之后点击菜单栏中的文件—>存储为,接着出现如下界面:


二、jpeg图像介绍与显示

我们常用的图片格式有很多,一般最常用的有三种:JPEG(或 JPG)、PNG、BMP。BMP 图像虽然没有失真、并且解析简单,但是由于图像数据没有进行任何压缩,因此,BMP 图像文件所占用的存储空间很大,不适合存储在磁盘设备中。

而 JPEG(或 JPG)、PNG 则是经过压缩处理的图像格式,将图像数据进行压缩编码,大大降低了图像文件的大小,适合存储在磁盘设备中,所以很常用。

2.1 JPEG 简介

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

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

2.2 libjpeg简介

JPEG 压缩标准使用了一套压缩算法对原始图像数据进行了压缩得到.jpg 或.jpeg 图像文件,如果想要在LCD 上显示.jpg 或.jpeg 图像文件,则需要对其进行解压缩、以得到图像的原始数据,譬如RGB 数据。

既然压缩过程使用了算法,那对.jpg 或.jpeg 图像文件进行解压同样也需要算法来处理,这里并不会教大家如何编写解压算法,这些算法的实现也是很复杂的,我不会,自然教不了大家!但是,我们可以使用别人写好的库、调用别人写好的库函数来解压.jpg 或.jpeg 图像文件—libjpeg 库。

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

2.3 libjpeg移植

libjpeg是一个开源 C 语言库,我们获取到它的源代码。

2.3.1 下载源码包

首先,打开http://www.ijg.org/files/链接地址,如下所示


目前最新的一个版本是v9e,对应的年份为2022年,这里我们选择一个适中的版本,笔者以v9b 为例,对应的文件名为jpegsrc.v9b.tar.gz,点击该文件即可下载。我把源码下在完成后放在共享文件夹下:

其实开发板出厂系统中已经移植了libjpeg库,但是版本太旧了!所以这里我们选择重新移植

2.3.2 编译源码

将 jpegsrc.v9b.tar.gz 压缩包文件放到共享文件夹下,如下所示:
执行命令解压

tar -vxzf jpegsrc.v9b.tar.gz

解压成功之后会生成 jpeg-9b 文件夹,也就是 libjpeg 源码文件夹。
编译之前,在家目录下的tools文件夹中创建一个名为jpeg的文件夹,该目录作为libjpeg库的安装目录:

mkdir jpeg

回到共享文件夹目录下,进入到libjpeg源码目录jpeg-9b中,该目录下包含的内容如下所示:

接下来对 libjpeg 源码进行交叉编译,跟编译 tslib 时步骤一样,包含三个步骤:

  • 配置工程;
  • 编译工程;
  • 安装;

一套流程下来非常地快!没有任何难点。在此之前,先对交叉编译工具的环境进行初始化,使用 source执行交叉编译工具安装目录下的environment-setup-cortexa7hf-neon-poky-linux-gnueabi脚本文件(如果已经
初始化过了,那就不用再进行初始化了):

source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi

执行下面这条命令对 libjpeg 工程进行配置:

./configure --host=arm-poky-linux-gnueabi --prefix=/home/zhiguoxin/linux/tool/jpeg

大家可以执行./configure --help查看它的配置选项以及含义,--host选项用于指定交叉编译得到的库文件是运行在哪个平台,通常将--host设置为交叉编译器名称的前缀,譬如arm-poky-linux-gnueabi-gcc前缀就是arm-poky-linux-gnueabi--prefix选项则用于指定库文件的安装路径,将/home/zhiguoxin/linux/tool目录作为libjpeg的安装目录。


接着执行 make 命令编译工程:

make


编译完成之后,执行命令安装 libjpeg:

make install

命令执行完成之后,我们的 libjpeg 也就安装成功了!

2.3.3 安装目录下的文件夹介绍

进入到 libjpeg 安装目录:

2.3.4 移植到开发板

开发板出厂系统已经移植了libjpeg库,前面给大家提到过,只是移植的版本太低了,所以这里不打算使用出厂系统移植的libjpeg库,而使用交叉编译好的libjpeg库。

进入到libjpeg安装目录下,将bin目录下的所有测试工具拷贝到开发板Linux系统/usr/bin目录;将 lib目录下的所有库文件拷贝到开发板 Linux 系统/usr/lib目录。

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

tar -czf lib.tar.gz ./*

再将lib.tar.gz压缩文件拷贝到开发板Linux的用户家目录下,在解压之前,将开发板出厂系统中已经移植的libjpeg库删除,执行命令:

rm -rf /usr/lib/libjpeg.*

Tips:注意!当出厂系统原有的libjpeg库被删除后,将会导致开发板下次启动后,出厂系统的 Qt GUI应用程序会出现一些问题,原本显示图片的位置变成了空白,显示不出来了!原因在于 Qt 程序处理图片(对jpeg 图片解码)时,它的底层使用到了 ibjpeg 库,而现在我们将出厂系统原有的 libjpeg 给删除了,自然就会导致Qt GUI应用程序中图片显示不出来(无法对 jpeg 图片进行解码)!

接着我们将lib.tar.gz压缩文件解压到开发板Linux系统/usr/lib目录下。我们可以使用scp命令将/home/zhiguoxin/linux/jpeg/lib目录下的lib.tar.gz发送到发开板中。

scp lib.tar.gz root@192.168.10.50:/home/root


注意:有的时候发下面这种错误,拷贝失败,可以关闭命令窗口,在重新打开一次就好了。


然后lib.tar.gz就拷贝到开发板了,然后使用下面命令将其解压至/usr/lib 目录下

tar -xzf lib.tar.gz -C /usr/lib
tar -xmzf lib.tar.gz -C /usr/lib

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

djpeg --help

2.4 libjpeg使用说明

libjpeg 提供的库函数对.jpg/.jpeg 进行解码(解压),得到 RGB 数据。首先,使用 libjpeg 库需要在我们的应用程序中包含它的头文件 jpeglib.h,该头文件包含了一些结构体数据结构以及 API 接口的申明。先来看看解码操作的过程:

⑴、创建 jpeg 解码对象;
⑵、指定解码数据源;
⑶、读取图像信息;
⑷、设置解码参数;
⑸、开始解码;
⑹、读取解码后的数据;
⑺、解码完毕;
⑻、释放/销毁解码对象。

2.5 libjpeg应用编程

对一个指定的 jpeg 图像进行解码,显示在 LCD 屏上,示例代码如下所示:

#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 = 正点原子I.MX6U-MINI应用篇8嵌入式Linux网络通信socket编程

正点原子I.MX6U-MINI应用篇9嵌入式Linux中的多线程编程pthread

正点原子I.MX6U-MINI应用篇6嵌入式Linux在LCD屏幕上显示字符

正点原子I.MX6U-MINI驱动篇1字符设备驱动开发-Hello驱动(不涉及硬件操作)

正点原子I.MX6U-MINI驱动篇2嵌入式 Linux驱动开发之点灯大法

正点原子I.MX6U-MINI应用篇1编写第一个应用App程序helloworld