影象處理之分割影象
我們在處理影象的時候,常常需要將影象的前景和背景做不同的處理,這時需要將前景和背景分割開。關於影象分割的方法我知道的有三種方法:K-means、分水嶺和GrabCut演算法進行物體分割。不能夠肯定的比較出誰優誰劣,各種演算法是分各種場合以及設定引數的優化。在此,只是簡單介紹,學習之路任重而道遠!
K-means方法進行分割:
它是一種最常用的聚類演算法。因為,人們不需要手動的為資料集裡的每個個體新增標籤,能自動的發現叢集結構,進行分類。是一種無監督的學習。那麼是什麼定義了叢集的呢?答案是通過中心和形狀定義的。然後,通過打分判斷依據是:在這個叢集中的分數高於在其他聚類中的分數和與本叢集中心點比其他叢集中心點更相似。具體的步驟是:首先,把觀測分配給最近的中心點。然後,把叢集中心點修改為被分配給原中心點觀測的均值,反覆這兩步操作,直到全部收斂。
關於OpenCV下的kmean演算法,函式為cv2.kmeans()
函式的格式為:kmeans(data, K, bestLabels, criteria, attempts, flags)
其中,K(分類數)和 attempts(Kmeans演算法重複次數)是需要根據具體的影象進行優化的引數。像bestLabels預設分類標籤可以不需要用None表示,criteria為迭代停止的模式選擇,格式為(type,max_iter,epsilon),其中type又有兩種選擇:cv2.TERM_CRITERIA_EPS :精確度(誤差)滿足epsilon停止和cv2.TERM_CRITERIA_MAX_ITER:迭代次數超過max_iter停止,也可以兩者結合,滿意任意一個就結束。而flags(初始類中心選擇),有兩種方法:cv2.KMEANS_PP_CENTERS ; cv2.KMEANS_RANDOM_CENTERS
下面,就嘗試一下修改K(分類數)和 attempts(Kmeans演算法重複次數)引數進行測試。
先將K設為預設值,調attempts次數。
# 以灰色匯入影象 img = cv2.imread('messi5.jpg',0)#image read be 'gray' plt.subplot(221),plt.imshow(img,'gray'),plt.title('original') plt.xticks([]),plt.yticks([]) # 改變影象的維度 img1 = img.reshape((img.shape[0]*img.shape[1],1)) img1 = np.float32(img1) # 設定一個criteria, criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER,10,1.0) # 設定一個初始類中心flags flags = cv2.KMEANS_RANDOM_CENTERS # 應用K-means compactness,labels,centers = cv2.kmeans(img1,2,None,criteria,5,flags) compactness_1,labels_1,centers_1 = cv2.kmeans(img1,2,None,criteria,10,flags) compactness_2,labels_2,centers_2 = cv2.kmeans(img1,2,None,criteria,15,flags) img2 = labels.reshape((img.shape[0],img.shape[1])) img3 = labels_1.reshape((img.shape[0],img.shape[1])) img4 = labels_2.reshape((img.shape[0],img.shape[1])) plt.subplot(222),plt.imshow(img2,'gray'),plt.title('kmeans_attempts_5') plt.xticks([]),plt.yticks([]) plt.subplot(223),plt.imshow(img3,'gray'),plt.title('kmeans_attempts_10') plt.xticks([]),plt.yticks([]) plt.subplot(224),plt.imshow(img4,'gray'),plt.title('kmeans_attempts_15') plt.xticks([]),plt.yticks([]) plt.savefig("kmeans_attempts.png") plt.show()
可以看出attempts次數不同,是會造成影象分割差異的。
再來調K值,這裡將attempts次數設為10.得到的影象為:
也可以看出K初始值不同,同樣造成影象分割差異。所以,可以說這兩個引數的優化是很重要的,但是,也不容易優化。看下一種方法:
分水嶺演算法
之所以叫分水嶺演算法,是因為它裡面有“水”的概念。把影象中低密度的區域(變化很少)想象成山谷,影象中高密度的區域(變化很多)想象成山峰。開始向山谷中注入水直到不同的山谷中的水開始彙集。為了阻止不同山谷的水匯聚,可以設定一些柵欄,最後得到的柵欄就是影象分割。
img = cv2.imread("water_coins.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 將顏色轉變為灰色之後,可為影象設一個閾值,將影象二值化。
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
# 下面用morphologyEx變換來除去噪聲資料,這是一種對影象進行膨脹之後再進行腐蝕的操作,它可以提取影象特徵:
kernel = np.ones((3,3), np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations= 2)
# 通過對morphologyEx變換之後的影象進行膨脹操作,可以得到大部分都是背景的區域:
sure_bg = cv2.dilate(opening, kernel, iterations=3)
# 接著通過distanceTransform來獲取確定前景區域,原理是應用一個閾值來決定哪些區域是前景,越是遠離背景區域的邊界的點越可能屬於前景。
dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
ret, sure_fg = cv2.threshold(dist_transform, 0.7 * dist_transform.max(), 255, 0)
# 考慮前景和背景中有重合的部分,通過sure_fg和sure_bg的集合相減得到。
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg, sure_fg)
# 現在有了這些區域,就可以設定“柵欄”來阻止水匯聚了,這通過connectedComponents函式來完成
ret, markers = cv2.connectedComponents(sure_fg)
# 在背景區域上加1, 這會將unknown區域設定為0:
markers = markers + 1
markers[unknown==255] = 0
# 最後開啟門,讓水漫起來並把柵欄繪成紅色
markers = cv2.watershed(img, markers)
img[markers == -1] = [255, 0, 0]
plt.imshow(img), plt.xticks([]),plt.yticks([])
plt.show()
能夠看出還是能大多數完整分割。
接下來介紹GrabCut演算法進行對影象的分割處理。
使用GrabCut演算法的實現步驟為:
1)在圖片中定義含有(一個或多個)物體的矩形
2)矩形外的區域被自動認為是背景
3)對於使用者定義的矩形區域,可用背景中的資料來區別它裡面的前景和背景區域
4)用高斯混合模型(GMM)來對背景和前景建模,並將末定義的畫素標記為可能的前景或背景
5)影象中的每一個畫素都被看作通過虛擬邊與周圍畫素相連線,而每一條邊都有一個屬於前景或背景的概率,這基於它與周圍畫素顏色上的相似性
6)每一個畫素會與一個前景或背景節點連線。若節點之間不屬於同一個終端(就是兩個相鄰的節點,一個節點屬於前景,一個節點屬於背景),則會切斷它們之間的邊,這就將影象各個部分分割出來了。
import numpy as np
import cv2
from matplotlib import pyplot as plt
# 首先載入圖片,然後建立一個與所載入圖片同形狀的掩模,並用0填充。
img = cv2.imread("messi5.jpg")
mask = np.zeros(img.shape[:2], np.uint8)
# 然後建立以0填充的前景和背景模型:
bgdModel = np.zeros((1, 65), np.float64)
fgdModel = np.zeros((1, 65), np.float64)
# 在實現GrabCut演算法前,先用一個標識出想要隔離的物件的矩形來初始化它,這個矩形我們用下面的一行程式碼定義(x,y,w,h):
rect = (100, 50, 421, 378)
# 接下來用指定的空模型和掩摸來執行GrabCut演算法
#mask, bgdModel, fgdModel = cv2.grabCut(img,mask,None,bgdModel,fgdModel,5,cv2.GC_INIT_WITH_MASK)
cv2.grabCut(img, mask, rect, bgdModel, fgdModel, 5, cv2.GC_INIT_WITH_RECT) # 5是指演算法的迭代次數。
# 然後,我們再設定一個掩模,用來過濾之前掩模中的值(0-3)。值為0和2的將轉為0,值為1和3的將轉化為1,這樣就可以過濾出所有的0值畫素(背景)。
mask2 = np.where((mask==2)|(mask==0), 0, 1).astype("uint8")
img = img * mask2[:, :, np.newaxis]
# 最後視覺化展現分割前後的影象
plt.subplot(1, 2, 1)
plt.imshow(img)
plt.title("grabcut"), plt.xticks([]), plt.yticks([])
plt.subplot(1, 2, 2)
plt.imshow(cv2.imread("messi5.jpg"))
plt.title("original"), plt.xticks([]), plt.yticks([])
plt.savefig("grabcut.png")
可以看出來分割的並不完整,而且頭髮和手都沒有被區分到前景中來,這是因為,在設定矩形的時候需要不斷優化的,且因每一張影象都有差異,所以矩形的範圍也是有差異的。還好在github上找到了一個grabcut演算法指令碼,能完美的解決這個問題。並用他的程式碼進行測試。如下圖:
參考:
《OpenCV3計算機視覺Python語言實現》
OpenCV幫助文件:
http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_grabcut/py_grabcut.html
https://github.com/opencv/opencv/blob/master/samples/python/grabcut.py