簡介
Python傳統的圖像處理庫PIL(Python Imaging Library ),可以說基本上是Python處理圖像的標准庫,功能強大,使用簡單。
但是由於PIL不支持Python3,而且更新緩慢。所以有志願者在PIL的基礎上創建了一個分支版本,命名為Pillow,Pillow目前最新支持到python3.6,更新活躍,並且增添了許多新的特性。所以我們安裝Pillow即可。
安裝
Pillow的安裝比較的簡單,直接pip安裝即可:
pip install Pillow
但是要注意的一點是,Pillow和PIL不能共存在同一個環境中,所以如果安裝的有PIL的話,那么安裝Pillow之前應該刪除PIL。
由於是繼承自PIL的分支,所以Pillow的導入是這樣的:
import PIL
# 或者
from PIL import Image
使用手冊
Image
Image是Pillow中最為重要的類,實現了Pillow中大部分的功能。要創建這個類的實例主要有三個方式:
-
從文件加載圖像
-
處理其他圖像獲得
-
創建一個新的圖像
讀取圖像
一般來說,我們都是都過從文件加載圖像來實例化這個類,如下所示:
from PIL import Image
picture = Image.open('happy.png')
如果沒有指定圖片格式的話,那么Pillow會自動識別文件內容為文件格式。
新建圖像
Pillow新建空白圖像使用new()方法, 第一個參數是mode即顏色空間模式,第二個參數指定了圖像的分辨率(寬x高),第三個參數是顏色。
-
可以直接填入常用顏色的名稱。如'red'。
-
也可以填入十六進制表示的顏色,如
#FF0000表示紅色。 -
還能傳入元組,比如(255, 0, 0, 255)或者(255, 0, 0)表示紅色。
picture = Image.new('RGB', (200, 100), 'red')
保存圖像
保存圖片的話需要使用save()方法:
picture.save('happy.png')
保存的時候,如果沒有指定圖片格式的話,那么Pillow會根據輸入的后綴名決定保存的文件格式。
圖像的坐標表示
在Pillow中,用的是圖像的左上角為坐標的原點(0,0),所以這意味着,x軸的數值是從左到右增長的,y軸的數值是從上到下增長的。
我們處理圖像時,常常需要去表示一個矩形的圖像區域。Pillow中很多方法都需要傳入一個表示矩形區域的元祖參數。
這個元組參數包含四個值,分別代表矩形四條邊的距離X軸或者Y軸的距離。順序是(左,頂,右,底)。其實就相當於,矩形的左上頂點坐標為(左,頂),矩形的右下頂點坐標為(右,底),兩個頂點就可以確定一個矩形的位置。
右和底坐標稍微特殊,跟python列表索引規則一樣,是左閉又開的。可以理解為[左, 右)和[頂, 底)這樣左閉右開的區間。比如(3, 2, 8, 9)就表示了橫坐標范圍[3, 7];縱坐標范圍[2, 8]的矩形區域。
常用屬性
-
PIL.Image.filename圖像源文件的文件名或者路徑,只有使用
open()方法創建的對象有這個屬性。類型:字符串
-
PIL.Image.format圖像源文件的文件格式。
-
PIL.Image.mode圖像的模式,一般來說是“1”, “L”, “RGB”, 或者“CMYK” 。
-
PIL.Image.size圖像的大小
-
PIL.Image.width圖像的寬度
-
PIL.Image.height圖像的高度
-
PIL.Image.info圖像的一些信息,為字典格式
常用方法
裁剪圖片
Image使用crop()方法來裁剪圖像,此方法需要傳入一個矩形元祖參數,返回一個新的Image對象,對原圖沒有影響。
croped_im = im.crop((100, 100, 200, 200))
復制與粘貼圖像
復制圖像使用copy()方法:
copyed_im = im.copy()
粘貼圖像使用paste()方法:
croped_im = im.crop((100, 100, 200, 200))
im.paste(croped_im, (0, 0))
im對象調用了paste()方法,第一個參數是被裁剪下來用來粘貼的圖像,第二個參數是一個位置參數元祖,這個位置參數是粘貼的圖像的左頂點。
調整圖像的大小
調整圖像大小使用resize()方法:
resized_im = im.resize((width, height))
resize()方法會返回一個重設了大小的Image對象。
或者使用thumbnail()方法
im = Image.open('test.jpg')
#獲得圖像尺寸
w, h = im.size
# 縮放到50%
im.htumbnail((w//2, h//2))
#顯示圖片
im.show()
thumbnail() 方法可以用來制作縮略圖。它接受一個二元數組作為縮略圖的尺寸,然后將示例縮小到指定尺寸
旋轉圖像和翻轉圖像
旋轉圖像使用rotate()方法,此方法按逆時針旋轉,並返回一個新的Image對象:
# 逆時針旋轉90度
im.rotate(90)
im.rotate(180)
im.rotate(20, expand=True)
旋轉的時候,會將圖片超出邊界的邊角裁剪掉。如果加入expand=True參數,就可以將圖片邊角保存住。
翻轉圖像使用transpose():
# 水平翻轉
im.transpose(Image.FLIP_LEFT_RIGHT)
# 垂直翻轉
im.transpose(Image.FLIP_TOP_BOTTOM)
獲得圖片通道名稱
im.getbands()
通過通道分割圖片
split()
split()可以將多通道圖片按通道分割為單通道圖片。返回各個通道的灰度圖組成的元組
R, G, B = im.split()
split()方法返回的是一個元祖,元祖中的元素則是分割后的單個通道的圖片。
getchannel(channel)
getchannel()可以獲取單個通道的圖片:
R = im.getchannel("R")
模式轉化
img = im.convert("L")
獲取單個像素的值
使用getpixel(xy)方法可以獲取單個像素位置的值:
im.getpixel((100, 100))
傳入的xy需要是一個元祖形式的坐標。
如果圖片是多通道的,那么返回的是一個元祖。
加載圖片全部數據
我們可以使用load()方法加載圖片所有的數據,並比較方便的修改像素的值:
pixdata = im.load()
pixdata[100,200] = 255
此方法返回的是一個PIL.PyAccess,可以通過這個類的索引來對指定坐標的像素點進行修改。
獲取全部像素內容
getdata(band = None) 方法,用來獲取 Image 類的對象中的像素內容
該方法會將圖片中的像素內容,逐行逐行地拼接起來,作為一個完整的序列返回。方法的返回類型,是 PIL 庫的內部類型。我們可以用 list(im.getdata()) 得到標准的 Python list 對象。
band 意味「通道」。當 band = None 時,方法返回所有通道的像素內容;當 band = 0時,則返回第一個通道的像素內容。例如,對於 RGB 模式的位圖,band = 0 返回 R 通道的內容;band = 2 返回 B 通道的內容。
from PIL import Image
im = Image.open('test.jpg')
print(im.getdata()) #獲取所有通道的值 類似生成器的對象
print(list(im.getdata(0))) #獲取第一個通道的值, 轉化為列表
####
關閉圖片並釋放內存
此方法會刪除圖片對象並釋放內存
im.close()
圖像類
這類驗證碼大多是數字、字母的組合,國內也有使用漢字的。在這個基礎上增加噪點、干擾線、變形、重疊、不同字體顏色等方法來增加識別難度。
相應的,驗證碼識別大體可以分為下面幾個步驟
-
灰度處理
-
增加對比度(可選)
-
二值化
-
-
傾斜校正分割字符
-
建立訓練庫
-
識別
0. 灰度化
像素點是最小的圖像單元,一張圖片由好多的像素點構成, 一個像素點的顏色是由RGB三個值來表現的,所以一個像素點矩陣對應三個顏色向量矩陣,我們對圖像的處理就是對這個像素點矩陣的操作,想要改變某個像素點的顏色,只要在這個像素點矩陣中找到這個像素點的位置(x, y),因為一個像素點的顏色由紅、綠、藍三個顏色變量表示,所以我們通過給這三個變量賦值,來改變這個像素點的顏色.
圖片的灰度化,就是讓像素點矩陣中的每一個像素點都滿足下面的關系:R=G=B,此時的這個值叫做灰度值.
灰度化的轉化公式一般為:
R = G = B = 處理前的 R*0.3 + G*0.59 + B*0.11
img = img.convert('L') #轉為灰度圖
1. 二值化
二值化就是讓圖像的像素點矩陣中的每個像素點的灰度值為0(黑)或者255(白) ,從而實現二值化,讓整個圖像呈現只有黑和白的效果。
原理是利用設定的一個閾值來判斷圖像像素為0還是255,小於閾值的變為0(黑色), 大於的變為255(白色)。
這個臨界灰度值就被稱為閾值,閾值的設置很重要。閾值過大或過小都會對圖片造成損壞。
選擇閾值的原則是:既要盡可能保存圖像信息,又要盡可能減少背景和噪聲的干擾,
常用方法
-
取閾值為127(0~255的中數,(0+255)/2=127 )
好處是計算量小速度快,
缺點也是很明顯的 ,對於圖片中內容色彩分布較大的圖片,很容易造成內容的缺失。
-
平均值法
計算像素點矩陣中的所有像素點的灰度值的平均值avg
(像素點1灰度值+...+像素點n灰度值)/ n = 像素點平均值avg
這樣做比方法1好一些。 但可能導致部分對象像素或者背景像素丟失。

def averageThreshold(img):
pixdata = img.load()
width,height = img.size
threshold = sum(img.getdata())/(width*height) #計算圖片的平均閾值
# 遍歷所有像素,大於閾值的為白色
for y in range(height):
for x in range(width):
if pixdata[x, y] < threshold:
pixdata[x, y] = 0
else:
pixdata[x,y] = 255
return img
-
雙峰法
圖像由前景和背景組成,在灰度直方圖上,前后二景都形成高峰,在雙峰之間的最低谷處就是圖像的閾值所在。 當前后景的對比較為強烈時,分割效果較好;否則基本無效。
-
迭代法
首先選擇一個近似閾值作為估計值的初始值,然后進行分割,產生子圖像,並根據子圖像的特性來選取新的閾值,再利用新的閾值分割圖像,經過幾次循環,使錯誤分割的圖像像素點降到最少。這樣做的效果好於用初始閾值直接分割圖像的效果。
-
求出圖象的最大灰度值和最小灰度值,分別記為Pmax和Pmin,令初始閾值T0=(Pmax+Pmin)/2
-
根據閾值TK將圖象分割為前景和背景,(小於 T0 的像素部分,大於T0的背景部分),並分別求其均值 avgPix, avgBac
-
求出新閾值TK = ( avgPix+avgBac) / 2;
-
若T0=TK,則所得即為閾值;否則轉2,迭代計算 。
-
from PIL import Image
def iterGetThreshold(img, pixdata, width, height):
pixPrs = pixBac = [] #用於統計前景和背景平均閾值
threshold = 0
pixel_min, pixel_max = img.getextrema() # 獲得圖片中最大和最小灰度值
newThreshold = int((pixel_min + pixel_max) / 2) # 初始閾值
while True:
if abs(threshold - newThreshold) < 5: #差值小於5,退出
break
for y in range(height):
for x in range(width):
if pixdata[x, y] >= newThreshold:
pixBac.append(pixdata[x,y]) #大於閾值 為背景
else:
pixPrs.append(pixdata[x,y]) #小於, 前景
avgPrs = sum(pixPrs)/len(pixPrs)
avgBac = sum(pixBac)/len(pixBac)
threshold = newThreshold
newThreshold = int((avgPrs+avgBac)/2)
return newThreshold
def binary(img, threshold=None):
img = img.convert('L') #轉為灰度圖
pixdata = img.load()
width, height = img.size
if not threshold:
threshold = iterGetThreshold(img, pixdata,width, height)
# 遍歷所有像素,大於閾值的為白色
for y in range(height):
for x in range(width):
if pixdata[x, y] < threshold:
pixdata[x, y] = 0
else:
pixdata[x,y] = 255
return img
img = Image.open('test-1.jpg')
img.show()
new_img = Binary(img)
new_img.show()
2. 降噪
從前面經過二值化處理,如果一個像素點是圖片或者干擾因素的一部分,那么它的灰度值一定是0,即黑色; 如果一個點是背景,則其灰度值應該是255,白色。
因此對於孤立的噪點,其周圍應該都是白色,或者大多數點都是白色pixel
如果圖片分辨率夠高,一個噪點實際上可能是有很多個點組成 ,所以此時的判斷條件應該放寬,即一個點是黑色的並且相鄰的8個點為白色點的個數大於一個固定值,那么這個點就是噪點 。
常見的4鄰域、8鄰域算法。所謂的X鄰域算法,可以參考手機九宮格輸入法,按鍵5為要判斷的像素點,4鄰域就是判斷上下左右,8鄰域就是判斷周圍8個像素點。如果這4或8個點中255的個數大於某個閾值則判斷這個點為噪音,閾值可以根據實際情況修改。
這個方法對小噪點比較好,如果閥值設的比較大,很多驗證碼字符也會受到很大影響,因為驗證碼可能就是一些斷斷續續的點連出來的,閥值設太大,盡管噪點沒了,驗證碼也會沒了。
def depoint(img, N=2):
pixdata = img.load()
width, height = img.size
for y in range(1, height - 1):
for x in range(1, width - 1):
count = 0
if pixdata[x, y - 1] == 255: # 上
count = count + 1
if pixdata[x, y + 1] == 255: # 下
count = count + 1
if pixdata[x - 1, y] == 255: # 左
count = count + 1
if pixdata[x + 1, y] == 255: # 右
count = count + 1
# if pixdata[x-1, y-1] == 255: #左上
# count = count + 1
# if pixdata[x+1, y-1] == 255: #右上
# count = count + 1
# if pixdata[x-1, y+1] == 255: #左下
# count = count + 1
# if pixdata[x+1, y+1] == 255: #右下
# count = count + 1
if count > N:
pixdata[x, y] = 255 #設置為白色
return img
depoint(img).show()
