簡單bmp圖片處理工具——python實現


發現問題,目前正在調試:

灰度化圖片時,如果圖片的高度比寬高,會提示超出數組邊界,沒想通,正在看。

是因為之前初始化像素個數的時候出了些問題,現在已經修正。github正在重新提交

 

預備實現功能:

  1、讀取bmp文件

  2、保存bmp文件

  3、對bmp圖片進行放大、縮小

  4、對bmp圖片進行灰度化

  5、對bmp圖片進行旋轉

 

bmp文件格式非常簡單,對於我這種初學者來說減少了不少不必要麻煩,故選擇寫一個處理bmp格式的工具。因為之前自學python一直沒有動手,所以語言選擇python。

 

第一步、熟悉bmp文件格式,完成bmp文件的解析、生成

  參考了如下博客

   1、http://blog.csdn.net/lanbing510/article/details/8176231

     2、http://blog.csdn.net/jemenchen/article/details/52658476

 根據上面的博客,了解了基本的bmp文件格式,bmp文件可以簡單的分為:

  1、文件信息頭

  2、位圖信息頭

  3、調色板

  4、像素信息

 因為現在的bmp圖片一般都沒有調色板信息(因為24位),所以忽略第三個。所以bmp文件一般的頭部信息(包括文件信息頭和位圖信息圖)總共占用54個字節

使用16進制查看器打開一張bmp格式圖片,我們可以看到如下信息

圖1 bmp文件

  圖中藍色部分就是bmp文件的頭部信息,后面的FF為像素數據。可以看到,頭部信息總共有54個字節(不包括調色板),具體這些數據由什么組成,可以查看上述的參考博文。

  所以現在我們的思路就非常清晰,讀取文件的頭部信息后,在讀取文件的位圖數據,即可完成對bmp文件的解析。

  所以我們構造了如下的類

 

文件信息頭類

class BmpFileHeader:
    def __init__(self):
        self.bfType = i_to_bytes(0, 2)  # 0x4d42 對應BM
        self.bfSize = i_to_bytes(0, 4)  # file size
        self.bfReserved1 = i_to_bytes(0, 2)
        self.bfReserved2 = i_to_bytes(0, 2)
        self.bfOffBits = i_to_bytes(0, 4)  # header info offset

 

位圖信息頭

class BmpStructHeader:
    def __init__(self):
        self.biSize = i_to_bytes(0, 4)  # bmpheader size
        self.biWidth = i_to_bytes(0, 4)
        self.biHeight = i_to_bytes(0, 4)
        self.biPlanes = i_to_bytes(0, 2)  # default 1
        self.biBitCount = i_to_bytes(0, 2)  # one pixel occupy how many bits
        self.biCompression = i_to_bytes(0, 4)
        self.biSizeImage = i_to_bytes(0, 4)
        self.biXPelsPerMeter = i_to_bytes(0, 4)
        self.biYPelsPerMeter = i_to_bytes(0, 4)
        self.biClrUsed = i_to_bytes(0, 4)
        self.biClrImportant = i_to_bytes(0, 4)

 

bmp類至少應該有三個信息:

  1. 文件信息頭
  2. 位圖信息頭
  3. 位圖數據

在擁有了上述信息后,下面的事情就變得非常簡單,只需要去讀取文件,並按照bmp的格式構造出頭部信息,位圖數據即可完成bmp的解析

解析過程中,我們要注意數據的大小端問題 

我們讀取、寫入數據要以小段模式(這個有點容易混淆,我可能解釋的也有點不清楚)

bmp類  

class Bmp(BmpFileHeader, BmpStructHeader):
    def __init__(self):
        BmpFileHeader.__init__(self)
        BmpStructHeader.__init__(self)
        self.__bitSize = 0  # pixels size
        self.bits = []  # pixel array

    @property
    def width(self):
        return bytes_to_i(self.biWidth)

    @property
    def height(self):
        return bytes_to_i(self.biHeight)

    # unit is byte
    @property
    def bit_count(self):
        return bytes_to_i(self.biBitCount) // 8

    @property
    def width_step(self):
        return self.bit_count * self.width

 

bmp類中解析方法:

 # resolve a bmp file
    def parse(self, file_name):
        file = open(file_name, 'rb')
        # 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.__bitSize = (int.from_bytes(self.bfSize,
                                         'little') -
                          int.from_bytes(self.bfOffBits, 'little')) \
                         // (int.from_bytes(self.biBitCount, 'little') // 8)
        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)
        #  load pixel info
        count = 0
        while count < self.__bitSize:
            bit_count = 0
            while bit_count < (int.from_bytes(self.biBitCount, 'little') // 8):
                self.bits.append(file.read(1))
                bit_count += 1
            count += 1
        file.close()

 

有了上述信息,我們再重新生成bmp文件就很簡單了,直接將數據再重新寫回去就可以了,如果有額外要求,可以自己構建頭部信息,然后再重新寫回

 

bmp類中生成方法

    def generate(self, file_name):
        file = open(file_name, 'wb+')
        # reconstruct File Header
        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
        for bit in self.bits:
            file.write(bit)
        file.close()

至此,我們就已經完成了bmp圖片的解析和生成。

隨后,我們就可以根據我們讀取出來的位圖數據,來進行我們需要的操作,從而達到處理圖像的功能。

 

第二步,實現圖片的放大,縮小

參考博客:http://blog.csdn.net/zhangla1220/article/details/41014541

作為入門級小白,我起初對圖像處理是一無所知,所以根本不知道應該如何去放大,縮小一張圖片,經過查閱百度后,發現常用的算法有兩種

  1. 最近鄰內插值
  2. 雙線性插值法

上述兩個算法博客中都有相應思路,這里只簡單提一下

最近鄰是最簡單的算法,基本思路也就是成比例放大后,將源圖像坐標映射到目標圖像,所以我選擇用這種算法作為我的處理算法。

雙線性法的處理效果比最近鄰要好,但是相對復雜,所以我暫時沒有實現該算法。

既然選擇了最近鄰算法,接下來要說明一些我們要獲取的參數

第一步:我們首先得明確一個概念,每一行(指的是圖片width * 每一個像素占的字節)的元素必須是4的倍數,否則計算機會無法識別該文件(這是我后面遇到的坑)

第二步:我們應該知道 源坐標和目的坐標的轉換方式,我們假設 w是源圖像寬,h是源圖像高,W是目標圖像寬,H是目標圖像高,設(x,y)是源圖像坐標,(X,Y)是目標圖像坐標

所以我們可以得到下列公式

x = h / H * X;

y = w / W * Y

第三步:我再解析bmp圖片時,是將圖片位圖數據存儲到了一個一維數組里,所以我們要進行對應的x,y轉換到我們的一維數組上(應該就是個行優先,列優先問題),要注意的是,一個像素是由多個字節組成的。

遇到問題:

因為我們計算機中圖像是以左上角為原點,所以在進行源圖像和目標圖像映射的時候,我們要進行轉換,否則就會不正常,這個是我解決了很久的問題,最后是查看了一個博文提到了這個問題,我才知道(博文地址找不到了。。。)

 

nni代碼

   def resize(self, width, height):
        self.__nni(width, height)

    # nearest_neighbor Interpolation
    def __nni(self, width, height):
        # width must be Multiple of four
        if width % 4 != 0:
            width -= width % 4
        w_ratio = (self.height / height)
        h_ratio = (self.width / height)
        # new pixels array
        new_bits = [b''] * height * width * self.bit_count
        for row in range(0, height):
            for col in range(0, width):
                for channel in range(0, self.bit_count):
                    old_r = round((row + 0.5) * w_ratio - 0.5)   # 這里的 +0.5 -0.5 就是對坐標進行轉換
                    old_c = round((col + 0.5) * h_ratio - 0.5)
                    new_index = row * width * self.bit_count + col * self.bit_count + channel
                    old_index = old_r * self.width_step + old_c * self.bit_count + channel
                    new_bits[new_index] = self.bits[old_index]
        self.bits = new_bits
        # reset header info
        self.bfSize = i_to_bytes(height * width * self.bit_count + 54, 4)
        self.biSizeImage = i_to_bytes(len(new_bits), 4)
        self.biWidth = i_to_bytes(width, 4)
        self.biHeight = i_to_bytes(height, 4)

實現的效果如下,將一個988*423的bmp文件縮小為8*8的

 

                  圖2 源圖像  

 

               

                 圖3 8*8

第三步,對圖片進行灰度化

灰度化就很簡單了,最簡單的做法就是求R,G,B的平均值

這里采用了常見的灰度化公式,這是整數算法,減少了浮點計算,在一定程度上提高了速度

Gray = (R*299 + G*587 + B*114 + 500) / 1000

 

灰度化代碼

   # put bmp graying
    def graying(self):
        new_bits = [b''] * self.width * self.height * self.bit_count
        for i in range(0, self.height):
            for j in range(0, self.width):
                s_index = i * self.width_step + j * self.bit_count
                target_index = i * self.width_step + j * self.bit_count
                r = int.from_bytes(self.bits[s_index + 2], 'little')
                g = int.from_bytes(self.bits[s_index + 1], 'little')
                b = int.from_bytes(self.bits[s_index], 'little')
                gray = (r * 30 + g * 59 + b * 11) / 100
                new_bits[target_index] = int(gray).to_bytes(1, 'little')
                new_bits[target_index + 1] = int(gray).to_bytes(1, 'little')
                new_bits[target_index + 2] = int(gray).to_bytes(1, 'little')
        self.bits = new_bits

 

實現效果:

                                                                                圖4 灰度化效果

第四步,對圖片進行旋轉

參考博文:

  1. http://blog.csdn.net/liyuan02/article/details/6750828
  2. http://blog.csdn.net/lkj345/article/details/50555870

第一篇博文完整分析了圖片旋轉的公式原理,說的非常清晰,有興趣可以手寫一遍,挺簡單,而且清晰。

原理我就不說了,不在這獻丑了~

來說說我遇到的問題

1、我不清楚如何要獲得我們旋轉后的寬和高,參考博文2中給出了公式,當時我並沒有看懂,后面找到了一張圖后,就明白了,這這里分享出這張圖。

                    圖5 旋轉原理圖

說明: w為原寬,h為原高 h'為旋轉后的高  w'為旋轉后的寬

從而可以看出

h' = h * cos + w * sin

w' = h * sin + w * cos

從而我們就得到了旋轉后的寬和高,旋轉后的圖片所在的矩形區域是圖中綠色部分。

 

我使用了上述博文中提到的反映射方法和最近鄰插值法

代碼如下:

 def rotate(self):
        self.__rotate(90)

    """
    reference: http://blog.csdn.net/liyuan02/article/details/6750828
    attention: in the loop, the x in real bmp is represent y, the y same too.
    """
    def __rotate(self, degree):
        cos_degree = math.cos(math.radians(degree))
        sin_degree = math.sin(math.radians(degree))
        h = math.ceil(self.height * cos_degree
                      + self.width * sin_degree)
        w = math.ceil(self.height * sin_degree
                      + self.width * cos_degree)
        h = abs(h)
        w = abs(w)
        if w % 4 != 0:
            w -= w % 4
        dx = -0.5 * w * cos_degree - 0.5 * h * sin_degree + 0.5 * self.width
        dy = 0.5 * w * sin_degree - 0.5 * h * cos_degree + 0.5 * self.height
        new_bits = [b''] * w * h * 3
        for x in range(0, h):
            for y in range(0, w):
                x0 = y * cos_degree + x * sin_degree + dx
                y0 = -y * sin_degree + x * cos_degree + dy
                src_index = round(y0) * self.width_step + round(x0) * self.bit_count
                dst_index = x * w * self.bit_count + y * self.bit_count
                if len(self.bits) - self.bit_count > src_index >= 0:
                    new_bits[dst_index + 2] = self.bits[src_index + 2]
                    new_bits[dst_index + 1] = self.bits[src_index + 1]
                    new_bits[dst_index] = self.bits[src_index]
                else:
                    new_bits[dst_index + 2] = i_to_bytes(255, 1)
                    new_bits[dst_index + 1] = i_to_bytes(255, 1)
                    new_bits[dst_index] = i_to_bytes(255, 1)
        self.bits = new_bits
        self.biWidth = i_to_bytes(w, 4)
        self.biHeight = i_to_bytes(h, 4)

要注意的問題是:

在for循環中的 x 是實際圖像的 高

在for循環中的 y是實際圖像中的寬

之前沒注意該問題,直接套公式,結果一直有問題。

我目前只是初版,選擇90°效果還行,其他度數的話,代碼可能要進行改動。

我這里是默認逆時針旋轉90°

效果如下:

         圖6  逆時針旋轉90°

 

到此,預期實現功能結束。

因為我所在地區比較偏遠,github訪問不便,等會上傳成功后,給出源碼鏈接

github:

https://github.com/zyp461476492/SimpleBmpResolver.git

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM