python不使用第三方库实现bmp图像处理
B站:后续!!!_bilibili
CSDN:python不使用第三方库实现bmp图像处理_百年后封笔-CSDN博客
Github:封笔
公众号:百年后封笔
一、 背景
加载bmp格式图像的方式有很多,对python而言,我们有很丰富的选择,比如使用如下的第三方库所提供的强大功能,我们可以轻松实现图像的加载、处理和保存等功能,例如:
- Pillow
- opencv-python
- skimage
前两个库是非常常用的图像处理第三方库,但有的时候要求我们不能使用第三方库来处理,这时候要处理bmp图像需要我们对bmp图像的存储格式有一定的了解,因此我们只能把图像当二进制文件来读取。这里主要参考了部分这位大佬的文章,下面我按照一下步骤来说明一下,使用python内置的函数实现bmp图像读取、resize、rotate和保存功能的具体做法:
二、具体功能实现
2.1 读取bmp图像
读取bmp图像主要分为两步,一个是需要去读图像的信息头,一般是由54个字节组成,然后再读取后面的真正的图像数据:最简单的方法如下:
with open(filename, "rb") as file:
# 读取图像的信息头
header = file.read(54)
# 读取图像数据
image_data = file.read()
# 转化为十进制数字像素列表
image = list(image_data)
为了灵活完成int和bytes的转换,我们先定义下面的辅助函数,int2byte -> i2b 和 byte2int -> b2i:
def i2b(number, length, byteorder='little'):
return number.to_bytes(length, byteorder)
def b2i(mbytes, byteorder='little'):
return int.from_bytes(mbytes, byteorder)
这样读取会很简单,如果你不做resize,那么header不需要改变;但当你的图像长宽等信息发生变化时,在保存bmp时,你就需要对里面header的信息进行修改,才能正常保存,因此我们需要详细解析一下header,如下:
with open(filename, "rb") as file:
# BmpFileHeader
bfType = file.read(2)
bfSize = file.read(4)
bfReserved1 = file.read(2)
bfReserved2 = file.read(2)
bfOffBits = file.read(4)
# BmpStructHeader
biSize = file.read(4)
biWidth = file.read(4)
biHeight = file.read(4)
biPlanes = file.read(2)
biBitCount = file.read(2)
# pixel size
biCompression = file.read(4)
biSizeImage = file.read(4)
biXPelsPerMeter = file.read(4)
biYPelsPerMeter = file.read(4)
biClrUsed = file.read(4)
biClrImportant = file.read(4)
print("bfType: ", b2i(bfType))
print("bfSize: ", b2i(bfSize))
print("biSize: ", b2i(biSize))
print("biWidth: ", b2i(biWidth))
print("biHeight: ", b2i(biHeight))
print("biBitCount: ", b2i(biBitCount))
# 读取图像数据
image_data = file.read()
# 转化为列表
image = list(image_data)
bfType: 19778
bfSize: 5248854
biSize: 40
biWidth: 1620
biHeight: 1080
biBitCount: 24
详细的bmp图像信息头的解析分析,可以看一下这位大佬的博客,其实bmp的header中主要包含的两类数据:文件信息头,图像结构信息和调色板信息。我们主要关心的就是图像的 宽高和通道信息,也就时biWidth,biHeight和biBitCount。
2.2 resize功能
resize的功能一般是通过将新图像的像素点映射到原图上,从而完成色彩的对应,但由于转换过程中存在非整数的像素位置,因此如果要实现更好的效果,那么可以使用一些图像插值的方法,例如:最近邻插值、双线性插值和双三次插值等方式,使得图像resize后的尺寸更加合理。这里只是实现了一种最简单的映射关系,代码如下:
def resize_image(self, new_w, new_h):
width, height = b2i(self.biWidth), b2i(self.biHeight)
new_width, new_height = new_w, new_h
scale_w, scale_h = new_width / width, new_height / height
# 创建新的图像数据列表
new_image = []
# 遍历每一行
for h in range(new_height):
# 计算原始图像中对应的行
original_row = int(h / scale_h)
# 遍历每一列
for w in range(new_width):
# 计算原始图像中对应的列
original_col = int(w / scale_w)
# 获取对应的像素值
pixel = self.img[
(original_row * width + original_col) * 3: (original_row * width + original_col) * 3 + 3]
# 添加到新的图像数据列表中
new_image += pixel
self.img = new_image
# 修改图像的header
self.bfSize = i2b(new_height * new_width * b2i(self.biBitCount) // 8 + 54, 4)
self.biSizeImage = i2b(len(new_image), 4)
self.biWidth = i2b(new_width, 4)
self.biHeight = i2b(new_height, 4)
return new_image
2.3 rotate功能
rotate其实也很简单,只是一个抛砖引玉功能,大家可以自己实现一些如图像的局部scale,形态学操作、锐化等等功能。这里rotate实现的是以图像中心点为中心,顺时针旋转angle角度的功能,大家可以写一个旋转前后点的向量对应关系来进行公式推导,这里就不具体说了,代码如下:
def rotate_image(self, angle):
width, height = b2i(self.biWidth), b2i(self.biHeight)
# 创建新的图像数据列表
new_image = [0] * (width * height * 3)
# 遍历每一行
for h in range(height):
# 遍历每一列
for w in range(width):
# 计算旋转后的行和列
new_row = int((h - height / 2) * math.cos(angle) - (w - width / 2) * math.sin(angle) + height / 2)
new_col = int((h - height / 2) * math.sin(angle) + (w - width / 2) * math.cos(angle) + width / 2)
# 判断是否在新的图像范围内
if new_row >= 0 and new_row < height and new_col >= 0 and new_col < width:
# 获取对应的像素值
pixel = self.img[(h * width + w) * 3: (h * width + w) * 3 + 3]
# 添加到新的图像数据列表中
new_image[(new_row * width + new_col) * 3: (new_row * width + new_col) * 3 + 3] = pixel
self.img = new_image
2.4 保存bmp图像
有了更改后的图像,我们当然需要将他保存起来才可以方便查看,如果你改变了图像的结构或者调色板信息,那么你就需要对2.1所读取的header中的具体信息进行修改,例如你resize了图像,那么你就需要改变,如下信息,别忘了。
# 修改图像的header
self.bfSize = i2b(new_height * new_width * b2i(self.biBitCount) // 8 + 54, 4)
self.biSizeImage = i2b(len(new_image), 4)
self.biWidth = i2b(new_width, 4)
self.biHeight = i2b(new_height, 4)
原图的header信息中的其他内容不需要改变,我们打开一个新的文件,然后按照二进制写入header,然后再把新图像写入即可:
with open(filename, 'wb') as file:
file.write(self.bfType)
file.write(self.bfSize)
file.write(self.bfReserved1)
file.write(self.bfReserved2)
file.write(self.bfOffBits)
# reconstruct bmp header
file.write(self.biSize)
file.write(self.biWidth)
file.write(self.biHeight)
file.write(self.biPlanes)
file.write(self.biBitCount)
file.write(self.biCompression)
file.write(self.biSizeImage)
file.write(self.biXPelsPerMeter)
file.write(self.biYPelsPerMeter)
file.write(self.biClrUsed)
file.write(self.biClrImportant)
# reconstruct pixels
file.write(bytes(self.img))
file.close()
三、完整代码
下面是直接可以运行的代码:
import math
import sys
def i2b(number, length, byteorder='little'):
return number.to_bytes(length, byteorder)
def b2i(mbytes, byteorder='little'):
return int.from_bytes(mbytes, byteorder)
class BMPReader:
def __init__(self):
self.img = None
def read_bmp(self, filename):
try:
with open(filename, "rb") as file:
# header = file.read(54)
# BmpFileHeader
self.bfType = file.read(2)
self.bfSize = file.read(4)
self.bfReserved1 = file.read(2)
self.bfReserved2 = file.read(2)
self.bfOffBits = file.read(4)
# BmpStructHeader
self.biSize = file.read(4)
self.biWidth = file.read(4)
self.biHeight = file.read(4)
self.biPlanes = file.read(2)
self.biBitCount = file.read(2)
# pixel size
self.biCompression = file.read(4)
self.biSizeImage = file.read(4)
self.biXPelsPerMeter = file.read(4)
self.biYPelsPerMeter = file.read(4)
self.biClrUsed = file.read(4)
self.biClrImportant = file.read(4)
print("bfType: ", b2i(self.bfType))
print("bfSize: ", b2i(self.bfSize))
print("biSize: ", b2i(self.biSize))
print("biWidth: ", b2i(self.biWidth))
print("biHeight: ", b2i(self.biHeight))
print("biBitCount: ", b2i(self.biBitCount))
# 读取图像数据
image_data = file.read()
# 转化为列表
image = list(image_data)
self.img = image
return 0
except Exception as e:
print(e)
return -1
def save_bmp(self, filename):
# 读取文件头
with open(filename, 'wb') as file:
file.write(self.bfType)
file.write(self.bfSize)
file.write(self.bfReserved1)
file.write(self.bfReserved2)
file.write(self.bfOffBits)
# reconstruct bmp header
file.write(self.biSize)
file.write(self.biWidth)
file.write(self.biHeight)
file.write(self.biPlanes)
file.write(self.biBitCount)
file.write(self.biCompression)
file.write(self.biSizeImage)
file.write(self.biXPelsPerMeter)
file.write(self.biYPelsPerMeter)
file.write(self.biClrUsed)
file.write(self.biClrImportant)
# reconstruct pixels
file.write(bytes(self.img))
file.close()
def resize_image(self, new_w, new_h):
width, height = b2i(self.biWidth), b2i(self.biHeight)
new_width, new_height = new_w, new_h
scale_w, scale_h = new_width / width, new_height / height
# 创建新的图像数据列表
new_image = []
# 遍历每一行
for h in range(new_height):
# 计算原始图像中对应的行
original_row = int(h / scale_h)
# 遍历每一列
for w in range(new_width):
# 计算原始图像中对应的列
original_col = int(w / scale_w)
# 获取对应的像素值
pixel = self.img[
(original_row * width + original_col) * 3: (original_row * width + original_col) * 3 + 3]
# 添加到新的图像数据列表中
new_image += pixel
self.img = new_image
self.bfSize = i2b(new_height * new_width * b2i(self.biBitCount) // 8 + 54, 4)
self.biSizeImage = i2b(len(new_image), 4)
self.biWidth = i2b(new_width, 4)
self.biHeight = i2b(new_height, 4)
return new_image
def rotate_image(self, angle):
width, height = b2i(self.biWidth), b2i(self.biHeight)
# 创建新的图像数据列表
new_image = [0] * (width * height * 3)
# 遍历每一行
for h in range(height):
# 遍历每一列
for w in range(width):
# 计算旋转后的行和列
new_row = int((h - height / 2) * math.cos(angle) - (w - width / 2) * math.sin(angle) + height / 2)
new_col = int((h - height / 2) * math.sin(angle) + (w - width / 2) * math.cos(angle) + width / 2)
# 判断是否在新的图像范围内
if new_row >= 0 and new_row < height and new_col >= 0 and new_col < width:
# 获取对应的像素值
pixel = self.img[(h * width + w) * 3: (h * width + w) * 3 + 3]
# 添加到新的图像数据列表中
new_image[(new_row * width + new_col) * 3: (new_row * width + new_col) * 3 + 3] = pixel
self.img = new_image
if __name__ == '__main__':
br = BMPReader()
br.read_bmp('ipc104.bmp')
br.resize_image(new_w=800, new_h=600)
br.rotate_image(90)
br.save_bmp('save.bmp')