15 繪制圖像輪廓
15.1 什么是輪廓
輪廓是一系列相連的點組成的曲線,代表了物體的基本外形。相對于邊緣,輪廓是連續的,邊緣不一定連續,如下圖所示。輪廓是一個閉合的、封閉的形狀。
-
輪廓的作用:
-
形狀分析
-
目標識別
-
圖像分割
-
15.2 尋找輪廓
在OpenCV中,使用cv2.findContours()來進行尋找輪廓,其原理過于復雜,這里只進行一個簡單的介紹,具體的實現原理可參考:
https://zhuanlan.zhihu.com/p/107257870
尋找輪廓需要將圖像做一個二值化處理,并且根據圖像的不同選擇不同的二值化方法來將圖像中要繪制輪廓的部分置為白色,其余部分置為黑色。也就是說,我們需要對原始的圖像進行灰度化、二值化的處理,令目標區域顯示為白色,其他區域顯示為黑色,如下圖所示。
之后,對圖像中的像素進行遍歷,當一個白色像素相鄰(上下左右及兩條對角線)位置有黑色像素存在或者一個黑色像素相鄰(上下左右及兩條對角線)位置有白色像素存在時,那么該像素點就會被認定為邊界像素點,輪廓就是有無數個這樣的邊界點組成的。
下面具體介紹一下cv2.findContours()函數,其函數原型為:
contours,hierarchy = cv2.findContours(image,mode,method)
返回值:[ 輪廓點坐標 ] 和 [ 層級關系 ]。
contours:表示獲取到的輪廓點的列表。檢測到有多少個輪廓,該列表就有多少子列表,每一個子列表都代表了一個輪廓中所有點的坐標。
hierarchy:表示輪廓之間的關系。對于第i條輪廓,hierarchy[i][0], hierarchy[i][1] , hierarchy[i][2] , hierarchy[i][3]分別表示其后一條輪廓、前一條輪廓、(同層次的第一個)子輪廓、父輪廓的索引(如果沒有相應的輪廓,則對應位置為-1)。該參數的使用情況會比較少。
image:表示輸入的二值化圖像。
mode:表示輪廓的檢索模式。
method:輪廓的表示方法。
15.2.1 mode參數
輪廓查找方式。返回不同的層級關系。
mode參數共有四個選項分別為:RETR_LIST,RETR_EXTERNAL,RETR_CCOMP,RETR_TREE。
RETR_EXTERNAL
表示只查找最外層的輪廓。并且在hierarchy里的輪廓關系中,每一個輪廓只有前一條輪廓與后一條輪廓的索引,而沒有父輪廓與子輪廓的索引。
2.3.4.會查找所有輪廓,但會有層級關系。
RETR_LIST
表示列出所有的輪廓。并且在hierarchy里的輪廓關系中,每一個輪廓只有前一條輪廓與后一條輪廓的索引,而沒有父輪廓與子輪廓的索引。
RETR_CCOMP
表示列出所有的輪廓。并且在hierarchy里的輪廓關系中,輪廓會按照成對的方式顯示。
在 RETR_CCOMP
模式下,輪廓被分為兩個層級:
層級 0:所有外部輪廓(最外層的邊界)。
層級 1:所有內部輪廓(孔洞或嵌套的區域)。
RETR_TREE
表示列出所有的輪廓。并且在hierarchy里的輪廓關系中,輪廓會按照樹的方式顯示,其中最外層的輪廓作為樹根,其子輪廓是一個個的樹枝。
15.2.2 method參數
輪廓存儲方法。輪廓近似方法。決定如何簡化輪廓點的數量。就是找到輪廓后怎么去存儲這些點。
method參數有三個選項:CHAIN_APPROX_NONE、CHAIN_APPROX_SIMPLE、CHAIN_APPROX_TC89_L1。
CHAIN_APPROX_NONE
表示將所有的輪廓點都進行存儲
CHAIN_APPROX_SIMPLE
表示只存儲有用的點,比如直線只存儲起點和終點,四邊形只存儲四個頂點,默認使用這個方法;
對于mode和method這兩個參數來說,一般使用RETR_EXTERNAL和CHAIN_APPROX_SIMPLE這兩個選項。
15.3 繪制輪廓
輪廓找出來后,其實返回的是一個輪廓點坐標的列表,因此我們需要根據這些坐標將輪廓畫出來,因此就用到了繪制輪廓的方法。
cv2.drawContours(image, contours, contourIdx, color, thickness)
image:原始圖像,一般為單通道或三通道的 numpy 數組。
contours:包含多個輪廓的列表,每個輪廓本身也是一個由點坐標構成的二維數組(numpy數組)。
contourIdx:要繪制的輪廓索引。如果設為 -1
,則會繪制所有輪廓。根據索引找到輪廓點繪制出來。默認是-1。
color:繪制輪廓的顏色,可以是 BGR 值或者是灰度值(對于灰度圖像)。
thickness:輪廓線的寬度,如果是正數,則畫實線;如果是負數,則填充輪廓內的區域。
?
img = cv.imread('images/de.jpg')
#灰度處理
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
#二值化 用反閾值法
ret, binary = cv.threshold(gray, 127, 255, cv.THRESH_BINARY_INV)
#查找輪廓
contours, hierarchy = cv.findContours(binary, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
#繪制輪廓
cv.drawContours(img, contours, -1, (100, 100, 255), 3)
#顯示結果
cv.imshow('Original Image', img)
cv.imshow('Binary Image', binary)
cv.waitKey(0)
cv.destroyAllWindows()
16 凸包特征檢測
在進行凸包特征檢測之前,首先要了解什么是凸包。通俗的講,凸包其實就是將一張圖片中物體的最外層的點連接起來構成的凸多邊形,它能包含物體中所有的內容。
一般來說,凸包都是伴隨著某類點集存在的,也被稱為某個點集的凸包。
對于一個點集來說,如果該點集存在凸包,那么這個點集里面的所有點要么在凸包上,要么在凸包內。
凸包檢測常用在物體識別、手勢識別、邊界檢測等領域。
窮舉法
QuickHull法
1.窮舉法
將集中的點進行兩兩配對,并進行連線,對于每條直線,檢查其余所有的點是否處于該直線的同一側,如果是,那么說明構成該直線的兩個點就是凸包點,其余的線依次進行計算,從而獲取所有的凸包點。
用向量的思想,點都是有坐標的,連起來就可以構成一個向量。再以其中一個點,連接另一個點,構成另一個向量,讓兩個向量做外積,就是叉積。也就是std=|向量a|*|向量b|*sin(\theta),能控制std的正負的只能是\theta,如果計算出來的std的正負都相同,說明這些點都在這條直線的同一側,那么這兩個點就是凸包的邊界點。然后換兩個點,就是說換一條直線,換一個向量,繼續進行檢測,直到找到凸包的所有的邊界點。
缺點:時間復雜度高,不斷使用for循環,耗時。
2.QuickHull法
將所有點放在二維坐標系中,找到橫坐標最小和最大的兩個點P1和P2并連線。此時整個點集被分為兩部分,直線上為上包,直線下為下包。
以上保暖為例,找到上包中的點距離該直線最遠的點P3,連線并尋找直線P1P3左側的點和P2P3右側的點,然后重復本步驟,直到找不到為止。對下包也是這樣操作。
我們以點集來舉例,假如有這么一些點,其分布如下圖所示:
那么經過凸包檢測并繪制之后,其結果應該如下圖所示:
可以看到,原圖像在經過凸包檢測之后,會將最外圍的幾個點進行連接,剩余的點都在這些點的包圍圈之內。那么凸包檢測到底是怎么檢測出哪些點是最外圍的點呢?
我們還是以上面的點集為例,假設我們知道這些點的坐標,那么我們就可以找出處于最左邊和最右邊的點,如下圖所示:
接著將這兩個點連接,并將點集分為上半區和下半區,我們以上半區為例:
找到上面這些點離直線最遠的點,其中,這條直線由于有兩個點的坐標,所以其表示的直線方程是已知的,并且上面的點的坐標也是已知的,那么我們就可以根據點到直線的距離公式來進行計算哪個點到直線的距離最遠,假設直線的方程為:A x+B y+C=0,那么點(x0,y0)到直線的距離公式為:
然后我們就可以得到距離這條線最遠的點,將其與左右兩點連起來,并分別命名為y1和y2,如下圖所示:
然后分別根據點的坐標求出y1和y2的直線方程,之后將上半區的每個點的坐標帶入下面公式中:
當d=0時,表明該點在直線上;當d>0時,表明點在直線的上方,在這里就代表該點在上圖所圍成的三角形的外面,也就意味著該三角形并沒有完全包圍住上半區的所有點,需要重新尋找凸包點;當d<0時,表明點在直線的下方,在這里就代表該點在上圖所圍成的三角形的里面,也就代表著這類點就不用管了。
當出現d>0時,我們需要將出現這種結果的兩個計算對象:某點和y1或y2這條線標記,并在最后重新計算出現這種現象的點集到y1或y2的距離來獲取新的凸包點的坐標。在本例子中,也就是如下圖所示的點和y2這條直線:
由于本例子中只有這一個點在這個三角形之外,所以毫無疑問的它就是一個凸包點,因此直接將它與y2直線的兩個端點相連即可。當有很多點在y2直線外時,就需要計算每個點到y2的距離,然后找到離得最遠的點與y2構建三角形,并重新計算是否還有點在該三角形之外,如果沒有,那么這個點就是新的凸包點,如果有,那就需要重復上面的步驟,直到所有的點都能被包圍住,那么構建直線的點就是凸包點。這是上半區尋找凸包點的過程,下半區尋找凸包點的思路與此一模一樣,只不過是需要篩選d<0(也就是點在直線的下方)的點,并重新構建三角形,尋找新的凸包點。
上面的過程都是基于我們知道點的坐標進行的,實際上,對于未經處理的圖像,我們無法直接獲取點的坐標。特別是對于彩色圖像,我們需要將其轉換為二值圖像,并使用輪廓檢測技術來獲取輪廓邊界的點的坐標。然后,我們才能進行上述尋找凸包點的過程。因此,在處理圖像時,我們需要將彩色圖像轉換為二值圖像,并通過輪廓檢測技術來獲取輪廓邊界的點的坐標,然后才能進行凸包點的尋找過程。
16.1 獲取凸包點
cv2.convexHull(points)
points
:輸入參數,圖像的輪廓
16.2 繪制凸包
cv2.polylines(image, pts, isClosed, color, thickness=1)
image
:要繪制線條的目標圖像,它應該是一個OpenCV格式的二維圖像數組(如numpy數組)。
pts
:一個二維 numpy 數組,每個元素是一維數組,代表一個多邊形的一系列頂點坐標。
isClosed
:布爾值,表示是否閉合多邊形,如果為 True,會在最后一個頂點和第一個頂點間自動添加一條線段,形成封閉的多邊形。
color
:線條顏色,可以是一個三元組或四元組,分別對應BGR或BGRA通道的顏色值,或者是灰度圖像的一個整數值。
thickness
(可選):線條寬度,默認值為1。
img = cv.imread('images/menghuwang.jpg')
#灰度圖
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
#二值化處理,閾值法 因為目標是白色我們需要白色
ret, binary = cv.threshold(gray, 200, 255, cv.THRESH_BINARY_INV)
#查找輪廓
contours, hierarchy = cv.findContours(binary, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
for i in range(len(contours)):#查找凸包hull = cv.convexHull(contours[i])#繪制凸包cv.polylines(img, [hull], True, (100, 100, 255), 3)
print(hull)
cv.imshow('1', img)
cv.imshow('2', binary)
cv.waitKey(0)
cv.destroyAllWindows()