继续折腾ST7789v液晶屏幕,这次我们要在屏幕上显示BMP文件图片。
BMP图片文件格式问题,网上有很多文章能找到,我也找了很多资料来学习。关于文件头的问题,网上的文章大概都讲得很透彻。我只是拿这个ST7789v液晶来玩玩,没那么多时间去研究和兼容每种格式,所以我只打算锚定其中一种比较简单的24位真彩色格式来显示在我的液晶上,其他格式可以在电脑上转换成24位真彩色格式后再传给液晶。
所以,在文章的开头,我还是得啰嗦一下24位真彩色格式的BMP文件结构。
BMP文件包含四个部分:
1.位图文件头(BITMAPFILEHEADER)(14字节)
2.位图信息头(BITMAPINFOHEADER)(40字节)
3.颜色表*(RGBQUAD[])(不一定存在)
4.像素阵列(Pixels[][])(图像数据)
其中1和2是固定的大小;3在24位真彩色格式中是没有的,可以忽略;4就是图像数据了。
文件头的结构定义如下:
typedef struct tagBITMAPFILEHEADER{
WORD bfType; // 位图文件的类型,必须为BMP (2个字节)
DWORD bfSize; // 位图文件的大小,以字节为单位 (4个字节)
WORD bfReserved1; // 位图文件保留字,必须为0 (2个字节)
WORD bfReserved2; // 位图文件保留字,必须为0 (2个字节)
DWORD bfOffBits; // 位图数据的起始位置,以相对于位图 (4个字节)
} BITMAPFILEHEADER;
其中文件的大小bfSize稍微有点用,可以用以下代码读取出来:
f.seek(2)#文件大小
buff=f.read(4)
fileSize=buff[3]*1024+buff[2]*512+buff[1]*256+buff[0]
print("BMP文件大小:",fileSize,"字节")
下图是本文所用的一张图片用二进制编辑器打开后的文件大小数值。
注意,低位在后,高位在前。图中文件大小是十六进制的0x00016926,十进制是92454字节。
后续占用多个字节的数值都是用这种低位在后,高位在前的规则。
信息头的结构体定义如下:
typedef struct tagBITMAPINFOHEADER{
DWORD biSize; // 本结构所占用字节数 (4个字节)
LONG biWidth; // 位图的宽度,以像素为单位(4个字节)
LONG biHeight; // 位图的高度,以像素为单位(4个字节)
WORD biPlanes; // 目标设备的级别,必须为1(2个字节)
WORD biBitCount; // 每个像素所需的位数,必须是1(双色)、// 4(16色)、8(256色)、
//24(真彩色)或32(增强真彩色)之一 (2个字节)
DWORD biCompression; // 位图压缩类型,必须是 0(不压缩)、 1(BI_RLE8
// 压缩类型)或2(BI_RLE4压缩类型)之一 ) (4个字节)
DWORD biSizeImage; // 位图的大小,以字节为单位(4个字节)
LONG biXPelsPerMeter; // 位图水平分辨率,每米像素数(4个字节)
LONG biYPelsPerMeter; // 位图垂直分辨率,每米像素数(4个字节)
DWORD biClrUsed; // 位图实际使用的颜色表中的颜色数(4个字节)
DWORD biClrImportant; // 位图显示过程中重要的颜色数(4个字节)
} BITMAPINFOHEADER;
其中有几个信息是必须要取出来的。
biWidth; // 位图的宽度,以像素为单位(4个字节),--即图片横向的像素宽度。
biHeight; // 位图的高度,以像素为单位(4个字节),--即图片纵向的像素高度。
biBitCount; // 每个像素所需的位数,必须是1(双色)、// 4(16色)、8(256色)、
//24(真彩色)或32(增强真彩色)之一 (2个字节)。---判断图片格式是不是24位真彩色
24位真彩色格式没有颜色表,直接忽略。
很多文章里没有详细介绍这部分的格式,我也走了写弯路,所以我必须把这部分详细讲一下。
24位真彩色文件格式里,像素阵列是从第55个字节开始的(文件头14+信息头40),从0开始的话是54。
1个像素占3个字节,顺序分别是B、G、R(注意顺序不是RGB!!!)。
但是的但是,你要注意了!第55字节并不是图片左上第一个像素点!!!
数据的规律是这样的。
首先,由于某些原因(DWORD),像素阵列对图片每一行数据的字节数进行了约束,必须是4的整数倍。比如,图像横向时5个像素点,每像素3字节,3*5=15字节;为了是4的倍数,这时就会填充以0x00填充1个字节。这时图片一行所占用的字节数是16,而不是15,这个知识点网上大多数文章都提到了。
然后下面我要讲的这个知识点网上那些文章就没有告诉你了。
用个例子说明:
假设一张图片是横向5像素,纵向4像素。前面讲了,横向每行占用的字节数是16字节。一共4行,占用的字节数就应该是16*4=64字节。
这64字节在BMP文件中的存放时,行顺序是倒着像下图这样存放的(图中蓝色箭头的顺序):
即:首先是最后一行,然后倒数第二行。。。。最后是第一行。
如果不知道这个规律,你的图像显示可能变成这个样子(倒着显示):
好了,知识点到此就讲完了,剩下的就是源代码了。
'''
本程序只处理24位真彩色bmp文件。24位真彩色即RGB888,分别用8位(1个字节)
来表达一个像素点的R、G、B色彩信息。
BMP文件包含四个部分:
1.位图文件头(BITMAPFILEHEADER)
2.位图信息头(BITMAPINFOHEADER)
3.颜色表*(RGBQUAD[])----不一定存在
4.像素阵列(Pixels[][])
各个部分的结构:
1.文件头(14字节)
typedef struct tagBITMAPFILEHEADER{
WORD bfType; // 位图文件的类型,必须为BMP (2个字节)
DWORD bfSize; // 位图文件的大小,以字节为单位 (4个字节)
WORD bfReserved1; // 位图文件保留字,必须为0 (2个字节)
WORD bfReserved2; // 位图文件保留字,必须为0 (2个字节)
DWORD bfOffBits; // 位图数据的起始位置,以相对于位图 (4个字节)
} BITMAPFILEHEADER;
2.信息头(40字节),用于描述大小等信息
typedef struct tagBITMAPINFOHEADER{
DWORD biSize; // 本结构所占用字节数 (4个字节)
LONG biWidth; // 位图的宽度,以像素为单位(4个字节)
LONG biHeight; // 位图的高度,以像素为单位(4个字节)
WORD biPlanes; // 目标设备的级别,必须为1(2个字节)
WORD biBitCount; // 每个像素所需的位数,必须是1(双色)、// 4(16色)、8(256色)、
//24(真彩色)或32(增强真彩色)之一 (2个字节)
DWORD biCompression; // 位图压缩类型,必须是 0(不压缩)、 1(BI_RLE8
// 压缩类型)或2(BI_RLE4压缩类型)之一 ) (4个字节)
DWORD biSizeImage; // 位图的大小,以字节为单位(4个字节)
LONG biXPelsPerMeter; // 位图水平分辨率,每米像素数(4个字节)
LONG biYPelsPerMeter; // 位图垂直分辨率,每米像素数(4个字节)
DWORD biClrUsed; // 位图实际使用的颜色表中的颜色数(4个字节)
DWORD biClrImportant; // 位图显示过程中重要的颜色数(4个字节)
} BITMAPINFOHEADER;
3.颜色表
typedef struct tagRGBQUAD
{
BYTE rgbBlue; // 蓝色的亮度(值范围为0-255)
BYTE rgbGreen; // 绿色的亮度(值范围为0-255)
BYTE rgbRed; // 红色的亮度(值范围为0-255)
BYTE rgbReserved; // 保留,必须为0
} RGBQUAD;
可以看到一个RGB表项为4个字节。
颜色表中RGBQUAD结构数据的个数由位图信息头中的biBitCount来确定:
当biBitCount=1, 4, 8时,分别有2, 16,256个表项
!!!!!!!!当biBitCount=24时,没有颜色表项!!!!!!!!!
4.像素阵列
记录了位图的每一个像素值,记录顺序是在扫描行内是从左到右,扫描行之间是从下到上。位图的一个像素值所占的字节数如下:
当biBitCount=1时,8个像素占1个字节;
当biBitCount=4时,2个像素占1个字节;
当biBitCount=8时,1个像素占1个字节;
当biBitCount=24时,1个像素占3个字节,分别是R、G、B;
Windows规定一个扫描行所占的字节数必须是4的倍数(即以long为单位),不足的以0填充。
'''
f=open('csdn.bmp',"rb")
buff=f.read(1)
cnt=0
f.seek(2)#文件大小
buff=f.read(4)
fileSize=buff[3]*1024+buff[2]*512+buff[1]*256+buff[0]
print("BMP文件大小:",fileSize,"字节")
f.seek(18)#水平分辨率
buff=f.read(4)
HResolution=buff[3]*1024+buff[2]*512+buff[1]*256+buff[0]
print("图片水平分辨率:",HResolution,"像素")
f.seek(22)#垂直分辨率
buff=f.read(4)
VResolution=buff[3]*1024+buff[2]*512+buff[1]*256+buff[0]
print("图片垂直分辨率:",VResolution,"像素")
f.seek(28)#每个像素所需的位数
buff=f.read(2)
colorMode=buff[1]*256+buff[0]
print("颜色模式:",colorMode)
BMPBuffer=bytearray(0) #准备传给液晶的buffer
#计算图片每行占用的字节数(向上往4的整数倍靠)
if HResolution*3%4==0:
bytesPerRow=(HResolution*3//4)*4
else:
bytesPerRow=(HResolution*3//4+1)*4
rgb565=bytearray(2)
for rowCnt in range(VResolution-1,-1,-1):#行号要倒着来
f.seek(bytesPerRow*rowCnt+54)#注意偏移量54,因为图像数据是从54字节开始的
for cntInARow in range(int(bytesPerRow/3)):#读取行中的有效数据,填充的0x00将被忽略
buff=f.read(3)#RGB888格式,一次读取3字节
#从24位真彩色RGB888转换为RGB565格式
blue=buff[0]&0xf8#注意第一字节为蓝色blue数据
green=buff[1]&0xfc
red=buff[2]&0xf8
rgb565[0]=red | (green & 0xe0)>>5
rgb565[1]=(blue>>3) | ((green & 0x1b) <<3)
BMPBuffer.append(rgb565[0])
BMPBuffer.append(rgb565[1])
f.close()
tft.blit_buffer(BMPBuffer, x=80, y=40, width=HResolution, height=VResolution)
显示效果: