OpenCV学习——轮廓检测
前言
輪廓檢測是傳統視覺中非常常用的功能,這里簡單記錄一下opencv中的輪廓檢測算法使用方法,至于理論,后續有機會再去細品。
國際慣例:
OpenCV官方的輪廓檢測教程python版
OpenCV中的二值化方法教程
OpenCV輪廓層級官方文檔
維基百科:圖像矩(Image Moment)
調用流程和方法
OpenCV里面通常要求是針對二值圖像進行二值化,所以輪廓檢測包含如下步驟:
- 載入圖像
- 灰度化
- 二值化
- 輪廓檢測
代碼實現如下:
img =cv2.imread("blackBG.jpg") # grayscale # https://docs.opencv.org/4.5.0/d7/d4d/tutorial_py_thresholding.html gray_img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) ret,bin_img = cv2.threshold(gray_img,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)二值化
注意二值化方法,這里使用的是threshold函數,它的第三個參數代表的意義可以查詢此處的官方文檔,這里將方法截圖貼出來
其實除了threshold還有一個adaptiveThreshold函數可以做二值化,調用方法:
#dst=cv.adaptiveThreshold(src,maxValue,adaptiveMethod,thresholdType,blockSize,C[, dst]) bin_img1 = cv.adaptiveThreshold(img,255,cv.ADAPTIVE_THRESH_MEAN_C,\cv.THRESH_BINARY,11,2) bin_img2 = cv.adaptiveThreshold(img,255,cv.ADAPTIVE_THRESH_GAUSSIAN_C,\cv.THRESH_BINARY,11,2)從第三個參數可以發現也有兩個二值化方法:
- ADAPTIVE_THRESH_MEAN_C:閾值是每個像素鄰域區域的均值減去常量C
- ADAPTIVE_THRESH_GAUSSIAN_C::閾值是每個像素相鄰域區域的高斯加權和減去常量C
輪廓檢測
python的調用方法如下:
contours, hierarchy =cv.findContours(image,mode,method[,contours[, hierarchy[, offset]]])返回的參數
- contours:檢測到的輪廓,每個輪廓是由一些點構成的向量組成
- hierarchy:記錄輪廓之間的關系,四個維度分別代表:同級后一個輪廓的序號、同級上一個輪廓的序號、第一個孩子序號,父親序號
第二個數參數mode是檢測輪廓的層級關系排列規則:
- RETR_EXTERNAL:僅僅檢測外圈輪廓
- RETR_LIST:檢測所有輪廓,但是沒有層級關系
- RETR_CCOMP:僅僅兩層包含關系,即只有外層和內層,假設有夾層,那么夾層也算外層,只要某個輪廓還包含有輪廓,都算外部輪廓
- RETR_TREE:檢測所有的輪廓,并建議非常完整的層級關系
- RETR_FLOODFILL:無描述
第三個參數method是輪廓點的存儲方式:
- CHAIN_APPROX_NONE:相鄰的輪廓點坐標只相差一個像素,所以是連續輪廓點
- CHAIN_APPROX_SIMPLE:橫、豎、對角線段只保存斷點數據,比如矩形就只保存四個頂點。
- 還有兩種沒做過多敘述:CHAIN_APPROX_TC89_L1和CHAIN_APPROX_TC89_KCOS是Teh-Chin chain近似算法里面采取的兩種表示
畫圖函數
就一個函數drawContours,調用方法如下:
image=cv.drawContours(image, contours, contourIdx, color[, thickness[, lineType[, hierarchy[, maxLevel[, offset]]]]] )輸入參數:
- contours:是list類型的數組,里面存儲了很多array數組去代表各個輪廓
- contourIdx:從上面的輪廓list中取出哪一個畫出來,-1代表全部
- color:線條顏色
- thickness:線條粗細,-1代表填充式畫輪廓,整個輪廓內部被指定顏色填充
- lineType:線條類型,虛線、實線之類的
【注意】如果將原圖傳入畫圖函數,這個原圖會被畫上輪廓,所以畫圖時候最好建立一個副本,在副本上畫圖。
輪廓檢測函數驗證
主要驗證檢測時的層級結構和記錄關鍵點的方式,也就是第2和3個參數。
檢測黑色還是白色邊界
黑色背景圖,以下圖為例
先檢測所有的輪廓并且畫出來
img =cv2.imread("blackBG.jpg") gray_img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) ret,bin_img = cv2.threshold(gray_img,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU) contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)cv2.drawContours(img,contours,-1,(0,255,0),5) plt.imshow(img[...,::-1]) plt.axis('off')白色背景圖以下圖左為例,同時以同樣的代碼盡心輪廓檢測,輪廓圖為下圖右:
結論:檢測白色背景的圖片,會有一個和圖像寬高相等的輪廓,而黑色區域沒有;所以輪廓檢測是針對白色區域的邊緣進行的,這個和圖像等寬高的輪廓經常會影響一些邏輯的書寫。
層級關系
-
RETR_EXTERNAL:僅外圈輪廓
# RETR_EXTERNAL:僅外圈輪廓 contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)cv2.drawContours(img,contours,-1,(0,255,0),5) plt.imshow(img[...,::-1]) plt.axis('off')print(hierarchy) ''' [[[ 1 -1 -1 -1][ 2 0 -1 -1][-1 1 -1 -1]]] '''
從輪廓圖可以發現,僅僅只有確定為最外圈的輪廓被畫出來,而且輸出的hierarchy數組可以發現,前兩列分別代表當前層級當前輪廓的下一個輪廓和上一個輪廓索引,而后兩列分別代表當前層級的子層級的第一個輪廓索引和父層級的輪廓索引,因為RETR_EXTERNAL只提取最外層輪廓,所以上下層級都是-1
-
RETR_LIST:所有輪廓都包含,但是沒有層級關系
# RETR_EXTERNAL:全部輪廓,無層級關系 contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)cv2.drawContours(img,contours,-1,(0,255,0),5) plt.imshow(img[...,::-1]) plt.axis('off')print(hierarchy) ''' [[[ 1 -1 -1 -1][ 2 0 -1 -1][ 3 1 -1 -1][ 4 2 -1 -1][ 5 3 -1 -1][ 6 4 -1 -1][ 7 5 -1 -1][-1 6 -1 -1]]] '''
代表當前層級父子層級的后兩個維度依舊為-1,但是輪廓全部都提取出來了。
-
RETR_CCOMP:僅僅兩層關系,是否為內層或者是否為外層,而且這個內層一定是這個外層的洞,這個洞的定義指內外層組合構成一片白色區域。如下圖代碼測試
# RETR_CCOMP:全部輪廓,只有兩種層級關系 contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)img_show = img.copy() for i in range(len(contours)):if(hierarchy[0,i,3]!=-1):cv2.drawContours(img_show,contours,i,colormap[i],5)cv2.drawContours(img_show,contours,hierarchy[0,i,3],colormap[i],5) plt.imshow(img_show[...,::-1]) plt.axis('off')print(hierarchy) # 紅:0 橙;1 黃:2 綠:3 青:4 藍:5 紫:6 灰:7 ''' [[ 1 -1 -1 -1][ 2 0 -1 -1][ 4 1 3 -1][-1 -1 -1 2][ 6 2 5 -1][-1 -1 -1 4][ 7 4 -1 -1][-1 6 -1 -1]]] '''上述代碼表示將當前輪廓與其父輪廓用同色畫出來:
可以發現四個輪廓組成的兩個白色區域被顯示出來,綠色區域為3號輪廓,從hierarchy中找到3號輪廓的結構為[-1 -1 -1 2],自行可視化可以發現這個3號輪廓是白色區域中最內層的那個輪廓,而其父親索引為2,輪廓2的結構為[ 4 1 3 -1],可以發現它的第一個孩子是3,而由于是外輪廓(不管是否為最外圈),所以父親索引為-1。其余輪廓同理分析。
【注】這個輪廓結構有點繞,但是只需要記住只有內、外輪廓,只要當前輪廓有內輪廓一起組成白色區域,那么這個輪廓就是外輪廓,不管它在不在其它輪廓內部
可視化時候本來用當前輪廓和子輪廓來顯示,但是想到hierarchy只記錄第一個子輪廓,當時差點以為組成“洞”的只可能有兩個輪廓,也就是一個輪廓有且只可能有一個子輪廓,但是發現問題,一個輪廓可能會有兩個子輪廓,所以必須用當前輪廓與父輪廓可視化,而不是當前輪廓和子輪廓可視化,比如下面這個圖,及其對應的輪廓圖和層級關系:
輪廓對應順序分別是紅、橙、黃,其CCOMP層級關系為:
[[[-1 -1 1 -1][ 2 -1 -1 0][-1 1 -1 0]]]可以發現,內部兩個輪廓的父親都是0,證明這個洞是由三個輪廓組成的。
-
RETR_TREE:這個是非常嚴謹的表達輪廓間層級關系的參數
contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) print(hierarchy) # 紅:0 橙;1 黃:2 綠:3 青:4 藍:5 紫:6 灰:7 ''' [[[ 6 -1 1 -1][-1 -1 2 0][-1 -1 3 1][-1 -1 4 2][ 5 -1 -1 3][-1 4 -1 3][ 7 0 -1 -1][-1 6 -1 -1]]] '''
直接輸出hierarchy看看:真正的由外向內,一層一層的編號;是CCOMP的更進一步細化,如果CCOMP中構成洞的兩個輪廓的外輪廓在其它輪廓內部,那么就是從其它輪廓編號繼續編號,即洞的外輪廓的父親是包含它的緊鄰著的輪廓編號。
通過判斷父親是否相同,將輪廓按照層級畫出來
img_show = img.copy() for i in range(len(contours)):cv2.drawContours(img_show,contours,i,colormap[hierarchy[0,i,3]+1],5) plt.imshow(img_show[...,::-1]) plt.axis('off')
可以發現,紅色部分就是最外圈輪廓,父親為-1;而最內部的青色(菱形、六角星)的孩子是-1,父親是綠色的輪廓3。
存儲方法
CHAIN_APPROX_NONE和CHAIN_APPROX_SIMPLE的區別就在于輪廓為線段的部分,是否僅存儲端點坐標。
比如上述圖片的最外層的矩形輪廓,分別使用兩種存儲參數去存儲輪廓點的值:
使用SIMPLE只保存端點
contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) img_show = img.copy() cnt_idx = 0 cnt = contours[cnt_idx] for i in range(cnt.shape[0]):cv2.circle(img_show,(cnt[i,0,0],cnt[i,0,1]),5,(0,255,0),5) plt.imshow(img_show[...,::-1])使用NONE按像素保存
contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE) img_show = img.copy() cnt_idx = 0 cnt = contours[cnt_idx] for i in range(cnt.shape[0]):cv2.circle(img_show,(cnt[i,0,0],cnt[i,0,1]),5,(0,255,0),5) plt.imshow(img_show[...,::-1])輪廓的其它特征和屬性
包括輪廓的圖像矩、面積、周長、多邊形逼近、外接凸多邊形、凸性判斷、外接矩形、外接圓、外接橢圓、直線擬合。
圖像矩
維基百科中的解釋是:指圖像的某些特定像素灰度的加權平均值,或者是圖像具有類似功能或意義的屬性。可以通過圖像的矩來獲得圖像的部分性質,包括面積(或總體亮度),以及有關幾何中心和方向的信息。它可以被用來獲得相對于特定變換的不變性(平移、縮放、旋轉不變性) 。具體可查閱維基百科中圖像矩的描述,這里列一下矩的計算方法:
-
對于二維連續函數f(x,y)f(x,y)f(x,y),(p+q)(p+q)(p+q)階的矩被定義為:
Mpq=∫?∞∞∫?∞∞xpyqf(x,y)dxdyM_{pq}=\int_{-\infty}^{\infty}\int_{-\infty}^{\infty}x^py^qf(x,y)dxdy Mpq?=∫?∞∞?∫?∞∞?xpyqf(x,y)dxdy -
對于灰度圖像的像素強度I(x,y)I(x,y)I(x,y),原始圖像的矩MijM_{ij}Mij?計算方法:
Mij=∑x∑yxiyiI(x,y)M_{ij}=\sum_x\sum_yx^iy^iI(x,y) Mij?=x∑?y∑?xiyiI(x,y) -
原始矩包含以下的一些的有關原始圖像屬性的信息:
- 二值圖像的面積或灰度圖像的像素總和,可以表示為M00M_{00}M00?
- 圖像的幾何中心可以表示為{xˉ,yˉ}={M10M00,M01M00}\{\bar x,\bar y\}= \{\frac{M_{10}}{M_{00}},\frac{M_{01}}{M_{00}}\}{xˉ,yˉ?}={M00?M10??,M00?M01??}
在OpenCV中的表示為:
cnt = contours[0] M = cv.moments(cnt)中心為:
cx = int(M['m10']/M['m00']) cy = int(M['m01']/M['m00'])輪廓面積和周長
獲取指定輪廓所包含的面積
area = cv.contourArea(cnt)獲取指定輪廓所包含的周長,第二個參數指示當前輸入為閉合輪廓(true)還是非閉合曲線(false)
perimeter = cv.arcLength(cnt,True)輪廓多邊形逼近
通過具有更少輪廓點的形狀在允許誤差范圍內逼近指定輪廓,比如你提取一個矩形,但是有鋸齒導致輪廓不是矩形,可以使用此功能將矩形近似逼近出來
epsilon = 0.1*cv.arcLength(cnt,True) approx = cv.approxPolyDP(cnt,epsilon,True)這個意思就是新的輪廓的周長和原始輪廓周長的誤差范圍在原周長的十分之一以內。
比如最開始的例子中,最內部的六角星的輪廓點并不是規整的五角星輪廓,也就是說使用SIMPLE存儲的時候不是存的每條邊的端點。
下圖就是使用這個逼近函數去找到端點的結果:
contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) img_show = img.copy() cnt_idx = 4 cnt = contours[cnt_idx] for i in range(cnt.shape[0]):cv2.circle(img_show,(cnt[i,0,0],cnt[i,0,1]),5,(0,255,0),5)epsilon = 0.1*cv2.arcLength(cnt,True) approx = cv2.approxPolyDP(cnt,epsilon,True) for i in range(approx.shape[0]):cv2.circle(img_show,(approx[i,0,0],approx[i,0,1]),5,(0,0,255),5)plt.imshow(img_show[...,::-1]) plt.axis('off')綠色為原始輪廓點,紅色為逼近后的輪廓點,可以發現六角星的所有邊的頂點都保存了
輪廓凸多邊形逼近
上面的多邊形逼近不管簡化的輪廓是否為凸的,所以又提供了一個檢測凸多邊形逼近的函數
hull = cv.convexHull(points[, hull[, clockwise[, returnPoints]]輸入分別為:輪廓點、輸出(不管這個參數)、順時針(true)/逆時針(false)、返回多邊形坐標在原輪廓點序中的索引(False)/直接返回坐標(true)
還是那個六角星:
contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) img_show = img.copy() cnt_idx = 4 cnt = contours[cnt_idx] for i in range(cnt.shape[0]):cv2.circle(img_show,(cnt[i,0,0],cnt[i,0,1]),5,(0,255,0),5)approx = cv2.convexHull(cnt) for i in range(approx.shape[0]):cv2.circle(img_show,(approx[i,0,0],approx[i,0,1]),5,(0,0,255),5)plt.imshow(img_show[...,::-1]) plt.axis('off')綠色為原始輪廓點,紅色為凸多邊形逼近后的輪廓點,可以發現比多邊形逼近函數的結果少了內凹角頂點。
凸性檢測
如何判斷一個輪廓是否為凸的,有一個函數k = cv.isContourConvex(cnt),返回true就是凸的。
OpenCV對這個凸多邊形還提供了提取更詳細信息的函數convexityDefects,用于獲取凸多邊形和輪廓之間的關系:
void convexityDefects(InputArray contour, InputArray convexhull, OutputArrayconvexityDefects)輸入:原始輪廓點、凸多邊形頂點對應原輪廓中的索引、輸出(不管)
所以針對那個五角星的調用方法是:
contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE) cnt_idx = 4 cnt = contours[cnt_idx] approx = cv2.convexHull(cnt,returnPoints=False)#使用false獲取多邊形頂點索引print(cv2.convexityDefects(cnt,approx)) ''' [[[ 321 0 357 4403]][[ 0 67 31 4403]][[ 67 128 96 4230]][[ 128 194 163 4223]][[ 194 260 225 4223]][[ 260 321 292 4230]]] '''得到了和多邊形逼近線段個數相同行的列為4的矩陣,分別代表:起始點索引、結束點索引、當前線段截取的輪廓點中距離線段最遠的點索引、這個最遠點與當前線段的距離
驗證一下,把第三個維度,也就是距離每條邊最遠的輪廓點畫出來:
邊界框
分為矩形、圓形邊界
-
矩形:不考慮形狀的旋轉,獲取直邊界矩形
x,y,w,h = cv2.boundingRect(cnt) -
矩形考慮旋轉,獲取最小的外接矩形
rect = cv2.minAreaRect(cnt) box = cv2.boxPoints(rect) box = np.int0(box)
畫圖看看區別,直邊界用綠色,最小外接矩形用紅色
img_show = img.copy() #無旋轉矩形 cv2.rectangle(img_show,(x,y),(x+w,y+h),(0,255,0),4) #有旋轉矩形 cv2.drawContours(img_show,[box],0,(0,0,255),4)plt.imshow(img_show[...,::-1]) plt.axis('off')外接圓:minEnclosingCircle
# 最小外接圓 (x,y),radius = cv2.minEnclosingCircle(cnt) center = (int(x),int(y)) radius = int(radius)img_show = img.copy() cv2.circle(img,center,radius,(0,255,0),10) plt.imshow(img_show[...,::-1]) plt.axis('off')形狀擬合
包括橢圓、直線擬合
橢圓擬合:fitEllipse,將里面的那個菱形擬合
# 橢圓擬合 ellipse = cv2.fitEllipse(contours[5])img_show = img.copy() cv2.ellipse(img_show,ellipse,(0,255,0),10) plt.imshow(img_show[...,::-1]) plt.axis('off')直線擬合:fitLine,使得當前輪廓所有點與直線距離和最短
rows,cols = img.shape[:2] [vx,vy,x,y] = cv2.fitLine(cnt, cv2.DIST_L2,0,0.01,0.01) lefty = int((-x*vy/vx) + y) righty = int(((cols-x)*vy/vx)+y)img_show = img.copy() cv2.line(img_show,(cols-1,righty),(0,lefty),(0,255,0),2) plt.imshow(img_show[...,::-1]) plt.axis('off')點與輪廓的關系
通過pointPolygonTest函數判斷某個點是否在輪廓內部后者外部,然后返回距離輪廓的最短距離
retval=cv.pointPolygonTest(contour,pt,measureDist)輸入分別為:輪廓、某個點、是否返回距離;如果僅僅需要判斷點是否再輪廓內部,第三個參數設置False,在內部為+1,外部為-1,在輪廓上為0。
形狀匹配
可以利用matchShapes輸入兩個輪廓,計算相似度,得分越低越相似
retval = cv.matchShapes( contour1, contour2, method, parameter )輸入為:第一個形狀的輪廓、第二給形狀的輪廓、匹配算法、參數(暫不支持,不管)
匹配算法是基于圖像的Hu矩,計算方法為:
mi=sign(hi)?log?him_i = sign(h_i)\cdot \log h_i mi?=sign(hi?)?loghi?
其中hih_ihi?代表Hu矩。
匹配算法分為:
使用案例,先構建一些圖像,然后計算相似度:
img1 = cv2.imread('shape1.png',0) img2 = cv2.imread('shape2.png',0) img3 = cv2.imread('shape3.png',0) ret, thresh = cv2.threshold(img1, 127, 255,0) ret, thresh2 = cv2.threshold(img2, 127, 255,0) ret, thresh3 = cv2.threshold(img3, 127, 255,0) contours,hierarchy = cv2.findContours(thresh,2,1) cnt1 = contours[0] contours,hierarchy = cv2.findContours(thresh2,2,1) cnt2 = contours[0] contours,hierarchy = cv2.findContours(thresh3,2,1) cnt3 = contours[0] ret1 = cv2.matchShapes(cnt1,cnt2,1,0.0) ret2 = cv2.matchShapes(cnt1,cnt3,1,0.0) print( ret1,ret2 ) plt.subplot(131) plt.imshow(img1,cmap='gray') plt.axis('off') plt.subplot(132) plt.imshow(img2,cmap='gray') plt.axis('off') plt.subplot(133) plt.imshow(img3,cmap='gray') plt.axis('off') ''' 0.14475720763533126 0.3168697153308031 '''可以發現形狀1和2的非常接近,一個四角星一個五角星,他倆得分很低,越相似。
其它屬性
-
獲取掩膜(mask)
mask = np.zeros(gray_img.shape,np.uint8) cv2.drawContours(mask,[cnt],0,255,-1) pixelpoints = np.transpose(np.nonzero(mask)) #pixelpoints = cv.findNonZero(mask) plt.imshow(mask,cmap='gray')
注意使用可以使用numpy或者OpenCV去查找到掩膜內所有像素坐標,但是他倆的位置不一樣,因此numpy的坐標需要轉置才能與OpenCV保持一致,列是x,行是y
-
獲取局部最大值、最小值級它們的位置
min_val, max_val, min_loc, max_loc = cv.minMaxLoc(imgray,mask = mask)注意第一個參數必須是單通道的圖,第二個參數可有可無,用于選擇特定區域。
這個在Openpose中,從每個關節的特征圖中提取關節坐標用到過,具體可看之前解析OpenPose的文章。
-
均值:通道分開
mean_val = cv.mean(im,mask = mask)
后記
圖像處理經常遇到輪廓相關的問題,比如二維碼檢測定位之類的大都是用二維碼四個角的定位符和矯正符的比例特征來定位。這里對官方的教程做了簡單的綜合整理。
完整的python腳本實現放在微信公眾號的簡介中描述的github中,有興趣可以去找找,同時文章也同步到微信公眾號中,有疑問或者興趣歡迎公眾號私信。
總結
以上是生活随笔為你收集整理的OpenCV学习——轮廓检测的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: VUE-周日历的实现
- 下一篇: OpenCV学习——形态学