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角度的功能,大家可以写一个旋转前后点的向量对应关系来进行公式推导,这里就不具体说了,代码如下:

扫描二维码关注公众号,回复: 16633775 查看本文章
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')

猜你喜欢

转载自blog.csdn.net/qq_36306288/article/details/128692110