常见的bmp图片是一种不压缩的24位真彩色图片。不压缩是指每个像素点都存储了888的RGB数据,即红绿蓝都是8bit,一共可以表示16777216(2^24次方)种颜色。
由于这种无损的特征,bmp图片常常在数字验证中作为测试数据。在scoreboard中对比时可以做到零误差,对调试比较有利。
bmp格式介绍
bmp图片由两部分组成:文件头和RGB数据。
文件头的格式如下图:
其中,对于24位真彩BMP图来说,
以一幅分辨率为1080x1920的FHD的BMP图片为例,手工计算bmp文件头过程如下:
1080是一行的像素个数,即宽度为00 00 04 38
1920是高度,即00 00 07 80
一行的字节数是1080x3,是4的整倍数,不需要补0
RGB数据总大小1080x3x1920,即00 5e ec 00
文件总大小1080x3x1920+54,即00 5e ec 36
按照文件头的字段顺序进行拼装
下图是一幅FHD的图片按十六进制显示的截图,可以看到与我们手工计算的结果一致。
用python来产生bmp文件头
我们定义一个class bmp来处理bmp文件相关的数据结构和操作。self.w和self.h分别是图片宽度和高度。有了图片分辨率后bmp文件头就完全确定了。需要注意的是如果一行的字节数不是4的整倍数需要补0。
class bmp:
""" bmp data structure """
def __init__(self, w=1080, h=1920):
self.w = w
self.h = h
def calc_data_size (self):
if((self.w*3)%4 == 0):
self.dataSize = self.w * 3 * self.h
else:
self.dataSize = (((self.w * 3) // 4 + 1) * 4) * self.h
self.fileSize = self.dataSize + 54
def conv2byte(self, l, num, len):
tmp = num
for i in range(len):
l.append(tmp & 0x000000ff)
tmp >>= 8
def gen_bmp_header (self):
self.calc_data_size();
self.bmp_header = [0x42, 0x4d]
self.conv2byte(self.bmp_header, self.fileSize, 4)
self.conv2byte(self.bmp_header, 0, 2)
self.conv2byte(self.bmp_header, 0, 2)
self.conv2byte(self.bmp_header, 54, 4)
self.conv2byte(self.bmp_header, 40, 4)
self.conv2byte(self.bmp_header, self.w, 4)
self.conv2byte(self.bmp_header, self.h, 4)
self.conv2byte(self.bmp_header, 1, 2)
self.conv2byte(self.bmp_header, 24, 2)
self.conv2byte(self.bmp_header, 0, 4)
self.conv2byte(self.bmp_header, self.dataSize, 4)
self.conv2byte(self.bmp_header, 0, 4)
self.conv2byte(self.bmp_header, 0, 4)
self.conv2byte(self.bmp_header, 0, 4)
self.conv2byte(self.bmp_header, 0, 4)
def print_bmp_header (self):
length = len(self.bmp_header)
for i in range(length):
print("{:0>2x}".format(self.bmp_header[i]), end=' ')
if i%16 == 15:
print('')
print('')
在图片上画线和矩形
在图片上画线和矩形的操作本质上就是对rgbData数组进行覆盖写操作。
比如画线,给起始点(x1, y1)和结束点(x2, y2),根据两点间的直线方程y = (y2-y1)/(x2-x1)*(x-x1) + y1,来描点。矩形比较简单,就是把行列范围内覆盖掉。
def paint_bgcolor(self, color=0xffffff):
self.rgbData = []
for r in range(self.h):
self.rgbDataRow = []
for c in range(self.w):
self.rgbDataRow.append(color)
self.rgbData.append(self.rgbDataRow)
def paint_line(self, x1, y1, x2, y2, color):
k = (y2 - y1) / (x2 - x1)
for x in range(x1, x2+1):
y = int(k * (x - x1) + y1)
self.rgbData[y][x] = color
def paint_rect(self, x1, y1, w, h, color):
for x in range(x1, x1+w):
for y in range(y1, y1+h):
self.rgbData[y][x] = color
把bmp图片写到文件
写bmp图片就是写二进制文件,用‘wb’。
列表转bytes数组的方法:array.array('B', list).tobytes()
,注意list里的数据需要在0~255之间。
注意在bmp文件里,蓝色在低字节,红色在高字节。
def save_image(self, name="save.bmp"):
f = open(name, 'wb')
f.write(array.array('B', self.bmp_header).tobytes())
zeroBytes = self.dataSize // self.h - self.w * 3
for r in range(self.h):
l = []
for i in range(len(self.rgbData[r])):
p = self.rgbData[r][i]
l.append(p & 0x0000ff)
p >>= 8
l.append(p & 0x0000ff)
p >>= 8
l.append(p & 0x0000ff)
f.write(array.array('B', l).tobytes())
for i in range(zeroBytes):
f.write(bytes([0x00]))
f.close()
测试程序
新建一幅500x500分辨率的图片,背景填充绿色,画一条线,起点(50,50),终点(450,450),再画一个矩形,起点(100,100),宽100,高200。
if __name__ == '__main__':
image = bmp(500, 500)
image.gen_bmp_header()
image.print_bmp_header()
image.paint_bgcolor(0x00ff00)
image.paint_line(50, 50, 450, 450, 0xff0000)
image.paint_rect(100, 100, 100, 200, 0x0000ff)
image.save_image("save1.bmp")
最终效果如下图: