Python中的影象處理
第 1 章 基本的影象操作和處理
本章講解操作和處理影象的基礎知識,將通過大量示例介紹處理影象所需的 Python 工具包,並介紹用於讀取影象、影象轉換和縮放、計算導數、畫圖和儲存結果等的基本工具。這些工具的使用將貫穿本書的剩餘章節。
1.1 PIL:Python影象處理類庫
PIL(Python Imaging Library Python,影象處理類庫)提供了通用的影象處理功能,以及大量有用的基本影象操作,比如影象縮放、裁剪、旋轉、顏色轉換等。PIL 是免費的,可以從 http://www.pythonware.com/products/pil/ 下載。
利用 PIL 中的函式,我們可以從大多數影象格式的檔案中讀取資料,然後寫入最常見的影象格式檔案中。PIL 中最重要的模組為 Image
- from PIL importImage
- pil_im =Image.open(’empire.jpg’)
上述程式碼的返回值 pil_im
是一個 PIL 影象物件。
影象的顏色轉換可以使用 convert()
方法來實現。要讀取一幅影象,並將其轉換成灰度影象,只需要加上 convert('L')
,如下所示:
- pil_im =Image.open('empire.jpg').convert('L')
圖 1-1:用 PIL 處理影象的例子
1.1.1 轉換影象格式
通過 save()
方法,PIL 可以將影象儲存成多種格式的檔案。下面的例子從檔名列表(filelist
- from PIL importImage
- import os
- for infile in filelist:
- outfile = os.path.splitext(infile)[0]+".jpg"
- if infile != outfile:
- try:
- Image.open(infile).save(outfile)
- exceptIOError:
- print"cannot convert", infile
PIL 的 open()
函式用於建立 PIL 影象物件,save()
方法用於儲存影象到具有指定檔名的檔案。除了字尾變為“.jpg”,上述程式碼的新檔名和原檔名相同。PIL 是個足夠智慧的類庫,可以根據副檔名來判定影象的格式。PIL 函式會進行簡單的檢查,如果檔案不是 JPEG 格式,會自動將其轉換成 JPEG 格式;如果轉換失敗,它會在控制檯輸出一條報告失敗的訊息。
本書會處理大量影象列表。下面將建立一個包含資料夾中所有影象檔案的檔名列表。首先新建一個檔案,命名為 imtools.py,來儲存一些經常使用的影象操作,然後將下面的函式新增進去:
- import os
- def get_imlist(path):
- """ 返回目錄中所有JPG 影象的檔名列表"""
- return[os.path.join(path,f)for f in os.listdir(path)if f.endswith('.jpg')]
現在,回到 PIL。
1.1.2 建立縮圖
使用 PIL 可以很方便地建立影象的縮圖。thumbnail()
方法接受一個元組引數(該引數指定生成縮圖的大小),然後將影象轉換成符合元組引數指定大小的縮圖。例如,建立最長邊為 128 畫素的縮圖,可以使用下列命令:
- pil_im.thumbnail((128,128))
1.1.3 複製和貼上影象區域
使用 crop()
方法可以從一幅影象中裁剪指定區域:
- box =(100,100,400,400)
- region = pil_im.crop(box)
該區域使用四元組來指定。四元組的座標依次是(左,上,右,下)。PIL 中指定座標系的左上角座標為(0,0)。我們可以旋轉上面程式碼中獲取的區域,然後使用
paste()
方法將該區域放回去,具體實現如下:
- region = region.transpose(Image.ROTATE_180)
- pil_im.paste(region,box)
1.1.4 調整尺寸和旋轉
要調整一幅影象的尺寸,我們可以呼叫 resize()
方法。該方法的引數是一個元組,用來指定新影象的大小:
- out= pil_im.resize((128,128))
要旋轉一幅影象,可以使用逆時針方式表示旋轉角度,然後呼叫 rotate()
方法:
- out= pil_im.rotate(45)
上述例子的輸出結果如圖 1-1 所示。最左端是原始影象,然後是灰度影象、貼上有旋轉後裁剪影象的原始影象,最後是縮圖。
1.2 Matplotlib
我們處理數學運算、繪製圖表,或者在影象上繪製點、直線和曲線時,Matplotlib
是個很好的類庫,具有比 PIL 更強大的繪圖功能。Matplotlib
可以繪製出高質量的圖表,就像本書中的許多插圖一樣。Matplotlib
中的
PyLab
介面包含很多方便使用者建立影象的函式。Matplotlib
是開源工具,可以從
http://matplotlib.sourceforge.net/ 免費下載。該連結中包含非常詳盡的使用說明和教程。下面的例子展示了本書中需要使用的大部分函式。
1.2.1 繪製圖像、點和線
儘管 Matplotlib
可以繪製出較好的條形圖、餅狀圖、散點圖等,但是對於大多數計算機視覺應用來說,僅僅需要用到幾個繪圖命令。最重要的是,我們想用點和線來表示一些事物,比如興趣點、對應點以及檢測出的物體。下面是用幾個點和一條線繪製圖像的例子:
- from PIL importImage
- from pylab import*
- # 讀取影象到陣列中
- im = array(Image.open('empire.jpg'))
- # 繪製圖像
- imshow(im)
- # 一些點
- x =[100,100,400,400]
- y =[200,500,200,500]
- # 使用紅色星狀標記繪製點
- plot(x,y,'r*')
- # 繪製連線前兩個點的線
- plot(x[:2],y[:2])
- # 新增標題,顯示繪製的影象
- title('Plotting: "empire.jpg"')
- show()
上面的程式碼首先繪製出原始影象,然後在 x 和 y 列表中給定點的 x 座標和 y 座標上繪製出紅色星狀標記點,最後在兩個列表表示的前兩個點之間繪製一條線段(預設為藍色)。該例子的繪製結果如圖 1-2 所示。show()
命令首先開啟圖形使用者介面(GUI),然後新建一個影象視窗。該圖形使用者介面會迴圈阻斷指令碼,然後暫停,直到最後一個影象視窗關閉。在每個腳本里,你只能呼叫一次
show()
命令,而且通常是在指令碼的結尾呼叫。注意,在 PyLab
庫中,我們約定影象的左上角為座標原點。
影象的座標軸是一個很有用的除錯工具;但是,如果你想繪製出較美觀的影象,加上下列命令可以使座標軸不顯示:
- axis('off')
上面的命令將繪製出如圖 1-2 右邊所示的影象。
圖 1-2:Matplotlib
繪圖示例。帶有座標軸和不帶座標軸的包含點和一條線段的影象
在繪圖時,有很多選項可以控制影象的顏色和樣式。最有用的一些短命令如表 1-1、表 1-2 和表 1-3 所示。使用方法見下面的例子:
- plot(x,y)# 預設為藍色實線
- plot(x,y,'r*')# 紅色星狀標記
- plot(x,y,'go-')# 帶有圓圈標記的綠線
- plot(x,y,'ks:')# 帶有正方形標記的黑色虛線
表1-1:用PyLab
庫繪圖的基本顏色格式命令
顏色 |
|
---|---|
|
藍色 |
|
綠色 |
|
紅色 |
|
青色 |
|
品紅 |
|
黃色 |
|
黑色 |
|
白色 |
表1-2:用PyLab
庫繪圖的基本線型格式命令
線型 |
|
---|---|
|
實線 |
|
虛線 |
|
點線 |
表1-3:用PyLab
庫繪圖的基本繪製標記格式命令
標記 |
|
---|---|
|
點 |
|
圓圈 |
|
正方形 |
|
星形 |
|
加號 |
|
叉號 |
1.2.2 影象輪廓和直方圖
下面來看兩個特別的繪圖示例:影象的輪廓和直方圖。繪製圖像的輪廓(或者其他二維函式的等輪廓線)在工作中非常有用。因為繪製輪廓需要對每個座標 [x, y] 的畫素值施加同一個閾值,所以首先需要將影象灰度化:
- from PIL importImage
- from pylab import*
- # 讀取影象到陣列中
- im = array(Image.open('empire.jpg').convert('L'))
- # 新建一個影象
- figure()
- # 不使用顏色資訊
- gray()
- # 在原點的左上角顯示輪廓影象
- contour(im, origin='image')
- axis('equal')
- axis('off')
像之前的例子一樣,這裡用 PIL 的 convert()
方法將影象轉換成灰度影象。
影象的直方圖用來表徵該影象畫素值的分佈情況。用一定數目的小區間(bin)來指定表徵畫素值的範圍,每個小區間會得到落入該小區間表示範圍的畫素數目。該(灰度)影象的直方圖可以使用
hist()
函式繪製:
- figure()
- hist(im.flatten(),128)
- show()
hist()
函式的第二個引數指定小區間的數目。需要注意的是,因為 hist()
只接受一維陣列作為輸入,所以我們在繪製圖像直方圖之前,必須先對影象進行壓平處理。flatten()
方法將任意陣列按照行優先準則轉換成一維陣列。圖 1-3 為等輪廓線和直方圖影象。
圖 1-3:用 Matplotlib
繪製圖像等輪廓線和直方圖
1.2.3 互動式標註
有時使用者需要和某些應用互動,例如在一幅影象中標記一些點,或者標註一些訓練資料。PyLab
庫中的 ginput()
函式就可以實現互動式標註。下面是一個簡短的例子:
- from PIL importImage
- from pylab import*
- im = array(Image.open('empire.jpg'))
- imshow(im)
- print'Please click 3 points'
- x = ginput(3)
- print'you clicked:',x
- show()
上面的指令碼首先繪製一幅影象,然後等待使用者在繪圖視窗的影象區域點選三次。程式將這些點選的座標 [x, y] 自動儲存在 x 列表裡。
1.3 NumPy
NumPy
(http://www.scipy.org/NumPy/)是非常有名的 Python 科學計算工具包,其中包含了大量有用的思想,比如陣列物件(用來表示向量、矩陣、影象等)以及線性代數函式。NumPy
中的陣列物件幾乎貫穿用於本書的所有例子中
1 陣列物件可以幫助你實現陣列中重要的操作,比如矩陣乘積、轉置、解方程系統、向量乘積和歸一化,這為影象變形、對變化進行建模、影象分類、影象聚類等提供了基礎。
1PyLab
實際上包含 NumPy
的一些內容,如陣列型別。這也是我們能夠在 1.2 節使用陣列型別的原因。
1.3.1 影象陣列表示
在先前的例子中,當載入影象時,我們通過呼叫 array()
方法將影象轉換成 NumPy
的陣列物件,但當時並沒有進行詳細介紹。NumPy
中的陣列物件是多維的,可以用來表示向量、矩陣和影象。一個數組物件很像一個列表(或者是列表的列表),但是陣列中所有的元素必須具有相同的資料型別。除非建立陣列物件時指定資料型別,否則資料型別會按照資料的型別自動確定。
對於影象資料,下面的例子闡述了這一點:
- im = array(Image.open('empire.jpg'))
- print im.shape, im.dtype
- im = array(Image.open('empire.jpg').convert('L'),'f')
- print im.shape, im.dtype
控制檯輸出結果如下所示:
- (800,569,3) uint8
- (800,569) float32
每行的第一個元組表示影象陣列的大小(行、列、顏色通道),緊接著的字串表示陣列元素的資料型別。因為影象通常被編碼成無符號八位整數(uint8),所以在第一種情況下,載入影象並將其轉換到陣列中,陣列的資料型別為“uint8”。在第二種情況下,對影象進行灰度化處理,並且在建立陣列時使用額外的引數“f”;該引數將資料型別轉換為浮點型。關於更多資料型別選項,可以參考圖書 [24]。注意,由於灰度影象沒有顏色資訊,所以在形狀元組中,它只有兩個數值。
陣列中的元素可以使用下標訪問。位於座標 i、j,以及顏色通道 k 的畫素值可以像下面這樣訪問:
- value = im[i,j,k]
多個數組元素可以使用陣列切片方式訪問。切片方式返回的是以指定間隔下標訪問該陣列的元素值。下面是有關灰度影象的一些例子:
- im[i,:]= im[j,:]# 將第 j 行的數值賦值給第 i 行
- im[:,i]=100# 將第 i 列的所有數值設為100
- im[:100,:50].sum()# 計算前100 行、前 50 列所有數值的和
- im[50:100,50:100]# 50~100 行,50~100 列(不包括第 100 行和第 100 列)
- im[i].mean()# 第 i 行所有數值的平均值
- im[:,-1]# 最後一列
- im[-2,:](or im[-2])# 倒數第二行
注意,示例僅僅使用一個下標訪問陣列。如果僅使用一個下標,則該下標為行下標。注意,在最後幾個例子中,負數切片表示從最後一個元素逆向計數。我們將會頻繁地使用切片技術訪問畫素值,這也是一個很重要的思想。
我們有很多操作和方法來處理陣列物件。本書將在使用到的地方逐一介紹。你可以查閱線上文件或者開源圖書 [24] 獲取更多資訊。
1.3.2 灰度變換
將影象讀入 NumPy
陣列物件後,我們可以對它們執行任意數學操作。一個簡單的例子就是影象的灰度變換。考慮任意函式 f,它將 0…255 區間(或者 0…1 區間)對映到自身(意思是說,輸出區間的範圍和輸入區間的範圍相同)。下面是關於灰度變換的一些例子:
- from PIL importImage
- from numpy import*
- im = array(Image.open('empire.jpg').convert('L'))
- im2 =255- im # 對影象進行反相處理
- im3 =(100.0/255)* im +100# 將影象畫素值變換到100...200 區間
- im4 =255.0*(im/255.0)**2# 對影象畫素值求平方後得到的影象
第一個例子將灰度影象進行反相處理;第二個例子將影象的畫素值變換到 100…200 區間;第三個例子對影象使用二次函式變換,使較暗的畫素值變得更小。圖 1-4 為所使用的變換函式影象。圖 1-5 是輸出的影象結果。你可以使用下面的命令檢視影象中的最小和最大畫素值:
- printint(im.min()),int(im.max())
圖 1-4:灰度變換示例。三個例子中所使用函式的影象,其中虛線表示恆等變換
圖 1-5:灰度變換。對影象應用圖 1-4 中的函式:f(x)=255-x 對影象進行反相處理(左);f(x)=(100/255)x+100 對影象進行變換(中);f(x)=255(x/255)2 對影象做二次變換(右)
如果試著對上面例子檢視最小值和最大值,可以得到下面的輸出結果:
- 2255
- 0253
- 100200
- 0255
array()
變換的相反操作可以使用 PIL 的 fromarray()
函式完成:
- pil_im =Image.fromarray(im)
如果你通過一些操作將“uint8”資料型別轉換為其他資料型別,比如之前例子中的 im3 或者 im4,那麼在建立 PIL 影象之前,需要將資料型別轉換回來:
- pil_im =Image.fromarray(uint8(im))
如果你並不十分確定輸入資料的型別,安全起見,應該先轉換回來。注意,NumPy
總是將陣列資料型別轉換成能夠表示資料的“最低”資料型別。對浮點數做乘積或除法操作會使整數型別的陣列變成浮點型別。
1.3.3 影象縮放
NumPy
的陣列物件是我們處理影象和資料的主要工具。想要對影象進行縮放處理沒有現成簡單的方法。我們可以使用之前 PIL 對影象物件轉換的操作,寫一個簡單的用於影象縮放的函式。把下面的函式新增到 imtool.py 檔案裡:
- def imresize(im,sz):
- """ 使用PIL 物件重新定義影象陣列的大小"""
- pil_im =Image.fromarray(uint8(im))
- return array(pil_im.resize(sz))
我們將會在接下來的內容中使用這個函式。
1.3.4 直方圖均衡化
影象灰度變換中一個非常有用的例子就是直方圖均衡化。直方圖均衡化是指將一幅影象的灰度直方圖變平,使變換後的影象中每個灰度值的分佈概率都相同。在對影象做進一步處理之前,直方圖均衡化通常是對影象灰度值進行歸一化的一個非常好的方法,並且可以增強影象的對比度。
在這種情況下,直方圖均衡化的變換函式是影象中畫素值的累積分佈函式(cumulative distribution function,簡寫為 cdf,將畫素值的範圍對映到目標範圍的歸一化操作)。
下面的函式是直方圖均衡化的具體實現。將這個函式新增到 imtool.py 裡:
- def histeq(im,nbr_bins=256):
- """ 對一幅灰度影象進行直方圖均衡化"""
- # 計算影象的直方圖
- imhist,bins = histogram(im.flatten(),nbr_bins,normed=True)
- cdf = imhist.cumsum()# cumulative distribution function
- cdf =255* cdf / cdf[-1]# 歸一化
- # 使用累積分佈函式的線性插值,計算新的畫素值
- im2 = interp(im.flatten(),bins[:-1],cdf)
- return im2.reshape(im.shape), cdf
該函式有兩個輸入引數,一個是灰度影象,一個是直方圖中使用小區間的數目。函式返回直方圖均衡化後的影象,以及用來做畫素值對映的累積分佈函式。注意,函式中使用到累積分佈函式的最後一個元素(下標為 -1),目的是將其歸一化到 0…1 範圍。你可以像下面這樣使用該函式:
- from PIL importImage
- from numpy import*
- im = array(Image.open('AquaTermi_lowcontrast.jpg').convert('L'))
- im2,cdf = imtools.histeq(im)
圖 1-6 和圖 1-7 為上面直方圖均衡化例子的結果。上面一行顯示的分別是直方圖均衡化之前和之後的灰度直方圖,以及累積概率分佈函式對映影象。可以看到,直方圖均衡化後圖像的對比度增強了,原先影象灰色區域的細節變得清晰。
圖 1-6:直方圖均衡化示例。左側為原始影象和直方圖,中間圖為灰度變換函式,右側為直方圖均衡化後的影象和相應直方圖
圖 1-7:直方圖均衡化示例。左側為原始影象和直方圖,中間圖為灰度變換函式,右側為直方圖均衡化後的影象和相應直方圖
1.3.5 影象平均
影象平均操作是減少影象噪聲的一種簡單方式,通常用於藝術特效。我們可以簡單地從影象列表中計算出一幅平均影象。假設所有的影象具有相同的大小,我們可以將這些影象簡單地相加,然後除以影象的數目,來計算平均影象。下面的函式可以用於計算平均影象,將其新增到 imtool.py 檔案裡:
- def compute_average(imlist):
- """ 計算影象列表的平均影象"""
- # 開啟第一幅影象,將其儲存在浮點型陣列中
- averageim = array(Image.open(imlist[0]),'f')
- for imname in imlist[1:]:
- try:
- averageim += array(Image.open(imname))
- except:
- print imname +'...skipped'
- averageim /= len(imlist)
- # 返回uint8 型別的平均影象
- return array(averageim,'uint8')
該函式包括一些基本的異常處理技巧,可以自動跳過不能開啟的影象。我們還可以使用 mean()
函式計算平均影象。mean()
函式需要將所有的影象堆積到一個數組中;也就是說,如果有很多影象,該處理方式需要佔用很多記憶體。我們將會在下一節中使用該函式。
1.3.6 影象的主成分分析(PCA)
PCA(Principal Component Analysis,主成分分析)是一個非常有用的降維技巧。它可以在使用盡可能少維數的前提下,儘量多地保持訓練資料的資訊,在此意義上是一個最佳技巧。即使是一幅 100×100 畫素的小灰度影象,也有 10 000 維,可以看成 10 000 維空間中的一個點。一兆畫素的影象具有百萬維。由於影象具有很高的維數,在許多計算機視覺應用中,我們經常使用降維操作。PCA 產生的投影矩陣可以被視為將原始座標變換到現有的座標系,座標系中的各個座標按照重要性遞減排列。
為了對影象資料進行 PCA 變換,影象需要轉換成一維向量表示。我們可以使用 NumPy
類庫中的 flatten()
方法進行變換。
將變平的影象堆積起來,我們可以得到一個矩陣,矩陣的一行表示一幅影象。在計算主方向之前,所有的行影象按照平均影象進行了中心化。我們通常使用 SVD(Singular Value Decomposition,奇異值分解)方法來計算主成分;但當矩陣的維數很大時,SVD 的計算非常慢,所以此時通常不使用 SVD 分解。下面就是 PCA 操作的程式碼:
- from PIL importImage
- from numpy import*
- def pca(X):
- """ 主成分分析:
- 輸入:矩陣X ,其中該矩陣中儲存訓練資料,每一行為一條訓練資料
- 返回:投影矩陣(按照維度的重要性排序)、方差和均值"""
- # 獲取維數
- num_data,dim = X.shape
- # 資料中心化
- mean_X = X.mean(axis=0)
- X = X - mean_X
- if dim