什么是 BMP 格式?
BMP是英文Bitmap(位图)的简写,它是Windows操作系统中的标准图像文件格式,能够被多种Windows应用程序所支持。随着Windows操作系统的流行与丰富的Windows应用程序的开发,BMP位图格式理所当然地被广泛应用。这种格式的特点是包含的图像信息较丰富,几乎不进行压缩,但由此导致了它与生俱生来的缺点–占用磁盘空间过大。所以,目前BMP在单机上比较流行。
下面,我们通过一个例子来验证一下同一张图片保存为 png 格式和 bmp 格式 的大小对比。我们保存 bmp 格式时选择 24位位图。
上图足以证明一张图片保存为 png 只需要 12k,而保存为 bmp 格式,则需要 1.4M。所以 bmp 相比于其它图片格式来说占用磁盘空间非常大。
BMP 的文件结构
上面我们已经了解 bmp 格式的图片是不会压缩的,所以占用空间非常大。那我们可不可以根据一张 bmp 格式的图片来计算图片的大小呢?比如下面一张 bmp 图片,它的详细信息如下:
可以看到,这张图片宽 800 像素,高 480 像素,它的大小为 1.09 M(1.152.054 字节)。
大小 = 800 * 480 * 3(RGB) = 1152000。最后 54 个字节就是 文件头 + 信息头。
那么 bmp 文件的文件头和信息头又包含什么信息呢?
14字节文件头:
字节 | 字节号 | 解释说明 |
---|---|---|
424D | 1-2 | 可以转换为字符 ‘B’、‘M’,是用于识别 BMP 文件的标志 |
B634 1800 | 3-6 | 存放的是位图大小 |
0000 0000 | 7-10 | 保留字节,必须为0 |
3600 0000 | 11-14 | 表示位图阵列相对于文件头的偏移量,计算得54,即位图阵列从第 55 个字节开始 |
40字节信息头
读取 BMP 图片
首先我们定义一个结构体来接收 BMP 文件的文件头:
// 定义一个结构体(存储 BMP 的文件头)
typedef struct{
char bfType[2]; // 识别 BMP 的标志:'B'/'M'
int bfSize; // 位图文件的大小
int bfReserved; // 保留字节,必须为0
int bfOffBits; // 偏移量
}bfHeader;
但是这里会涉及到一个内存对齐的问题,一个 fileHeader 结构体会占用 16 个字节,但是我们的文件头只有 14 个字节。所以 我们把 bfType 单独定义,不放在结构体中。
char bfType[3]; // 识别 BMP 的标志:'B'/'M',最后一个字节放'/0'
// 定义一个结构体(存储 BMP 的文件头)
typedef struct{
int bfSize; // 位图文件的大小
int bfReserved; // 保留字节,必须为0
int bfOffBits; // 偏移量
}bfHeader;
我们有定义一个结构体来存储 BMP 文件的信息头:
typedef struct{
int biSize; // 信息头的大小
int biWidth; // 图像宽度(单位:像素)
int biHeight; // 图像高度(单位:像素)
short biPlanes; // 目标设备说明颜色平面数,总被设为1
short biBitCount; // 说明 比特数/像素数,值有 1、2、4、8、16、32
int biCompression; // 说明图像的压缩类型,最常用的就是0,不压缩
int biSizeImages; // 位图数据的大小
int biXPelsPerMeter;// 水平分辨率,单位是 像素/米
int biYPelsPerMeter;// 垂直分辨率,单位是 像素/米
int biClrUsed; // 位图使用的调色板中的颜色索引数
int biClrImportant; // 对图像显示有重要影响的颜色索引数
}bfInfo;
下面我们就可以读取 BMP 文件数据了,代码如下:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <assert.h>
char bfType[3]; // 识别 BMP 的标志:'B'/'M',最后一个字节放'/0'
// 定义一个结构体(存储 BMP 的文件头)
typedef struct{
int bfSize; // 位图文件的大小
int bfReserved; // 保留字节,必须为0
int bfOffBits; // 偏移量
}bfHeader;
typedef struct{
int biSize; // 信息头的大小
int biWidth; // 图像宽度(单位:像素)
int biHeight; // 图像高度(单位:像素)
short biPlanes; // 目标设备说明颜色平面数,总被设为1
short biBitCount; // 说明 比特数/像素数,值有 1、2、4、8、16、32
int biCompression; // 说明图像的压缩类型,最常用的就是0,不压缩
int biSizeImages; // 位图数据的大小
int biXPelsPerMeter;// 水平分辨率,单位是 像素/米
int biYPelsPerMeter;// 垂直分辨率,单位是 像素/米
int biClrUsed; // 位图使用的调色板中的颜色索引数
int biClrImportant; // 对图像显示有重要影响的颜色索引数
}bfInfo;
int main()
{
int rfd, ret;
bfHeader fh; // 文件头结构体
bfInfo info; // 信息头结构体
rfd = open("./img/test.bmp", O_RDONLY);
assert(rfd > 0);
// 先读取2个字节放到 type 中
ret = read(rfd, bfType, 2);
assert(ret == 2);
// 读取12个字节到 fh 中
ret = read(rfd, &fh, sizeof(fh));
assert(ret == sizeof(fh));
// 读取信息头
ret = read(rfd, &info, sizeof(info));
assert(ret == sizeof(info));
printf("bfType = %s\n", bfType);
printf("bfSize = %d Bytes\n", fh.bfSize);
printf("biWidth = %d, biHeight = %d\n", info.biWidth, info.biHeight);
close(rfd);
return 0;
}
运行结果如下:
上面我们已经对 BMP 文件的信息头和文件头进行了读取,下面就应该对 BMP文件数据进行读取了。
我们首先需要知道的是没在数据部分,每个像素点(RGB三个字节)是反向存储的。如下图
读取图片数据代码如下:
//rgb像素点占用的总字节数
int rgb_size = pic_info.width*pic_info.height*3;
//在堆空间申请内存存放RGB数据
//unsigned char *rgb_space = malloc(rgb_size);
unsigned char *rgb_space = new unsigned char[rgb_size];
if(rgb_space == NULL)
{
perror("malloc rgb_space fail!");
return -1;
}
//读取图片的RGB数据
read(pic_fd, rgb_space, rgb_size);
//ARGB像素点占用的总字节数
int argb_size = pic_info.width*pic_info.height*4;
//在堆空间申请内存存放RGB数据
//unsigned int *argb_space = malloc(argb_size);
unsigned int *argb_space = new unsigned int[argb_size];
if(argb_space == NULL)
{
perror("malloc argb_space fail!");
return -1;
}
int x, y, i=0;
unsigned char R,G,B;
//把RGB数据转换为32位的ARGB数据,并倒转图片缓存到ARGB缓存
for(y = 0; y < pic_info.height; y++)
{
for(x = 0; x < pic_info.width; x++)
{
B = *(rgb_space + 3*i + 0);
G = *(rgb_space + 3*i + 1);
R = *(rgb_space + 3*i + 2);
i++;
//480-->479行 y=0 -->480 需要pic_info.height-1=479
//把RGB数据转换为32位的ARGB数据
*(argb_space + (pic_info.height - 1 - y) * pic_info.width + x) = 0 | R<<16 | G<<8 | B<<0;
}
}
i= 0;
//显示处理
// (x_s, y_s):表示你想从屏幕的(x_s, y_s)坐标开始显示图片
for(y = y_s; y < pic_info.height + y_s; y++)
{
for(x = x_s; x < pic_info.width + x_s; x++)
{
i++;
if((x > 0 && x < 800) && (y > 0 && y < 480))
{
//把转换后的ARGB数据显示
*(lcd_p + y*800 + x) = *(argb_space + i); //*(argb_space+y*pic_info.height+x);
}
}
}
到此,在屏幕上显式 bmp 图片的代码编写完成了。下面是完整代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdlib.h>
char f_type[3]; //储存文件类型,"BM"
struct fileinfo
{
int f_size; //文件大小
int res; //保留,为0
int f_seekbits; //从文件头开始到实际像素点的偏移量
};
struct bmpinfo
{
int b_size; //图片信息头大小
int width; //图片宽度
int height; //图片高度
short plane; //图层数量,24位图只有1层
short bit; //色深,24位图是24
int compress; //是否压缩,0表示不压缩
int image_size; //位图阵列大小
int x_pels; //水平分辨率
int y_pels; //垂直分辨率
int clr; //调色板用到的颜色数量,24位图没有调色板
int clr_important; //调色板重要的颜色
};
int show_bmp(int x_s, int y_s, char *filename, unsigned int *lcd_p)
{
//以只读的方式打开图片文件
int pic_fd = open(filename, O_RDONLY);
if(pic_fd == -1)
{
perror("open pic fail!");
return -1;
}
struct fileinfo finfo;
struct bmpinfo pic_info;
//读取14字节的文件信息
read(pic_fd, f_type, 2); //文件类型类型,2字节
read(pic_fd, &finfo, sizeof(finfo)); //剩余12字节的文件信息
//40字节的图片信息
read(pic_fd, &pic_info, sizeof(pic_info));
printf("type:%s\n", f_type);
printf("width :%d\n", pic_info.width);
printf("height:%d\n", pic_info.height);
printf("bit:%d\n", pic_info.bit);
//rgb像素点占用的总字节数
int rgb_size = pic_info.width*pic_info.height*3;
//在堆空间申请内存存放RGB数据
//unsigned char *rgb_space = malloc(rgb_size);
unsigned char *rgb_space = new unsigned char[rgb_size];
if(rgb_space == NULL)
{
perror("malloc rgb_space fail!");
return -1;
}
//读取图片的RGB数据
read(pic_fd, rgb_space, rgb_size);
//ARGB像素点占用的总字节数
int argb_size = pic_info.width*pic_info.height*4;
//在堆空间申请内存存放RGB数据
//unsigned int *argb_space = malloc(argb_size);
unsigned int *argb_space = new unsigned int[argb_size];
if(argb_space == NULL)
{
perror("malloc argb_space fail!");
return -1;
}
int x, y, i=0;
unsigned char R,G,B;
//把RGB数据转换为32位的ARGB数据,并倒转图片缓存到ARGB缓存
for(y = 0; y < pic_info.height; y++)
{
for(x = 0; x < pic_info.width; x++)
{
B = *(rgb_space + 3*i + 0);
G = *(rgb_space + 3*i + 1);
R = *(rgb_space + 3*i + 2);
i++;
//480-->479行 y=0 -->480 需要pic_info.height-1=479
//把RGB数据转换为32位的ARGB数据
*(argb_space + (pic_info.height - 1 - y) * pic_info.width + x) = 0 | R<<16 | G<<8 | B<<0;
}
}
i=0;
//显示处理
for(y = y_s; y < pic_info.height + y_s; y++)
{
for(x = x_s; x < pic_info.width + x_s; x++)
{
i++;
if((x > 0 && x < 800) && (y > 0 && y < 480))
{
//把转换后的ARGB数据显示
*(lcd_p + y*800 + x) = *(argb_space + i); //*(argb_space+y*pic_info.height+x);
}
}
}
//释放堆空间内存
delete rgb_space;
delete argb_space;
//关闭图片文件
close(pic_fd);
return 0;
}
我把它封装成了一个头文件,下面是测试代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdlib.h>
#include "show_bmp.h"
int main()
{
//以读写的方式打开屏幕驱动文件
int lcd_fd = open("/dev/ubuntu_lcd", O_RDWR);
if(lcd_fd == -1) //打开文件失败,结束程序
{
perror("open lcd_fd fail!");
return -1;
}
printf("lcd_fd = %d\n", lcd_fd);
unsigned int *lcd_p = mmap(NULL, 800*480*4, PROT_READ|PROT_WRITE, MAP_SHARED, lcd_fd, 0);
if(lcd_p == MAP_FAILED ) //打开文件失败,结束程序
{
perror("mmap lcd fail!");
return -1;
}
show_bmp(0, 0, "./pic/longmao.bmp", lcd_p); // 800 * 480
show_bmp(100, 100, "./pic/longmao2.bmp", lcd_p); // 400 * 240, 从(100,100)开始显示
// 移动特效
/*for(int x=-400; x<400; x+=10)
{
show_bmp(x, 100, "./pic/longmao2.bmp", lcd_p);
usleep(100*1000);
}*/
//解除屏幕映射
munmap(lcd_p, 800*480*4);
//关闭文件
close(lcd_fd);
return 0;
}
运行结果如下:可以观察到两张图片嵌套显示。
Linux 触摸事件
上面已经实现了再屏幕上显示 bmp 图片,那么我们可不可以给屏幕添加一个点击事件,当鼠标点击屏幕,打印出该点的左边。
首先,我们得了解 struct input_event 这个结构体。定义在头文件 #include <linux/input.h>
中
struct input_event {
struct timeval time; //按键按下的时间
__u16 type; //类型,用于区别是键盘还是触摸屏数据
__u16 code; //代码,用来确定键值、X坐标或者Y坐标
__s32 value; //键值,坐标值
};
- type:设备类型。可以设置为如下,我们主要会用到 键盘或者按键,0x01
define EV_SYN 0x00 表示设备支持所有的事件
define EV_KEY 0x01 键盘或者按键,表示一个键码
define EV_REL 0x02 鼠标设备,表示一个相对的光标位置结果
define EV_ABS 0x03 手写板产生的值,其是一个绝对整数值
define EV_MSC 0x04 其他类型
define EV_LED 0x11 LED灯设备
define EV_SND 0x12 蜂鸣器,输入声音
define EV_REP 0x14 允许重复按键类型
define EV_PWR 0x16 电源管理事件
define EV_FF_STATUS 0x17
define EV_MAX 0x1f
define EV_CNT (EV_MAX+1)
完整代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>
int main()
{
//以读写的方式打开触摸屏驱动文件
int ts_fd = open("/dev/ubuntu_event", O_RDWR);
if(ts_fd == -1) //打开文件失败,结束程序
{
perror("open ts fail!");
return -1;
}
printf("ts_fd = %d\n", ts_fd);
/*
struct input_event {
struct timeval time; //按键按下的时间
__u16 type; //类型,用于区别是键盘还是触摸屏数据
__u16 code; //代码,用来确定键值、X坐标或者Y坐标
__s32 value; //键值,坐标值
};
*/
struct input_event ts;
int x=0, y=0;
while(1)
{
read(ts_fd, &ts, sizeof(ts));
/*
printf("type = %d\n", ts.type);
printf("code = %d\n", ts.code);
printf("value= %d\n", ts.value);
type = 3 EV_ABS
code = 0 ABS_X
value= 442
type = 3 EV_ABS
code = 1 ABS_Y
value= 319
*/
//判断是否为触摸屏事件
if(ts.type == EV_ABS)
{
//判断是否为X轴的数据
if(ts.code == ABS_X)
{
x = ts.value;
}
//判断是否为Y轴的数据
else if(ts.code == ABS_Y)
{
y = ts.value;
}
}
if(x>0 && y>0)
{
printf("x:%d,y:%d\n", x, y);
x=y=0;
}
}
//关闭触摸屏
close(ts_fd);
return 0;
}
运行结果: