發現問題,目前正在調試:
灰度化圖片時,如果圖片的高度比寬高,會提示超出數組邊界,沒想通,正在看。
是因為之前初始化像素個數的時候出了些問題,現在已經修正。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類至少應該有三個信息:
- 文件信息頭
- 位圖信息頭
- 位圖數據
在擁有了上述信息后,下面的事情就變得非常簡單,只需要去讀取文件,並按照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
作為入門級小白,我起初對圖像處理是一無所知,所以根本不知道應該如何去放大,縮小一張圖片,經過查閱百度后,發現常用的算法有兩種
- 最近鄰內插值
- 雙線性插值法
上述兩個算法博客中都有相應思路,這里只簡單提一下
最近鄰是最簡單的算法,基本思路也就是成比例放大后,將源圖像坐標映射到目標圖像,所以我選擇用這種算法作為我的處理算法。
雙線性法的處理效果比最近鄰要好,但是相對復雜,所以我暫時沒有實現該算法。
既然選擇了最近鄰算法,接下來要說明一些我們要獲取的參數
第一步:我們首先得明確一個概念,每一行(指的是圖片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 灰度化效果
第四步,對圖片進行旋轉
參考博文:
- http://blog.csdn.net/liyuan02/article/details/6750828
- 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
